Files
MikeMail/MikeMail/MailIdleMonitor.swift
T
2026-11-08 22:00:47 +01:00

107 lines
4.5 KiB
Swift

import Foundation
import Network
import AppKit
@MainActor
final class MailIdleMonitor {
private var tasks: [UUID: Task<Void, Never>] = [:]
private var accounts: [UUID: MailAccount] = [:]
private var pathMonitor: NWPathMonitor?
private var wakeTask: Task<Void, Never>?
private var changed: ((UUID) -> Void)?
private var authenticationFailed: ((UUID) -> Void)?
private var report: ((UUID, String) -> Void)?
func configure(accounts values: [MailAccount], enabled: Bool,
changed: @escaping (UUID) -> Void, report: @escaping (UUID, String) -> Void,
authenticationFailed: @escaping (UUID) -> Void) {
self.authenticationFailed = authenticationFailed
self.changed = changed
self.report = report
let wanted = enabled ? Dictionary(uniqueKeysWithValues: values.map { ($0.id, $0) }) : [:]
for id in Array(tasks.keys) where wanted[id] != accounts[id] {
tasks.removeValue(forKey: id)?.cancel()
}
accounts = wanted
for account in wanted.values where tasks[account.id] == nil { launch(account) }
if !enabled || wanted.isEmpty {
pathMonitor?.cancel()
pathMonitor = nil
wakeTask?.cancel()
wakeTask = nil
} else if pathMonitor == nil {
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { [weak self] path in
guard path.status == .satisfied else { return }
Task { @MainActor [weak self] in self?.restart() }
}
monitor.start(queue: DispatchQueue(label: "MikeMail.IDLE.Network"))
pathMonitor = monitor
wakeTask = Task { [weak self] in
for await _ in NSWorkspace.shared.notificationCenter.notifications(named: NSWorkspace.didWakeNotification) {
guard !Task.isCancelled else { return }
self?.restart()
}
}
}
}
func restart(accountID: UUID? = nil) {
for account in accounts.values where accountID == nil || account.id == accountID {
tasks.removeValue(forKey: account.id)?.cancel()
launch(account)
}
}
private func launch(_ account: MailAccount) {
let changed = self.changed
let report = self.report
let authenticationFailed = self.authenticationFailed
tasks[account.id] = Task {
var retrySeconds = 2
while !Task.isCancelled {
do {
report?(account.id, "Verbindung wird aufgebaut …")
let client = try IMAPClient(account: account)
defer { client.close() }
try await withTaskCancellationHandler {
try Task.checkCancellation()
try await client.login(account: account, password: PasswordStore.password(for: account.id))
guard try await client.capabilities().contains("IDLE") else {
report?(account.id, "Server unterstützt kein IDLE; Intervallabruf bleibt verfügbar.")
return
}
while !Task.isCancelled {
try await client.idleInbox {
retrySeconds = 2
report?(account.id, "Sofortempfang aktiv")
// Reconcile the gap before the listener became ready, also after reconnects.
changed?(account.id)
} onChange: {
changed?(account.id)
}
}
} onCancel: {
client.cancel()
}
} catch {
if Task.isCancelled { return }
if case MailError.authentication = error {
report?(account.id, "Passwort erforderlich")
authenticationFailed?(account.id)
return
}
report?(account.id, "Verbindung unterbrochen; neuer Versuch in \(retrySeconds) s.")
}
do { try await Task.sleep(for: .seconds(retrySeconds)) }
catch { return }
retrySeconds = min(retrySeconds * 2, 60)
}
}
}
}
struct IdleConfiguration: Equatable {
let accounts: [MailAccount]
}