icloud-mcp 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"sessionId":"583b9a47-29dc-4265-b2f0-dc31dbbd06c3","pid":81825,"acquiredAt":1773517693304}
package/index.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  import { logRead, logWrite, logClear } from './lib/session.js';
21
21
  import { composeEmail, replyToEmail, forwardEmail, saveDraft } from './lib/smtp.js';
22
22
  import { listContacts, searchContacts, getContact, createContact, updateContact, deleteContact } from './lib/carddav.js';
23
+ import { getDigestState, updateDigestState } from './lib/digest.js';
23
24
  import { formatEmailForExtraction } from './lib/event-extractor.js';
24
25
  import { listCalendars, listEvents, getEvent, createEvent, updateEvent, deleteEvent, searchEvents } from './lib/caldav.js';
25
26
 
@@ -35,11 +36,59 @@ if (!IMAP_USER || !IMAP_PASSWORD) {
35
36
  }
36
37
  }
37
38
 
39
+ // ─── Multi-account support ────────────────────────────────────────────────────
40
+ // Configure additional accounts via numbered env vars:
41
+ // IMAP_ACCOUNT_1_USER, IMAP_ACCOUNT_1_PASSWORD, IMAP_ACCOUNT_1_HOST,
42
+ // IMAP_ACCOUNT_1_SMTP_HOST, IMAP_ACCOUNT_1_NAME
43
+ // Falls back to IMAP_USER / IMAP_PASSWORD (iCloud) when no numbered accounts set.
44
+
45
+ function parseAccounts() {
46
+ const accounts = {};
47
+ for (let i = 1; i <= 10; i++) {
48
+ const user = process.env[`IMAP_ACCOUNT_${i}_USER`];
49
+ const pass = process.env[`IMAP_ACCOUNT_${i}_PASSWORD`];
50
+ if (!user || !pass) continue;
51
+ const host = process.env[`IMAP_ACCOUNT_${i}_HOST`] || 'imap.mail.me.com';
52
+ const smtpHost = process.env[`IMAP_ACCOUNT_${i}_SMTP_HOST`] || 'smtp.mail.me.com';
53
+ const name = process.env[`IMAP_ACCOUNT_${i}_NAME`] || `account${i}`;
54
+ accounts[name] = { user, pass, host, smtpHost };
55
+ }
56
+ // Legacy fallback — single iCloud account via IMAP_USER / IMAP_PASSWORD
57
+ if (Object.keys(accounts).length === 0 && IMAP_USER) {
58
+ accounts['icloud'] = { user: IMAP_USER, pass: IMAP_PASSWORD, host: 'imap.mail.me.com', smtpHost: 'smtp.mail.me.com' };
59
+ }
60
+ return accounts;
61
+ }
62
+
63
+ const ACCOUNTS = parseAccounts();
64
+ const DEFAULT_ACCOUNT = Object.keys(ACCOUNTS)[0] || 'icloud';
65
+
66
+ function resolveCreds(account) {
67
+ const name = account || DEFAULT_ACCOUNT;
68
+ const creds = ACCOUNTS[name];
69
+ if (!creds) throw new Error(`Account '${name}' not configured. Available: ${Object.keys(ACCOUNTS).join(', ') || 'none'}`);
70
+ return creds;
71
+ }
72
+
73
+ // Gmail uses different folder names than iCloud for system folders.
74
+ const GMAIL_MAILBOX_MAP = {
75
+ 'Sent Messages': '[Gmail]/Sent Mail',
76
+ 'Archive': '[Gmail]/All Mail',
77
+ 'Deleted Messages':'[Gmail]/Trash',
78
+ 'Junk': '[Gmail]/Spam',
79
+ 'Drafts': '[Gmail]/Drafts',
80
+ };
81
+
82
+ function resolveMailbox(name, creds) {
83
+ if (!name || creds?.host !== 'imap.gmail.com') return name;
84
+ return GMAIL_MAILBOX_MAP[name] || name;
85
+ }
86
+
38
87
  // ─── MCP Server ───────────────────────────────────────────────────────────────
39
88
 
40
89
  async function main() {
41
90
  const server = new Server(
42
- { name: 'icloud-mail', version: '2.3.0' },
91
+ { name: 'icloud-mail', version: '2.5.0' },
43
92
  { capabilities: { tools: {} } }
44
93
  );
45
94
 
@@ -53,15 +102,22 @@ async function main() {
53
102
  flagged: { type: 'boolean', description: 'True for flagged only, false for unflagged only' },
54
103
  larger: { type: 'number', description: 'Only emails larger than this size in KB' },
55
104
  smaller: { type: 'number', description: 'Only emails smaller than this size in KB' },
56
- hasAttachment: { type: 'boolean', description: 'Only emails with attachments (client-side BODYSTRUCTURE scan — must be combined with other filters that narrow results to under 500 emails first)' }
105
+ hasAttachment: { type: 'boolean', description: 'Only emails with attachments (client-side BODYSTRUCTURE scan — must be combined with other filters that narrow results to under 500 emails first)' },
106
+ account: { type: 'string', description: "Account name to use (e.g. 'icloud', 'gmail'). Defaults to first configured account. Use list_accounts to see available accounts." }
57
107
  };
108
+ const accountSchema = filtersSchema.account;
58
109
 
59
110
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
60
111
  tools: [
112
+ {
113
+ name: 'list_accounts',
114
+ description: 'List all configured email accounts (names and IMAP hosts). Use the account name in any mail tool\'s account parameter.',
115
+ inputSchema: { type: 'object', properties: {} }
116
+ },
61
117
  {
62
118
  name: 'get_inbox_summary',
63
119
  description: 'Get a summary of a mailbox including total, unread, and recent email counts',
64
- inputSchema: { type: 'object', properties: { mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' } } }
120
+ inputSchema: { type: 'object', properties: { mailbox: { type: 'string', description: 'Mailbox name (default INBOX)' }, account: accountSchema } }
65
121
  },
66
122
  {
67
123
  name: 'get_mailbox_summary',
@@ -677,6 +733,25 @@ async function main() {
677
733
  required: ['to', 'subject']
678
734
  }
679
735
  },
736
+ // ── Digest State ──
737
+ {
738
+ name: 'get_digest_state',
739
+ description: 'Get the current inbox digest state — last run timestamp, processed email UIDs (to skip on next run), pending actions, and per-sender skip counts for smart unsubscribe.',
740
+ inputSchema: { type: 'object', properties: {} }
741
+ },
742
+ {
743
+ name: 'update_digest_state',
744
+ description: 'Update the digest state after a run. Merges new processed UIDs into the existing list, updates lastRun, replaces pendingActions, and accumulates per-sender skip counts.',
745
+ inputSchema: {
746
+ type: 'object',
747
+ properties: {
748
+ lastRun: { type: 'string', description: 'ISO timestamp of this run' },
749
+ processedUids: { type: 'array', items: { type: 'number' }, description: 'Email UIDs processed in this run — merged with existing and capped at 5000' },
750
+ pendingActions: { type: 'array', description: 'Full replacement list of pending action items to track across runs (deadlines, waiting-for-reply, etc.). Each item: { type, subject, to/from, dueDate?, notes? }' },
751
+ skipCounts: { type: 'object', description: 'Map of sender address to skip count increment for this run, e.g. { "bestbuy@email.bestbuy.com": 3 }. Accumulated across runs for smart unsubscribe.' }
752
+ }
753
+ }
754
+ },
680
755
  // ── CardDAV / Contacts ──
681
756
  {
682
757
  name: 'list_contacts',
@@ -886,94 +961,134 @@ async function main() {
886
961
  const { name, arguments: args } = request.params;
887
962
  try {
888
963
  let result;
964
+ // ── Account listing (no creds needed) ──
965
+ if (name === 'list_accounts') {
966
+ result = Object.entries(ACCOUNTS).map(([n, c]) => ({ name: n, host: c.host, smtpHost: c.smtpHost }));
889
967
  // ── Metadata tier (15s) ──
890
- if (name === 'get_inbox_summary') {
891
- result = await withTimeout('get_inbox_summary', TIMEOUT.METADATA, () => getInboxSummary(args.mailbox || 'INBOX'));
968
+ } else if (name === 'get_inbox_summary') {
969
+ const creds = resolveCreds(args.account);
970
+ result = await withTimeout('get_inbox_summary', TIMEOUT.METADATA, () => getInboxSummary(args.mailbox || 'INBOX', creds));
892
971
  } else if (name === 'get_mailbox_summary') {
893
- result = await withTimeout('get_mailbox_summary', TIMEOUT.METADATA, () => getMailboxSummary(args.mailbox));
972
+ const creds = resolveCreds(args.account);
973
+ result = await withTimeout('get_mailbox_summary', TIMEOUT.METADATA, () => getMailboxSummary(resolveMailbox(args.mailbox, creds), creds));
894
974
  } else if (name === 'count_emails') {
895
- const { mailbox, ...filters } = args;
896
- result = await withTimeout('count_emails', TIMEOUT.METADATA, () => countEmails(filters, mailbox || 'INBOX'));
975
+ const { mailbox, account, ...filters } = args;
976
+ const creds = resolveCreds(account);
977
+ result = await withTimeout('count_emails', TIMEOUT.METADATA, () => countEmails(filters, mailbox || 'INBOX', creds));
897
978
  } else if (name === 'list_mailboxes') {
898
- result = await withTimeout('list_mailboxes', TIMEOUT.METADATA, () => listMailboxes());
979
+ const creds = resolveCreds(args.account);
980
+ result = await withTimeout('list_mailboxes', TIMEOUT.METADATA, () => listMailboxes(creds));
899
981
  } else if (name === 'create_mailbox') {
900
- result = await withTimeout('create_mailbox', TIMEOUT.METADATA, () => createMailbox(args.name));
982
+ const creds = resolveCreds(args.account);
983
+ result = await withTimeout('create_mailbox', TIMEOUT.METADATA, () => createMailbox(args.name, creds));
901
984
  } else if (name === 'rename_mailbox') {
902
- result = await renameMailbox(args.oldName, args.newName); // already has its own 15s timeout
985
+ const creds = resolveCreds(args.account);
986
+ result = await renameMailbox(args.oldName, args.newName, creds); // already has its own 15s timeout
903
987
  } else if (name === 'delete_mailbox') {
904
- result = await deleteMailbox(args.name); // already has its own 15s timeout
988
+ const creds = resolveCreds(args.account);
989
+ result = await deleteMailbox(args.name, creds); // already has its own 15s timeout
905
990
  // ── Fetch tier (30s) ──
906
991
  } else if (name === 'read_inbox') {
907
- result = await withTimeout('read_inbox', TIMEOUT.FETCH, () => fetchEmails(args.mailbox || 'INBOX', args.limit || 10, args.onlyUnread || false, args.page || 1));
992
+ const creds = resolveCreds(args.account);
993
+ result = await withTimeout('read_inbox', TIMEOUT.FETCH, () => fetchEmails(args.mailbox || 'INBOX', args.limit || 10, args.onlyUnread || false, args.page || 1, creds));
908
994
  } else if (name === 'get_email') {
909
- result = await withTimeout('get_email', TIMEOUT.FETCH, () => getEmailContent(args.uid, args.mailbox || 'INBOX', args.maxChars || 8000, args.includeHeaders || false));
995
+ const creds = resolveCreds(args.account);
996
+ result = await withTimeout('get_email', TIMEOUT.FETCH, () => getEmailContent(args.uid, args.mailbox || 'INBOX', args.maxChars || 8000, args.includeHeaders || false, creds));
910
997
  } else if (name === 'list_attachments') {
911
- result = await withTimeout('list_attachments', TIMEOUT.FETCH, () => listAttachments(args.uid, args.mailbox || 'INBOX'));
998
+ const creds = resolveCreds(args.account);
999
+ result = await withTimeout('list_attachments', TIMEOUT.FETCH, () => listAttachments(args.uid, args.mailbox || 'INBOX', creds));
912
1000
  } else if (name === 'get_attachment') {
913
- result = await withTimeout('get_attachment', TIMEOUT.FETCH, () => getAttachment(args.uid, args.partId, args.mailbox || 'INBOX', args.offset ?? null, args.length ?? null));
1001
+ const creds = resolveCreds(args.account);
1002
+ result = await withTimeout('get_attachment', TIMEOUT.FETCH, () => getAttachment(args.uid, args.partId, args.mailbox || 'INBOX', args.offset ?? null, args.length ?? null, creds));
914
1003
  } else if (name === 'get_unsubscribe_info') {
915
- result = await withTimeout('get_unsubscribe_info', TIMEOUT.FETCH, () => getUnsubscribeInfo(args.uid, args.mailbox || 'INBOX'));
1004
+ const creds = resolveCreds(args.account);
1005
+ result = await withTimeout('get_unsubscribe_info', TIMEOUT.FETCH, () => getUnsubscribeInfo(args.uid, args.mailbox || 'INBOX', creds));
916
1006
  } else if (name === 'get_email_raw') {
917
- result = await withTimeout('get_email_raw', TIMEOUT.FETCH, () => getEmailRaw(args.uid, args.mailbox || 'INBOX'));
1007
+ const creds = resolveCreds(args.account);
1008
+ result = await withTimeout('get_email_raw', TIMEOUT.FETCH, () => getEmailRaw(args.uid, args.mailbox || 'INBOX', creds));
918
1009
  } else if (name === 'get_thread') {
919
- result = await withTimeout('get_thread', TIMEOUT.FETCH, () => getThread(args.uid, args.mailbox || 'INBOX'));
1010
+ const creds = resolveCreds(args.account);
1011
+ result = await withTimeout('get_thread', TIMEOUT.FETCH, () => getThread(args.uid, args.mailbox || 'INBOX', creds));
920
1012
  } else if (name === 'search_emails') {
921
- const { query, mailbox, limit, queryMode, subjectQuery, bodyQuery, fromQuery, includeSnippet, ...filters } = args;
922
- result = await withTimeout('search_emails', TIMEOUT.FETCH, () => searchEmails(query, mailbox || 'INBOX', limit || 10, filters, { queryMode, subjectQuery, bodyQuery, fromQuery, includeSnippet }));
1013
+ const { query, mailbox, limit, queryMode, subjectQuery, bodyQuery, fromQuery, includeSnippet, account, ...filters } = args;
1014
+ const creds = resolveCreds(account);
1015
+ result = await withTimeout('search_emails', TIMEOUT.FETCH, () => searchEmails(query, mailbox || 'INBOX', limit || 10, filters, { queryMode, subjectQuery, bodyQuery, fromQuery, includeSnippet }, creds));
923
1016
  } else if (name === 'get_emails_by_sender') {
924
- result = await withTimeout('get_emails_by_sender', TIMEOUT.FETCH, () => getEmailsBySender(args.sender, args.mailbox || 'INBOX', args.limit || 10));
1017
+ const creds = resolveCreds(args.account);
1018
+ result = await withTimeout('get_emails_by_sender', TIMEOUT.FETCH, () => getEmailsBySender(args.sender, args.mailbox || 'INBOX', args.limit || 10, creds));
925
1019
  } else if (name === 'get_emails_by_date_range') {
926
- result = await withTimeout('get_emails_by_date_range', TIMEOUT.FETCH, () => getEmailsByDateRange(args.startDate, args.endDate, args.mailbox || 'INBOX', args.limit || 10));
1020
+ const creds = resolveCreds(args.account);
1021
+ result = await withTimeout('get_emails_by_date_range', TIMEOUT.FETCH, () => getEmailsByDateRange(args.startDate, args.endDate, args.mailbox || 'INBOX', args.limit || 10, creds));
927
1022
  // ── Scan tier (60s) ──
928
1023
  } else if (name === 'get_top_senders') {
929
- result = await withTimeout('get_top_senders', TIMEOUT.SCAN, () => getTopSenders(args.mailbox || 'INBOX', args.sampleSize || 500, args.maxResults || 20));
1024
+ const creds = resolveCreds(args.account);
1025
+ result = await withTimeout('get_top_senders', TIMEOUT.SCAN, () => getTopSenders(args.mailbox || 'INBOX', args.sampleSize || 500, args.maxResults || 20, creds));
930
1026
  } else if (name === 'get_unread_senders') {
931
- result = await withTimeout('get_unread_senders', TIMEOUT.SCAN, () => getUnreadSenders(args.mailbox || 'INBOX', args.sampleSize || 500, args.maxResults || 20));
1027
+ const creds = resolveCreds(args.account);
1028
+ result = await withTimeout('get_unread_senders', TIMEOUT.SCAN, () => getUnreadSenders(args.mailbox || 'INBOX', args.sampleSize || 500, args.maxResults || 20, creds));
932
1029
  } else if (name === 'get_storage_report') {
933
- result = await withTimeout('get_storage_report', TIMEOUT.SCAN, () => getStorageReport(args.mailbox || 'INBOX', args.sampleSize || 100));
1030
+ const creds = resolveCreds(args.account);
1031
+ result = await withTimeout('get_storage_report', TIMEOUT.SCAN, () => getStorageReport(args.mailbox || 'INBOX', args.sampleSize || 100, creds));
934
1032
  // ── Bulk operation tier (60s) ──
935
1033
  } else if (name === 'bulk_delete_by_sender') {
936
- result = await withTimeout('bulk_delete_by_sender', TIMEOUT.BULK_OP, () => bulkDeleteBySender(args.sender, args.mailbox || 'INBOX'));
1034
+ const creds = resolveCreds(args.account);
1035
+ result = await withTimeout('bulk_delete_by_sender', TIMEOUT.BULK_OP, () => bulkDeleteBySender(args.sender, args.mailbox || 'INBOX', creds));
937
1036
  } else if (name === 'bulk_delete_by_subject') {
938
- result = await withTimeout('bulk_delete_by_subject', TIMEOUT.BULK_OP, () => bulkDeleteBySubject(args.subject, args.mailbox || 'INBOX'));
1037
+ const creds = resolveCreds(args.account);
1038
+ result = await withTimeout('bulk_delete_by_subject', TIMEOUT.BULK_OP, () => bulkDeleteBySubject(args.subject, args.mailbox || 'INBOX', creds));
939
1039
  } else if (name === 'bulk_mark_read') {
940
- result = await withTimeout('bulk_mark_read', TIMEOUT.BULK_OP, () => bulkMarkRead(args.mailbox || 'INBOX', args.sender || null));
1040
+ const creds = resolveCreds(args.account);
1041
+ result = await withTimeout('bulk_mark_read', TIMEOUT.BULK_OP, () => bulkMarkRead(args.mailbox || 'INBOX', args.sender || null, creds));
941
1042
  } else if (name === 'bulk_mark_unread') {
942
- result = await withTimeout('bulk_mark_unread', TIMEOUT.BULK_OP, () => bulkMarkUnread(args.mailbox || 'INBOX', args.sender || null));
1043
+ const creds = resolveCreds(args.account);
1044
+ result = await withTimeout('bulk_mark_unread', TIMEOUT.BULK_OP, () => bulkMarkUnread(args.mailbox || 'INBOX', args.sender || null, creds));
943
1045
  } else if (name === 'bulk_flag') {
944
- const { flagged, mailbox, ...filters } = args;
945
- result = await withTimeout('bulk_flag', TIMEOUT.BULK_OP, () => bulkFlag(filters, flagged, mailbox || 'INBOX'));
1046
+ const { flagged, mailbox, account, ...filters } = args;
1047
+ const creds = resolveCreds(account);
1048
+ result = await withTimeout('bulk_flag', TIMEOUT.BULK_OP, () => bulkFlag(filters, flagged, mailbox || 'INBOX', creds));
946
1049
  } else if (name === 'mark_older_than_read') {
947
- result = await withTimeout('mark_older_than_read', TIMEOUT.BULK_OP, () => markOlderThanRead(args.days, args.mailbox || 'INBOX'));
1050
+ const creds = resolveCreds(args.account);
1051
+ result = await withTimeout('mark_older_than_read', TIMEOUT.BULK_OP, () => markOlderThanRead(args.days, args.mailbox || 'INBOX', creds));
948
1052
  } else if (name === 'bulk_flag_by_sender') {
949
- result = await withTimeout('bulk_flag_by_sender', TIMEOUT.BULK_OP, () => bulkFlagBySender(args.sender, args.flagged, args.mailbox || 'INBOX'));
1053
+ const creds = resolveCreds(args.account);
1054
+ result = await withTimeout('bulk_flag_by_sender', TIMEOUT.BULK_OP, () => bulkFlagBySender(args.sender, args.flagged, args.mailbox || 'INBOX', creds));
950
1055
  } else if (name === 'delete_older_than') {
951
- result = await withTimeout('delete_older_than', TIMEOUT.BULK_OP, () => deleteOlderThan(args.days, args.mailbox || 'INBOX'));
1056
+ const creds = resolveCreds(args.account);
1057
+ result = await withTimeout('delete_older_than', TIMEOUT.BULK_OP, () => deleteOlderThan(args.days, args.mailbox || 'INBOX', creds));
952
1058
  } else if (name === 'empty_trash') {
953
- result = await withTimeout('empty_trash', TIMEOUT.BULK_OP, () => emptyTrash(args.dryRun || false));
1059
+ const creds = resolveCreds(args.account);
1060
+ result = await withTimeout('empty_trash', TIMEOUT.BULK_OP, () => emptyTrash(args.dryRun || false, creds));
954
1061
  // ── No top-level timeout — chunked with internal timeouts ──
955
1062
  } else if (name === 'bulk_move') {
956
- const { targetMailbox, sourceMailbox, dryRun, limit, ...filters } = args;
957
- result = await bulkMove(filters, targetMailbox, sourceMailbox || 'INBOX', dryRun || false, limit ?? null);
1063
+ const { targetMailbox, sourceMailbox, dryRun, limit, account, ...filters } = args;
1064
+ const creds = resolveCreds(account);
1065
+ result = await bulkMove(filters, resolveMailbox(targetMailbox, creds), sourceMailbox || 'INBOX', dryRun || false, limit ?? null, creds);
958
1066
  } else if (name === 'bulk_move_by_sender') {
959
- result = await bulkMoveBySender(args.sender, args.targetMailbox, args.sourceMailbox || 'INBOX', args.dryRun || false);
1067
+ const creds = resolveCreds(args.account);
1068
+ result = await bulkMoveBySender(args.sender, resolveMailbox(args.targetMailbox, creds), args.sourceMailbox || 'INBOX', args.dryRun || false, creds);
960
1069
  } else if (name === 'bulk_move_by_domain') {
961
- result = await bulkMoveByDomain(args.domain, args.targetMailbox, args.sourceMailbox || 'INBOX', args.dryRun || false);
1070
+ const creds = resolveCreds(args.account);
1071
+ result = await bulkMoveByDomain(args.domain, resolveMailbox(args.targetMailbox, creds), args.sourceMailbox || 'INBOX', args.dryRun || false, creds);
962
1072
  } else if (name === 'archive_older_than') {
963
- result = await archiveOlderThan(args.days, args.targetMailbox, args.sourceMailbox || 'INBOX', args.dryRun || false);
1073
+ const creds = resolveCreds(args.account);
1074
+ result = await archiveOlderThan(args.days, resolveMailbox(args.targetMailbox, creds), args.sourceMailbox || 'INBOX', args.dryRun || false, creds);
964
1075
  } else if (name === 'bulk_delete') {
965
- // IMPROVEMENT 3: bulk_delete now has per-chunk timeouts internally
966
- const { sourceMailbox, dryRun, ...filters } = args;
967
- result = await bulkDelete(filters, sourceMailbox || 'INBOX', dryRun || false);
1076
+ const { sourceMailbox, dryRun, account, ...filters } = args;
1077
+ const creds = resolveCreds(account);
1078
+ result = await bulkDelete(filters, sourceMailbox || 'INBOX', dryRun || false, creds);
968
1079
  // ── Single-email tier (15s) ──
969
1080
  } else if (name === 'flag_email') {
970
- result = await withTimeout('flag_email', TIMEOUT.SINGLE, () => flagEmail(args.uid, args.flagged, args.mailbox || 'INBOX'));
1081
+ const creds = resolveCreds(args.account);
1082
+ result = await withTimeout('flag_email', TIMEOUT.SINGLE, () => flagEmail(args.uid, args.flagged, args.mailbox || 'INBOX', creds));
971
1083
  } else if (name === 'mark_as_read') {
972
- result = await withTimeout('mark_as_read', TIMEOUT.SINGLE, () => markAsRead(args.uid, args.seen, args.mailbox || 'INBOX'));
1084
+ const creds = resolveCreds(args.account);
1085
+ result = await withTimeout('mark_as_read', TIMEOUT.SINGLE, () => markAsRead(args.uid, args.seen, args.mailbox || 'INBOX', creds));
973
1086
  } else if (name === 'delete_email') {
974
- result = await withTimeout('delete_email', TIMEOUT.SINGLE, () => deleteEmail(args.uid, args.mailbox || 'INBOX'));
1087
+ const creds = resolveCreds(args.account);
1088
+ result = await withTimeout('delete_email', TIMEOUT.SINGLE, () => deleteEmail(args.uid, args.mailbox || 'INBOX', creds));
975
1089
  } else if (name === 'move_email') {
976
- result = await withTimeout('move_email', TIMEOUT.SINGLE, () => moveEmail(args.uid, args.targetMailbox, args.sourceMailbox || 'INBOX'));
1090
+ const creds = resolveCreds(args.account);
1091
+ result = await withTimeout('move_email', TIMEOUT.SINGLE, () => moveEmail(args.uid, resolveMailbox(args.targetMailbox, creds), args.sourceMailbox || 'INBOX', creds));
977
1092
  // ── Move status (synchronous, no timeout needed) ──
978
1093
  } else if (name === 'get_move_status') {
979
1094
  result = getMoveStatus();
@@ -986,6 +1101,16 @@ async function main() {
986
1101
  result = logRead();
987
1102
  } else if (name === 'log_clear') {
988
1103
  result = logClear();
1104
+ // ── Digest state (synchronous, no timeout needed) ──
1105
+ } else if (name === 'get_digest_state') {
1106
+ result = getDigestState();
1107
+ } else if (name === 'update_digest_state') {
1108
+ result = updateDigestState({
1109
+ lastRun: args.lastRun,
1110
+ processedUids: args.processedUids,
1111
+ pendingActions: args.pendingActions,
1112
+ skipCounts: args.skipCounts
1113
+ });
989
1114
  // ── Saved rules (synchronous CRUD; run_rule/run_all_rules use internal chunk timeouts) ──
990
1115
  } else if (name === 'create_rule') {
991
1116
  result = createRule(args.name, args.filters || {}, args.action, args.description || '');
@@ -999,26 +1124,30 @@ async function main() {
999
1124
  result = await runAllRules(args.dryRun || false);
1000
1125
  // ── SMTP (email sending — uses SCAN tier 60s for two-phase fetch+send) ──
1001
1126
  } else if (name === 'compose_email') {
1127
+ const creds = resolveCreds(args.account);
1002
1128
  result = await withTimeout('compose_email', TIMEOUT.SCAN, () =>
1003
- composeEmail(args.to, args.subject, args.body, { html: args.html, cc: args.cc, bcc: args.bcc, replyTo: args.replyTo })
1129
+ composeEmail(args.to, args.subject, args.body, { html: args.html, cc: args.cc, bcc: args.bcc, replyTo: args.replyTo }, creds)
1004
1130
  );
1005
1131
  } else if (name === 'reply_to_email') {
1132
+ const creds = resolveCreds(args.account);
1006
1133
  const origEmail = await withTimeout('get_email_for_reply', TIMEOUT.FETCH, () =>
1007
- getEmailContent(args.uid, args.mailbox || 'INBOX', 5000, true)
1134
+ getEmailContent(args.uid, args.mailbox || 'INBOX', 5000, true, creds)
1008
1135
  );
1009
1136
  result = await withTimeout('reply_to_email', TIMEOUT.FETCH, () =>
1010
- replyToEmail(origEmail, args.body, { html: args.html, replyAll: args.replyAll || false, cc: args.cc })
1137
+ replyToEmail(origEmail, args.body, { html: args.html, replyAll: args.replyAll || false, cc: args.cc }, creds)
1011
1138
  );
1012
1139
  } else if (name === 'forward_email') {
1140
+ const creds = resolveCreds(args.account);
1013
1141
  const origEmail = await withTimeout('get_email_for_forward', TIMEOUT.FETCH, () =>
1014
- getEmailContent(args.uid, args.mailbox || 'INBOX', 5000, false)
1142
+ getEmailContent(args.uid, args.mailbox || 'INBOX', 5000, false, creds)
1015
1143
  );
1016
1144
  result = await withTimeout('forward_email', TIMEOUT.FETCH, () =>
1017
- forwardEmail(origEmail, args.to, args.note || '', { html: args.html, cc: args.cc })
1145
+ forwardEmail(origEmail, args.to, args.note || '', { html: args.html, cc: args.cc }, creds)
1018
1146
  );
1019
1147
  } else if (name === 'save_draft') {
1148
+ const creds = resolveCreds(args.account);
1020
1149
  result = await withTimeout('save_draft', TIMEOUT.FETCH, () =>
1021
- saveDraft(args.to, args.subject, args.body, { html: args.html, cc: args.cc, bcc: args.bcc })
1150
+ saveDraft(args.to, args.subject, args.body, { html: args.html, cc: args.cc, bcc: args.bcc }, creds)
1022
1151
  );
1023
1152
  // ── CardDAV / Contacts (FETCH tier 30s) ──
1024
1153
  } else if (name === 'list_contacts') {
@@ -1080,8 +1209,9 @@ async function main() {
1080
1209
  );
1081
1210
  // ── Smart extraction (SCAN tier 60s — LLM round-trip) ──
1082
1211
  } else if (name === 'suggest_event_from_email') {
1212
+ const creds = resolveCreds(args.account);
1083
1213
  const email = await withTimeout('get_email_for_extraction', TIMEOUT.FETCH, () =>
1084
- getEmailContent(args.uid, args.mailbox || 'INBOX', 10000, false)
1214
+ getEmailContent(args.uid, resolveMailbox(args.mailbox || 'INBOX', creds), 10000, false, creds)
1085
1215
  );
1086
1216
  result = formatEmailForExtraction(email);
1087
1217
  } else {
package/lib/digest.js ADDED
@@ -0,0 +1,46 @@
1
+ // ─── Digest State ─────────────────────────────────────────────────────────────
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+
6
+ const DIGEST_FILE = join(homedir(), '.icloud-mcp-digest.json');
7
+
8
+ const DEFAULT_STATE = {
9
+ lastRun: null,
10
+ processedUids: [],
11
+ pendingActions: [],
12
+ skipCounts: {}
13
+ };
14
+
15
+ function readDigest() {
16
+ if (!existsSync(DIGEST_FILE)) return { ...DEFAULT_STATE };
17
+ try { return JSON.parse(readFileSync(DIGEST_FILE, 'utf8')); }
18
+ catch { return { ...DEFAULT_STATE }; }
19
+ }
20
+
21
+ function writeDigest(data) {
22
+ writeFileSync(DIGEST_FILE, JSON.stringify(data, null, 2));
23
+ }
24
+
25
+ export function getDigestState() {
26
+ return readDigest();
27
+ }
28
+
29
+ export function updateDigestState({ processedUids, lastRun, pendingActions, skipCounts } = {}) {
30
+ const state = readDigest();
31
+ if (lastRun !== undefined) state.lastRun = lastRun;
32
+ if (processedUids !== undefined) {
33
+ // Merge with existing, deduplicate, cap at 5000 to prevent unbounded growth
34
+ const merged = [...new Set([...state.processedUids, ...processedUids])];
35
+ state.processedUids = merged.slice(-5000);
36
+ }
37
+ if (pendingActions !== undefined) state.pendingActions = pendingActions;
38
+ if (skipCounts !== undefined) {
39
+ // Accumulate skip counts per sender for smart unsubscribe
40
+ for (const [sender, count] of Object.entries(skipCounts)) {
41
+ state.skipCounts[sender] = (state.skipCounts[sender] || 0) + count;
42
+ }
43
+ }
44
+ writeDigest(state);
45
+ return state;
46
+ }
package/lib/imap.js CHANGED
@@ -7,23 +7,24 @@ import {
7
7
  findTextPart, findAttachments, estimateEmailSize, stripSubjectPrefixes
8
8
  } from './mime.js';
9
9
 
10
- const IMAP_USER = process.env.IMAP_USER;
11
- const IMAP_PASSWORD = process.env.IMAP_PASSWORD;
12
-
13
10
  const MANIFEST_FILE = join(homedir(), '.icloud-mcp-move-manifest.json');
14
11
  const RULES_FILE = join(homedir(), '.icloud-mcp-rules.json');
15
12
  const MAX_HISTORY = 5;
16
13
 
17
- // ─── IMPROVEMENT 1: Connection-level timeout on createClient ──────────────────
18
- // ImapFlow supports connectionTimeout and greetingTimeout options.
19
- // This ensures we don't hang forever waiting for iCloud to respond.
14
+ // ─── Connection helpers ───────────────────────────────────────────────────────
15
+ // creds = { user, pass, host } — if omitted, falls back to IMAP_USER/IMAP_PASSWORD env vars
16
+ // and imap.mail.me.com. All exported functions accept an optional creds param
17
+ // as their last argument so callers can target any IMAP account.
20
18
 
21
- function createClient() {
19
+ function createClient(creds) {
20
+ const host = creds?.host || 'imap.mail.me.com';
21
+ const user = creds?.user || process.env.IMAP_USER;
22
+ const pass = creds?.pass || process.env.IMAP_PASSWORD;
22
23
  return new ImapFlow({
23
- host: 'imap.mail.me.com',
24
+ host,
24
25
  port: 993,
25
26
  secure: true,
26
- auth: { user: IMAP_USER, pass: IMAP_PASSWORD },
27
+ auth: { user, pass },
27
28
  logger: false,
28
29
  connectionTimeout: 15_000, // 15s to establish TCP+TLS connection
29
30
  greetingTimeout: 15_000, // 15s to receive IMAP greeting after connect
@@ -44,8 +45,8 @@ let _lastConnectTime = 0;
44
45
  let _connectGate = Promise.resolve();
45
46
  const MIN_CONNECT_INTERVAL = 10; // ms between connection initiations
46
47
 
47
- export function createRateLimitedClient() {
48
- const client = createClient();
48
+ export function createRateLimitedClient(creds) {
49
+ const client = createClient(creds);
49
50
  const originalConnect = client.connect.bind(client);
50
51
  client.connect = async () => {
51
52
  await new Promise(resolve => {
@@ -60,8 +61,8 @@ export function createRateLimitedClient() {
60
61
  return client;
61
62
  }
62
63
 
63
- async function openClient(mailbox) {
64
- const client = createRateLimitedClient();
64
+ async function openClient(mailbox, creds) {
65
+ const client = createRateLimitedClient(creds);
65
66
  await client.connect();
66
67
  if (mailbox) await client.mailboxOpen(mailbox);
67
68
  return client;
@@ -71,9 +72,9 @@ async function safeClose(client) {
71
72
  try { await client.logout(); } catch { try { client.close(); } catch { /* already gone */ } }
72
73
  }
73
74
 
74
- async function reconnect(client, mailbox) {
75
+ async function reconnect(client, mailbox, creds) {
75
76
  safeClose(client);
76
- return openClient(mailbox);
77
+ return openClient(mailbox, creds);
77
78
  }
78
79
 
79
80
  // ─── Move Manifest ────────────────────────────────────────────────────────────
@@ -476,7 +477,7 @@ async function verifyInTarget(targetClient, fingerprints, chunkIndex, knownTotal
476
477
 
477
478
  // Phase 1: Copy all chunks to target without deleting.
478
479
  // Returns { success, totalCopied, srcClient, errorResult }
479
- async function copyAllChunks(operation, srcClient, targetMailbox, sourceMailbox) {
480
+ async function copyAllChunks(operation, srcClient, targetMailbox, sourceMailbox, creds) {
480
481
  let totalCopied = 0;
481
482
 
482
483
  for (const chunk of operation.chunks) {
@@ -496,7 +497,7 @@ async function copyAllChunks(operation, srcClient, targetMailbox, sourceMailbox)
496
497
  } catch (err) {
497
498
  if (!isTransient(err)) throw err;
498
499
  moveLog(chunk.index, `fetch envelopes failed (${err.message}), reconnecting...`);
499
- srcClient = await reconnect(srcClient, sourceMailbox);
500
+ srcClient = await reconnect(srcClient, sourceMailbox, creds);
500
501
  for await (const msg of srcClient.fetch(chunkUids, { envelope: true }, { uid: true })) {
501
502
  envelopes.push(msg);
502
503
  }
@@ -521,7 +522,7 @@ async function copyAllChunks(operation, srcClient, targetMailbox, sourceMailbox)
521
522
  } catch (err) {
522
523
  if (!isTransient(err)) throw err;
523
524
  moveLog(chunk.index, `copy failed (${err.message}), reconnecting...`);
524
- srcClient = await reconnect(srcClient, sourceMailbox);
525
+ srcClient = await reconnect(srcClient, sourceMailbox, creds);
525
526
  await srcClient.messageCopy(chunkUids, targetMailbox, { uid: true });
526
527
  }
527
528
  moveLog(chunk.index, `copied ${chunkUids.length} emails to target (${elapsed(t)})`);
@@ -557,7 +558,7 @@ async function copyAllChunks(operation, srcClient, targetMailbox, sourceMailbox)
557
558
 
558
559
  // Phase 2: Verify all copied emails are present in target.
559
560
  // Returns { verification, tgtClient }
560
- async function verifyAllChunks(tgtClient, operation, targetMailbox) {
561
+ async function verifyAllChunks(tgtClient, operation, targetMailbox, creds) {
561
562
  const allFingerprints = operation.chunks.flatMap(c => c.fingerprints);
562
563
 
563
564
  updateManifest((data) => {
@@ -571,7 +572,7 @@ async function verifyAllChunks(tgtClient, operation, targetMailbox) {
571
572
  } catch (err) {
572
573
  if (!isTransient(err)) throw err;
573
574
  moveLog('global', `mailboxOpen failed (${err.message}), reconnecting...`);
574
- tgtClient = await reconnect(tgtClient, targetMailbox);
575
+ tgtClient = await reconnect(tgtClient, targetMailbox, creds);
575
576
  tgtMb = await tgtClient.mailboxOpen(targetMailbox);
576
577
  }
577
578
 
@@ -581,7 +582,7 @@ async function verifyAllChunks(tgtClient, operation, targetMailbox) {
581
582
  } catch (err) {
582
583
  if (!isTransient(err)) throw err;
583
584
  moveLog('global', `verify failed (${err.message}), reconnecting...`);
584
- tgtClient = await reconnect(tgtClient, targetMailbox);
585
+ tgtClient = await reconnect(tgtClient, targetMailbox, creds);
585
586
  verification = await verifyInTarget(tgtClient, allFingerprints, 'global');
586
587
  }
587
588
 
@@ -590,7 +591,7 @@ async function verifyAllChunks(tgtClient, operation, targetMailbox) {
590
591
 
591
592
  // Phase 3: Delete all source emails in a single EXPUNGE.
592
593
  // Returns { srcClient }
593
- async function deleteAllChunks(srcClient, operation, sourceMailbox) {
594
+ async function deleteAllChunks(srcClient, operation, sourceMailbox, creds) {
594
595
  const allUids = operation.chunks.flatMap(c => c.uids);
595
596
  const t = Date.now();
596
597
 
@@ -599,7 +600,7 @@ async function deleteAllChunks(srcClient, operation, sourceMailbox) {
599
600
  } catch (err) {
600
601
  if (!isTransient(err)) throw err;
601
602
  moveLog('global', `delete failed (${err.message}), reconnecting...`);
602
- srcClient = await reconnect(srcClient, sourceMailbox);
603
+ srcClient = await reconnect(srcClient, sourceMailbox, creds);
603
604
  // Retry is idempotent — expunging already-gone UIDs is a no-op
604
605
  await srcClient.messageDelete(allUids, { uid: true });
605
606
  }
@@ -610,19 +611,19 @@ async function deleteAllChunks(srcClient, operation, sourceMailbox) {
610
611
 
611
612
  // ─── Safe Move (Option B: COPY-all → VERIFY-all → single EXPUNGE) ─────────────
612
613
 
613
- async function safeMoveEmails(uids, sourceMailbox, targetMailbox) {
614
+ async function safeMoveEmails(uids, sourceMailbox, targetMailbox, creds) {
614
615
  const operation = startOperation(sourceMailbox, targetMailbox, uids);
615
616
  const opStart = Date.now();
616
617
 
617
618
  process.stderr.write(`[move] starting: ${uids.length} emails, ${operation.chunks.length} chunks, ${sourceMailbox} → ${targetMailbox}\n`);
618
619
 
619
- let srcClient = await openClient(sourceMailbox);
620
- let tgtClient = await openClient(targetMailbox);
620
+ let srcClient = await openClient(sourceMailbox, creds);
621
+ let tgtClient = await openClient(targetMailbox, creds);
621
622
 
622
623
  try {
623
624
  // Phase 1: COPY all chunks to target (no delete yet)
624
625
  process.stderr.write(`[move] phase 1/3: copying ${uids.length} emails in ${operation.chunks.length} chunks\n`);
625
- const copyResult = await copyAllChunks(operation, srcClient, targetMailbox, sourceMailbox);
626
+ const copyResult = await copyAllChunks(operation, srcClient, targetMailbox, sourceMailbox, creds);
626
627
  srcClient = copyResult.srcClient;
627
628
 
628
629
  if (!copyResult.success) {
@@ -637,7 +638,7 @@ async function safeMoveEmails(uids, sourceMailbox, targetMailbox) {
637
638
  let verifyResult;
638
639
  try {
639
640
  verifyResult = await withTimeout('verify all', TIMEOUT.VERIFY_ALL, () =>
640
- verifyAllChunks(tgtClient, operation, targetMailbox)
641
+ verifyAllChunks(tgtClient, operation, targetMailbox, creds)
641
642
  );
642
643
  tgtClient = verifyResult.tgtClient;
643
644
  } catch (err) {
@@ -677,7 +678,7 @@ async function safeMoveEmails(uids, sourceMailbox, targetMailbox) {
677
678
  let deleteResult;
678
679
  try {
679
680
  deleteResult = await withTimeout('delete all', TIMEOUT.DELETE_ALL, () =>
680
- deleteAllChunks(srcClient, operation, sourceMailbox)
681
+ deleteAllChunks(srcClient, operation, sourceMailbox, creds)
681
682
  );
682
683
  srcClient = deleteResult.srcClient;
683
684
  } catch (err) {
@@ -709,8 +710,8 @@ async function safeMoveEmails(uids, sourceMailbox, targetMailbox) {
709
710
 
710
711
  // ─── Email Functions ──────────────────────────────────────────────────────────
711
712
 
712
- export async function fetchEmails(mailbox = 'INBOX', limit = 10, onlyUnread = false, page = 1) {
713
- const client = createRateLimitedClient();
713
+ export async function fetchEmails(mailbox = 'INBOX', limit = 10, onlyUnread = false, page = 1, creds = null) {
714
+ const client = createRateLimitedClient(creds);
714
715
  await client.connect();
715
716
  const mb = await client.mailboxOpen(mailbox);
716
717
  const total = mb.exists;
@@ -763,16 +764,16 @@ export async function fetchEmails(mailbox = 'INBOX', limit = 10, onlyUnread = fa
763
764
  return { emails, page, limit, total, totalPages: Math.ceil(total / limit), hasMore: (page * limit) < total };
764
765
  }
765
766
 
766
- export async function getInboxSummary(mailbox = 'INBOX') {
767
- const client = createRateLimitedClient();
767
+ export async function getInboxSummary(mailbox = 'INBOX', creds = null) {
768
+ const client = createRateLimitedClient(creds);
768
769
  await client.connect();
769
770
  const status = await client.status(mailbox, { messages: true, unseen: true, recent: true });
770
771
  await client.logout();
771
772
  return { mailbox, total: status.messages, unread: status.unseen, recent: status.recent };
772
773
  }
773
774
 
774
- export async function getTopSenders(mailbox = 'INBOX', sampleSize = 500, maxResults = 20) {
775
- const client = createRateLimitedClient();
775
+ export async function getTopSenders(mailbox = 'INBOX', sampleSize = 500, maxResults = 20, creds = null) {
776
+ const client = createRateLimitedClient(creds);
776
777
  await client.connect();
777
778
  const mb = await client.mailboxOpen(mailbox);
778
779
  const total = mb.exists;
@@ -800,8 +801,8 @@ export async function getTopSenders(mailbox = 'INBOX', sampleSize = 500, maxResu
800
801
  return { sampledEmails: count, topAddresses, topDomains };
801
802
  }
802
803
 
803
- export async function getUnreadSenders(mailbox = 'INBOX', sampleSize = 500, maxResults = 20) {
804
- const client = createRateLimitedClient();
804
+ export async function getUnreadSenders(mailbox = 'INBOX', sampleSize = 500, maxResults = 20, creds = null) {
805
+ const client = createRateLimitedClient(creds);
805
806
  await client.connect();
806
807
  await client.mailboxOpen(mailbox);
807
808
  const uids = (await client.search({ seen: false }, { uid: true })) ?? [];
@@ -822,8 +823,8 @@ export async function getUnreadSenders(mailbox = 'INBOX', sampleSize = 500, maxR
822
823
  return Object.entries(senderCounts).sort((a, b) => b[1] - a[1]).slice(0, maxResults).map(([address, count]) => ({ address, count }));
823
824
  }
824
825
 
825
- export async function getEmailsBySender(sender, mailbox = 'INBOX', limit = 10) {
826
- const client = createRateLimitedClient();
826
+ export async function getEmailsBySender(sender, mailbox = 'INBOX', limit = 10, creds = null) {
827
+ const client = createRateLimitedClient(creds);
827
828
  await client.connect();
828
829
  await client.mailboxOpen(mailbox);
829
830
  const uids = (await client.search({ from: sender }, { uid: true })) ?? [];
@@ -847,8 +848,8 @@ export async function getEmailsBySender(sender, mailbox = 'INBOX', limit = 10) {
847
848
  return { total, showing: emails.length, emails };
848
849
  }
849
850
 
850
- export async function bulkDeleteBySender(sender, mailbox = 'INBOX') {
851
- const client = createRateLimitedClient();
851
+ export async function bulkDeleteBySender(sender, mailbox = 'INBOX', creds = null) {
852
+ const client = createRateLimitedClient(creds);
852
853
  await client.connect();
853
854
  await client.mailboxOpen(mailbox);
854
855
  const uids = (await client.search({ from: sender }, { uid: true })) ?? [];
@@ -863,8 +864,8 @@ export async function bulkDeleteBySender(sender, mailbox = 'INBOX') {
863
864
  return { deleted, sender };
864
865
  }
865
866
 
866
- export async function markOlderThanRead(days, mailbox = 'INBOX') {
867
- const client = createRateLimitedClient();
867
+ export async function markOlderThanRead(days, mailbox = 'INBOX', creds = null) {
868
+ const client = createRateLimitedClient(creds);
868
869
  await client.connect();
869
870
  await client.mailboxOpen(mailbox);
870
871
  const date = new Date();
@@ -877,26 +878,26 @@ export async function markOlderThanRead(days, mailbox = 'INBOX') {
877
878
  return { marked: uids.length, olderThan: date.toISOString() };
878
879
  }
879
880
 
880
- export async function bulkMoveByDomain(domain, targetMailbox, sourceMailbox = 'INBOX', dryRun = false) {
881
- const result = await bulkMove({ domain }, targetMailbox, sourceMailbox, dryRun);
881
+ export async function bulkMoveByDomain(domain, targetMailbox, sourceMailbox = 'INBOX', dryRun = false, creds = null) {
882
+ const result = await bulkMove({ domain }, targetMailbox, sourceMailbox, dryRun, null, creds);
882
883
  return { ...result, domain };
883
884
  }
884
885
 
885
- export async function bulkMoveBySender(sender, targetMailbox, sourceMailbox = 'INBOX', dryRun = false) {
886
- const client = createRateLimitedClient();
886
+ export async function bulkMoveBySender(sender, targetMailbox, sourceMailbox = 'INBOX', dryRun = false, creds = null) {
887
+ const client = createRateLimitedClient(creds);
887
888
  await client.connect();
888
889
  await client.mailboxOpen(sourceMailbox);
889
890
  const uids = (await client.search({ from: sender }, { uid: true })) ?? [];
890
891
  await client.logout();
891
892
  if (dryRun) return { dryRun: true, wouldMove: uids.length, sender, sourceMailbox, targetMailbox };
892
893
  if (uids.length === 0) return { moved: 0 };
893
- await ensureMailbox(targetMailbox);
894
- const result = await safeMoveEmails(uids, sourceMailbox, targetMailbox);
894
+ await ensureMailbox(targetMailbox, creds);
895
+ const result = await safeMoveEmails(uids, sourceMailbox, targetMailbox, creds);
895
896
  return { ...result, sender, targetMailbox };
896
897
  }
897
898
 
898
- export async function bulkDeleteBySubject(subject, mailbox = 'INBOX') {
899
- const client = createRateLimitedClient();
899
+ export async function bulkDeleteBySubject(subject, mailbox = 'INBOX', creds = null) {
900
+ const client = createRateLimitedClient(creds);
900
901
  await client.connect();
901
902
  await client.mailboxOpen(mailbox);
902
903
  const uids = (await client.search({ subject }, { uid: true })) ?? [];
@@ -911,8 +912,8 @@ export async function bulkDeleteBySubject(subject, mailbox = 'INBOX') {
911
912
  return { deleted, subject };
912
913
  }
913
914
 
914
- export async function deleteOlderThan(days, mailbox = 'INBOX') {
915
- const client = createRateLimitedClient();
915
+ export async function deleteOlderThan(days, mailbox = 'INBOX', creds = null) {
916
+ const client = createRateLimitedClient(creds);
916
917
  await client.connect();
917
918
  await client.mailboxOpen(mailbox);
918
919
  const date = new Date();
@@ -929,8 +930,8 @@ export async function deleteOlderThan(days, mailbox = 'INBOX') {
929
930
  return { deleted, olderThan: date.toISOString() };
930
931
  }
931
932
 
932
- export async function getEmailsByDateRange(startDate, endDate, mailbox = 'INBOX', limit = 10) {
933
- const client = createRateLimitedClient();
933
+ export async function getEmailsByDateRange(startDate, endDate, mailbox = 'INBOX', limit = 10, creds = null) {
934
+ const client = createRateLimitedClient(creds);
934
935
  await client.connect();
935
936
  await client.mailboxOpen(mailbox);
936
937
  const uids = (await client.search({ since: new Date(startDate), before: new Date(endDate) }, { uid: true })) ?? [];
@@ -954,8 +955,8 @@ export async function getEmailsByDateRange(startDate, endDate, mailbox = 'INBOX'
954
955
  return { total, showing: emails.length, emails };
955
956
  }
956
957
 
957
- export async function bulkMarkRead(mailbox = 'INBOX', sender = null) {
958
- const client = createRateLimitedClient();
958
+ export async function bulkMarkRead(mailbox = 'INBOX', sender = null, creds = null) {
959
+ const client = createRateLimitedClient(creds);
959
960
  await client.connect();
960
961
  await client.mailboxOpen(mailbox);
961
962
  const query = sender ? { from: sender, seen: false } : { seen: false };
@@ -966,8 +967,8 @@ export async function bulkMarkRead(mailbox = 'INBOX', sender = null) {
966
967
  return { marked: uids.length, sender: sender || 'all' };
967
968
  }
968
969
 
969
- export async function bulkMarkUnread(mailbox = 'INBOX', sender = null) {
970
- const client = createRateLimitedClient();
970
+ export async function bulkMarkUnread(mailbox = 'INBOX', sender = null, creds = null) {
971
+ const client = createRateLimitedClient(creds);
971
972
  await client.connect();
972
973
  await client.mailboxOpen(mailbox);
973
974
  const query = sender ? { from: sender, seen: true } : { seen: true };
@@ -978,8 +979,8 @@ export async function bulkMarkUnread(mailbox = 'INBOX', sender = null) {
978
979
  return { marked: uids.length, sender: sender || 'all' };
979
980
  }
980
981
 
981
- export async function bulkFlag(filters, flagged, mailbox = 'INBOX') {
982
- const client = createRateLimitedClient();
982
+ export async function bulkFlag(filters, flagged, mailbox = 'INBOX', creds = null) {
983
+ const client = createRateLimitedClient(creds);
983
984
  await client.connect();
984
985
  await client.mailboxOpen(mailbox);
985
986
  const query = buildQuery(filters);
@@ -994,8 +995,8 @@ export async function bulkFlag(filters, flagged, mailbox = 'INBOX') {
994
995
  return { [flagged ? 'flagged' : 'unflagged']: uids.length, filters };
995
996
  }
996
997
 
997
- export async function bulkFlagBySender(sender, flagged, mailbox = 'INBOX') {
998
- const client = createRateLimitedClient();
998
+ export async function bulkFlagBySender(sender, flagged, mailbox = 'INBOX', creds = null) {
999
+ const client = createRateLimitedClient(creds);
999
1000
  await client.connect();
1000
1001
  await client.mailboxOpen(mailbox);
1001
1002
  const raw = await client.search({ from: sender }, { uid: true });
@@ -1010,10 +1011,11 @@ export async function bulkFlagBySender(sender, flagged, mailbox = 'INBOX') {
1010
1011
  return { [flagged ? 'flagged' : 'unflagged']: uids.length, sender };
1011
1012
  }
1012
1013
 
1013
- export async function emptyTrash(dryRun = false) {
1014
+ export async function emptyTrash(dryRun = false, creds = null) {
1014
1015
  const t0 = Date.now();
1015
- const trashFolders = ['Deleted Messages', 'Trash'];
1016
- const client = createRateLimitedClient();
1016
+ // iCloud uses 'Deleted Messages'; Gmail uses '[Gmail]/Trash'; standard IMAP uses 'Trash'
1017
+ const trashFolders = ['Deleted Messages', '[Gmail]/Trash', 'Trash'];
1018
+ const client = createRateLimitedClient(creds);
1017
1019
  await client.connect();
1018
1020
 
1019
1021
  let mailbox = null;
@@ -1058,8 +1060,8 @@ export async function emptyTrash(dryRun = false) {
1058
1060
  return { deleted, mailbox, timeTaken: ((Date.now() - t0) / 1000).toFixed(1) + 's' };
1059
1061
  }
1060
1062
 
1061
- export async function archiveOlderThan(days, targetMailbox, sourceMailbox = 'INBOX', dryRun = false) {
1062
- const client = createRateLimitedClient();
1063
+ export async function archiveOlderThan(days, targetMailbox, sourceMailbox = 'INBOX', dryRun = false, creds = null) {
1064
+ const client = createRateLimitedClient(creds);
1063
1065
  await client.connect();
1064
1066
  await client.mailboxOpen(sourceMailbox);
1065
1067
  const date = new Date();
@@ -1069,13 +1071,13 @@ export async function archiveOlderThan(days, targetMailbox, sourceMailbox = 'INB
1069
1071
  await client.logout();
1070
1072
  if (dryRun) return { dryRun: true, wouldMove: uids.length, olderThan: date.toISOString(), sourceMailbox, targetMailbox };
1071
1073
  if (uids.length === 0) return { moved: 0, olderThan: date.toISOString(), sourceMailbox, targetMailbox };
1072
- await ensureMailbox(targetMailbox);
1073
- const result = await safeMoveEmails(uids, sourceMailbox, targetMailbox);
1074
+ await ensureMailbox(targetMailbox, creds);
1075
+ const result = await safeMoveEmails(uids, sourceMailbox, targetMailbox, creds);
1074
1076
  return { ...result, olderThan: date.toISOString(), sourceMailbox, targetMailbox };
1075
1077
  }
1076
1078
 
1077
- export async function getStorageReport(mailbox = 'INBOX', sampleSize = 100) {
1078
- const client = createRateLimitedClient();
1079
+ export async function getStorageReport(mailbox = 'INBOX', sampleSize = 100, creds = null) {
1080
+ const client = createRateLimitedClient(creds);
1079
1081
  await client.connect();
1080
1082
  await client.mailboxOpen(mailbox);
1081
1083
 
@@ -1130,9 +1132,9 @@ export async function getStorageReport(mailbox = 'INBOX', sampleSize = 100) {
1130
1132
  };
1131
1133
  }
1132
1134
 
1133
- export async function getThread(uid, mailbox = 'INBOX') {
1135
+ export async function getThread(uid, mailbox = 'INBOX', creds = null) {
1134
1136
  const THREAD_CANDIDATE_CAP = 100;
1135
- const client = createRateLimitedClient();
1137
+ const client = createRateLimitedClient(creds);
1136
1138
  await client.connect();
1137
1139
  await client.mailboxOpen(mailbox);
1138
1140
 
@@ -1225,16 +1227,16 @@ export async function getThread(uid, mailbox = 'INBOX') {
1225
1227
  };
1226
1228
  }
1227
1229
 
1228
- export async function createMailbox(name) {
1229
- const client = createRateLimitedClient();
1230
+ export async function createMailbox(name, creds = null) {
1231
+ const client = createRateLimitedClient(creds);
1230
1232
  await client.connect();
1231
1233
  await client.mailboxCreate(name);
1232
1234
  await client.logout();
1233
1235
  return { created: name };
1234
1236
  }
1235
1237
 
1236
- export async function renameMailbox(oldName, newName) {
1237
- const client = createRateLimitedClient();
1238
+ export async function renameMailbox(oldName, newName, creds = null) {
1239
+ const client = createRateLimitedClient(creds);
1238
1240
  await client.connect();
1239
1241
  try {
1240
1242
  await Promise.race([
@@ -1249,8 +1251,8 @@ export async function renameMailbox(oldName, newName) {
1249
1251
  return { renamed: { from: oldName, to: newName } };
1250
1252
  }
1251
1253
 
1252
- export async function deleteMailbox(name) {
1253
- const client = createRateLimitedClient();
1254
+ export async function deleteMailbox(name, creds = null) {
1255
+ const client = createRateLimitedClient(creds);
1254
1256
  await client.connect();
1255
1257
  try {
1256
1258
  await Promise.race([
@@ -1265,8 +1267,8 @@ export async function deleteMailbox(name) {
1265
1267
  return { deleted: name };
1266
1268
  }
1267
1269
 
1268
- export async function getMailboxSummary(mailbox) {
1269
- const client = createRateLimitedClient();
1270
+ export async function getMailboxSummary(mailbox, creds = null) {
1271
+ const client = createRateLimitedClient(creds);
1270
1272
  await client.connect();
1271
1273
  const status = await client.status(mailbox, { messages: true, unseen: true, recent: true });
1272
1274
  await client.logout();
@@ -1275,8 +1277,8 @@ export async function getMailboxSummary(mailbox) {
1275
1277
 
1276
1278
  // ─── Email content fetcher (MIME-aware) ───────────────────────────────────────
1277
1279
 
1278
- export async function getEmailContent(uid, mailbox = 'INBOX', maxChars = 8000, includeHeaders = false) {
1279
- const client = createRateLimitedClient();
1280
+ export async function getEmailContent(uid, mailbox = 'INBOX', maxChars = 8000, includeHeaders = false, creds = null) {
1281
+ const client = createRateLimitedClient(creds);
1280
1282
  await client.connect();
1281
1283
  await client.mailboxOpen(mailbox);
1282
1284
 
@@ -1384,8 +1386,8 @@ export async function getEmailContent(uid, mailbox = 'INBOX', maxChars = 8000, i
1384
1386
  return result;
1385
1387
  }
1386
1388
 
1387
- export async function listAttachments(uid, mailbox = 'INBOX') {
1388
- const client = createRateLimitedClient();
1389
+ export async function listAttachments(uid, mailbox = 'INBOX', creds = null) {
1390
+ const client = createRateLimitedClient(creds);
1389
1391
  await client.connect();
1390
1392
  await client.mailboxOpen(mailbox);
1391
1393
  const meta = await client.fetchOne(uid, { envelope: true, bodyStructure: true }, { uid: true });
@@ -1400,8 +1402,8 @@ export async function listAttachments(uid, mailbox = 'INBOX') {
1400
1402
  };
1401
1403
  }
1402
1404
 
1403
- export async function getUnsubscribeInfo(uid, mailbox = 'INBOX') {
1404
- const client = createRateLimitedClient();
1405
+ export async function getUnsubscribeInfo(uid, mailbox = 'INBOX', creds = null) {
1406
+ const client = createRateLimitedClient(creds);
1405
1407
  await client.connect();
1406
1408
  await client.mailboxOpen(mailbox);
1407
1409
  const meta = await client.fetchOne(uid, { headers: new Set(['list-unsubscribe', 'list-unsubscribe-post']) }, { uid: true });
@@ -1414,9 +1416,9 @@ export async function getUnsubscribeInfo(uid, mailbox = 'INBOX') {
1414
1416
  return { uid, email, url, raw };
1415
1417
  }
1416
1418
 
1417
- export async function getEmailRaw(uid, mailbox = 'INBOX') {
1419
+ export async function getEmailRaw(uid, mailbox = 'INBOX', creds = null) {
1418
1420
  const MAX_RAW_BYTES = 1 * 1024 * 1024; // 1 MB cap
1419
- const client = createRateLimitedClient();
1421
+ const client = createRateLimitedClient(creds);
1420
1422
  await client.connect();
1421
1423
  await client.mailboxOpen(mailbox);
1422
1424
  const msg = await client.fetchOne(uid, { source: true }, { uid: true });
@@ -1434,8 +1436,8 @@ export async function getEmailRaw(uid, mailbox = 'INBOX') {
1434
1436
  };
1435
1437
  }
1436
1438
 
1437
- export async function getAttachment(uid, partId, mailbox = 'INBOX', offset = null, length = null) {
1438
- const client = createRateLimitedClient();
1439
+ export async function getAttachment(uid, partId, mailbox = 'INBOX', offset = null, length = null, creds = null) {
1440
+ const client = createRateLimitedClient(creds);
1439
1441
  await client.connect();
1440
1442
  await client.mailboxOpen(mailbox);
1441
1443
 
@@ -1527,8 +1529,8 @@ export async function getAttachment(uid, partId, mailbox = 'INBOX', offset = nul
1527
1529
  };
1528
1530
  }
1529
1531
 
1530
- export async function flagEmail(uid, flagged, mailbox = 'INBOX') {
1531
- const client = createRateLimitedClient();
1532
+ export async function flagEmail(uid, flagged, mailbox = 'INBOX', creds = null) {
1533
+ const client = createRateLimitedClient(creds);
1532
1534
  await client.connect();
1533
1535
  await client.mailboxOpen(mailbox);
1534
1536
  if (flagged) {
@@ -1540,8 +1542,8 @@ export async function flagEmail(uid, flagged, mailbox = 'INBOX') {
1540
1542
  return true;
1541
1543
  }
1542
1544
 
1543
- export async function markAsRead(uid, seen, mailbox = 'INBOX') {
1544
- const client = createRateLimitedClient();
1545
+ export async function markAsRead(uid, seen, mailbox = 'INBOX', creds = null) {
1546
+ const client = createRateLimitedClient(creds);
1545
1547
  await client.connect();
1546
1548
  await client.mailboxOpen(mailbox);
1547
1549
  if (seen) {
@@ -1553,8 +1555,8 @@ export async function markAsRead(uid, seen, mailbox = 'INBOX') {
1553
1555
  return true;
1554
1556
  }
1555
1557
 
1556
- export async function deleteEmail(uid, mailbox = 'INBOX') {
1557
- const client = createRateLimitedClient();
1558
+ export async function deleteEmail(uid, mailbox = 'INBOX', creds = null) {
1559
+ const client = createRateLimitedClient(creds);
1558
1560
  await client.connect();
1559
1561
  await client.mailboxOpen(mailbox);
1560
1562
  await client.messageDelete(uid, { uid: true });
@@ -1562,8 +1564,8 @@ export async function deleteEmail(uid, mailbox = 'INBOX') {
1562
1564
  return true;
1563
1565
  }
1564
1566
 
1565
- export async function listMailboxes() {
1566
- const client = createRateLimitedClient();
1567
+ export async function listMailboxes(creds = null) {
1568
+ const client = createRateLimitedClient(creds);
1567
1569
  await client.connect();
1568
1570
  const tree = await client.listTree();
1569
1571
  const mailboxes = [];
@@ -1578,9 +1580,9 @@ export async function listMailboxes() {
1578
1580
  return mailboxes;
1579
1581
  }
1580
1582
 
1581
- export async function searchEmails(query, mailbox = 'INBOX', limit = 10, filters = {}, options = {}) {
1583
+ export async function searchEmails(query, mailbox = 'INBOX', limit = 10, filters = {}, options = {}, creds = null) {
1582
1584
  const { queryMode = 'or', subjectQuery, bodyQuery, fromQuery, includeSnippet = false } = options;
1583
- const client = createRateLimitedClient();
1585
+ const client = createRateLimitedClient(creds);
1584
1586
  await client.connect();
1585
1587
  await client.mailboxOpen(mailbox);
1586
1588
 
@@ -1666,8 +1668,8 @@ export async function searchEmails(query, mailbox = 'INBOX', limit = 10, filters
1666
1668
  return { total: uids.length, showing: emails.length, emails };
1667
1669
  }
1668
1670
 
1669
- export async function moveEmail(uid, targetMailbox, sourceMailbox = 'INBOX') {
1670
- const client = createRateLimitedClient();
1671
+ export async function moveEmail(uid, targetMailbox, sourceMailbox = 'INBOX', creds = null) {
1672
+ const client = createRateLimitedClient(creds);
1671
1673
  await client.connect();
1672
1674
  await client.mailboxOpen(sourceMailbox);
1673
1675
  await client.messageMove(uid, targetMailbox, { uid: true });
@@ -1705,15 +1707,15 @@ async function filterUidsByAttachment(client, uids) {
1705
1707
  return result;
1706
1708
  }
1707
1709
 
1708
- async function ensureMailbox(name) {
1709
- const client = createRateLimitedClient();
1710
+ async function ensureMailbox(name, creds) {
1711
+ const client = createRateLimitedClient(creds);
1710
1712
  await client.connect();
1711
1713
  try { await client.mailboxCreate(name); } catch { /* already exists */ }
1712
1714
  await client.logout();
1713
1715
  }
1714
1716
 
1715
- export async function bulkMove(filters, targetMailbox, sourceMailbox = 'INBOX', dryRun = false, limit = null) {
1716
- const client = createRateLimitedClient();
1717
+ export async function bulkMove(filters, targetMailbox, sourceMailbox = 'INBOX', dryRun = false, limit = null, creds = null) {
1718
+ const client = createRateLimitedClient(creds);
1717
1719
  await client.connect();
1718
1720
  await client.mailboxOpen(sourceMailbox);
1719
1721
  const query = buildQuery(filters);
@@ -1734,8 +1736,8 @@ export async function bulkMove(filters, targetMailbox, sourceMailbox = 'INBOX',
1734
1736
  }
1735
1737
  if (uids.length === 0) return { moved: 0, sourceMailbox, targetMailbox };
1736
1738
 
1737
- await ensureMailbox(targetMailbox);
1738
- const result = await safeMoveEmails(uids, sourceMailbox, targetMailbox);
1739
+ await ensureMailbox(targetMailbox, creds);
1740
+ const result = await safeMoveEmails(uids, sourceMailbox, targetMailbox, creds);
1739
1741
  return { ...result, sourceMailbox, targetMailbox, filters };
1740
1742
  }
1741
1743
 
@@ -1744,8 +1746,8 @@ export async function bulkMove(filters, targetMailbox, sourceMailbox = 'INBOX',
1744
1746
  // timeout. If a single chunk hangs, we bail with a partial result instead of
1745
1747
  // hanging forever.
1746
1748
 
1747
- export async function bulkDelete(filters, sourceMailbox = 'INBOX', dryRun = false) {
1748
- const client = createRateLimitedClient();
1749
+ export async function bulkDelete(filters, sourceMailbox = 'INBOX', dryRun = false, creds = null) {
1750
+ const client = createRateLimitedClient(creds);
1749
1751
  await client.connect();
1750
1752
  await client.mailboxOpen(sourceMailbox);
1751
1753
  const query = buildQuery(filters);
@@ -1788,8 +1790,8 @@ export async function bulkDelete(filters, sourceMailbox = 'INBOX', dryRun = fals
1788
1790
  return { deleted, sourceMailbox, filters };
1789
1791
  }
1790
1792
 
1791
- export async function countEmails(filters, mailbox = 'INBOX') {
1792
- const client = createRateLimitedClient();
1793
+ export async function countEmails(filters, mailbox = 'INBOX', creds = null) {
1794
+ const client = createRateLimitedClient(creds);
1793
1795
  await client.connect();
1794
1796
  await client.mailboxOpen(mailbox);
1795
1797
  const query = buildQuery(filters);
@@ -1856,8 +1858,8 @@ export function deleteRule(name) {
1856
1858
  return { deleted: true, name };
1857
1859
  }
1858
1860
 
1859
- async function bulkMarkByFilters(filters, read, mailbox = 'INBOX') {
1860
- const client = createRateLimitedClient();
1861
+ async function bulkMarkByFilters(filters, read, mailbox = 'INBOX', creds = null) {
1862
+ const client = createRateLimitedClient(creds);
1861
1863
  await client.connect();
1862
1864
  await client.mailboxOpen(mailbox);
1863
1865
  const base = buildQuery(filters);
package/lib/smtp.js CHANGED
@@ -1,20 +1,20 @@
1
1
  import nodemailer from 'nodemailer';
2
2
  import { ImapFlow } from 'imapflow';
3
3
 
4
- const SMTP_HOST = 'smtp.mail.me.com';
5
4
  const SMTP_PORT = 587;
6
5
 
7
- function getCredentials() {
8
- const user = process.env.IMAP_USER;
9
- const pass = process.env.IMAP_PASSWORD;
6
+ // creds = { user, pass, host (IMAP), smtpHost } — falls back to env vars / iCloud defaults
7
+ function getCredentials(creds) {
8
+ const user = creds?.user || process.env.IMAP_USER;
9
+ const pass = creds?.pass || process.env.IMAP_PASSWORD;
10
10
  if (!user || !pass) throw new Error('IMAP_USER and IMAP_PASSWORD are required for SMTP operations');
11
- return { user, pass };
11
+ return { user, pass, imapHost: creds?.host || 'imap.mail.me.com', smtpHost: creds?.smtpHost || 'smtp.mail.me.com' };
12
12
  }
13
13
 
14
- function createTransport() {
15
- const { user, pass } = getCredentials();
14
+ function createTransport(creds) {
15
+ const { user, pass, smtpHost } = getCredentials(creds);
16
16
  return nodemailer.createTransport({
17
- host: SMTP_HOST,
17
+ host: smtpHost,
18
18
  port: SMTP_PORT,
19
19
  secure: false, // STARTTLS on port 587
20
20
  auth: { user, pass },
@@ -63,9 +63,9 @@ function applyBody(mailOptions, body, html) {
63
63
 
64
64
  // ─── compose_email ────────────────────────────────────────────────────────────
65
65
 
66
- export async function composeEmail(to, subject, body, opts = {}) {
67
- const { user } = getCredentials();
68
- const transport = createTransport();
66
+ export async function composeEmail(to, subject, body, opts = {}, creds = null) {
67
+ const { user } = getCredentials(creds);
68
+ const transport = createTransport(creds);
69
69
  const mailOptions = { from: user, to: normalizeAddresses(to), subject };
70
70
  applyBody(mailOptions, body, opts.html);
71
71
  if (opts.cc) mailOptions.cc = normalizeAddresses(opts.cc);
@@ -84,9 +84,9 @@ export async function composeEmail(to, subject, body, opts = {}) {
84
84
  // ─── reply_to_email ───────────────────────────────────────────────────────────
85
85
  // email = getEmailContent(uid, mailbox, maxChars, includeHeaders: true) result
86
86
 
87
- export async function replyToEmail(email, body, opts = {}) {
88
- const { user } = getCredentials();
89
- const transport = createTransport();
87
+ export async function replyToEmail(email, body, opts = {}, creds = null) {
88
+ const { user } = getCredentials(creds);
89
+ const transport = createTransport(creds);
90
90
 
91
91
  const originalSubject = email.subject ?? '';
92
92
  const originalMessageId = email.headers?.messageId ?? null;
@@ -138,9 +138,9 @@ export async function replyToEmail(email, body, opts = {}) {
138
138
  // ─── forward_email ────────────────────────────────────────────────────────────
139
139
  // email = getEmailContent result (no need for includeHeaders)
140
140
 
141
- export async function forwardEmail(email, to, note = '', opts = {}) {
142
- const { user } = getCredentials();
143
- const transport = createTransport();
141
+ export async function forwardEmail(email, to, note = '', opts = {}, creds = null) {
142
+ const { user } = getCredentials(creds);
143
+ const transport = createTransport(creds);
144
144
 
145
145
  const originalSubject = email.subject ?? '';
146
146
  const subject = /^fwd:/i.test(originalSubject) ? originalSubject : `Fwd: ${originalSubject}`;
@@ -173,8 +173,8 @@ export async function forwardEmail(email, to, note = '', opts = {}) {
173
173
  // ─── save_draft ───────────────────────────────────────────────────────────────
174
174
  // Builds the raw MIME message without sending, then APPENDs to Drafts via IMAP.
175
175
 
176
- export async function saveDraft(to, subject, body, opts = {}) {
177
- const { user, pass } = getCredentials();
176
+ export async function saveDraft(to, subject, body, opts = {}, creds = null) {
177
+ const { user, pass, imapHost } = getCredentials(creds);
178
178
 
179
179
  const mailOptions = { from: user, to: normalizeAddresses(to), subject };
180
180
  applyBody(mailOptions, body, opts.html);
@@ -187,7 +187,7 @@ export async function saveDraft(to, subject, body, opts = {}) {
187
187
 
188
188
  // APPEND the raw message to the Drafts folder via IMAP
189
189
  const client = new ImapFlow({
190
- host: 'imap.mail.me.com',
190
+ host: imapHost,
191
191
  port: 993,
192
192
  secure: true,
193
193
  auth: { user, pass },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icloud-mcp",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "A Model Context Protocol (MCP) server for iCloud Mail",
5
5
  "main": "index.js",
6
6
  "bin": {
package/run-digest.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // ─── Inbox Intelligence — Digest Status Helper ────────────────────────────────
3
+ // Shows current digest state and what's pending. Not the runner itself —
4
+ // Claude Code is the runner. Use this to inspect state or reset it.
5
+ //
6
+ // Usage:
7
+ // node run-digest.js → show current state
8
+ // node run-digest.js --reset → clear all processed UIDs and state
9
+
10
+ import { getDigestState, updateDigestState } from './lib/digest.js';
11
+
12
+ const args = process.argv.slice(2);
13
+
14
+ if (args.includes('--reset')) {
15
+ updateDigestState({
16
+ lastRun: null,
17
+ processedUids: [],
18
+ pendingActions: [],
19
+ skipCounts: {}
20
+ });
21
+ // Force full reset by re-reading and overwriting
22
+ const { writeFileSync } = await import('fs');
23
+ const { homedir } = await import('os');
24
+ const { join } = await import('path');
25
+ writeFileSync(
26
+ join(homedir(), '.icloud-mcp-digest.json'),
27
+ JSON.stringify({ lastRun: null, processedUids: [], pendingActions: [], skipCounts: {} }, null, 2)
28
+ );
29
+ console.log('✓ Digest state reset.');
30
+ process.exit(0);
31
+ }
32
+
33
+ const state = getDigestState();
34
+
35
+ console.log('\n── Digest State ──────────────────────────────────────────');
36
+ console.log(`Last run: ${state.lastRun ? new Date(state.lastRun).toLocaleString() : 'never'}`);
37
+ console.log(`Processed UIDs: ${state.processedUids.length}`);
38
+ console.log(`Pending actions: ${state.pendingActions.length}`);
39
+
40
+ if (state.pendingActions.length > 0) {
41
+ for (const a of state.pendingActions) {
42
+ console.log(` • [${a.type}] ${a.subject}${a.dueDate ? ' — due ' + a.dueDate : ''}`);
43
+ }
44
+ }
45
+
46
+ const candidates = Object.entries(state.skipCounts)
47
+ .filter(([, c]) => c >= 3)
48
+ .sort((a, b) => b[1] - a[1]);
49
+
50
+ if (candidates.length > 0) {
51
+ console.log(`\nUnsubscribe candidates (skipped 3+ times):`);
52
+ for (const [sender, count] of candidates) {
53
+ console.log(` • ${sender} (${count}×)`);
54
+ }
55
+ }
56
+
57
+ console.log('──────────────────────────────────────────────────────────\n');