decode RFC 2047 subjects/sender names, parse RFC 2822 dates from IMAP envelopes

- decode RFC 2047 encoded words in subject and fromName via RFC2047Decoder
  before storing in MessageRecord (fixes =?utf-8?Q?...?= raw display)
- add RFC 2822 date formatters to Queries.parseDate (EEE, dd MMM yyyy HH:mm:ss Z)
  so IMAP envelope dates parse correctly instead of falling back to distantPast
- parseDate now tries ISO 8601, RFC 2822 with day-of-week, RFC 2822 without

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 12:30:34 +01:00
parent 0c31bdc152
commit a71f609f3b
2 changed files with 28 additions and 4 deletions

View File

@@ -82,8 +82,29 @@ extension MailStore {
ISO8601DateFormatter() ISO8601DateFormatter()
}() }()
static func parseDate(_ iso: String) -> Date? { nonisolated(unsafe) private static let rfc2822Formatter: DateFormatter = {
isoFormatterWithFractional.date(from: iso) ?? isoFormatter.date(from: iso) 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 rfc2822FormatterNoDow: DateFormatter = {
let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.dateFormat = "dd MMM yyyy HH:mm:ss Z"
return f
}()
static func parseDate(_ dateString: String) -> Date? {
let trimmed = dateString.trimmingCharacters(in: .whitespaces)
// Try ISO 8601 first
if let d = isoFormatterWithFractional.date(from: trimmed) { return d }
if let d = isoFormatter.date(from: trimmed) { return d }
// Try RFC 2822 (IMAP envelope date format)
if let d = rfc2822Formatter.date(from: trimmed) { return d }
if let d = rfc2822FormatterNoDow.date(from: trimmed) { return d }
return nil
} }
public static func toMessageSummary(_ record: MessageRecord) -> MessageSummary { public static func toMessageSummary(_ record: MessageRecord) -> MessageSummary {

View File

@@ -277,6 +277,9 @@ public final class SyncCoordinator {
) -> MessageRecord { ) -> MessageRecord {
let toJson = encodeAddresses(envelope.to) let toJson = encodeAddresses(envelope.to)
let ccJson = encodeAddresses(envelope.cc) let ccJson = encodeAddresses(envelope.cc)
// Decode RFC 2047 encoded words in subject and sender name
let decodedSubject = envelope.subject.map { RFC2047Decoder.decode($0) }
let decodedFromName = envelope.from?.name.map { RFC2047Decoder.decode($0) }
return MessageRecord( return MessageRecord(
id: UUID().uuidString, id: UUID().uuidString,
accountId: accountId, accountId: accountId,
@@ -285,9 +288,9 @@ public final class SyncCoordinator {
messageId: envelope.messageId, messageId: envelope.messageId,
inReplyTo: envelope.inReplyTo, inReplyTo: envelope.inReplyTo,
refs: envelope.references, refs: envelope.references,
subject: envelope.subject, subject: decodedSubject,
fromAddress: envelope.from?.address, fromAddress: envelope.from?.address,
fromName: envelope.from?.name, fromName: decodedFromName,
toAddresses: toJson, toAddresses: toJson,
ccAddresses: ccJson, ccAddresses: ccJson,
date: envelope.date, date: envelope.date,