summaryrefslogtreecommitdiff
path: root/app/html-cleaner/src/main/java
diff options
context:
space:
mode:
authortest@dangofactory.cos <you@example.com>2026-08-20 16:57:18 +0000
committertest@dangofactory.cos <you@example.com>2026-08-20 16:57:18 +0000
commit50ce5d5e6cd0d720ff437d35eace545816848428 (patch)
treeb59af206a36265cf081945e434900b32b0c43edd /app/html-cleaner/src/main/java
downloadmonocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.gz
monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.bz2
monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.zip
Inital
Diffstat (limited to 'app/html-cleaner/src/main/java')
-rw-r--r--app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt78
-rw-r--r--app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt75
-rw-r--r--app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt5
-rw-r--r--app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt25
-rw-r--r--app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt16
5 files changed, 199 insertions, 0 deletions
diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt
new file mode 100644
index 0000000..0f0e5cd
--- /dev/null
+++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/BodyCleaner.kt
@@ -0,0 +1,78 @@
+package app.k9mail.html.cleaner
+
+import org.jsoup.nodes.Document
+import org.jsoup.safety.Cleaner
+import org.jsoup.safety.Safelist
+
+internal class BodyCleaner {
+ private val cleaner: Cleaner
+ private val allowedBodyAttributes = setOf(
+ "id", "class", "dir", "lang", "style",
+ "alink", "background", "bgcolor", "link", "text", "vlink"
+ )
+
+ init {
+ val allowList = Safelist.relaxed()
+ .addTags("font", "hr", "ins", "del", "center", "map", "area", "title", "tt", "kbd", "samp", "var")
+ .addAttributes("font", "color", "face", "size")
+ .addAttributes("a", "name")
+ .addAttributes("div", "align")
+ .addAttributes(
+ "table",
+ "align",
+ "background",
+ "bgcolor",
+ "border",
+ "cellpadding",
+ "cellspacing",
+ "width"
+ )
+ .addAttributes("tr", "align", "background", "bgcolor", "valign")
+ .addAttributes(
+ "th",
+ "align", "background", "bgcolor", "colspan", "headers", "height", "nowrap", "rowspan", "scope",
+ "sorted", "valign", "width"
+ )
+ .addAttributes(
+ "td",
+ "align", "background", "bgcolor", "colspan", "headers", "height", "nowrap", "rowspan", "scope",
+ "valign", "width"
+ )
+ .addAttributes("map", "name")
+ .addAttributes("area", "shape", "coords", "href", "alt")
+ .addProtocols("area", "href", "http", "https")
+ .addAttributes("img", "usemap")
+ .addAttributes(":all", "class", "style", "id", "dir")
+ .addProtocols("img", "src", "http", "https", "cid", "data")
+ // Allow all URI schemes in links
+ .removeProtocols("a", "href", "ftp", "http", "https", "mailto")
+
+ cleaner = Cleaner(allowList)
+ }
+
+ fun clean(dirtyDocument: Document): Document {
+ val cleanedDocument = cleaner.clean(dirtyDocument)
+ copyDocumentType(dirtyDocument, cleanedDocument)
+ copyBodyAttributes(dirtyDocument, cleanedDocument)
+ return cleanedDocument
+ }
+
+ private fun copyDocumentType(dirtyDocument: Document, cleanedDocument: Document) {
+ dirtyDocument.documentType()?.let { documentType ->
+ cleanedDocument.insertChildren(0, documentType)
+ }
+ }
+
+ private fun copyBodyAttributes(dirtyDocument: Document, cleanedDocument: Document) {
+ val cleanedBody = cleanedDocument.body()
+ for (attribute in dirtyDocument.body().attributes()) {
+ if (attribute.key !in allowedBodyAttributes) continue
+
+ if (attribute.hasDeclaredValue()) {
+ cleanedBody.attr(attribute.key, attribute.value)
+ } else {
+ cleanedBody.attr(attribute.key, true)
+ }
+ }
+ }
+}
diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt
new file mode 100644
index 0000000..1485291
--- /dev/null
+++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HeadCleaner.kt
@@ -0,0 +1,75 @@
+package app.k9mail.html.cleaner
+
+import org.jsoup.nodes.DataNode
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import org.jsoup.nodes.Node
+import org.jsoup.nodes.TextNode
+import org.jsoup.parser.Tag
+import org.jsoup.select.NodeTraversor
+import org.jsoup.select.NodeVisitor
+
+private val ALLOWED_TAGS = listOf("style", "meta", "base")
+
+internal class HeadCleaner {
+ fun clean(dirtyDocument: Document, cleanedDocument: Document) {
+ copySafeNodes(dirtyDocument.head(), cleanedDocument.head())
+ }
+
+ private fun copySafeNodes(source: Element, destination: Element) {
+ val cleaningVisitor = CleaningVisitor(source, destination)
+ NodeTraversor.traverse(cleaningVisitor, source)
+ }
+}
+
+internal class CleaningVisitor(
+ private val root: Element,
+ private var destination: Element
+) : NodeVisitor {
+ private var elementToSkip: Element? = null
+
+ override fun head(source: Node, depth: Int) {
+ if (elementToSkip != null) return
+
+ if (source is Element) {
+ if (isSafeTag(source)) {
+ val sourceTag = source.tagName()
+ val destinationAttributes = source.attributes().clone()
+ val destinationChild = Element(Tag.valueOf(sourceTag), source.baseUri(), destinationAttributes)
+ destination.appendChild(destinationChild)
+ destination = destinationChild
+ } else if (source !== root) {
+ elementToSkip = source
+ }
+ } else if (source is TextNode) {
+ val destinationText = TextNode(source.wholeText)
+ destination.appendChild(destinationText)
+ } else if (source is DataNode && isSafeTag(source.parent())) {
+ val destinationData = DataNode(source.wholeData)
+ destination.appendChild(destinationData)
+ }
+ }
+
+ override fun tail(source: Node, depth: Int) {
+ if (source === elementToSkip) {
+ elementToSkip = null
+ } else if (source is Element && isSafeTag(source)) {
+ destination = destination.parent() ?: error("Missing parent")
+ }
+ }
+
+ private fun isSafeTag(node: Node?): Boolean {
+ if (node == null || isMetaRefresh(node)) return false
+
+ val tag = node.nodeName().lowercase()
+ return tag in ALLOWED_TAGS
+ }
+
+ private fun isMetaRefresh(node: Node): Boolean {
+ val tag = node.nodeName().lowercase()
+ if (tag != "meta") return false
+
+ val attributeValue = node.attributes().getIgnoreCase("http-equiv").trim().lowercase()
+ return attributeValue == "refresh"
+ }
+}
diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt
new file mode 100644
index 0000000..cf234cc
--- /dev/null
+++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlHeadProvider.kt
@@ -0,0 +1,5 @@
+package app.k9mail.html.cleaner
+
+interface HtmlHeadProvider {
+ val headHtml: String
+}
diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt
new file mode 100644
index 0000000..2da379b
--- /dev/null
+++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlProcessor.kt
@@ -0,0 +1,25 @@
+package app.k9mail.html.cleaner
+
+import org.jsoup.nodes.Document
+
+class HtmlProcessor(private val htmlHeadProvider: HtmlHeadProvider) {
+ private val htmlSanitizer = HtmlSanitizer()
+
+ fun processForDisplay(html: String): String {
+ return htmlSanitizer.sanitize(html)
+ .addCustomHeadContents()
+ .toCompactString()
+ }
+
+ private fun Document.addCustomHeadContents() = apply {
+ head().append(htmlHeadProvider.headHtml)
+ }
+
+ private fun Document.toCompactString(): String {
+ outputSettings()
+ .prettyPrint(false)
+ .indentAmount(0)
+
+ return html()
+ }
+}
diff --git a/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt
new file mode 100644
index 0000000..d11bb30
--- /dev/null
+++ b/app/html-cleaner/src/main/java/app/k9mail/html/cleaner/HtmlSanitizer.kt
@@ -0,0 +1,16 @@
+package app.k9mail.html.cleaner
+
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+
+internal class HtmlSanitizer {
+ private val headCleaner = HeadCleaner()
+ private val bodyCleaner = BodyCleaner()
+
+ fun sanitize(html: String): Document {
+ val dirtyDocument = Jsoup.parse(html)
+ val cleanedDocument = bodyCleaner.clean(dirtyDocument)
+ headCleaner.clean(dirtyDocument, cleanedDocument)
+ return cleanedDocument
+ }
+}