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.
- package/.claude/scheduled_tasks.lock +1 -0
- package/index.js +185 -55
- package/lib/digest.js +46 -0
- package/lib/imap.js +118 -116
- package/lib/smtp.js +20 -20
- package/package.json +1 -1
- package/run-digest.js +57 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
979
|
+
const creds = resolveCreds(args.account);
|
|
980
|
+
result = await withTimeout('list_mailboxes', TIMEOUT.METADATA, () => listMailboxes(creds));
|
|
899
981
|
} else if (name === 'create_mailbox') {
|
|
900
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
966
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ───
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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
|
|
24
|
+
host,
|
|
24
25
|
port: 993,
|
|
25
26
|
secure: true,
|
|
26
|
-
auth: { user
|
|
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
|
-
|
|
1016
|
-
const
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
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:
|
|
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:
|
|
190
|
+
host: imapHost,
|
|
191
191
|
port: 993,
|
|
192
192
|
secure: true,
|
|
193
193
|
auth: { user, pass },
|
package/package.json
CHANGED
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');
|