chrischall/swift-mail-automation
Swift library for driving Apple Mail.app via AppleScript + Spotlight
Install
// Package.swift
dependencies: [
.package(url: "https://github.com/chrischall/swift-mail-automation.git", from: "1.0.0"),
]Quickstart
import MailAutomation
let mail = MailService(runner: NSAppleScriptRunner())
// Discover what's configured in Mail.app
let accounts = try await mail.listAccounts()
// ["iCloud", "Google", "Work"]
let iCloudBoxes = try await mail.listMailboxes(account: "iCloud")
// ["INBOX", "Sent", "Drafts", ...]
// Unread mail (across all accounts, or scoped)
let unread = try await mail.getUnread(limit: 10)
let workUnread = try await mail.getUnread(limit: 10, account: "Work")
// Search: tries Spotlight first (sub-second when indexed), falls back
// to AppleScript whose() with a date bound. Scoping by account/mailbox
// forces the AppleScript path since Spotlight doesn't know about
// Mail's account grouping.
let invoices = try await mail.search(
query: "invoice",
limit: 5,
sinceDaysAgo: 90
)
// Send — with optional cc/bcc, file-backed body so multi-line +
// quotes don't need escaping.
try await mail.send(
to: "alice@example.com",
subject: "Hi",
body: "Multi-line\nbody\n\"with quotes\"",
cc: "bob@example.com"
)API reference
MailService
The main entry point. Construct once, reuse across calls. All methods are async and throw AppleScriptError or MailServiceError.
| Method | Purpose | |---|---| | listAccounts() -> [String] | Discover Mail account names ("iCloud", "Google", …) | | listMailboxes(account:) -> [String] | Mailboxes inside an account | | getUnread(limit:account:) -> [EmailMessage] | Unread messages, optionally scoped | | search(query:limit:account:mailbox:sinceDaysAgo:forceBackend:) -> [EmailMessage] | Subject+body search, Spotlight-first with AppleScript fallback | | send(to:subject:body:cc:bcc:) | Compose + send a new message |
SearchBackend
search(forceBackend:) accepts:
.spotlight— forces Spotlight regardless of scoping.applescript— forces the AppleScript path (slower but knows read-state and mailbox)nil(default) — Spotlight when unscoped and indexed; AppleScript otherwise
EmailMessage
Value type returned from search / unread:
subject: String
sender: String // "Name <addr@example.com>"
dateSent: String // AppleScript date string
content: String // truncated body preview
isRead: Bool // false for unread; always true for Spotlight hits
mailbox: String // "account — mailbox"AppleScriptRunner / NSAppleScriptRunner
Protocol + production impl. Inject a fake in unit tests (see below).
Capabilities and limits
Supported:
- List accounts + mailboxes
- Read unread mail (across accounts or scoped)
- Subject + body search, date-bounded, with Spotlight fast path
- Send with optional cc/bcc, multi-line bodies, quoted content
Limits:
- Spotlight search requires
~/Library/Mailto be indexed by the
system. Check System Settings → Siri & Spotlight → Search Privacy if results are empty.
- AppleScript-backed operations are slow on Gmail-heavy setups —
Mail.app iterates each label as a separate mailbox. Scope with account + mailbox when possible.
- No IMAP/SMTP — this is a local Mail.app wrapper, not a mail
client library. For that, use MailCore2.
- Spotlight results always report
isRead == truebecause Spotlight
doesn't index read state.
Permissions
The calling process needs:
- Automation for Mail (System Settings → Privacy & Security →
Automation → Your binary → Mail)
- For Spotlight search: Full Disk Access on the calling binary
so mdfind can read the Mail index.
On macOS 14+ your binary's Info.plist should declare NSAppleEventsUsageDescription and, if you intend to use Spotlight, ensure it's granted FDA.
Testing
AppleScriptRunner is a protocol, so you can inject a fake in unit tests:
import MailAutomation
final class MyFakeRunner: AppleScriptRunner {
var response = ""
func run(source: String) async throws -> String { response }
}
let mail = MailService(runner: MyFakeRunner())License
MIT. See LICENSE.
Package Metadata
Repository: chrischall/swift-mail-automation
Stars: 1
Forks: 0
Open issues: 0
Default branch: main
Primary language: swift
License: MIT
README: README.md