macsync/@packages/imail/Sources/IMailSync/Sender.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)
}
}