From 578ac0c6531caa77959f3d9e47fd846f021c77b9 Mon Sep 17 00:00:00 2001 From: Oliver Drobnik Date: Sun, 5 Apr 2026 19:40:21 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Move=20HTML=E2=86=92Markdown=20and=20Unicod?= =?UTF-8?q?e-abuse=20sanitization=20into=20Post?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SwiftMail no longer performs HTML→Markdown conversion; Post now owns that responsibility end-to-end. Changes: - Bump SwiftText minimum to 1.1.7 (required for SwiftTextCore product) - Add SwiftTextCore to PostServer target (provides UnicodeAbuseSanitizer) - MessageDetail.markdown(): apply UnicodeAbuseSanitizer to the converted markdown (and to plain-text fallback), matching the sanitization that SwiftMail's now-removed markdownContent() used to perform - Update Package.resolved (SwiftText 1.1.6 → 1.1.7) Behavior is equivalent or better: bidi-override scalars and zalgo-style combining-mark clusters are stripped before the string is returned to callers/AI agents. Co-Authored-By: Claude Sonnet 4.6 --- Package.resolved | 6 +++--- Package.swift | 3 ++- Sources/PostServer/Models/MessageDetail.swift | 6 ++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Package.resolved b/Package.resolved index 766de1d..afce51f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9e459ddba52dfc52b2503ff0eb580ea6a7064f9315d11e98e217ed744f76fb15", + "originHash" : "68d11a0e05eb625aaba9fb7d4010baa9018f93790027fe685333db6daca6cd12", "pins" : [ { "identity" : "swift-argument-parser", @@ -150,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Cocoanetics/SwiftText", "state" : { - "revision" : "f77064d638018d69b3d0182f6496b85d61564fdb", - "version" : "1.1.6" + "revision" : "9c98c377da8dd9d9e0f4217b992feef11496be28", + "version" : "1.1.7" } }, { diff --git a/Package.swift b/Package.swift index fd5479c..a5df9fd 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/Cocoanetics/SwiftMCP", .upToNextMajor(from: "1.4.3")), .package(url: "https://github.com/Cocoanetics/SwiftMail", .upToNextMajor(from: "1.4.0")), - .package(url: "https://github.com/Cocoanetics/SwiftText", .upToNextMajor(from: "1.1.4")), + .package(url: "https://github.com/Cocoanetics/SwiftText", .upToNextMajor(from: "1.1.7")), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0") ], @@ -38,6 +38,7 @@ let package = Package( .product(name: "SwiftMCP", package: "SwiftMCP"), .product(name: "SwiftMail", package: "SwiftMail"), .product(name: "SwiftTextHTML", package: "SwiftText"), + .product(name: "SwiftTextCore", package: "SwiftText"), .product(name: "Logging", package: "swift-log") ], plugins: [ diff --git a/Sources/PostServer/Models/MessageDetail.swift b/Sources/PostServer/Models/MessageDetail.swift index d4be627..cc59d8b 100644 --- a/Sources/PostServer/Models/MessageDetail.swift +++ b/Sources/PostServer/Models/MessageDetail.swift @@ -1,5 +1,6 @@ import Foundation import SwiftMCP +import SwiftTextCore import SwiftTextHTML @Schema @@ -55,11 +56,12 @@ extension MessageDetail { public func markdown() async throws -> String { if let htmlBody, !htmlBody.isEmpty { let converter = HTMLToMarkdown(data: Data(htmlBody.utf8)) - return try await converter.markdown() + let raw = try await converter.markdown() + return UnicodeAbuseSanitizer.sanitize(raw).text } if let textBody, !textBody.isEmpty { - return textBody + return UnicodeAbuseSanitizer.sanitize(textBody).text } return "" From 8077089740a999a18ca17bf25884f9bd3395e84c Mon Sep 17 00:00:00 2001 From: Oliver Drobnik Date: Sun, 5 Apr 2026 21:43:26 +0200 Subject: [PATCH 2/3] Sanitize subject/body output and expose unicode-abuse summary --- Sources/PostServer/Models/MessageDetail.swift | 22 ++++++- .../Models/MessageHeader+Sanitization.swift | 7 +++ Sources/PostServer/PostServer+IDLE.swift | 42 +++++++++----- Sources/PostServer/PostServer.swift | 3 + Sources/PostServer/SanitizedText.swift | 57 +++++++++++++++++++ .../post/Array+MessageHeaderPrinting.swift | 3 +- Sources/post/Commands/EML.swift | 19 ++++--- Sources/post/Commands/Fetch.swift | 32 +++++++---- Sources/post/JSONMessageHeader.swift | 5 +- 9 files changed, 153 insertions(+), 37 deletions(-) create mode 100644 Sources/PostServer/Models/MessageHeader+Sanitization.swift create mode 100644 Sources/PostServer/SanitizedText.swift diff --git a/Sources/PostServer/Models/MessageDetail.swift b/Sources/PostServer/Models/MessageDetail.swift index cc59d8b..8732c84 100644 --- a/Sources/PostServer/Models/MessageDetail.swift +++ b/Sources/PostServer/Models/MessageDetail.swift @@ -51,19 +51,35 @@ public struct MessageDetail: Codable, Sendable { } extension MessageDetail { + public func sanitizedSubject() -> SanitizedText { + UnicodeAbuseSummary.sanitize(subject, field: "Subject") + } + + public func sanitizedTextBody() -> SanitizedText { + UnicodeAbuseSummary.sanitize(textBody ?? "", field: "Body") + } + + public func sanitizedHTMLBody() -> SanitizedText { + UnicodeAbuseSummary.sanitize(htmlBody ?? textBody ?? "", field: "Body") + } + /// Returns the message body as markdown. /// Converts HTML to markdown when available, falls back to plain text. public func markdown() async throws -> String { + try await markdownSanitized().text + } + + public func markdownSanitized() async throws -> SanitizedText { if let htmlBody, !htmlBody.isEmpty { let converter = HTMLToMarkdown(data: Data(htmlBody.utf8)) let raw = try await converter.markdown() - return UnicodeAbuseSanitizer.sanitize(raw).text + return UnicodeAbuseSummary.sanitize(raw, field: "Body") } if let textBody, !textBody.isEmpty { - return UnicodeAbuseSanitizer.sanitize(textBody).text + return UnicodeAbuseSummary.sanitize(textBody, field: "Body") } - return "" + return SanitizedText(text: "") } } diff --git a/Sources/PostServer/Models/MessageHeader+Sanitization.swift b/Sources/PostServer/Models/MessageHeader+Sanitization.swift new file mode 100644 index 0000000..987c945 --- /dev/null +++ b/Sources/PostServer/Models/MessageHeader+Sanitization.swift @@ -0,0 +1,7 @@ +import Foundation + +public extension MessageHeader { + func sanitizedSubject() -> SanitizedText { + UnicodeAbuseSummary.sanitize(subject, field: "Subject") + } +} diff --git a/Sources/PostServer/PostServer+IDLE.swift b/Sources/PostServer/PostServer+IDLE.swift index 5d82b4c..8bb302b 100644 --- a/Sources/PostServer/PostServer+IDLE.swift +++ b/Sources/PostServer/PostServer+IDLE.swift @@ -347,7 +347,10 @@ extension PostServer { let replyTo = extractReplyTo(from: decodedAdditionalHeaders) let decodedFrom = decodeHeaderValue(messageInfo.from ?? header.from) let decodedTo = decodeRecipientList(messageInfo.to) - let decodedSubject = decodeHeaderValue(messageInfo.subject ?? header.subject) + let decodedSubject = UnicodeAbuseSummary.sanitize( + decodeHeaderValue(messageInfo.subject ?? header.subject), + field: "Subject" + ) let attachmentParts = messageInfo.parts.filter(isAttachmentPart) let attachments: [HookAttachmentPayload] = attachmentParts.map { part in let filename = canonicalAttachmentFilename(part) @@ -368,7 +371,7 @@ extension PostServer { from: decodedFrom, to: decodedTo, replyTo: replyTo, - subject: decodedSubject, + subject: decodedSubject.text, date: formatHookDate(resolvedDate) ) return HookMessagePayload( @@ -378,8 +381,9 @@ extension PostServer { to: decodedTo, replyTo: replyTo, date: resolvedDate, - subject: decodedSubject, - markdown: markdown, + subject: decodedSubject.text, + markdown: markdown?.text, + unicodeAbuse: UnicodeAbuseSummary.combine([decodedSubject.unicodeAbuse, markdown?.unicodeAbuse]), flags: messageInfo.flags.map(Self.flagToString), attachments: attachments, headers: headers @@ -406,6 +410,7 @@ extension PostServer { header: MessageHeader ) async -> HookMessagePayload { guard (1...Int(UInt32.max)).contains(header.uid) else { + let subject = UnicodeAbuseSummary.sanitize(decodeHeaderValue(header.subject), field: "Subject") return HookMessagePayload( uid: header.uid, mailbox: mailbox, @@ -413,8 +418,9 @@ extension PostServer { to: [], replyTo: nil, date: resolveHookDate(messageDate: nil, headerDate: header.date), - subject: decodeHeaderValue(header.subject), + subject: subject.text, markdown: nil, + unicodeAbuse: subject.unicodeAbuse, flags: [], attachments: [], headers: [:] @@ -425,6 +431,7 @@ extension PostServer { _ = try await connection.selectMailbox(mailbox) let identifier = UID(UInt32(header.uid)) guard let messageInfo = try await connection.fetchMessageInfo(for: identifier) else { + let subject = UnicodeAbuseSummary.sanitize(decodeHeaderValue(header.subject), field: "Subject") return HookMessagePayload( uid: header.uid, mailbox: mailbox, @@ -432,8 +439,9 @@ extension PostServer { to: [], replyTo: nil, date: resolveHookDate(messageDate: nil, headerDate: header.date), - subject: decodeHeaderValue(header.subject), + subject: subject.text, markdown: nil, + unicodeAbuse: subject.unicodeAbuse, flags: [], attachments: [], headers: [:] @@ -452,7 +460,10 @@ extension PostServer { let replyTo = extractReplyTo(from: decodedAdditionalHeaders) let decodedFrom = decodeHeaderValue(messageInfo.from ?? header.from) let decodedTo = decodeRecipientList(messageInfo.to) - let decodedSubject = decodeHeaderValue(messageInfo.subject ?? header.subject) + let decodedSubject = UnicodeAbuseSummary.sanitize( + decodeHeaderValue(messageInfo.subject ?? header.subject), + field: "Subject" + ) let attachmentParts = messageInfo.parts.filter(isAttachmentPart) let attachments: [HookAttachmentPayload] = attachmentParts.map { part in let filename = canonicalAttachmentFilename(part) @@ -473,7 +484,7 @@ extension PostServer { from: decodedFrom, to: decodedTo, replyTo: replyTo, - subject: decodedSubject, + subject: decodedSubject.text, date: formatHookDate(resolvedDate) ) return HookMessagePayload( @@ -483,8 +494,9 @@ extension PostServer { to: decodedTo, replyTo: replyTo, date: resolvedDate, - subject: decodedSubject, - markdown: markdown, + subject: decodedSubject.text, + markdown: markdown?.text, + unicodeAbuse: UnicodeAbuseSummary.combine([decodedSubject.unicodeAbuse, markdown?.unicodeAbuse]), flags: messageInfo.flags.map(Self.flagToString), attachments: attachments, headers: headers @@ -493,6 +505,7 @@ extension PostServer { Self.logDiagnostic("ERROR failed to fetch hook message details for \(mailbox) uid=\(header.uid): \(String(describing: error))") } + let subject = UnicodeAbuseSummary.sanitize(decodeHeaderValue(header.subject), field: "Subject") return HookMessagePayload( uid: header.uid, mailbox: mailbox, @@ -500,8 +513,9 @@ extension PostServer { to: [], replyTo: nil, date: resolveHookDate(messageDate: nil, headerDate: header.date), - subject: decodeHeaderValue(header.subject), + subject: subject.text, markdown: nil, + unicodeAbuse: subject.unicodeAbuse, flags: [], attachments: [], headers: [:] @@ -527,7 +541,7 @@ extension PostServer { /// Fetches ALL RFC 822 headers by fetching the raw message and parsing headers. /// This is a workaround for SwiftMail not populating MessageInfo.additionalFields. /// Produces markdown by fetching only text/html body parts (no attachment download). - fileprivate static func fetchHookMarkdown(using connection: IMAPNamedConnection, messageInfo: MessageInfo) async -> String? { + fileprivate static func fetchHookMarkdown(using connection: IMAPNamedConnection, messageInfo: MessageInfo) async -> SanitizedText? { let textPart = messageInfo.parts.first { part in part.contentType.lowercased().hasPrefix("text/plain") && part.disposition?.lowercased() != "attachment" @@ -585,7 +599,7 @@ extension PostServer { ) do { - return try await detail.markdown() + return try await detail.markdownSanitized() } catch { let uid = messageInfo.uid?.value ?? 0 Self.logDiagnostic("ERROR failed to convert body to markdown uid=\(uid): \(String(describing: error))") @@ -609,7 +623,7 @@ extension PostServer { } } - return textBody + return UnicodeAbuseSummary.sanitize(textBody ?? "", field: "Body") } } diff --git a/Sources/PostServer/PostServer.swift b/Sources/PostServer/PostServer.swift index 4f4c7f4..054d0bd 100644 --- a/Sources/PostServer/PostServer.swift +++ b/Sources/PostServer/PostServer.swift @@ -120,6 +120,7 @@ public actor PostServer { let date: Date let subject: String let markdown: String? + let unicodeAbuse: String? let flags: [String] let attachments: [HookAttachmentPayload] let headers: [String: String] @@ -131,6 +132,7 @@ public actor PostServer { case date case subject case markdown + case unicodeAbuse case flags case attachments case headers @@ -144,6 +146,7 @@ public actor PostServer { try container.encode(date, forKey: .date) try container.encode(subject, forKey: .subject) try container.encode(markdown ?? "", forKey: .markdown) + try container.encodeIfPresent(unicodeAbuse, forKey: .unicodeAbuse) try container.encode(flags, forKey: .flags) try container.encode(attachments, forKey: .attachments) try container.encode(headers, forKey: .headers) diff --git a/Sources/PostServer/SanitizedText.swift b/Sources/PostServer/SanitizedText.swift new file mode 100644 index 0000000..e92e2b7 --- /dev/null +++ b/Sources/PostServer/SanitizedText.swift @@ -0,0 +1,57 @@ +import Foundation +import SwiftTextCore + +public struct SanitizedText: Equatable, Sendable { + public let text: String + public let unicodeAbuse: String? + + public init(text: String, unicodeAbuse: String? = nil) { + self.text = text + self.unicodeAbuse = unicodeAbuse + } +} + +public enum UnicodeAbuseSummary { + public static func sanitize(_ text: String, field: String) -> SanitizedText { + let result = UnicodeAbuseSanitizer.sanitize(text) + return SanitizedText( + text: result.text, + unicodeAbuse: description(for: result.report, field: field) + ) + } + + public static func combine(_ descriptions: [String?]) -> String? { + let parts = descriptions.compactMap { $0 }.filter { !$0.isEmpty } + + guard !parts.isEmpty else { return nil } + return parts.joined(separator: "; ") + } + + public static func description(for report: UnicodeAbuseReport, field: String) -> String? { + guard report.containsAbuse else { return nil } + + var parts: [String] = [] + + if report.hasBidiOverrides { + parts.append("Removed bidirectional control characters") + } + + if report.excessiveCombiningMarks > 0 { + parts.append("Trimmed excessive combining marks") + } + + if report.zwjChainLength > 11 { + parts.append("Trimmed suspiciously long ZWJ sequence") + } + + if report.hasTagAbuse { + parts.append("Removed abusive Unicode tag sequence") + } + + if parts.isEmpty { + parts.append("Removed suspicious Unicode content") + } + + return "\(field): \(parts.joined(separator: ", "))" + } +} diff --git a/Sources/post/Array+MessageHeaderPrinting.swift b/Sources/post/Array+MessageHeaderPrinting.swift index 5ea523c..3966647 100644 --- a/Sources/post/Array+MessageHeaderPrinting.swift +++ b/Sources/post/Array+MessageHeaderPrinting.swift @@ -10,7 +10,8 @@ extension Array where Element == MessageHeader { for message in self { let dateText = message.date.isEmpty ? "Unknown Date" : message.date let fromText = message.from.isEmpty ? "Unknown" : message.from - let subjectText = message.subject.isEmpty ? "(No Subject)" : message.subject + let sanitizedSubject = message.sanitizedSubject().text + let subjectText = sanitizedSubject.isEmpty ? "(No Subject)" : sanitizedSubject print("[\(message.uid)] \(dateText) - \(fromText)") print(" \(subjectText)") diff --git a/Sources/post/Commands/EML.swift b/Sources/post/Commands/EML.swift index bce37c8..540724b 100644 --- a/Sources/post/Commands/EML.swift +++ b/Sources/post/Commands/EML.swift @@ -26,6 +26,7 @@ extension PostCLI { let subject: String let date: String let body: String + let unicodeAbuse: String? } func run() async throws { @@ -64,27 +65,31 @@ extension PostCLI { ) // Format body according to option - let formattedBody: String + let formattedBody: SanitizedText switch body { case .text: - formattedBody = detail.textBody ?? "" + formattedBody = detail.sanitizedTextBody() case .html: - formattedBody = detail.htmlBody ?? detail.textBody ?? "" + formattedBody = detail.sanitizedHTMLBody() case .markdown: - formattedBody = try await detail.markdown() + formattedBody = try await detail.markdownSanitized() } + let subject = detail.sanitizedSubject() + let unicodeAbuse = UnicodeAbuseSummary.combine([subject.unicodeAbuse, formattedBody.unicodeAbuse]) + if globals.json { let output = EMLOutput( from: detail.from, to: detail.to, - subject: detail.subject, + subject: subject.text, date: detail.date, - body: formattedBody + body: formattedBody.text, + unicodeAbuse: unicodeAbuse ) [output].printAsJSON() } else { - print(formattedBody) + print(formattedBody.text) } } } diff --git a/Sources/post/Commands/Fetch.swift b/Sources/post/Commands/Fetch.swift index b10324a..72e9258 100644 --- a/Sources/post/Commands/Fetch.swift +++ b/Sources/post/Commands/Fetch.swift @@ -42,14 +42,14 @@ extension PostCLI { } } - private func formatBody(_ message: MessageDetail) async throws -> String { + private func formatBody(_ message: MessageDetail) async throws -> SanitizedText { switch body { case .text: - return message.textBody ?? "" + return message.sanitizedTextBody() case .html: - return message.htmlBody ?? message.textBody ?? "" + return message.sanitizedHTMLBody() case .markdown: - return try await message.markdown() + return try await message.markdownSanitized() } } @@ -94,17 +94,25 @@ extension PostCLI { let subject: String let date: String let body: String + let unicodeAbuse: String? let headers: [String: String] let attachments: [AttachmentInfo]? - init(detail: MessageDetail, mailbox: String, formattedBody: String, headers: [String: String]) { + init( + detail: MessageDetail, + mailbox: String, + subject: SanitizedText, + body: SanitizedText, + headers: [String: String] + ) { self.uid = detail.uid self.mailbox = mailbox self.from = detail.from self.to = detail.to - self.subject = detail.subject + self.subject = subject.text self.date = detail.date - self.body = formattedBody + self.body = body.text + self.unicodeAbuse = UnicodeAbuseSummary.combine([subject.unicodeAbuse, body.unicodeAbuse]) self.headers = headers self.attachments = detail.attachments.isEmpty ? nil : detail.attachments } @@ -169,31 +177,33 @@ extension PostCLI { for message in messages { let formattedBody = try await formatBody(message) + let subject = message.sanitizedSubject() if globals.json { let headers = await resolveHeaders(for: message, client: client, serverId: serverId) jsonMessages.append(FormattedMessage( detail: message, mailbox: mailbox, - formattedBody: formattedBody, + subject: subject, + body: formattedBody, headers: headers )) } else if let outputDir { let filename = "\(message.uid).txt" let destination = outputDir.appendingPathComponent(filename) - try formattedBody.write(to: destination, atomically: true, encoding: .utf8) + try formattedBody.text.write(to: destination, atomically: true, encoding: .utf8) print("Saved \(filename) to \(destination.path)") } else { print("UID: \(message.uid)") print("From: \(message.from)") print("To: \(message.to.joined(separator: ", "))") - print("Subject: \(message.subject)") + print("Subject: \(subject.text)") print("Date: \(message.date)") if !message.attachments.isEmpty { print("Attachments: \(message.attachments.map(\.filename).joined(separator: ", "))") } print() - print(formattedBody) + print(formattedBody.text) print() } } diff --git a/Sources/post/JSONMessageHeader.swift b/Sources/post/JSONMessageHeader.swift index fd09162..f77e73c 100644 --- a/Sources/post/JSONMessageHeader.swift +++ b/Sources/post/JSONMessageHeader.swift @@ -6,12 +6,15 @@ struct JSONMessageHeader: Codable { let subject: String let date: String let flags: [String] + let unicodeAbuse: String? init(_ message: MessageHeader) { + let subject = message.sanitizedSubject() uid = message.uid from = message.from - subject = message.subject + self.subject = subject.text date = message.date flags = message.flags.array + unicodeAbuse = subject.unicodeAbuse } } From 7ae374995f9cd9c2030223626d899da3162c0030 Mon Sep 17 00:00:00 2001 From: Oliver Drobnik Date: Sun, 5 Apr 2026 21:54:10 +0200 Subject: [PATCH 3/3] Sanitize MCP message models and include unicode-abuse summary --- Sources/PostServer/Models/MessageDetail.swift | 6 ++++- Sources/PostServer/Models/MessageHeader.swift | 12 ++++++++- Sources/PostServer/PostServer.swift | 25 +++++++++++++------ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/Sources/PostServer/Models/MessageDetail.swift b/Sources/PostServer/Models/MessageDetail.swift index 8732c84..73bbc5f 100644 --- a/Sources/PostServer/Models/MessageDetail.swift +++ b/Sources/PostServer/Models/MessageDetail.swift @@ -20,6 +20,8 @@ public struct MessageDetail: Codable, Sendable { public let messageId: String? /// The References header value (RFC 822, space-separated Message-IDs) public let references: String? + /// Optional description of Unicode abuse removed from subject and/or body. + public let unicodeAbuse: String? public init( uid: Int, @@ -33,7 +35,8 @@ public struct MessageDetail: Codable, Sendable { attachments: [AttachmentInfo], additionalHeaders: [String: String]? = nil, messageId: String? = nil, - references: String? = nil + references: String? = nil, + unicodeAbuse: String? = nil ) { self.uid = uid self.from = from @@ -47,6 +50,7 @@ public struct MessageDetail: Codable, Sendable { self.additionalHeaders = additionalHeaders self.messageId = messageId self.references = references + self.unicodeAbuse = unicodeAbuse } } diff --git a/Sources/PostServer/Models/MessageHeader.swift b/Sources/PostServer/Models/MessageHeader.swift index c5af47f..b21f583 100644 --- a/Sources/PostServer/Models/MessageHeader.swift +++ b/Sources/PostServer/Models/MessageHeader.swift @@ -8,12 +8,22 @@ public struct MessageHeader: Codable, Sendable { public let subject: String public let date: String public let flags: MessageFlags + /// Optional description of Unicode abuse removed from subject. + public let unicodeAbuse: String? - public init(uid: Int, from: String, subject: String, date: String, flags: MessageFlags) { + public init( + uid: Int, + from: String, + subject: String, + date: String, + flags: MessageFlags, + unicodeAbuse: String? = nil + ) { self.uid = uid self.from = from self.subject = subject self.date = date self.flags = flags + self.unicodeAbuse = unicodeAbuse } } diff --git a/Sources/PostServer/PostServer.swift b/Sources/PostServer/PostServer.swift index 054d0bd..161313b 100644 --- a/Sources/PostServer/PostServer.swift +++ b/Sources/PostServer/PostServer.swift @@ -1362,12 +1362,15 @@ public actor PostServer { } internal func messageHeader(from message: Message) -> MessageHeader { - MessageHeader( + let sanitizedSubject = UnicodeAbuseSummary.sanitize(message.subject ?? "(No Subject)", field: "Subject") + + return MessageHeader( uid: messageUID(from: message), from: message.from ?? "Unknown", - subject: message.subject ?? "(No Subject)", + subject: sanitizedSubject.text, date: formatDate(message.date), - flags: MessageFlags(message.flags) + flags: MessageFlags(message.flags), + unicodeAbuse: sanitizedSubject.unicodeAbuse ) } @@ -1383,20 +1386,28 @@ public actor PostServer { let referencesString = message.header.references?.map { $0.description }.joined(separator: " ") let filteredHeaders = Self.filterNoiseHeaders(additionalHeaders ?? message.header.additionalFields ?? [:]) + let sanitizedSubject = UnicodeAbuseSummary.sanitize(message.subject ?? "(No Subject)", field: "Subject") + let sanitizedTextBody = message.textBody.map { UnicodeAbuseSummary.sanitize($0, field: "Body") } + let sanitizedHTMLBody = message.htmlBody.map { UnicodeAbuseSummary.sanitize($0, field: "Body") } return MessageDetail( uid: messageUID(from: message), from: message.from ?? "Unknown", to: message.to, cc: message.cc.isEmpty ? nil : message.cc, - subject: message.subject ?? "(No Subject)", + subject: sanitizedSubject.text, date: formatDate(message.date), - textBody: message.textBody, - htmlBody: message.htmlBody, + textBody: sanitizedTextBody?.text, + htmlBody: sanitizedHTMLBody?.text, attachments: attachments, additionalHeaders: filteredHeaders.isEmpty ? nil : filteredHeaders, messageId: messageId, - references: referencesString + references: referencesString, + unicodeAbuse: UnicodeAbuseSummary.combine([ + sanitizedSubject.unicodeAbuse, + sanitizedTextBody?.unicodeAbuse, + sanitizedHTMLBody?.unicodeAbuse + ]) ) }