107 lines
4.5 KiB
Swift
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]
|
|
}
|