94 lines
3.7 KiB
Swift
94 lines
3.7 KiB
Swift
import Foundation
|
|
|
|
/// Send an email via Mail.app using AppleScript.
|
|
///
|
|
/// Mail.app was chosen as the send path (vs. direct SMTP) because:
|
|
/// - Mail.app is already authenticated with the Apple ID — no credentials need to be stored.
|
|
/// - The `send` AppleScript command is simple and well-tested on macOS.
|
|
/// - Any outbound message appears in the Sent folder automatically, maintaining a consistent
|
|
/// mail history without additional server-side bookkeeping.
|
|
///
|
|
/// Mail.app must be running and the user must have granted automation access.
|
|
public final class Sender: @unchecked Sendable {
|
|
public static let shared = Sender()
|
|
private init() {}
|
|
|
|
public struct SendRequest {
|
|
public let to: [String]
|
|
public let cc: [String]
|
|
public let bcc: [String]
|
|
public let subject: String
|
|
public let body: String
|
|
public let isHTML: Bool
|
|
|
|
public init(to: [String], cc: [String] = [], bcc: [String] = [],
|
|
subject: String, body: String, isHTML: Bool = false) {
|
|
self.to = to
|
|
self.cc = cc
|
|
self.bcc = bcc
|
|
self.subject = subject
|
|
self.body = body
|
|
self.isHTML = isHTML
|
|
}
|
|
}
|
|
|
|
public struct SendResult {
|
|
public let success: Bool
|
|
public let error: String?
|
|
}
|
|
|
|
/// Send a message via Mail.app AppleScript bridge.
|
|
///
|
|
/// Constructs an AppleScript that:
|
|
/// 1. Creates a new outgoing message.
|
|
/// 2. Adds recipients.
|
|
/// 3. Sets subject + body.
|
|
/// 4. Calls `send`.
|
|
///
|
|
/// Runs synchronously on the calling thread — callers should dispatch to a background queue.
|
|
public func send(_ request: SendRequest) -> SendResult {
|
|
guard !request.to.isEmpty else {
|
|
return SendResult(success: false, error: "No recipients specified")
|
|
}
|
|
|
|
let toLines = request.to.map { "make new to recipient at end of to recipients of theMessage with properties {address:\"\($0)\"}" }
|
|
let ccLines = request.cc.map { "make new cc recipient at end of cc recipients of theMessage with properties {address:\"\($0)\"}" }
|
|
let bccLines = request.bcc.map { "make new bcc recipient at end of bcc recipients of theMessage with properties {address:\"\($0)\"}" }
|
|
|
|
// Escape backslashes and double-quotes in body/subject
|
|
let safeSubject = request.subject
|
|
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
let safeBody = request.body
|
|
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
|
|
let allRecipientLines = (toLines + ccLines + bccLines).joined(separator: "\n ")
|
|
|
|
let script = """
|
|
tell application "Mail"
|
|
set theMessage to make new outgoing message with properties {subject:"\(safeSubject)", content:"\(safeBody)", visible:false}
|
|
tell theMessage
|
|
\(allRecipientLines)
|
|
end tell
|
|
send theMessage
|
|
end tell
|
|
"""
|
|
|
|
var errorDict: NSDictionary?
|
|
guard let appleScript = NSAppleScript(source: script) else {
|
|
return SendResult(success: false, error: "Failed to compile AppleScript")
|
|
}
|
|
|
|
appleScript.executeAndReturnError(&errorDict)
|
|
|
|
if let err = errorDict {
|
|
let msg = err[NSAppleScript.errorMessage] as? String ?? "Unknown AppleScript error"
|
|
NSLog("IMailSender: AppleScript error: \(msg)")
|
|
return SendResult(success: false, error: msg)
|
|
}
|
|
|
|
NSLog("IMailSender: sent '\(request.subject)' to \(request.to.joined(separator: ", "))")
|
|
return SendResult(success: true, error: nil)
|
|
}
|
|
}
|