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
/// 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
}
}