| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- /**
- * Apple Mail Manager
- *
- * Handles all interactions with Apple Mail via AppleScript.
- * This is the core service layer for the MCP server.
- *
- * Architecture:
- * - Text escaping is handled by dedicated helper functions
- * - AppleScript generation uses template builders for consistency
- * - All public methods return typed results (no raw strings)
- * - Error handling is consistent across all operations
- *
- * @module services/appleMailManager
- */
- import type { Message, MessageContent, Mailbox, Account, Attachment, HealthCheckResult, MailStats, BatchOperationResult, SyncStatus, RecentlyReceivedStats, MailRule, Contact, EmailTemplate, SerialEmailRecipient, SerialEmailResult } from "../types.js";
- /**
- * Emits AppleScript that builds a date into the variable `varName` from numeric
- * components.
- *
- * This is locale-independent, unlike `date "May 30, 2026"` string coercion,
- * which AppleScript parses using the system locale. On a non-English locale
- * (e.g. pt_PT) the English month name throws "Invalid date and time (-30720)";
- * because the comparison happens inside the per-message `try` in searchMessages,
- * that error is swallowed and every message is skipped, so the search returns
- * zero results even when matches exist. See issue #15.
- *
- * `day` is reset to 1 before assigning month/year so an existing day-of-month
- * (e.g. 31) cannot overflow into the next month when the month is changed.
- *
- * Exported for unit testing.
- */
- export declare function buildAppleScriptDate(varName: string, d: Date): string;
- /**
- * Manager class for Apple Mail operations.
- *
- * Provides methods for:
- * - Reading and searching messages
- * - Sending emails
- * - Managing mailboxes
- * - Listing accounts
- *
- * All operations are synchronous since they rely on AppleScript
- * execution via osascript. Error handling is consistent: methods
- * return null/false/empty-array on failure rather than throwing.
- */
- export interface SearchConditionFilters {
- query?: string;
- from?: string;
- subject?: string;
- isRead?: boolean;
- isFlagged?: boolean;
- }
- /**
- * Build the AppleScript `whose` clause for searchMessages from a filter set.
- *
- * - `query` is a subject-OR-sender substring match, parenthesized so it groups
- * correctly when ANDed with other filters.
- * - `from` and `subject` are substring matches (`sender`/`subject` contains).
- * - `isRead` / `isFlagged` are boolean status checks.
- * - Returns "" when no filters are set. Every interpolated value is escaped.
- *
- * Exported for unit testing: the bug this addresses (filters declared in the
- * tool schema but silently dropped) lived in this logic, so it gets direct
- * coverage independent of Mail.app.
- */
- export declare function buildSearchCondition(filters: SearchConditionFilters): string;
- export declare class AppleMailManager {
- /**
- * Default account used when no account is specified.
- */
- private defaultAccount;
- /**
- * TTL cache for expensive AppleScript queries that rarely change.
- * Caches account list and per-account mailbox names to avoid
- * redundant AppleScript roundtrips on every tool call.
- */
- private cache;
- /** Cache TTL in milliseconds (60 seconds). */
- private readonly CACHE_TTL_MS;
- /**
- * Returns cached accounts or fetches fresh data if cache is expired/empty.
- */
- private getCachedAccounts;
- /**
- * Returns cached mailbox names for an account, or fetches fresh.
- * This caches only the name list used by resolveMailbox(), not the
- * full Mailbox objects with counts (which change frequently).
- */
- private getCachedMailboxNames;
- /**
- * Invalidate all caches. Call after operations that change
- * mailbox structure (create/delete/rename mailbox).
- */
- private invalidateCache;
- /**
- * Resolves the account to use for an operation.
- * Queries Mail.app's configured default send account, then falls back
- * to the first available account.
- */
- private resolveAccount;
- /**
- * Resolves a mailbox name to its actual name in the account.
- *
- * Different account types (IMAP, Exchange, iCloud) use different
- * mailbox naming conventions:
- * - IMAP/Gmail: "INBOX", "Sent", "Drafts"
- * - Exchange: "Inbox", "Sent Items", "Deleted Items"
- * - iCloud: "INBOX", "Sent", "Trash"
- *
- * This method tries to find a matching mailbox by:
- * 1. Exact match
- * 2. Case-insensitive match
- * 3. Known aliases (e.g., "Sent" -> "Sent Items")
- *
- * @param mailbox - Requested mailbox name
- * @param account - Account to search in
- * @returns Actual mailbox name, or original if not found
- */
- private resolveMailbox;
- /**
- * Search for messages matching criteria.
- *
- * @param query - Text to search for in subject or sender
- * @param mailbox - Mailbox to search in (e.g., "INBOX")
- * @param account - Account to search in
- * @param limit - Maximum number of results
- * @returns Array of matching messages
- */
- searchMessages(query?: string, mailbox?: string, account?: string, limit?: number, dateFrom?: string, dateTo?: string, from?: string, subject?: string, isRead?: boolean, isFlagged?: boolean): Message[];
- /**
- * Get a message by ID.
- *
- * Note: Mail.app message IDs are unique per mailbox. This method searches
- * all mailboxes in all accounts to find the message.
- */
- getMessageById(id: string): Message | null;
- /**
- * Get the content of a message.
- */
- getMessageContent(id: string): MessageContent | null;
- /**
- * Get the raw MIME source of a message.
- * Used as fallback for attachment extraction when AppleScript
- * mail attachments returns empty.
- *
- * Timeout is 2x the default (120s) because `source of msg` returns
- * the entire raw message including base64-encoded attachments —
- * a 20MB attachment can take several seconds over Exchange/IMAP.
- */
- getRawSource(id: string): string | null;
- /**
- * List messages in a mailbox.
- *
- * @param mailbox - Mailbox to list from (default: INBOX)
- * @param account - Account to list from
- * @param limit - Maximum number of messages
- * @returns Array of messages
- */
- listMessages(mailbox?: string, account?: string, limit?: number, from?: string, offset?: number): Message[];
- /**
- * Parse message list output from AppleScript.
- *
- * Two emission schemas, disambiguated by length:
- * 7 fields: single-mailbox — ...|hasAtt (mailbox from caller)
- * 8 fields: all-mailboxes — ...|mailbox|hasAtt
- *
- * `hasAttachments` here is the fast-path AppleScript count only; it will
- * false-negative for MIME-embedded attachments (a known AppleScript
- * limitation). Use getMessage or list-attachments for authoritative info.
- */
- private parseMessageList;
- /**
- * Send an email.
- *
- * @param to - Recipient email addresses
- * @param subject - Email subject
- * @param body - Email body (plain text)
- * @param cc - CC recipients
- * @param bcc - BCC recipients
- * @param account - Account to send from
- * @returns true if sent successfully
- */
- sendEmail(to: string[], subject: string, body: string, cc?: string[], bcc?: string[], account?: string, attachments?: string[]): boolean;
- /**
- * Send individual personalized emails to a list of recipients (mail merge).
- *
- * Replaces {{placeholder}} tokens in subject and body with per-recipient values.
- * Each recipient receives their own individual email.
- *
- * @param recipients - List of recipient objects with email and variable values
- * @param subject - Email subject (may contain {{placeholders}})
- * @param body - Email body (may contain {{placeholders}})
- * @param account - Account to send from
- * @param delayMs - Delay between sends in milliseconds (default: 500, max: 10000)
- * @returns Array of per-recipient results
- */
- sendSerialEmail(recipients: SerialEmailRecipient[], subject: string, body: string, account?: string, delayMs?: number): SerialEmailResult[];
- /**
- * Create a draft email (saved to Drafts folder, not sent).
- *
- * @param to - Recipient email addresses
- * @param subject - Email subject
- * @param body - Email body (plain text)
- * @param cc - CC recipients
- * @param bcc - BCC recipients
- * @param account - Account to create draft in
- * @returns true if draft created successfully
- */
- createDraft(to: string[], subject: string, body: string, cc?: string[], bcc?: string[], account?: string, attachments?: string[]): boolean;
- /**
- * Reply to a message.
- *
- * @param id - Message ID to reply to
- * @param body - Reply body
- * @param replyAll - If true, reply to all recipients
- * @param send - If true, send immediately; if false, save as draft
- * @returns true if reply created/sent successfully
- */
- replyToMessage(id: string, body: string, replyAll?: boolean, send?: boolean): boolean;
- /**
- * Forward a message.
- *
- * @param id - Message ID to forward
- * @param to - Recipients to forward to
- * @param body - Optional body to prepend
- * @param send - If true, send immediately; if false, save as draft
- * @returns true if forward created/sent successfully
- */
- forwardMessage(id: string, to: string[], body?: string, send?: boolean): boolean;
- /**
- * Helper to find and operate on a message by ID.
- */
- private findMessageScript;
- /**
- * Mark a message as read.
- */
- markAsRead(id: string): boolean;
- /**
- * Mark a message as unread.
- */
- markAsUnread(id: string): boolean;
- /**
- * Flag a message.
- */
- flagMessage(id: string): boolean;
- /**
- * Unflag a message.
- */
- unflagMessage(id: string): boolean;
- /**
- * Delete a message.
- */
- deleteMessage(id: string): boolean;
- /**
- * Move a message to a different mailbox.
- */
- /**
- * Move a message to a destination mailbox, with full nested-mailbox support.
- *
- * Resolving the destination as `mailbox "X" of account "Y"` only finds
- * top-level mailboxes, so nested destinations (e.g. a "Moore" subfolder)
- * silently failed. Instead we walk the target account's full mailbox tree and
- * match by name. Resolution is:
- * - account-scoped (won't move to a same-named mailbox in another account)
- * - ambiguity-aware: if the name matches more than one mailbox in the
- * account we refuse to guess and return an error — silently moving mail to
- * the wrong folder is worse than failing.
- * The source message is located by walking every account's tree breadth-first
- * (top-level mailboxes like Inbox are checked first), so messages in nested
- * mailboxes are found too.
- *
- * Returns a result object so batch callers can surface the specific failure
- * (destination not found / ambiguous / message not found).
- */
- private moveMessageInternal;
- moveMessage(id: string, mailbox: string, account?: string): boolean;
- /**
- * Delete multiple messages at once.
- *
- * @param ids - Array of message IDs to delete
- * @returns Array of results for each message
- */
- batchDeleteMessages(ids: string[]): BatchOperationResult[];
- /**
- * Move multiple messages to a mailbox at once.
- *
- * @param ids - Array of message IDs to move
- * @param mailbox - Destination mailbox name
- * @param account - Account containing the destination mailbox
- * @returns Array of results for each message
- */
- batchMoveMessages(ids: string[], mailbox: string, account?: string): BatchOperationResult[];
- /**
- * Mark multiple messages as read at once.
- */
- batchMarkAsRead(ids: string[]): BatchOperationResult[];
- /**
- * Mark multiple messages as unread at once.
- */
- batchMarkAsUnread(ids: string[]): BatchOperationResult[];
- /**
- * Flag multiple messages at once.
- */
- batchFlagMessages(ids: string[]): BatchOperationResult[];
- /**
- * Unflag multiple messages at once.
- */
- batchUnflagMessages(ids: string[]): BatchOperationResult[];
- /**
- * List attachments for a message.
- * Tries AppleScript first, falls back to MIME source parsing
- * when AppleScript returns empty (known issue across all account types).
- */
- listAttachments(id: string): Attachment[];
- /**
- * Save an attachment from a message to disk.
- * Tries AppleScript first, falls back to MIME source extraction
- * when AppleScript can't find the attachment.
- */
- saveAttachment(id: string, attachmentName: string, savePath: string): boolean;
- /**
- * List all mailboxes for an account.
- */
- listMailboxes(account?: string): Mailbox[];
- /**
- * Get unread count for a mailbox.
- */
- getUnreadCount(mailbox?: string, account?: string): number;
- /**
- * Create a new mailbox.
- */
- createMailbox(name: string, account?: string): boolean;
- /**
- * Delete a mailbox.
- */
- deleteMailbox(name: string, account?: string): boolean;
- /**
- * Rename a mailbox by creating a new one, moving messages, and deleting the old one.
- */
- renameMailbox(oldName: string, newName: string, account?: string): boolean;
- /**
- * List all mail accounts (uses cache).
- */
- listAccounts(): Account[];
- /**
- * Fetches account list directly from Mail.app via AppleScript.
- * Used internally by the cache; prefer getCachedAccounts() or listAccounts().
- */
- private fetchAccounts;
- /**
- * Fetches mailbox names for an account directly from Mail.app.
- * Used internally by the cache; prefer getCachedMailboxNames().
- */
- private fetchMailboxNames;
- /**
- * List all mail rules.
- */
- listRules(): MailRule[];
- /**
- * Enable or disable a mail rule.
- */
- setRuleEnabled(ruleName: string, enabled: boolean): boolean;
- /**
- * Search contacts by name or email.
- */
- searchContacts(query: string): Contact[];
- private templates;
- private nextTemplateId;
- /**
- * List all stored templates.
- */
- listTemplates(): EmailTemplate[];
- /**
- * Get a template by ID.
- */
- getTemplate(id: string): EmailTemplate | null;
- /**
- * Create or update a template.
- */
- saveTemplate(name: string, subject: string, body: string, to?: string[], cc?: string[], id?: string): EmailTemplate;
- /**
- * Delete a template.
- */
- deleteTemplate(id: string): boolean;
- /**
- * Use a template to create a draft.
- */
- useTemplate(id: string, overrides?: {
- to?: string[];
- cc?: string[];
- subject?: string;
- body?: string;
- }): boolean;
- /**
- * Run health check on Mail.app connectivity.
- */
- healthCheck(): HealthCheckResult;
- /**
- * Get mail statistics.
- */
- getMailStats(): MailStats;
- /**
- * Get counts of recently received messages.
- *
- * Only counts messages in INBOX for performance (scanning all mailboxes
- * is too slow for large accounts).
- *
- * @returns Counts of messages received in last 24h, 7d, and 30d
- */
- getRecentlyReceivedStats(): RecentlyReceivedStats;
- /**
- * Get sync status for Mail.app.
- *
- * Checks for sync activity indicators like:
- * - Activity monitor status
- * - Network activity status
- * - Background refresh indicators
- *
- * @returns Sync status information
- */
- getSyncStatus(): SyncStatus;
- }
- //# sourceMappingURL=appleMailManager.d.ts.map
|