apple-mail-mcp 1.6.10 → 1.7.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/README.md +37 -0
- package/build/index.js +24 -2
- package/build/services/imapClient.d.ts +75 -0
- package/build/services/imapClient.d.ts.map +1 -0
- package/build/services/imapClient.js +177 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -270,6 +270,43 @@ Then send:
|
|
|
270
270
|
|
|
271
271
|
The default `applescript` transport is unchanged; SMTP is opt-in per call.
|
|
272
272
|
|
|
273
|
+
##### IMAP backend (read/search) — opt-in, Phase 1
|
|
274
|
+
|
|
275
|
+
AppleScript runs `search`/`list` predicates client-side over the Apple Event
|
|
276
|
+
bridge, which is slow and can time out (false-empty) on large Gmail/IMAP
|
|
277
|
+
mailboxes (see [#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)). When
|
|
278
|
+
an account is configured for IMAP, `search-messages` and `list-messages` instead
|
|
279
|
+
run a **server-side IMAP search** ([#43](https://github.com/sweetrb/apple-mail-mcp/issues/43)) —
|
|
280
|
+
typically sub-second and correct on the same mailbox where AppleScript times out.
|
|
281
|
+
This is **opt-in and additive**: any account without IMAP configured behaves
|
|
282
|
+
exactly as before (AppleScript). Read-only for now — `get-message` and all
|
|
283
|
+
mutations stay on AppleScript (the IMAP rows report message **UIDs**, noted in
|
|
284
|
+
the output).
|
|
285
|
+
|
|
286
|
+
Routing is conservative: only a call whose explicit `account` matches the
|
|
287
|
+
configured IMAP account goes to IMAP; everything else falls through to
|
|
288
|
+
AppleScript.
|
|
289
|
+
|
|
290
|
+
| Variable | Required | Default | Description |
|
|
291
|
+
|----------|----------|---------|-------------|
|
|
292
|
+
| `APPLE_MAIL_MCP_IMAP_USER` | Yes | — | Login address; setting it enables IMAP |
|
|
293
|
+
| `APPLE_MAIL_MCP_IMAP_ACCOUNT` | No | = user | Mail account name to match for routing |
|
|
294
|
+
| `APPLE_MAIL_MCP_IMAP_HOST` | No | `imap.gmail.com` | IMAP server hostname |
|
|
295
|
+
| `APPLE_MAIL_MCP_IMAP_PORT` | No | `993` | IMAP port (993 = implicit TLS) |
|
|
296
|
+
| `APPLE_MAIL_MCP_IMAP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
|
|
297
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | No | — | Keychain item service/server name |
|
|
298
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
|
|
299
|
+
|
|
300
|
+
As with SMTP, the password is read from the macOS **Keychain** by default (use
|
|
301
|
+
an app-specific password for Gmail/Workspace/iCloud), so no secret goes in
|
|
302
|
+
config. Gmail label semantics: common names (`All Mail`, `Sent`, `Trash`,
|
|
303
|
+
`Spam`, `Important`, …) map to their `[Gmail]/…` IMAP paths automatically.
|
|
304
|
+
|
|
305
|
+
> Note: each call currently opens its own IMAP connection (no pooling yet), so
|
|
306
|
+
> expect a few seconds of connection overhead per call. Phase 2 (IMAP-backed
|
|
307
|
+
> mutations + folder ops, which would also resolve the IMAP slice of
|
|
308
|
+
> [#42](https://github.com/sweetrb/apple-mail-mcp/issues/42)) is not yet implemented.
|
|
309
|
+
|
|
273
310
|
---
|
|
274
311
|
|
|
275
312
|
#### `send-serial-email`
|
package/build/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import { AppleMailManager } from "./services/appleMailManager.js";
|
|
27
27
|
import { sendViaSmtp } from "./services/smtpMailer.js";
|
|
28
|
+
import { isImapAccount, imapSearchMessages, imapListMessages } from "./services/imapClient.js";
|
|
28
29
|
import { createSerialGate } from "./utils/serialize.js";
|
|
29
30
|
// =============================================================================
|
|
30
31
|
// Shared Validation Schemas
|
|
@@ -157,7 +158,23 @@ server.tool("search-messages", {
|
|
|
157
158
|
dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
|
|
158
159
|
dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
|
|
159
160
|
limit: z.number().optional().describe("Maximum number of results (default: 50)"),
|
|
160
|
-
}, withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
|
|
161
|
+
}, withErrorHandling(async ({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
|
|
162
|
+
// IMAP backend (issue #43): server-side search when this account is
|
|
163
|
+
// explicitly configured for IMAP; otherwise fall through to AppleScript.
|
|
164
|
+
if (isImapAccount(account)) {
|
|
165
|
+
return successResponse(await imapSearchMessages({
|
|
166
|
+
query,
|
|
167
|
+
mailbox,
|
|
168
|
+
account,
|
|
169
|
+
limit,
|
|
170
|
+
dateFrom,
|
|
171
|
+
dateTo,
|
|
172
|
+
from,
|
|
173
|
+
subject,
|
|
174
|
+
isRead,
|
|
175
|
+
isFlagged,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
161
178
|
const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(query, mailbox, account, limit, dateFrom, dateTo, from, subject, isRead, isFlagged);
|
|
162
179
|
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
163
180
|
if (messages.length === 0) {
|
|
@@ -200,7 +217,12 @@ server.tool("list-messages", {
|
|
|
200
217
|
offset: z.number().optional().describe("Number of messages to skip (for pagination)"),
|
|
201
218
|
from: z.string().optional().describe("Filter by sender email address or name"),
|
|
202
219
|
unreadOnly: z.boolean().optional().describe("Only show unread messages"),
|
|
203
|
-
}, withErrorHandling(({ mailbox, account, limit = 50, offset = 0, from }) => {
|
|
220
|
+
}, withErrorHandling(async ({ mailbox, account, limit = 50, offset = 0, from, unreadOnly }) => {
|
|
221
|
+
// IMAP backend (issue #43): server-side listing when this account is
|
|
222
|
+
// explicitly configured for IMAP; otherwise fall through to AppleScript.
|
|
223
|
+
if (isImapAccount(account)) {
|
|
224
|
+
return successResponse(await imapListMessages({ mailbox, account, limit, offset, from, unreadOnly }));
|
|
225
|
+
}
|
|
204
226
|
const { messages, diagnostics } = mailManager.listMessagesWithDiagnostics(mailbox, account, limit, from, offset);
|
|
205
227
|
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
206
228
|
if (messages.length === 0) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export declare const IMAP_ENV: {
|
|
2
|
+
readonly user: "APPLE_MAIL_MCP_IMAP_USER";
|
|
3
|
+
readonly account: "APPLE_MAIL_MCP_IMAP_ACCOUNT";
|
|
4
|
+
readonly host: "APPLE_MAIL_MCP_IMAP_HOST";
|
|
5
|
+
readonly port: "APPLE_MAIL_MCP_IMAP_PORT";
|
|
6
|
+
readonly password: "APPLE_MAIL_MCP_IMAP_PASSWORD";
|
|
7
|
+
readonly keychainService: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE";
|
|
8
|
+
readonly keychainAccount: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT";
|
|
9
|
+
};
|
|
10
|
+
export interface ImapConfig {
|
|
11
|
+
host: string;
|
|
12
|
+
port: number;
|
|
13
|
+
secure: boolean;
|
|
14
|
+
user: string;
|
|
15
|
+
pass: string;
|
|
16
|
+
accountLabel: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ImapSearchArgs {
|
|
19
|
+
query?: string;
|
|
20
|
+
account?: string;
|
|
21
|
+
from?: string;
|
|
22
|
+
subject?: string;
|
|
23
|
+
mailbox?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
dateFrom?: string;
|
|
26
|
+
dateTo?: string;
|
|
27
|
+
isRead?: boolean;
|
|
28
|
+
isFlagged?: boolean;
|
|
29
|
+
unreadOnly?: boolean;
|
|
30
|
+
offset?: number;
|
|
31
|
+
}
|
|
32
|
+
interface ImapAddress {
|
|
33
|
+
name?: string;
|
|
34
|
+
address?: string;
|
|
35
|
+
}
|
|
36
|
+
interface ImapEnvelope {
|
|
37
|
+
subject?: string;
|
|
38
|
+
date?: Date | string;
|
|
39
|
+
from?: ImapAddress[];
|
|
40
|
+
}
|
|
41
|
+
interface ImapMessage {
|
|
42
|
+
uid: number;
|
|
43
|
+
envelope?: ImapEnvelope;
|
|
44
|
+
flags?: Set<string>;
|
|
45
|
+
}
|
|
46
|
+
interface MailboxLock {
|
|
47
|
+
release: () => void;
|
|
48
|
+
}
|
|
49
|
+
export interface ImapClientLike {
|
|
50
|
+
connect(): Promise<void>;
|
|
51
|
+
getMailboxLock(path: string): Promise<MailboxLock>;
|
|
52
|
+
search(query: Record<string, unknown>, opts: {
|
|
53
|
+
uid: true;
|
|
54
|
+
}): Promise<number[] | false>;
|
|
55
|
+
fetch(range: string, query: Record<string, unknown>, opts: {
|
|
56
|
+
uid: true;
|
|
57
|
+
}): AsyncIterable<ImapMessage>;
|
|
58
|
+
logout(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export type ImapConnect = (cfg: ImapConfig) => Promise<ImapClientLike>;
|
|
61
|
+
/** True only when IMAP is configured AND the explicit `account` matches it. */
|
|
62
|
+
export declare function isImapAccount(account: string | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
63
|
+
export declare function resolveImapConfig(env?: NodeJS.ProcessEnv): ImapConfig;
|
|
64
|
+
/** Map common (Gmail) mailbox names to their IMAP paths. */
|
|
65
|
+
export declare function resolveMailboxPath(mailbox: string | undefined, mode: "search" | "list"): string;
|
|
66
|
+
export declare function imapSearchMessages(args: ImapSearchArgs, deps?: {
|
|
67
|
+
connect?: ImapConnect;
|
|
68
|
+
config?: ImapConfig;
|
|
69
|
+
}): Promise<string>;
|
|
70
|
+
export declare function imapListMessages(args: ImapSearchArgs, deps?: {
|
|
71
|
+
connect?: ImapConnect;
|
|
72
|
+
config?: ImapConfig;
|
|
73
|
+
}): Promise<string>;
|
|
74
|
+
export {};
|
|
75
|
+
//# sourceMappingURL=imapClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IMAP backend for read/search (issue #43, Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* AppleScript-over-Mail.app is the default. When an account is explicitly
|
|
5
|
+
* configured for IMAP (env below), `search-messages` and `list-messages` route
|
|
6
|
+
* here instead, running a SERVER-SIDE search — orders of magnitude faster than
|
|
7
|
+
* AppleScript's client-side `whose` enumeration on large Gmail mailboxes, and
|
|
8
|
+
* correct (no false-empty on timeout). Read-only; mutations stay on AppleScript.
|
|
9
|
+
*
|
|
10
|
+
* Opt-in via env (mirrors the SMTP transport pattern):
|
|
11
|
+
* APPLE_MAIL_MCP_IMAP_USER (required — enables IMAP; the login address)
|
|
12
|
+
* APPLE_MAIL_MCP_IMAP_ACCOUNT (Mail account name to match for routing; default = USER)
|
|
13
|
+
* APPLE_MAIL_MCP_IMAP_HOST (default imap.gmail.com)
|
|
14
|
+
* APPLE_MAIL_MCP_IMAP_PORT (default 993, implicit TLS)
|
|
15
|
+
* APPLE_MAIL_MCP_IMAP_PASSWORD (else Keychain via the two vars below)
|
|
16
|
+
* APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE / _KEYCHAIN_ACCOUNT
|
|
17
|
+
*
|
|
18
|
+
* @module services/imapClient
|
|
19
|
+
*/
|
|
20
|
+
import { ImapFlow } from "imapflow";
|
|
21
|
+
import { readKeychainPassword } from "../services/smtpMailer.js";
|
|
22
|
+
export const IMAP_ENV = {
|
|
23
|
+
user: "APPLE_MAIL_MCP_IMAP_USER",
|
|
24
|
+
account: "APPLE_MAIL_MCP_IMAP_ACCOUNT",
|
|
25
|
+
host: "APPLE_MAIL_MCP_IMAP_HOST",
|
|
26
|
+
port: "APPLE_MAIL_MCP_IMAP_PORT",
|
|
27
|
+
password: "APPLE_MAIL_MCP_IMAP_PASSWORD",
|
|
28
|
+
keychainService: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE",
|
|
29
|
+
keychainAccount: "APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT",
|
|
30
|
+
};
|
|
31
|
+
/** True only when IMAP is configured AND the explicit `account` matches it. */
|
|
32
|
+
export function isImapAccount(account, env = process.env) {
|
|
33
|
+
const user = env[IMAP_ENV.user]?.trim();
|
|
34
|
+
if (!user || !account)
|
|
35
|
+
return false;
|
|
36
|
+
const label = env[IMAP_ENV.account]?.trim() || user;
|
|
37
|
+
return account === label || account === user;
|
|
38
|
+
}
|
|
39
|
+
export function resolveImapConfig(env = process.env) {
|
|
40
|
+
const user = env[IMAP_ENV.user]?.trim();
|
|
41
|
+
if (!user) {
|
|
42
|
+
throw new Error(`IMAP not configured. Set ${IMAP_ENV.user} (login address) to enable it.`);
|
|
43
|
+
}
|
|
44
|
+
const host = env[IMAP_ENV.host]?.trim() || "imap.gmail.com";
|
|
45
|
+
const port = env[IMAP_ENV.port] ? Number.parseInt(env[IMAP_ENV.port], 10) : 993;
|
|
46
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
47
|
+
throw new Error(`Invalid ${IMAP_ENV.port}: "${env[IMAP_ENV.port]}".`);
|
|
48
|
+
}
|
|
49
|
+
let pass = env[IMAP_ENV.password];
|
|
50
|
+
if (!pass) {
|
|
51
|
+
const svc = env[IMAP_ENV.keychainService]?.trim();
|
|
52
|
+
const acct = env[IMAP_ENV.keychainAccount]?.trim() || user;
|
|
53
|
+
if (svc)
|
|
54
|
+
pass = readKeychainPassword(svc, acct) ?? undefined;
|
|
55
|
+
}
|
|
56
|
+
if (!pass) {
|
|
57
|
+
throw new Error(`No IMAP password. Set ${IMAP_ENV.password}, or ${IMAP_ENV.keychainService}/${IMAP_ENV.keychainAccount} for the Keychain.`);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
host,
|
|
61
|
+
port,
|
|
62
|
+
secure: port === 993,
|
|
63
|
+
user,
|
|
64
|
+
pass,
|
|
65
|
+
accountLabel: env[IMAP_ENV.account]?.trim() || user,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const defaultConnect = async (cfg) => {
|
|
69
|
+
const client = new ImapFlow({
|
|
70
|
+
host: cfg.host,
|
|
71
|
+
port: cfg.port,
|
|
72
|
+
secure: cfg.secure,
|
|
73
|
+
auth: { user: cfg.user, pass: cfg.pass },
|
|
74
|
+
logger: false,
|
|
75
|
+
});
|
|
76
|
+
await client.connect();
|
|
77
|
+
return client;
|
|
78
|
+
};
|
|
79
|
+
/** Map common (Gmail) mailbox names to their IMAP paths. */
|
|
80
|
+
export function resolveMailboxPath(mailbox, mode) {
|
|
81
|
+
if (!mailbox)
|
|
82
|
+
return mode === "search" ? "[Gmail]/All Mail" : "INBOX";
|
|
83
|
+
const map = {
|
|
84
|
+
"all mail": "[Gmail]/All Mail",
|
|
85
|
+
"sent mail": "[Gmail]/Sent Mail",
|
|
86
|
+
sent: "[Gmail]/Sent Mail",
|
|
87
|
+
trash: "[Gmail]/Trash",
|
|
88
|
+
drafts: "[Gmail]/Drafts",
|
|
89
|
+
spam: "[Gmail]/Spam",
|
|
90
|
+
junk: "[Gmail]/Spam",
|
|
91
|
+
starred: "[Gmail]/Starred",
|
|
92
|
+
important: "[Gmail]/Important",
|
|
93
|
+
};
|
|
94
|
+
return map[mailbox.trim().toLowerCase()] ?? mailbox;
|
|
95
|
+
}
|
|
96
|
+
function buildCriteria(a, listMode) {
|
|
97
|
+
const c = {};
|
|
98
|
+
if (a.query)
|
|
99
|
+
c.or = [{ subject: a.query }, { from: a.query }];
|
|
100
|
+
if (a.from)
|
|
101
|
+
c.from = a.from;
|
|
102
|
+
if (a.subject)
|
|
103
|
+
c.subject = a.subject;
|
|
104
|
+
if (a.isRead === true)
|
|
105
|
+
c.seen = true;
|
|
106
|
+
if (a.isRead === false)
|
|
107
|
+
c.unseen = true;
|
|
108
|
+
if (a.unreadOnly && listMode)
|
|
109
|
+
c.unseen = true;
|
|
110
|
+
if (a.isFlagged === true)
|
|
111
|
+
c.flagged = true;
|
|
112
|
+
if (a.isFlagged === false)
|
|
113
|
+
c.unflagged = true;
|
|
114
|
+
if (a.dateFrom)
|
|
115
|
+
c.since = new Date(a.dateFrom);
|
|
116
|
+
if (a.dateTo)
|
|
117
|
+
c.before = new Date(a.dateTo);
|
|
118
|
+
if (Object.keys(c).length === 0)
|
|
119
|
+
c.all = true;
|
|
120
|
+
return c;
|
|
121
|
+
}
|
|
122
|
+
function formatRow(m) {
|
|
123
|
+
const env = m.envelope ?? {};
|
|
124
|
+
const subject = env.subject || "(no subject)";
|
|
125
|
+
const a = env.from?.[0];
|
|
126
|
+
const from = a
|
|
127
|
+
? a.name
|
|
128
|
+
? `${a.name} <${a.address ?? ""}>`
|
|
129
|
+
: (a.address ?? "(unknown)")
|
|
130
|
+
: "(unknown)";
|
|
131
|
+
const date = env.date ? new Date(env.date).toLocaleDateString() : "";
|
|
132
|
+
const read = m.flags?.has("\\Seen") ? "read" : "unread";
|
|
133
|
+
return ` - UID: ${m.uid} | ${date} | ${subject} (from: ${from}) [${read}]`;
|
|
134
|
+
}
|
|
135
|
+
async function run(args, listMode, deps) {
|
|
136
|
+
const cfg = deps.config ?? resolveImapConfig();
|
|
137
|
+
const client = await (deps.connect ?? defaultConnect)(cfg);
|
|
138
|
+
try {
|
|
139
|
+
const path = resolveMailboxPath(args.mailbox, listMode ? "list" : "search");
|
|
140
|
+
const lock = await client.getMailboxLock(path);
|
|
141
|
+
try {
|
|
142
|
+
const found = await client.search(buildCriteria(args, listMode), { uid: true });
|
|
143
|
+
const uids = Array.isArray(found) ? found : [];
|
|
144
|
+
if (uids.length === 0) {
|
|
145
|
+
return `No messages found via IMAP in "${path}" (account ${cfg.accountLabel}).`;
|
|
146
|
+
}
|
|
147
|
+
const limit = args.limit ?? 50;
|
|
148
|
+
const offset = args.offset ?? 0;
|
|
149
|
+
// UIDs are ascending → newest are the highest. Apply offset+limit from the newest end.
|
|
150
|
+
const newest = uids
|
|
151
|
+
.slice()
|
|
152
|
+
.reverse()
|
|
153
|
+
.slice(offset, offset + limit);
|
|
154
|
+
const byUid = new Map();
|
|
155
|
+
for await (const msg of client.fetch(newest.join(","), { envelope: true, flags: true }, { uid: true })) {
|
|
156
|
+
byUid.set(msg.uid, formatRow(msg));
|
|
157
|
+
}
|
|
158
|
+
const rows = newest.map((u) => byUid.get(u)).filter((r) => Boolean(r));
|
|
159
|
+
const verb = listMode ? "listed" : "matched";
|
|
160
|
+
return (`Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, mailbox "${path}"; ${uids.length} total ${verb}):\n` +
|
|
161
|
+
rows.join("\n") +
|
|
162
|
+
`\n\nNote: IDs are IMAP UIDs. get-message and message mutations still use the AppleScript path (Phase 1 is read/search only).`);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
lock.release();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
await client.logout().catch(() => undefined);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export function imapSearchMessages(args, deps = {}) {
|
|
173
|
+
return run(args, false, deps);
|
|
174
|
+
}
|
|
175
|
+
export function imapListMessages(args, deps = {}) {
|
|
176
|
+
return run(args, true, deps);
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-mail-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
62
|
+
"imapflow": "^1.4.1",
|
|
62
63
|
"nodemailer": "^9.0.0",
|
|
63
64
|
"zod": "^3.22.4"
|
|
64
65
|
},
|