Trigger the Mail consent prompt via a real Apple Event, not the pre-flight API

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).
This commit is contained in:
2026-08-19 14:30:16 +02:00
parent 56d49d7854
commit 56a5d1d56f
+23 -16
View File
@@ -79,24 +79,31 @@ final class AppleMailService {
/// Triggers the real macOS "Confab wants to control Mail" Automation permission prompt if /// 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). /// 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 /// Two earlier attempts used `AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true)`
/// `-default-isolation=MainActor` makes it run on the main actor anyway it didn't work live /// first called directly (relying on implicit MainActor isolation), then wrapped in
/// (button flashed, no dialog, no state change, confirmed still broken even after a /// `DispatchQueue.main.async`. Neither worked live: logs showed it returning -1743 in ~9ms,
/// `tccutil reset`). Being "MainActor-isolated" inside Swift's structured-concurrency/Task /// far too fast for any real human response, immediately followed by the passive status check
/// execution model is NOT the same as a classic synchronous AppKit call this blocking, /// (`askUserIfNeeded: false`) still reporting -1744 ("would need to ask") i.e. the OS never
/// modal-dialog-presenting legacy C API needs to run from a genuine `DispatchQueue.main.async` /// actually recorded a decision despite the "denied" return. That points at
/// dispatch (the same execution shape as a normal button target-action), not from inside a /// `AEDeterminePermissionToAutomateTarget` itself misbehaving in this context (possibly a
/// suspended `Task` continuation frame, to correctly nest its own run loop for the prompt. /// 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 @discardableResult
func requestAccess() async -> Bool { func requestAccess() async -> Bool {
Log.mail.info("requestAccess: called") Log.mail.info("requestAccess: called — attempting a real Apple Event (listAccounts) to trigger the OS consent prompt")
return await withCheckedContinuation { continuation in switch await runHandler("listAccounts", arguments: []) {
DispatchQueue.main.async { case .success:
Log.mail.info("requestAccess: about to call AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true)") Log.mail.info("requestAccess: real Apple Event succeeded — access granted")
let status = Self.checkMailAutomationPermission(askUserIfNeeded: true) return true
Log.mail.info("requestAccess: AEDeterminePermissionToAutomateTarget(askUserIfNeeded: true) returned \(status)") case .failure(let error):
continuation.resume(returning: status == 0) Log.mail.info("requestAccess: real Apple Event failed — \(String(describing: error))")
} return false
} }
} }