fix remaining code review issues, add HTML email rendering, charset-aware MIME decoding
- add charset-aware string decoding in MIMEParser (supports UTF-8, Latin-1, Windows-1252, etc.) - fix prefetchBodies: remove broken ISO8601 date filter that prevented body fetching - fix ensureBodyLoaded to use fetchFullMessage + MIMEParser instead of broken fetchBody - add N+1 query fix: inboxMessagesExcludingDeferred uses SQL LEFT JOIN instead of per-message deferral check - add inboxMessageCountExcludingDeferred for efficient perspective counts - add unreadMessageCount, totalMessageCount queries to MailStore - wire mailbox unread/total counts in loadMailboxes (were hardcoded to 0) - add flag sync: reconcileFlags fetches flags for existing UIDs, updates local read/flagged state - move account config from UserDefaults to Application Support file, auto-migrate existing config - render HTML emails by default (toggle to plain text), render plain text as HTML for proper Unicode/emoji - replace print() with os_log Logger in SyncCoordinator Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import Foundation
|
||||
import os
|
||||
import Models
|
||||
import IMAPClient
|
||||
import MailStore
|
||||
import MIMEParser
|
||||
import TaskStore
|
||||
|
||||
private let logger = Logger(subsystem: "de.felixfoertsch.MagnumOpus", category: "SyncCoordinator")
|
||||
|
||||
@Observable
|
||||
@MainActor
|
||||
public final class SyncCoordinator {
|
||||
@@ -84,7 +87,7 @@ public final class SyncCoordinator {
|
||||
}
|
||||
} catch {
|
||||
// Resurfacing is non-fatal; log and continue
|
||||
print("[SyncCoordinator] resurfaceDeferrals error: \(error)")
|
||||
logger.warning("[SyncCoordinator] resurfaceDeferrals error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +171,11 @@ public final class SyncCoordinator {
|
||||
emit(.newMessages(count: envelopes.count, mailbox: remoteMailbox.name))
|
||||
}
|
||||
|
||||
// Reconcile flags for existing messages (read/unread state from other devices)
|
||||
if lastUid > 0 {
|
||||
await reconcileFlags(mailboxId: mailboxId, uidRange: 1...lastUid)
|
||||
}
|
||||
|
||||
await prefetchBodies(mailboxId: mailboxId)
|
||||
|
||||
// Detect and store mailbox role from LIST attributes
|
||||
@@ -183,14 +191,30 @@ public final class SyncCoordinator {
|
||||
)
|
||||
}
|
||||
|
||||
/// Reconcile read/flagged state for existing messages with the server.
|
||||
private func reconcileFlags(mailboxId: String, uidRange: ClosedRange<Int>) async {
|
||||
do {
|
||||
let remoteFlagPairs = try await imapClient.fetchFlags(uids: uidRange)
|
||||
let localMessages = try store.messages(mailboxId: mailboxId)
|
||||
let localByUid = Dictionary(localMessages.map { ($0.uid, $0) }, uniquingKeysWith: { first, _ in first })
|
||||
|
||||
for pair in remoteFlagPairs {
|
||||
guard let local = localByUid[pair.uid] else { continue }
|
||||
if local.isRead != pair.isRead || local.isFlagged != pair.isFlagged {
|
||||
try store.updateFlags(messageId: local.id, isRead: pair.isRead, isFlagged: pair.isFlagged)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Flag reconciliation is non-fatal
|
||||
logger.warning("[SyncCoordinator] reconcileFlags error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch full RFC822 messages and parse MIME for body + attachments
|
||||
private func prefetchBodies(mailboxId: String) async {
|
||||
let thirtyDaysAgo = ISO8601DateFormatter().string(
|
||||
from: Calendar.current.date(byAdding: .day, value: -30, to: Date())!
|
||||
)
|
||||
do {
|
||||
let messages = try store.messages(mailboxId: mailboxId)
|
||||
let recent = messages.filter { $0.bodyText == nil && $0.bodyHtml == nil && $0.date >= thirtyDaysAgo }
|
||||
let recent = messages.filter { $0.bodyText == nil && $0.bodyHtml == nil }
|
||||
for message in recent.prefix(50) {
|
||||
guard !Task.isCancelled else { break }
|
||||
let rawMessage = try await imapClient.fetchFullMessage(uid: message.uid)
|
||||
@@ -221,7 +245,7 @@ public final class SyncCoordinator {
|
||||
}
|
||||
} catch {
|
||||
// Background prefetch failure is non-fatal
|
||||
print("[SyncCoordinator] prefetchBodies error: \(error)")
|
||||
logger.warning("[SyncCoordinator] prefetchBodies error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,14 +324,14 @@ public final class SyncCoordinator {
|
||||
/// Must be called after at least one successful sync (so capabilities are cached).
|
||||
public func startIdleMonitoring() async {
|
||||
guard let credentials else {
|
||||
print("[SyncCoordinator] No credentials provided, cannot start IDLE")
|
||||
logger.warning("[SyncCoordinator] No credentials provided, cannot start IDLE")
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let caps = try await imapClient.capabilities()
|
||||
guard caps.contains("IDLE") else {
|
||||
print("[SyncCoordinator] Server does not support IDLE, using periodic sync only")
|
||||
logger.warning("[SyncCoordinator] Server does not support IDLE, using periodic sync only")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -326,7 +350,7 @@ public final class SyncCoordinator {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("[SyncCoordinator] Failed to start IDLE monitoring: \(error)")
|
||||
logger.warning("[SyncCoordinator] Failed to start IDLE monitoring: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user