Add a real permission prompt/status for Mail access, matching Personal Data

Settings previously only surfaced an error message after a failed Test
Connection attempt — there was no way to trigger the actual macOS
Automation permission dialog or see live grant/deny status, unlike
Calendar/Contacts/Reminders/Location.

AEDeterminePermissionToAutomateTarget (askUserIfNeeded: false/true) turns
out to provide exactly that: a non-prompting status check and an explicit
prompt-and-wait call, mirroring EKEventStore.authorizationStatus(for:)/
requestFullAccessToEvents(). Mail's Settings row now uses the same
personalDataRow component as Calendar/Contacts — live status badge plus
a real "Request Access" button — instead of a one-off Test-Connection-only
UI. Test Connection stays as a secondary functional check.
This commit is contained in:
2026-08-19 14:01:29 +02:00
parent cf82720b88
commit 21d598d88a
3 changed files with 75 additions and 5 deletions
+45
View File
@@ -53,6 +53,51 @@ final class AppleMailService {
// through one queue, mirroring MCPService.runBashCommand's off-main-actor pattern.
private let queue = DispatchQueue(label: "com.oai.applemail", qos: .userInitiated)
// MARK: - Automation Permission (Settings UI status badge + Request Access button)
/// Live tri-state permission check via `AEDeterminePermissionToAutomateTarget`, which unlike
/// sending an actual Apple Event can query Automation permission for Mail.app without
/// triggering any script execution or the system prompt (`askUserIfNeeded: false`). This is
/// the Apple-Events equivalent of `EKEventStore.authorizationStatus(for:)`, reusing the same
/// `PersonalDataAccessState` enum EventKitService/ContactsService/LocationMapsService use.
var accessState: PersonalDataAccessState {
Self.mapAutomationPermissionStatus(Self.checkMailAutomationPermission(askUserIfNeeded: false))
}
/// Pure mapping from an `AEDeterminePermissionToAutomateTarget` status code to the shared
/// tri-state enum, kept separate from the live AE call for testability.
nonisolated static func mapAutomationPermissionStatus(_ status: OSStatus) -> PersonalDataAccessState {
switch status {
case 0: return .granted // noErr
case -1743: return .denied // errAEEventNotPermitted
default: return .notDetermined // errAEEventWouldRequireUserConsent (-1744) and others
}
}
/// Triggers the real macOS "Confab wants to control Mail" Automation permission prompt if
/// the user hasn't been asked yet (a no-op if already granted or denied). Blocks until the
/// user responds, so this must run off the main thread.
@discardableResult
func requestAccess() async -> Bool {
await withCheckedContinuation { continuation in
queue.async {
let status = Self.checkMailAutomationPermission(askUserIfNeeded: true)
continuation.resume(returning: status == 0)
}
}
}
private static func checkMailAutomationPermission(askUserIfNeeded: Bool) -> OSStatus {
let bundleID = "com.apple.mail"
var target = AEAddressDesc()
let createStatus = bundleID.withCString { cString in
AECreateDesc(typeApplicationBundleID, cString, bundleID.utf8.count, &target)
}
guard createStatus == 0 else { return OSStatus(createStatus) }
defer { AEDisposeDesc(&target) }
return AEDeterminePermissionToAutomateTarget(&target, typeWildCard, typeWildCard, askUserIfNeeded)
}
@ObservationIgnored
private lazy var compiledScript: NSAppleScript? = {
guard let script = NSAppleScript(source: Self.scriptSource) else { return nil }