From fc90d71021a193f5e10f49a5a4813a2abdb1e0bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Sun, 15 Mar 2026 12:34:10 +0100 Subject: [PATCH] decode RFC 2047 in To/CC names, convert dates to ISO 8601 for correct sorting - decode RFC 2047 encoded words in To/CC address display names - convert IMAP RFC 2822 dates to ISO 8601 before storing in MessageRecord, so SQLite ORDER BY text comparison sorts chronologically - strip parenthesized timezone names from dates (e.g. "+0100 (CET)") Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Sources/SyncEngine/SyncCoordinator.swift | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/Packages/MagnumOpusCore/Sources/SyncEngine/SyncCoordinator.swift b/Packages/MagnumOpusCore/Sources/SyncEngine/SyncCoordinator.swift index d626191..50ec88b 100644 --- a/Packages/MagnumOpusCore/Sources/SyncEngine/SyncCoordinator.swift +++ b/Packages/MagnumOpusCore/Sources/SyncEngine/SyncCoordinator.swift @@ -275,11 +275,22 @@ public final class SyncCoordinator { private func envelopeToRecord( _ envelope: FetchedEnvelope, accountId: String, mailboxId: String ) -> MessageRecord { - let toJson = encodeAddresses(envelope.to) - let ccJson = encodeAddresses(envelope.cc) - // Decode RFC 2047 encoded words in subject and sender name + // Decode RFC 2047 encoded words in all display names + let decodedTo = envelope.to.map { EmailAddress(name: $0.name.map { RFC2047Decoder.decode($0) }, address: $0.address) } + let decodedCc = envelope.cc.map { EmailAddress(name: $0.name.map { RFC2047Decoder.decode($0) }, address: $0.address) } + let toJson = encodeAddresses(decodedTo) + let ccJson = encodeAddresses(decodedCc) let decodedSubject = envelope.subject.map { RFC2047Decoder.decode($0) } let decodedFromName = envelope.from?.name.map { RFC2047Decoder.decode($0) } + + // Convert RFC 2822 date to ISO 8601 for correct text-based sorting in SQLite + let isoDate: String + if let parsed = Self.parseRFC2822Date(envelope.date) { + isoDate = ISO8601DateFormatter().string(from: parsed) + } else { + isoDate = envelope.date + } + return MessageRecord( id: UUID().uuidString, accountId: accountId, @@ -293,7 +304,7 @@ public final class SyncCoordinator { fromName: decodedFromName, toAddresses: toJson, ccAddresses: ccJson, - date: envelope.date, + date: isoDate, snippet: envelope.snippet, bodyText: envelope.bodyText, bodyHtml: envelope.bodyHtml, @@ -303,6 +314,29 @@ public final class SyncCoordinator { ) } + nonisolated(unsafe) private static let rfc2822Fmt: DateFormatter = { + let f = DateFormatter() + f.locale = Locale(identifier: "en_US_POSIX") + f.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z" + return f + }() + + nonisolated(unsafe) private static let rfc2822FmtNoDow: DateFormatter = { + let f = DateFormatter() + f.locale = Locale(identifier: "en_US_POSIX") + f.dateFormat = "dd MMM yyyy HH:mm:ss Z" + return f + }() + + private static func parseRFC2822Date(_ dateString: String) -> Date? { + // Strip parenthesized timezone name if present: "... +0100 (CET)" → "... +0100" + var cleaned = dateString.trimmingCharacters(in: .whitespaces) + if let parenRange = cleaned.range(of: #"\s*\([^)]*\)\s*$"#, options: .regularExpression) { + cleaned = String(cleaned[.. String? { guard !addresses.isEmpty else { return nil } struct Addr: Codable { var name: String?; var address: String }