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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user