From 56a5d1d56f84a0204aacff1f6e5a4de9d7c6557d Mon Sep 17 00:00:00 2001 From: Rune Olsen Date: Wed, 19 Aug 2026 14:30:16 +0200 Subject: [PATCH] Trigger the Mail consent prompt via a real Apple Event, not the pre-flight API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rune's log capture proved the theory wrong: AEDeterminePermissionToAutomate- Target(askUserIfNeeded: true) returned -1743 in ~9ms — far too fast for any real dialog to have been shown and answered — and the very next passive status check still reported -1744 (not yet determined), meaning the OS never actually recorded a decision despite the "denied" return. That's the pre-flight API misbehaving, not a threading issue (both previous fixes addressed threading and neither helped). requestAccess() now attempts a real, harmless Apple Event (listAccounts) via the same runHandler path mail_list_accounts/testConnection() already use — sending an actual Apple Event is the standard, proven mechanism for triggering macOS's first-time Automation consent prompt, and this path is already confirmed working (it's what correctly reported "not authorized" in the first live test). --- oAI/Services/AppleMailService.swift | 39 +++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/oAI/Services/AppleMailService.swift b/oAI/Services/AppleMailService.swift index 9b57161..edb1343 100644 --- a/oAI/Services/AppleMailService.swift +++ b/oAI/Services/AppleMailService.swift @@ -79,24 +79,31 @@ final class AppleMailService { /// 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). /// - /// First attempt just called this synchronously from an `async` method, reasoning that - /// `-default-isolation=MainActor` makes it run on the main actor anyway — it didn't work live - /// (button flashed, no dialog, no state change, confirmed still broken even after a - /// `tccutil reset`). Being "MainActor-isolated" inside Swift's structured-concurrency/Task - /// execution model is NOT the same as a classic synchronous AppKit call — this blocking, - /// modal-dialog-presenting legacy C API needs to run from a genuine `DispatchQueue.main.async` - /// dispatch (the same execution shape as a normal button target-action), not from inside a - /// suspended `Task` continuation frame, to correctly nest its own run loop for the prompt. + /// Two earlier attempts used `AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true)` — + /// first called directly (relying on implicit MainActor isolation), then wrapped in + /// `DispatchQueue.main.async`. Neither worked live: logs showed it returning -1743 in ~9ms, + /// far too fast for any real human response, immediately followed by the passive status check + /// (`askUserIfNeeded: false`) still reporting -1744 ("would need to ask") — i.e. the OS never + /// actually recorded a decision despite the "denied" return. That points at + /// `AEDeterminePermissionToAutomateTarget` itself misbehaving in this context (possibly a + /// macOS 27 beta issue — this project has hit other real TCC/permission regressions on this + /// beta before, see CLAUDE.md's Personal Data Tools Apple Feedback note), not a threading bug. + /// + /// Switched strategy entirely: attempt a real, harmless Apple Event via the same `runHandler` + /// path `mail_list_accounts`/`testConnection()` already use — sending an actual Apple Event is + /// the standard, well-proven way macOS shows the first-time "X wants to control Y" Automation + /// prompt, and this exact path is already confirmed working (it's what correctly reported + /// "not authorized" in the very first live test). @discardableResult func requestAccess() async -> Bool { - Log.mail.info("requestAccess: called") - return await withCheckedContinuation { continuation in - DispatchQueue.main.async { - Log.mail.info("requestAccess: about to call AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true)") - let status = Self.checkMailAutomationPermission(askUserIfNeeded: true) - Log.mail.info("requestAccess: AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true) returned \(status)") - continuation.resume(returning: status == 0) - } + Log.mail.info("requestAccess: called — attempting a real Apple Event (listAccounts) to trigger the OS consent prompt") + switch await runHandler("listAccounts", arguments: []) { + case .success: + Log.mail.info("requestAccess: real Apple Event succeeded — access granted") + return true + case .failure(let error): + Log.mail.info("requestAccess: real Apple Event failed — \(String(describing: error))") + return false } }