appleMailManager.d.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /**
  2. * Apple Mail Manager
  3. *
  4. * Handles all interactions with Apple Mail via AppleScript.
  5. * This is the core service layer for the MCP server.
  6. *
  7. * Architecture:
  8. * - Text escaping is handled by dedicated helper functions
  9. * - AppleScript generation uses template builders for consistency
  10. * - All public methods return typed results (no raw strings)
  11. * - Error handling is consistent across all operations
  12. *
  13. * @module services/appleMailManager
  14. */
  15. import type { Message, MessageContent, Mailbox, Account, Attachment, HealthCheckResult, MailStats, BatchOperationResult, SyncStatus, RecentlyReceivedStats, MailRule, Contact, EmailTemplate, SerialEmailRecipient, SerialEmailResult } from "../types.js";
  16. /**
  17. * Emits AppleScript that builds a date into the variable `varName` from numeric
  18. * components.
  19. *
  20. * This is locale-independent, unlike `date "May 30, 2026"` string coercion,
  21. * which AppleScript parses using the system locale. On a non-English locale
  22. * (e.g. pt_PT) the English month name throws "Invalid date and time (-30720)";
  23. * because the comparison happens inside the per-message `try` in searchMessages,
  24. * that error is swallowed and every message is skipped, so the search returns
  25. * zero results even when matches exist. See issue #15.
  26. *
  27. * `day` is reset to 1 before assigning month/year so an existing day-of-month
  28. * (e.g. 31) cannot overflow into the next month when the month is changed.
  29. *
  30. * Exported for unit testing.
  31. */
  32. export declare function buildAppleScriptDate(varName: string, d: Date): string;
  33. /**
  34. * Manager class for Apple Mail operations.
  35. *
  36. * Provides methods for:
  37. * - Reading and searching messages
  38. * - Sending emails
  39. * - Managing mailboxes
  40. * - Listing accounts
  41. *
  42. * All operations are synchronous since they rely on AppleScript
  43. * execution via osascript. Error handling is consistent: methods
  44. * return null/false/empty-array on failure rather than throwing.
  45. */
  46. export interface SearchConditionFilters {
  47. query?: string;
  48. from?: string;
  49. subject?: string;
  50. isRead?: boolean;
  51. isFlagged?: boolean;
  52. }
  53. /**
  54. * Build the AppleScript `whose` clause for searchMessages from a filter set.
  55. *
  56. * - `query` is a subject-OR-sender substring match, parenthesized so it groups
  57. * correctly when ANDed with other filters.
  58. * - `from` and `subject` are substring matches (`sender`/`subject` contains).
  59. * - `isRead` / `isFlagged` are boolean status checks.
  60. * - Returns "" when no filters are set. Every interpolated value is escaped.
  61. *
  62. * Exported for unit testing: the bug this addresses (filters declared in the
  63. * tool schema but silently dropped) lived in this logic, so it gets direct
  64. * coverage independent of Mail.app.
  65. */
  66. export declare function buildSearchCondition(filters: SearchConditionFilters): string;
  67. export declare class AppleMailManager {
  68. /**
  69. * Default account used when no account is specified.
  70. */
  71. private defaultAccount;
  72. /**
  73. * TTL cache for expensive AppleScript queries that rarely change.
  74. * Caches account list and per-account mailbox names to avoid
  75. * redundant AppleScript roundtrips on every tool call.
  76. */
  77. private cache;
  78. /** Cache TTL in milliseconds (60 seconds). */
  79. private readonly CACHE_TTL_MS;
  80. /**
  81. * Returns cached accounts or fetches fresh data if cache is expired/empty.
  82. */
  83. private getCachedAccounts;
  84. /**
  85. * Returns cached mailbox names for an account, or fetches fresh.
  86. * This caches only the name list used by resolveMailbox(), not the
  87. * full Mailbox objects with counts (which change frequently).
  88. */
  89. private getCachedMailboxNames;
  90. /**
  91. * Invalidate all caches. Call after operations that change
  92. * mailbox structure (create/delete/rename mailbox).
  93. */
  94. private invalidateCache;
  95. /**
  96. * Resolves the account to use for an operation.
  97. * Queries Mail.app's configured default send account, then falls back
  98. * to the first available account.
  99. */
  100. private resolveAccount;
  101. /**
  102. * Resolves a mailbox name to its actual name in the account.
  103. *
  104. * Different account types (IMAP, Exchange, iCloud) use different
  105. * mailbox naming conventions:
  106. * - IMAP/Gmail: "INBOX", "Sent", "Drafts"
  107. * - Exchange: "Inbox", "Sent Items", "Deleted Items"
  108. * - iCloud: "INBOX", "Sent", "Trash"
  109. *
  110. * This method tries to find a matching mailbox by:
  111. * 1. Exact match
  112. * 2. Case-insensitive match
  113. * 3. Known aliases (e.g., "Sent" -> "Sent Items")
  114. *
  115. * @param mailbox - Requested mailbox name
  116. * @param account - Account to search in
  117. * @returns Actual mailbox name, or original if not found
  118. */
  119. private resolveMailbox;
  120. /**
  121. * Search for messages matching criteria.
  122. *
  123. * @param query - Text to search for in subject or sender
  124. * @param mailbox - Mailbox to search in (e.g., "INBOX")
  125. * @param account - Account to search in
  126. * @param limit - Maximum number of results
  127. * @returns Array of matching messages
  128. */
  129. searchMessages(query?: string, mailbox?: string, account?: string, limit?: number, dateFrom?: string, dateTo?: string, from?: string, subject?: string, isRead?: boolean, isFlagged?: boolean): Message[];
  130. /**
  131. * Get a message by ID.
  132. *
  133. * Note: Mail.app message IDs are unique per mailbox. This method searches
  134. * all mailboxes in all accounts to find the message.
  135. */
  136. getMessageById(id: string): Message | null;
  137. /**
  138. * Get the content of a message.
  139. */
  140. getMessageContent(id: string): MessageContent | null;
  141. /**
  142. * Get the raw MIME source of a message.
  143. * Used as fallback for attachment extraction when AppleScript
  144. * mail attachments returns empty.
  145. *
  146. * Timeout is 2x the default (120s) because `source of msg` returns
  147. * the entire raw message including base64-encoded attachments —
  148. * a 20MB attachment can take several seconds over Exchange/IMAP.
  149. */
  150. getRawSource(id: string): string | null;
  151. /**
  152. * List messages in a mailbox.
  153. *
  154. * @param mailbox - Mailbox to list from (default: INBOX)
  155. * @param account - Account to list from
  156. * @param limit - Maximum number of messages
  157. * @returns Array of messages
  158. */
  159. listMessages(mailbox?: string, account?: string, limit?: number, from?: string, offset?: number): Message[];
  160. /**
  161. * Parse message list output from AppleScript.
  162. *
  163. * Two emission schemas, disambiguated by length:
  164. * 7 fields: single-mailbox — ...|hasAtt (mailbox from caller)
  165. * 8 fields: all-mailboxes — ...|mailbox|hasAtt
  166. *
  167. * `hasAttachments` here is the fast-path AppleScript count only; it will
  168. * false-negative for MIME-embedded attachments (a known AppleScript
  169. * limitation). Use getMessage or list-attachments for authoritative info.
  170. */
  171. private parseMessageList;
  172. /**
  173. * Send an email.
  174. *
  175. * @param to - Recipient email addresses
  176. * @param subject - Email subject
  177. * @param body - Email body (plain text)
  178. * @param cc - CC recipients
  179. * @param bcc - BCC recipients
  180. * @param account - Account to send from
  181. * @returns true if sent successfully
  182. */
  183. sendEmail(to: string[], subject: string, body: string, cc?: string[], bcc?: string[], account?: string, attachments?: string[]): boolean;
  184. /**
  185. * Send individual personalized emails to a list of recipients (mail merge).
  186. *
  187. * Replaces {{placeholder}} tokens in subject and body with per-recipient values.
  188. * Each recipient receives their own individual email.
  189. *
  190. * @param recipients - List of recipient objects with email and variable values
  191. * @param subject - Email subject (may contain {{placeholders}})
  192. * @param body - Email body (may contain {{placeholders}})
  193. * @param account - Account to send from
  194. * @param delayMs - Delay between sends in milliseconds (default: 500, max: 10000)
  195. * @returns Array of per-recipient results
  196. */
  197. sendSerialEmail(recipients: SerialEmailRecipient[], subject: string, body: string, account?: string, delayMs?: number): SerialEmailResult[];
  198. /**
  199. * Create a draft email (saved to Drafts folder, not sent).
  200. *
  201. * @param to - Recipient email addresses
  202. * @param subject - Email subject
  203. * @param body - Email body (plain text)
  204. * @param cc - CC recipients
  205. * @param bcc - BCC recipients
  206. * @param account - Account to create draft in
  207. * @returns true if draft created successfully
  208. */
  209. createDraft(to: string[], subject: string, body: string, cc?: string[], bcc?: string[], account?: string, attachments?: string[]): boolean;
  210. /**
  211. * Reply to a message.
  212. *
  213. * @param id - Message ID to reply to
  214. * @param body - Reply body
  215. * @param replyAll - If true, reply to all recipients
  216. * @param send - If true, send immediately; if false, save as draft
  217. * @returns true if reply created/sent successfully
  218. */
  219. replyToMessage(id: string, body: string, replyAll?: boolean, send?: boolean): boolean;
  220. /**
  221. * Forward a message.
  222. *
  223. * @param id - Message ID to forward
  224. * @param to - Recipients to forward to
  225. * @param body - Optional body to prepend
  226. * @param send - If true, send immediately; if false, save as draft
  227. * @returns true if forward created/sent successfully
  228. */
  229. forwardMessage(id: string, to: string[], body?: string, send?: boolean): boolean;
  230. /**
  231. * Helper to find and operate on a message by ID.
  232. */
  233. private findMessageScript;
  234. /**
  235. * Mark a message as read.
  236. */
  237. markAsRead(id: string): boolean;
  238. /**
  239. * Mark a message as unread.
  240. */
  241. markAsUnread(id: string): boolean;
  242. /**
  243. * Flag a message.
  244. */
  245. flagMessage(id: string): boolean;
  246. /**
  247. * Unflag a message.
  248. */
  249. unflagMessage(id: string): boolean;
  250. /**
  251. * Delete a message.
  252. */
  253. deleteMessage(id: string): boolean;
  254. /**
  255. * Move a message to a different mailbox.
  256. */
  257. /**
  258. * Move a message to a destination mailbox, with full nested-mailbox support.
  259. *
  260. * Resolving the destination as `mailbox "X" of account "Y"` only finds
  261. * top-level mailboxes, so nested destinations (e.g. a "Moore" subfolder)
  262. * silently failed. Instead we walk the target account's full mailbox tree and
  263. * match by name. Resolution is:
  264. * - account-scoped (won't move to a same-named mailbox in another account)
  265. * - ambiguity-aware: if the name matches more than one mailbox in the
  266. * account we refuse to guess and return an error — silently moving mail to
  267. * the wrong folder is worse than failing.
  268. * The source message is located by walking every account's tree breadth-first
  269. * (top-level mailboxes like Inbox are checked first), so messages in nested
  270. * mailboxes are found too.
  271. *
  272. * Returns a result object so batch callers can surface the specific failure
  273. * (destination not found / ambiguous / message not found).
  274. */
  275. private moveMessageInternal;
  276. moveMessage(id: string, mailbox: string, account?: string): boolean;
  277. /**
  278. * Delete multiple messages at once.
  279. *
  280. * @param ids - Array of message IDs to delete
  281. * @returns Array of results for each message
  282. */
  283. batchDeleteMessages(ids: string[]): BatchOperationResult[];
  284. /**
  285. * Move multiple messages to a mailbox at once.
  286. *
  287. * @param ids - Array of message IDs to move
  288. * @param mailbox - Destination mailbox name
  289. * @param account - Account containing the destination mailbox
  290. * @returns Array of results for each message
  291. */
  292. batchMoveMessages(ids: string[], mailbox: string, account?: string): BatchOperationResult[];
  293. /**
  294. * Mark multiple messages as read at once.
  295. */
  296. batchMarkAsRead(ids: string[]): BatchOperationResult[];
  297. /**
  298. * Mark multiple messages as unread at once.
  299. */
  300. batchMarkAsUnread(ids: string[]): BatchOperationResult[];
  301. /**
  302. * Flag multiple messages at once.
  303. */
  304. batchFlagMessages(ids: string[]): BatchOperationResult[];
  305. /**
  306. * Unflag multiple messages at once.
  307. */
  308. batchUnflagMessages(ids: string[]): BatchOperationResult[];
  309. /**
  310. * List attachments for a message.
  311. * Tries AppleScript first, falls back to MIME source parsing
  312. * when AppleScript returns empty (known issue across all account types).
  313. */
  314. listAttachments(id: string): Attachment[];
  315. /**
  316. * Save an attachment from a message to disk.
  317. * Tries AppleScript first, falls back to MIME source extraction
  318. * when AppleScript can't find the attachment.
  319. */
  320. saveAttachment(id: string, attachmentName: string, savePath: string): boolean;
  321. /**
  322. * List all mailboxes for an account.
  323. */
  324. listMailboxes(account?: string): Mailbox[];
  325. /**
  326. * Get unread count for a mailbox.
  327. */
  328. getUnreadCount(mailbox?: string, account?: string): number;
  329. /**
  330. * Create a new mailbox.
  331. */
  332. createMailbox(name: string, account?: string): boolean;
  333. /**
  334. * Delete a mailbox.
  335. */
  336. deleteMailbox(name: string, account?: string): boolean;
  337. /**
  338. * Rename a mailbox by creating a new one, moving messages, and deleting the old one.
  339. */
  340. renameMailbox(oldName: string, newName: string, account?: string): boolean;
  341. /**
  342. * List all mail accounts (uses cache).
  343. */
  344. listAccounts(): Account[];
  345. /**
  346. * Fetches account list directly from Mail.app via AppleScript.
  347. * Used internally by the cache; prefer getCachedAccounts() or listAccounts().
  348. */
  349. private fetchAccounts;
  350. /**
  351. * Fetches mailbox names for an account directly from Mail.app.
  352. * Used internally by the cache; prefer getCachedMailboxNames().
  353. */
  354. private fetchMailboxNames;
  355. /**
  356. * List all mail rules.
  357. */
  358. listRules(): MailRule[];
  359. /**
  360. * Enable or disable a mail rule.
  361. */
  362. setRuleEnabled(ruleName: string, enabled: boolean): boolean;
  363. /**
  364. * Search contacts by name or email.
  365. */
  366. searchContacts(query: string): Contact[];
  367. private templates;
  368. private nextTemplateId;
  369. /**
  370. * List all stored templates.
  371. */
  372. listTemplates(): EmailTemplate[];
  373. /**
  374. * Get a template by ID.
  375. */
  376. getTemplate(id: string): EmailTemplate | null;
  377. /**
  378. * Create or update a template.
  379. */
  380. saveTemplate(name: string, subject: string, body: string, to?: string[], cc?: string[], id?: string): EmailTemplate;
  381. /**
  382. * Delete a template.
  383. */
  384. deleteTemplate(id: string): boolean;
  385. /**
  386. * Use a template to create a draft.
  387. */
  388. useTemplate(id: string, overrides?: {
  389. to?: string[];
  390. cc?: string[];
  391. subject?: string;
  392. body?: string;
  393. }): boolean;
  394. /**
  395. * Run health check on Mail.app connectivity.
  396. */
  397. healthCheck(): HealthCheckResult;
  398. /**
  399. * Get mail statistics.
  400. */
  401. getMailStats(): MailStats;
  402. /**
  403. * Get counts of recently received messages.
  404. *
  405. * Only counts messages in INBOX for performance (scanning all mailboxes
  406. * is too slow for large accounts).
  407. *
  408. * @returns Counts of messages received in last 24h, 7d, and 30d
  409. */
  410. getRecentlyReceivedStats(): RecentlyReceivedStats;
  411. /**
  412. * Get sync status for Mail.app.
  413. *
  414. * Checks for sync activity indicators like:
  415. * - Activity monitor status
  416. * - Network activity status
  417. * - Background refresh indicators
  418. *
  419. * @returns Sync status information
  420. */
  421. getSyncStatus(): SyncStatus;
  422. }
  423. //# sourceMappingURL=appleMailManager.d.ts.map