hypermail-mcp 0.7.8 → 0.7.9
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 +27 -57
- package/dist/cli.js +404 -450
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17038,7 +17038,7 @@ function registerAccountTools(server, ctx) {
|
|
|
17038
17038
|
}
|
|
17039
17039
|
|
|
17040
17040
|
// src/tools/browse.ts
|
|
17041
|
-
import { z as
|
|
17041
|
+
import { z as z5 } from "zod";
|
|
17042
17042
|
|
|
17043
17043
|
// src/html-to-markdown.ts
|
|
17044
17044
|
import TurndownService from "turndown";
|
|
@@ -17066,32 +17066,288 @@ function selectBody(msg, format) {
|
|
|
17066
17066
|
}
|
|
17067
17067
|
}
|
|
17068
17068
|
|
|
17069
|
-
// src/tools/
|
|
17070
|
-
|
|
17071
|
-
|
|
17072
|
-
|
|
17069
|
+
// src/tools/new-emails.ts
|
|
17070
|
+
import { z as z4 } from "zod";
|
|
17071
|
+
var DEFAULT_LIMIT = 10;
|
|
17072
|
+
var BODY_LIMIT = 2e4;
|
|
17073
|
+
var PAGE_SIZE = 100;
|
|
17074
|
+
var MISSING_RECEIVED_AT = "1970-01-01T00:00:00.000Z";
|
|
17075
|
+
function registerNewEmailTool(server, ctx) {
|
|
17076
|
+
const { store, registry, tools } = ctx;
|
|
17077
|
+
if (!shouldRegister("get_new_emails", tools)) return;
|
|
17078
|
+
const newEmailOutputSchema = z4.object({
|
|
17073
17079
|
account: z4.string(),
|
|
17074
|
-
|
|
17075
|
-
|
|
17076
|
-
|
|
17077
|
-
|
|
17080
|
+
id: z4.string(),
|
|
17081
|
+
subject: z4.string(),
|
|
17082
|
+
from: emailAddrOutputSchema.optional(),
|
|
17083
|
+
to: z4.array(emailAddrOutputSchema).optional(),
|
|
17084
|
+
cc: z4.array(emailAddrOutputSchema).optional(),
|
|
17085
|
+
bcc: z4.array(emailAddrOutputSchema).optional(),
|
|
17086
|
+
receivedAt: z4.string().optional(),
|
|
17087
|
+
preview: z4.string().optional(),
|
|
17088
|
+
isRead: z4.boolean().optional(),
|
|
17089
|
+
hasAttachments: z4.boolean().optional(),
|
|
17090
|
+
folder: z4.string().optional(),
|
|
17091
|
+
attachments: z4.array(attachmentMetaOutputSchema).optional(),
|
|
17092
|
+
body: z4.string(),
|
|
17093
|
+
bodyFormat: z4.literal("markdown"),
|
|
17094
|
+
bodyTruncated: z4.boolean(),
|
|
17095
|
+
bodyOriginalLength: z4.number()
|
|
17078
17096
|
});
|
|
17079
|
-
const
|
|
17080
|
-
account: z4.string(),
|
|
17097
|
+
const outputSchema = z4.object({
|
|
17081
17098
|
count: z4.number(),
|
|
17082
|
-
|
|
17099
|
+
emails: z4.array(newEmailOutputSchema),
|
|
17100
|
+
errors: z4.array(z4.object({ account: z4.string(), message: z4.string() }))
|
|
17101
|
+
});
|
|
17102
|
+
server.registerTool(
|
|
17103
|
+
"get_new_emails",
|
|
17104
|
+
{
|
|
17105
|
+
description: "Fetch a bounded batch of inbox emails not previously returned by this tool. Agents should call this on their own schedule. Bodies are returned as markdown and may be truncated.",
|
|
17106
|
+
inputSchema: z4.object({
|
|
17107
|
+
account: z4.string().email().optional().describe(
|
|
17108
|
+
"Email account to poll. If omitted, polls all accounts with one global limit."
|
|
17109
|
+
),
|
|
17110
|
+
limit: z4.number().int().min(0).default(DEFAULT_LIMIT).optional().describe(
|
|
17111
|
+
"Maximum emails to return. Defaults to 10. Use 0 to initialize/check without fetching bodies."
|
|
17112
|
+
)
|
|
17113
|
+
}),
|
|
17114
|
+
outputSchema
|
|
17115
|
+
},
|
|
17116
|
+
async (args) => {
|
|
17117
|
+
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
17118
|
+
if (args.account) {
|
|
17119
|
+
try {
|
|
17120
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
17121
|
+
const result = await collectCandidatesForAccount(store, provider, account);
|
|
17122
|
+
const selected2 = oldestCandidatesFirst(result.candidates).slice(0, limit);
|
|
17123
|
+
const emails2 = limit === 0 ? [] : await hydrateAndAdvance(store, provider, result.account, selected2);
|
|
17124
|
+
const data2 = { count: emails2.length, emails: emails2, errors: [] };
|
|
17125
|
+
return ok(data2, data2);
|
|
17126
|
+
} catch (err) {
|
|
17127
|
+
return fail(errMsg(err));
|
|
17128
|
+
}
|
|
17129
|
+
}
|
|
17130
|
+
const accounts = store.listAccounts();
|
|
17131
|
+
if (accounts.length === 0) {
|
|
17132
|
+
return fail("no accounts registered. Call add_account first.");
|
|
17133
|
+
}
|
|
17134
|
+
const errors = [];
|
|
17135
|
+
const collected = [];
|
|
17136
|
+
const providersByEmail = /* @__PURE__ */ new Map();
|
|
17137
|
+
const accountsByEmail = /* @__PURE__ */ new Map();
|
|
17138
|
+
for (const stored of accounts) {
|
|
17139
|
+
try {
|
|
17140
|
+
const { provider, account } = registry.resolveByEmail(stored.email);
|
|
17141
|
+
const result = await collectCandidatesForAccount(store, provider, account);
|
|
17142
|
+
providersByEmail.set(result.account.email, provider);
|
|
17143
|
+
accountsByEmail.set(result.account.email, result.account);
|
|
17144
|
+
collected.push(...result.candidates);
|
|
17145
|
+
} catch (err) {
|
|
17146
|
+
errors.push({ account: stored.email, message: errMsg(err) });
|
|
17147
|
+
}
|
|
17148
|
+
}
|
|
17149
|
+
const selected = oldestCandidatesFirst(collected).slice(0, limit);
|
|
17150
|
+
const emails = [];
|
|
17151
|
+
if (limit > 0) {
|
|
17152
|
+
const byAccount = /* @__PURE__ */ new Map();
|
|
17153
|
+
for (const candidate of selected) {
|
|
17154
|
+
const items = byAccount.get(candidate.account.email) ?? [];
|
|
17155
|
+
items.push(candidate);
|
|
17156
|
+
byAccount.set(candidate.account.email, items);
|
|
17157
|
+
}
|
|
17158
|
+
for (const [email, accountCandidates] of byAccount) {
|
|
17159
|
+
const provider = providersByEmail.get(email);
|
|
17160
|
+
const account = accountsByEmail.get(email);
|
|
17161
|
+
if (!provider || !account) continue;
|
|
17162
|
+
try {
|
|
17163
|
+
emails.push(
|
|
17164
|
+
...await hydrateAndAdvance(
|
|
17165
|
+
store,
|
|
17166
|
+
provider,
|
|
17167
|
+
account,
|
|
17168
|
+
accountCandidates
|
|
17169
|
+
)
|
|
17170
|
+
);
|
|
17171
|
+
} catch (err) {
|
|
17172
|
+
errors.push({ account: email, message: errMsg(err) });
|
|
17173
|
+
}
|
|
17174
|
+
}
|
|
17175
|
+
}
|
|
17176
|
+
const orderedEmails = emails.sort(compareNewEmailOutputOldestFirst);
|
|
17177
|
+
const data = { count: orderedEmails.length, emails: orderedEmails, errors };
|
|
17178
|
+
return ok(data, data);
|
|
17179
|
+
}
|
|
17180
|
+
);
|
|
17181
|
+
}
|
|
17182
|
+
async function collectCandidatesForAccount(store, provider, account) {
|
|
17183
|
+
const checkpoint = normalizeCheckpoint(account.newEmailCheckpoint);
|
|
17184
|
+
if (!checkpoint) {
|
|
17185
|
+
await initializeCheckpoint(store, provider, account);
|
|
17186
|
+
return { account, candidates: [] };
|
|
17187
|
+
}
|
|
17188
|
+
const deliveredAtCheckpoint = new Set(checkpoint.deliveredIdsAtReceivedAt ?? []);
|
|
17189
|
+
const candidates = [];
|
|
17190
|
+
let skip = 0;
|
|
17191
|
+
while (true) {
|
|
17192
|
+
const { items, hasMore } = await provider.listEmails(account, {
|
|
17193
|
+
folder: "inbox",
|
|
17194
|
+
limit: PAGE_SIZE,
|
|
17195
|
+
skip
|
|
17196
|
+
});
|
|
17197
|
+
if (items.length === 0) break;
|
|
17198
|
+
let sawOlderThanCheckpoint = false;
|
|
17199
|
+
for (const item of items) {
|
|
17200
|
+
const timestamp = effectiveReceivedAt(item.receivedAt);
|
|
17201
|
+
const comparison = compareTimestamp(timestamp, checkpoint.receivedAt);
|
|
17202
|
+
if (comparison > 0) {
|
|
17203
|
+
candidates.push({ account, summary: item, timestamp });
|
|
17204
|
+
} else if (comparison === 0) {
|
|
17205
|
+
if (!deliveredAtCheckpoint.has(item.id)) {
|
|
17206
|
+
candidates.push({ account, summary: item, timestamp });
|
|
17207
|
+
}
|
|
17208
|
+
} else {
|
|
17209
|
+
sawOlderThanCheckpoint = true;
|
|
17210
|
+
}
|
|
17211
|
+
}
|
|
17212
|
+
if (sawOlderThanCheckpoint || !hasMore) break;
|
|
17213
|
+
skip += items.length;
|
|
17214
|
+
}
|
|
17215
|
+
return { account, candidates };
|
|
17216
|
+
}
|
|
17217
|
+
async function initializeCheckpoint(store, provider, account) {
|
|
17218
|
+
const { items } = await provider.listEmails(account, {
|
|
17219
|
+
folder: "inbox",
|
|
17220
|
+
limit: PAGE_SIZE
|
|
17221
|
+
});
|
|
17222
|
+
const first = items[0];
|
|
17223
|
+
const receivedAt = first ? effectiveReceivedAt(first.receivedAt) : (/* @__PURE__ */ new Date()).toISOString();
|
|
17224
|
+
const deliveredIdsAtReceivedAt = items.filter((item) => effectiveReceivedAt(item.receivedAt) === receivedAt).map((item) => item.id);
|
|
17225
|
+
await store.upsertAccount({
|
|
17226
|
+
...account,
|
|
17227
|
+
newEmailCheckpoint: { receivedAt, deliveredIdsAtReceivedAt }
|
|
17228
|
+
});
|
|
17229
|
+
}
|
|
17230
|
+
async function hydrateAndAdvance(store, provider, account, selected) {
|
|
17231
|
+
if (selected.length === 0) return [];
|
|
17232
|
+
const emails = [];
|
|
17233
|
+
for (const candidate of selected) {
|
|
17234
|
+
const full = await provider.readEmail(account, candidate.summary.id);
|
|
17235
|
+
emails.push(formatNewEmail(account.email, full, candidate.summary));
|
|
17236
|
+
}
|
|
17237
|
+
await advanceCheckpoint(store, account, selected);
|
|
17238
|
+
return emails;
|
|
17239
|
+
}
|
|
17240
|
+
function formatNewEmail(account, msg, summary) {
|
|
17241
|
+
const body = selectBody(msg, "markdown");
|
|
17242
|
+
const bodyTruncated = body.length > BODY_LIMIT;
|
|
17243
|
+
return {
|
|
17244
|
+
account,
|
|
17245
|
+
id: msg.id,
|
|
17246
|
+
subject: msg.subject,
|
|
17247
|
+
from: msg.from,
|
|
17248
|
+
to: msg.to,
|
|
17249
|
+
cc: msg.cc,
|
|
17250
|
+
bcc: msg.bcc,
|
|
17251
|
+
receivedAt: msg.receivedAt ?? summary.receivedAt,
|
|
17252
|
+
preview: msg.preview,
|
|
17253
|
+
isRead: msg.isRead,
|
|
17254
|
+
hasAttachments: msg.hasAttachments,
|
|
17255
|
+
folder: msg.folder,
|
|
17256
|
+
attachments: msg.attachments,
|
|
17257
|
+
body: bodyTruncated ? body.slice(0, BODY_LIMIT) : body,
|
|
17258
|
+
bodyFormat: "markdown",
|
|
17259
|
+
bodyTruncated,
|
|
17260
|
+
bodyOriginalLength: body.length
|
|
17261
|
+
};
|
|
17262
|
+
}
|
|
17263
|
+
async function advanceCheckpoint(store, account, selected) {
|
|
17264
|
+
const ordered = oldestCandidatesFirst(selected);
|
|
17265
|
+
const newest = ordered[ordered.length - 1];
|
|
17266
|
+
if (!newest) return;
|
|
17267
|
+
const previous = normalizeCheckpoint(account.newEmailCheckpoint);
|
|
17268
|
+
const newestTimestamp = newest.timestamp;
|
|
17269
|
+
const idsAtNewest = ordered.filter((candidate) => candidate.timestamp === newestTimestamp).map((candidate) => candidate.summary.id);
|
|
17270
|
+
let deliveredIdsAtReceivedAt = idsAtNewest;
|
|
17271
|
+
if (previous?.receivedAt === newestTimestamp) {
|
|
17272
|
+
deliveredIdsAtReceivedAt = [
|
|
17273
|
+
.../* @__PURE__ */ new Set([
|
|
17274
|
+
...previous.deliveredIdsAtReceivedAt ?? [],
|
|
17275
|
+
...idsAtNewest
|
|
17276
|
+
])
|
|
17277
|
+
];
|
|
17278
|
+
}
|
|
17279
|
+
await store.upsertAccount({
|
|
17280
|
+
...account,
|
|
17281
|
+
newEmailCheckpoint: {
|
|
17282
|
+
receivedAt: newestTimestamp,
|
|
17283
|
+
deliveredIdsAtReceivedAt
|
|
17284
|
+
}
|
|
17285
|
+
});
|
|
17286
|
+
}
|
|
17287
|
+
function normalizeCheckpoint(checkpoint) {
|
|
17288
|
+
const receivedAt = normalizeTimestamp(checkpoint?.receivedAt);
|
|
17289
|
+
if (!receivedAt) return null;
|
|
17290
|
+
return {
|
|
17291
|
+
receivedAt,
|
|
17292
|
+
deliveredIdsAtReceivedAt: checkpoint?.deliveredIdsAtReceivedAt ?? []
|
|
17293
|
+
};
|
|
17294
|
+
}
|
|
17295
|
+
function effectiveReceivedAt(receivedAt) {
|
|
17296
|
+
return normalizeTimestamp(receivedAt) ?? MISSING_RECEIVED_AT;
|
|
17297
|
+
}
|
|
17298
|
+
function normalizeTimestamp(value) {
|
|
17299
|
+
if (!value) return null;
|
|
17300
|
+
const ms = Date.parse(value);
|
|
17301
|
+
if (!Number.isFinite(ms)) return null;
|
|
17302
|
+
return new Date(ms).toISOString();
|
|
17303
|
+
}
|
|
17304
|
+
function oldestCandidatesFirst(items) {
|
|
17305
|
+
return [...items].sort((a, b) => {
|
|
17306
|
+
const byTimestamp = compareTimestamp(a.timestamp, b.timestamp);
|
|
17307
|
+
if (byTimestamp !== 0) return byTimestamp;
|
|
17308
|
+
return a.summary.id.localeCompare(b.summary.id);
|
|
17309
|
+
});
|
|
17310
|
+
}
|
|
17311
|
+
function compareNewEmailOutputOldestFirst(a, b) {
|
|
17312
|
+
const byTimestamp = compareTimestamp(
|
|
17313
|
+
effectiveReceivedAt(a.receivedAt),
|
|
17314
|
+
effectiveReceivedAt(b.receivedAt)
|
|
17315
|
+
);
|
|
17316
|
+
if (byTimestamp !== 0) return byTimestamp;
|
|
17317
|
+
return a.id.localeCompare(b.id);
|
|
17318
|
+
}
|
|
17319
|
+
function compareTimestamp(a, b) {
|
|
17320
|
+
if (a < b) return -1;
|
|
17321
|
+
if (a > b) return 1;
|
|
17322
|
+
return 0;
|
|
17323
|
+
}
|
|
17324
|
+
|
|
17325
|
+
// src/tools/browse.ts
|
|
17326
|
+
function registerBrowseTools(server, ctx) {
|
|
17327
|
+
const { store, registry, tools } = ctx;
|
|
17328
|
+
const emailListOutputSchema = z5.object({
|
|
17329
|
+
account: z5.string(),
|
|
17330
|
+
count: z5.number(),
|
|
17331
|
+
items: z5.array(emailSummaryOutputSchema),
|
|
17332
|
+
skip: z5.number(),
|
|
17333
|
+
hasMore: z5.boolean()
|
|
17334
|
+
});
|
|
17335
|
+
const searchEmailsOutputSchema = z5.object({
|
|
17336
|
+
account: z5.string(),
|
|
17337
|
+
count: z5.number(),
|
|
17338
|
+
items: z5.array(emailSummaryOutputSchema)
|
|
17083
17339
|
});
|
|
17084
17340
|
if (shouldRegister("list_emails", tools)) {
|
|
17085
17341
|
server.registerTool(
|
|
17086
17342
|
"list_emails",
|
|
17087
17343
|
{
|
|
17088
17344
|
description: "List recent emails in a folder of the given account. Pass the user's email address as `account`; the server routes to the correct backend automatically.",
|
|
17089
|
-
inputSchema:
|
|
17090
|
-
account:
|
|
17091
|
-
folder:
|
|
17092
|
-
limit:
|
|
17093
|
-
unreadOnly:
|
|
17094
|
-
skip:
|
|
17345
|
+
inputSchema: z5.object({
|
|
17346
|
+
account: z5.string().email(),
|
|
17347
|
+
folder: z5.string().default("inbox").optional(),
|
|
17348
|
+
limit: z5.number().int().positive().max(100).optional(),
|
|
17349
|
+
unreadOnly: z5.boolean().optional(),
|
|
17350
|
+
skip: z5.number().int().min(0).optional()
|
|
17095
17351
|
}),
|
|
17096
17352
|
outputSchema: emailListOutputSchema
|
|
17097
17353
|
},
|
|
@@ -17118,15 +17374,16 @@ function registerBrowseTools(server, ctx) {
|
|
|
17118
17374
|
}
|
|
17119
17375
|
);
|
|
17120
17376
|
}
|
|
17377
|
+
registerNewEmailTool(server, { store, registry, tools });
|
|
17121
17378
|
if (shouldRegister("search_emails", tools)) {
|
|
17122
17379
|
server.registerTool(
|
|
17123
17380
|
"search_emails",
|
|
17124
17381
|
{
|
|
17125
17382
|
description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries.",
|
|
17126
|
-
inputSchema:
|
|
17127
|
-
account:
|
|
17128
|
-
query:
|
|
17129
|
-
limit:
|
|
17383
|
+
inputSchema: z5.object({
|
|
17384
|
+
account: z5.string().email(),
|
|
17385
|
+
query: z5.string().min(1),
|
|
17386
|
+
limit: z5.number().int().positive().max(100).optional()
|
|
17130
17387
|
}),
|
|
17131
17388
|
outputSchema: searchEmailsOutputSchema
|
|
17132
17389
|
},
|
|
@@ -17148,31 +17405,31 @@ function registerBrowseTools(server, ctx) {
|
|
|
17148
17405
|
}
|
|
17149
17406
|
);
|
|
17150
17407
|
}
|
|
17151
|
-
const readEmailOutputSchema =
|
|
17152
|
-
id:
|
|
17153
|
-
subject:
|
|
17408
|
+
const readEmailOutputSchema = z5.object({
|
|
17409
|
+
id: z5.string(),
|
|
17410
|
+
subject: z5.string(),
|
|
17154
17411
|
from: emailAddrOutputSchema.optional(),
|
|
17155
|
-
to:
|
|
17156
|
-
cc:
|
|
17157
|
-
bcc:
|
|
17158
|
-
receivedAt:
|
|
17159
|
-
preview:
|
|
17160
|
-
isRead:
|
|
17161
|
-
hasAttachments:
|
|
17162
|
-
folder:
|
|
17163
|
-
attachments:
|
|
17164
|
-
body:
|
|
17165
|
-
bodyFormat:
|
|
17412
|
+
to: z5.array(emailAddrOutputSchema).optional(),
|
|
17413
|
+
cc: z5.array(emailAddrOutputSchema).optional(),
|
|
17414
|
+
bcc: z5.array(emailAddrOutputSchema).optional(),
|
|
17415
|
+
receivedAt: z5.string().optional(),
|
|
17416
|
+
preview: z5.string().optional(),
|
|
17417
|
+
isRead: z5.boolean().optional(),
|
|
17418
|
+
hasAttachments: z5.boolean().optional(),
|
|
17419
|
+
folder: z5.string().optional(),
|
|
17420
|
+
attachments: z5.array(attachmentMetaOutputSchema).optional(),
|
|
17421
|
+
body: z5.string(),
|
|
17422
|
+
bodyFormat: z5.enum(["markdown", "html", "text"])
|
|
17166
17423
|
});
|
|
17167
17424
|
if (shouldRegister("read_email", tools)) {
|
|
17168
17425
|
server.registerTool(
|
|
17169
17426
|
"read_email",
|
|
17170
17427
|
{
|
|
17171
17428
|
description: "Fetch a single email with full body and recipients by id. Body is returned as `body` with `bodyFormat` indicating the format. Default format is 'markdown' \u2014 HTML is automatically converted to save context tokens.",
|
|
17172
|
-
inputSchema:
|
|
17173
|
-
account:
|
|
17174
|
-
id:
|
|
17175
|
-
format:
|
|
17429
|
+
inputSchema: z5.object({
|
|
17430
|
+
account: z5.string().email(),
|
|
17431
|
+
id: z5.string().min(1),
|
|
17432
|
+
format: z5.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
|
|
17176
17433
|
"Output body format. 'markdown' converts HTML to Markdown (default), 'html' returns the raw HTML, 'text' returns plain text."
|
|
17177
17434
|
)
|
|
17178
17435
|
}),
|
|
@@ -17207,20 +17464,20 @@ function registerBrowseTools(server, ctx) {
|
|
|
17207
17464
|
}
|
|
17208
17465
|
);
|
|
17209
17466
|
}
|
|
17210
|
-
const readAttachmentOutputSchema =
|
|
17211
|
-
name:
|
|
17212
|
-
contentType:
|
|
17213
|
-
path:
|
|
17467
|
+
const readAttachmentOutputSchema = z5.object({
|
|
17468
|
+
name: z5.string(),
|
|
17469
|
+
contentType: z5.string().optional(),
|
|
17470
|
+
path: z5.string()
|
|
17214
17471
|
});
|
|
17215
17472
|
if (shouldRegister("read_attachment", tools)) {
|
|
17216
17473
|
server.registerTool(
|
|
17217
17474
|
"read_attachment",
|
|
17218
17475
|
{
|
|
17219
17476
|
description: "Download an email attachment to a temporary file and return its path. Use messageId and attachmentId from a prior read_email call.",
|
|
17220
|
-
inputSchema:
|
|
17221
|
-
account:
|
|
17222
|
-
messageId:
|
|
17223
|
-
attachmentId:
|
|
17477
|
+
inputSchema: z5.object({
|
|
17478
|
+
account: z5.string().email(),
|
|
17479
|
+
messageId: z5.string().min(1),
|
|
17480
|
+
attachmentId: z5.string().min(1)
|
|
17224
17481
|
}),
|
|
17225
17482
|
outputSchema: readAttachmentOutputSchema
|
|
17226
17483
|
},
|
|
@@ -17242,22 +17499,22 @@ function registerBrowseTools(server, ctx) {
|
|
|
17242
17499
|
}
|
|
17243
17500
|
|
|
17244
17501
|
// src/tools/folders.ts
|
|
17245
|
-
import { z as
|
|
17502
|
+
import { z as z6 } from "zod";
|
|
17246
17503
|
function registerFolderTools(server, ctx) {
|
|
17247
17504
|
const { registry, tools } = ctx;
|
|
17248
|
-
const listFoldersOutputSchema =
|
|
17249
|
-
account:
|
|
17250
|
-
count:
|
|
17251
|
-
items:
|
|
17505
|
+
const listFoldersOutputSchema = z6.object({
|
|
17506
|
+
account: z6.string(),
|
|
17507
|
+
count: z6.number(),
|
|
17508
|
+
items: z6.array(folderInfoOutputSchema)
|
|
17252
17509
|
});
|
|
17253
17510
|
if (shouldRegister("list_folders", tools)) {
|
|
17254
17511
|
server.registerTool(
|
|
17255
17512
|
"list_folders",
|
|
17256
17513
|
{
|
|
17257
17514
|
description: "List available mail folders. Returns top-level folders by default, or child folders of the given parent when `parentFolderId` is provided.",
|
|
17258
|
-
inputSchema:
|
|
17259
|
-
account:
|
|
17260
|
-
parentFolderId:
|
|
17515
|
+
inputSchema: z6.object({
|
|
17516
|
+
account: z6.string().email(),
|
|
17517
|
+
parentFolderId: z6.string().optional().describe(
|
|
17261
17518
|
"When provided, lists child folders of this folder. When omitted, lists top-level folders (children of the root)."
|
|
17262
17519
|
)
|
|
17263
17520
|
}),
|
|
@@ -17281,8 +17538,8 @@ function registerFolderTools(server, ctx) {
|
|
|
17281
17538
|
}
|
|
17282
17539
|
);
|
|
17283
17540
|
}
|
|
17284
|
-
const createFolderOutputSchema =
|
|
17285
|
-
created:
|
|
17541
|
+
const createFolderOutputSchema = z6.object({
|
|
17542
|
+
created: z6.literal(true),
|
|
17286
17543
|
folder: folderInfoOutputSchema
|
|
17287
17544
|
});
|
|
17288
17545
|
if (shouldRegister("create_folder", tools)) {
|
|
@@ -17290,10 +17547,10 @@ function registerFolderTools(server, ctx) {
|
|
|
17290
17547
|
"create_folder",
|
|
17291
17548
|
{
|
|
17292
17549
|
description: "Create a new mail folder. Creates under the root folder by default, or under the specified parent when `parentFolderId` is provided. Disabled in --read-only mode.",
|
|
17293
|
-
inputSchema:
|
|
17294
|
-
account:
|
|
17295
|
-
displayName:
|
|
17296
|
-
parentFolderId:
|
|
17550
|
+
inputSchema: z6.object({
|
|
17551
|
+
account: z6.string().email(),
|
|
17552
|
+
displayName: z6.string().min(1).describe("Name of the new folder"),
|
|
17553
|
+
parentFolderId: z6.string().optional().describe(
|
|
17297
17554
|
"When provided, creates the folder as a child of this folder. When omitted, creates under the root folder."
|
|
17298
17555
|
)
|
|
17299
17556
|
}),
|
|
@@ -17314,18 +17571,18 @@ function registerFolderTools(server, ctx) {
|
|
|
17314
17571
|
}
|
|
17315
17572
|
);
|
|
17316
17573
|
}
|
|
17317
|
-
const deleteFolderOutputSchema =
|
|
17318
|
-
deleted:
|
|
17319
|
-
id:
|
|
17574
|
+
const deleteFolderOutputSchema = z6.object({
|
|
17575
|
+
deleted: z6.literal(true),
|
|
17576
|
+
id: z6.string()
|
|
17320
17577
|
});
|
|
17321
17578
|
if (shouldRegister("delete_folder", tools)) {
|
|
17322
17579
|
server.registerTool(
|
|
17323
17580
|
"delete_folder",
|
|
17324
17581
|
{
|
|
17325
17582
|
description: "Delete a mail folder by ID. Disabled in --read-only mode.",
|
|
17326
|
-
inputSchema:
|
|
17327
|
-
account:
|
|
17328
|
-
folderId:
|
|
17583
|
+
inputSchema: z6.object({
|
|
17584
|
+
account: z6.string().email(),
|
|
17585
|
+
folderId: z6.string().min(1).describe("ID of the folder to delete")
|
|
17329
17586
|
}),
|
|
17330
17587
|
outputSchema: deleteFolderOutputSchema
|
|
17331
17588
|
},
|
|
@@ -17341,8 +17598,8 @@ function registerFolderTools(server, ctx) {
|
|
|
17341
17598
|
}
|
|
17342
17599
|
);
|
|
17343
17600
|
}
|
|
17344
|
-
const renameFolderOutputSchema =
|
|
17345
|
-
renamed:
|
|
17601
|
+
const renameFolderOutputSchema = z6.object({
|
|
17602
|
+
renamed: z6.literal(true),
|
|
17346
17603
|
folder: folderInfoOutputSchema
|
|
17347
17604
|
});
|
|
17348
17605
|
if (shouldRegister("rename_folder", tools)) {
|
|
@@ -17350,10 +17607,10 @@ function registerFolderTools(server, ctx) {
|
|
|
17350
17607
|
"rename_folder",
|
|
17351
17608
|
{
|
|
17352
17609
|
description: "Rename an existing mail folder. Disabled in --read-only mode.",
|
|
17353
|
-
inputSchema:
|
|
17354
|
-
account:
|
|
17355
|
-
folderId:
|
|
17356
|
-
newName:
|
|
17610
|
+
inputSchema: z6.object({
|
|
17611
|
+
account: z6.string().email(),
|
|
17612
|
+
folderId: z6.string().min(1).describe("ID of the folder to rename"),
|
|
17613
|
+
newName: z6.string().min(1).describe("New display name for the folder")
|
|
17357
17614
|
}),
|
|
17358
17615
|
outputSchema: renameFolderOutputSchema
|
|
17359
17616
|
},
|
|
@@ -17376,7 +17633,7 @@ function registerFolderTools(server, ctx) {
|
|
|
17376
17633
|
}
|
|
17377
17634
|
|
|
17378
17635
|
// src/tools/organize.ts
|
|
17379
|
-
import { z as
|
|
17636
|
+
import { z as z7 } from "zod";
|
|
17380
17637
|
function registerOrganizeTools(server, ctx) {
|
|
17381
17638
|
const { registry, tools } = ctx;
|
|
17382
17639
|
async function moveToWellKnown(args, destination, resultKey) {
|
|
@@ -17392,13 +17649,13 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17392
17649
|
const data = { marked: true, id: args.id, isRead };
|
|
17393
17650
|
return ok(data, data);
|
|
17394
17651
|
}
|
|
17395
|
-
const archiveMoveSchema =
|
|
17396
|
-
account:
|
|
17397
|
-
id:
|
|
17652
|
+
const archiveMoveSchema = z7.object({
|
|
17653
|
+
account: z7.string().email(),
|
|
17654
|
+
id: z7.string().min(1).describe("Message ID to move")
|
|
17398
17655
|
});
|
|
17399
|
-
const archiveOutputSchema =
|
|
17400
|
-
archived:
|
|
17401
|
-
id:
|
|
17656
|
+
const archiveOutputSchema = z7.object({
|
|
17657
|
+
archived: z7.literal(true),
|
|
17658
|
+
id: z7.string()
|
|
17402
17659
|
});
|
|
17403
17660
|
if (shouldRegister("archive_email", tools)) {
|
|
17404
17661
|
server.registerTool(
|
|
@@ -17417,9 +17674,9 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17417
17674
|
}
|
|
17418
17675
|
);
|
|
17419
17676
|
}
|
|
17420
|
-
const trashOutputSchema =
|
|
17421
|
-
trashed:
|
|
17422
|
-
id:
|
|
17677
|
+
const trashOutputSchema = z7.object({
|
|
17678
|
+
trashed: z7.literal(true),
|
|
17679
|
+
id: z7.string()
|
|
17423
17680
|
});
|
|
17424
17681
|
if (shouldRegister("trash_email", tools)) {
|
|
17425
17682
|
server.registerTool(
|
|
@@ -17438,20 +17695,20 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17438
17695
|
}
|
|
17439
17696
|
);
|
|
17440
17697
|
}
|
|
17441
|
-
const moveEmailOutputSchema =
|
|
17442
|
-
moved:
|
|
17443
|
-
id:
|
|
17444
|
-
destination:
|
|
17698
|
+
const moveEmailOutputSchema = z7.object({
|
|
17699
|
+
moved: z7.literal(true),
|
|
17700
|
+
id: z7.string(),
|
|
17701
|
+
destination: z7.string()
|
|
17445
17702
|
});
|
|
17446
17703
|
if (shouldRegister("move_email", tools)) {
|
|
17447
17704
|
server.registerTool(
|
|
17448
17705
|
"move_email",
|
|
17449
17706
|
{
|
|
17450
17707
|
description: "Move a message to any folder by well-known name (e.g. 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or custom folder ID. Disabled in --read-only mode.",
|
|
17451
|
-
inputSchema:
|
|
17452
|
-
account:
|
|
17453
|
-
id:
|
|
17454
|
-
destination:
|
|
17708
|
+
inputSchema: z7.object({
|
|
17709
|
+
account: z7.string().email(),
|
|
17710
|
+
id: z7.string().min(1).describe("Message ID to move"),
|
|
17711
|
+
destination: z7.string().min(1).describe(
|
|
17455
17712
|
"Destination folder \u2014 a well-known folder name ('archive', 'deleteditems', 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or a raw folder ID."
|
|
17456
17713
|
)
|
|
17457
17714
|
}),
|
|
@@ -17473,14 +17730,14 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17473
17730
|
}
|
|
17474
17731
|
);
|
|
17475
17732
|
}
|
|
17476
|
-
const markReadInputSchema =
|
|
17477
|
-
account:
|
|
17478
|
-
id:
|
|
17733
|
+
const markReadInputSchema = z7.object({
|
|
17734
|
+
account: z7.string().email(),
|
|
17735
|
+
id: z7.string().min(1).describe("Message ID to mark as read")
|
|
17479
17736
|
});
|
|
17480
|
-
const markReadOutputSchema =
|
|
17481
|
-
marked:
|
|
17482
|
-
id:
|
|
17483
|
-
isRead:
|
|
17737
|
+
const markReadOutputSchema = z7.object({
|
|
17738
|
+
marked: z7.literal(true),
|
|
17739
|
+
id: z7.string(),
|
|
17740
|
+
isRead: z7.boolean()
|
|
17484
17741
|
});
|
|
17485
17742
|
if (shouldRegister("mark_read", tools)) {
|
|
17486
17743
|
server.registerTool(
|
|
@@ -17519,7 +17776,7 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17519
17776
|
}
|
|
17520
17777
|
|
|
17521
17778
|
// src/tools/compose.ts
|
|
17522
|
-
import { z as
|
|
17779
|
+
import { z as z8 } from "zod";
|
|
17523
17780
|
import { readFileSync } from "fs";
|
|
17524
17781
|
import { basename, extname } from "path";
|
|
17525
17782
|
function registerComposeTools(server, ctx) {
|
|
@@ -17549,32 +17806,32 @@ function registerComposeTools(server, ctx) {
|
|
|
17549
17806
|
".mp3": "audio/mpeg",
|
|
17550
17807
|
".mp4": "video/mp4"
|
|
17551
17808
|
};
|
|
17552
|
-
const sendEmailSchema =
|
|
17553
|
-
account:
|
|
17554
|
-
to:
|
|
17555
|
-
cc:
|
|
17556
|
-
bcc:
|
|
17557
|
-
subject:
|
|
17558
|
-
body:
|
|
17559
|
-
format:
|
|
17809
|
+
const sendEmailSchema = z8.object({
|
|
17810
|
+
account: z8.string().email(),
|
|
17811
|
+
to: z8.array(emailAddrSchema).min(1),
|
|
17812
|
+
cc: z8.array(emailAddrSchema).optional(),
|
|
17813
|
+
bcc: z8.array(emailAddrSchema).optional(),
|
|
17814
|
+
subject: z8.string(),
|
|
17815
|
+
body: z8.string(),
|
|
17816
|
+
format: z8.enum(["html", "markdown"]).describe(
|
|
17560
17817
|
"Body format. 'html' sends the body as-is (must be valid HTML). 'markdown' converts the body from Markdown to HTML for clean rendering on the recipient side."
|
|
17561
17818
|
),
|
|
17562
|
-
include_signature:
|
|
17819
|
+
include_signature: z8.boolean().describe(
|
|
17563
17820
|
"Whether to append the account's saved HTML signature to the email. If true, don't include a signature in the body param to avoid double signature. Returns an error if true but no signature is configured for this account."
|
|
17564
17821
|
),
|
|
17565
|
-
inReplyTo:
|
|
17822
|
+
inReplyTo: z8.union([z8.string(), z8.literal(false)]).describe(
|
|
17566
17823
|
"Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically. Set to `false` for a new email (not a reply)."
|
|
17567
17824
|
),
|
|
17568
|
-
replyAll:
|
|
17825
|
+
replyAll: z8.boolean().default(false).optional().describe(
|
|
17569
17826
|
"When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
|
|
17570
17827
|
),
|
|
17571
|
-
forwardMessageId:
|
|
17828
|
+
forwardMessageId: z8.string().optional().describe(
|
|
17572
17829
|
"Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
|
|
17573
17830
|
),
|
|
17574
|
-
attachments:
|
|
17575
|
-
|
|
17576
|
-
filePath:
|
|
17577
|
-
name:
|
|
17831
|
+
attachments: z8.array(
|
|
17832
|
+
z8.object({
|
|
17833
|
+
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
17834
|
+
name: z8.string().optional().describe(
|
|
17578
17835
|
"Attachment filename. Defaults to the file's basename."
|
|
17579
17836
|
)
|
|
17580
17837
|
})
|
|
@@ -17637,8 +17894,8 @@ function registerComposeTools(server, ctx) {
|
|
|
17637
17894
|
}
|
|
17638
17895
|
}
|
|
17639
17896
|
const sendEmailOutputSchema = {
|
|
17640
|
-
sent:
|
|
17641
|
-
id:
|
|
17897
|
+
sent: z8.literal(true),
|
|
17898
|
+
id: z8.string()
|
|
17642
17899
|
};
|
|
17643
17900
|
if (shouldRegister("send_email", tools)) {
|
|
17644
17901
|
server.registerTool(
|
|
@@ -17657,9 +17914,9 @@ function registerComposeTools(server, ctx) {
|
|
|
17657
17914
|
);
|
|
17658
17915
|
}
|
|
17659
17916
|
const draftEmailOutputSchema = {
|
|
17660
|
-
draft:
|
|
17661
|
-
id:
|
|
17662
|
-
draftHtml:
|
|
17917
|
+
draft: z8.literal(true),
|
|
17918
|
+
id: z8.string(),
|
|
17919
|
+
draftHtml: z8.string().optional()
|
|
17663
17920
|
};
|
|
17664
17921
|
if (shouldRegister("draft_email", tools)) {
|
|
17665
17922
|
server.registerTool(
|
|
@@ -17677,38 +17934,38 @@ function registerComposeTools(server, ctx) {
|
|
|
17677
17934
|
)
|
|
17678
17935
|
);
|
|
17679
17936
|
}
|
|
17680
|
-
const editDraftSchema =
|
|
17681
|
-
account:
|
|
17682
|
-
id:
|
|
17683
|
-
to:
|
|
17684
|
-
cc:
|
|
17685
|
-
bcc:
|
|
17686
|
-
subject:
|
|
17687
|
-
body:
|
|
17688
|
-
format:
|
|
17937
|
+
const editDraftSchema = z8.object({
|
|
17938
|
+
account: z8.string().email(),
|
|
17939
|
+
id: z8.string().min(1).describe("Draft message ID to edit"),
|
|
17940
|
+
to: z8.array(emailAddrSchema).optional(),
|
|
17941
|
+
cc: z8.array(emailAddrSchema).optional(),
|
|
17942
|
+
bcc: z8.array(emailAddrSchema).optional(),
|
|
17943
|
+
subject: z8.string().optional(),
|
|
17944
|
+
body: z8.string().optional(),
|
|
17945
|
+
format: z8.enum(["html", "markdown"]).optional().describe(
|
|
17689
17946
|
"Body format. Only meaningful when `body` is also provided. 'html' sends the body as-is (must be valid HTML). 'markdown' converts the body from Markdown to HTML for clean rendering on the recipient side."
|
|
17690
17947
|
),
|
|
17691
|
-
include_signature:
|
|
17948
|
+
include_signature: z8.boolean().optional().describe(
|
|
17692
17949
|
"Whether to re-apply the account's saved HTML signature to the body. If true, don't include a signature in the body param. Only meaningful when `body` is also provided. Returns an error if true but no signature is configured for this account."
|
|
17693
17950
|
),
|
|
17694
|
-
new_attachments:
|
|
17695
|
-
|
|
17696
|
-
filePath:
|
|
17697
|
-
name:
|
|
17951
|
+
new_attachments: z8.array(
|
|
17952
|
+
z8.object({
|
|
17953
|
+
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
17954
|
+
name: z8.string().optional().describe(
|
|
17698
17955
|
"Attachment filename. Defaults to the file's basename."
|
|
17699
17956
|
)
|
|
17700
17957
|
})
|
|
17701
17958
|
).optional().describe(
|
|
17702
17959
|
"New file attachments to add to the draft. The server reads the files from disk and base64-encodes them automatically."
|
|
17703
17960
|
),
|
|
17704
|
-
remove_attachments:
|
|
17961
|
+
remove_attachments: z8.array(z8.string().min(1)).optional().describe(
|
|
17705
17962
|
"Attachment IDs to remove from the draft. Get attachment IDs from read_email."
|
|
17706
17963
|
)
|
|
17707
17964
|
});
|
|
17708
17965
|
const editDraftOutputSchema = {
|
|
17709
|
-
edited:
|
|
17710
|
-
id:
|
|
17711
|
-
draftHtml:
|
|
17966
|
+
edited: z8.literal(true),
|
|
17967
|
+
id: z8.string(),
|
|
17968
|
+
draftHtml: z8.string().optional()
|
|
17712
17969
|
};
|
|
17713
17970
|
if (shouldRegister("edit_draft", tools)) {
|
|
17714
17971
|
server.registerTool(
|
|
@@ -17800,8 +18057,8 @@ function registerComposeTools(server, ctx) {
|
|
|
17800
18057
|
);
|
|
17801
18058
|
}
|
|
17802
18059
|
const sendDraftOutputSchema = {
|
|
17803
|
-
sent:
|
|
17804
|
-
id:
|
|
18060
|
+
sent: z8.literal(true),
|
|
18061
|
+
id: z8.string()
|
|
17805
18062
|
};
|
|
17806
18063
|
if (shouldRegister("send_draft", tools)) {
|
|
17807
18064
|
server.registerTool(
|
|
@@ -17809,8 +18066,8 @@ function registerComposeTools(server, ctx) {
|
|
|
17809
18066
|
{
|
|
17810
18067
|
description: "Send an existing draft email by ID. Use this with draft IDs returned by `draft_email` or `edit_draft`. Disabled in --read-only mode.",
|
|
17811
18068
|
inputSchema: {
|
|
17812
|
-
account:
|
|
17813
|
-
id:
|
|
18069
|
+
account: z8.string().email(),
|
|
18070
|
+
id: z8.string().min(1).describe("Draft message ID to send")
|
|
17814
18071
|
},
|
|
17815
18072
|
outputSchema: sendDraftOutputSchema
|
|
17816
18073
|
},
|
|
@@ -17832,7 +18089,7 @@ function registerComposeTools(server, ctx) {
|
|
|
17832
18089
|
function registerTools(server, opts) {
|
|
17833
18090
|
const { store, registry, tools } = opts;
|
|
17834
18091
|
registerAccountTools(server, { store, registry, tools });
|
|
17835
|
-
registerBrowseTools(server, { registry, tools });
|
|
18092
|
+
registerBrowseTools(server, { store, registry, tools });
|
|
17836
18093
|
registerFolderTools(server, { registry, tools });
|
|
17837
18094
|
registerOrganizeTools(server, { registry, tools });
|
|
17838
18095
|
registerComposeTools(server, { store, registry, tools });
|
|
@@ -17841,7 +18098,7 @@ function registerTools(server, opts) {
|
|
|
17841
18098
|
// package.json
|
|
17842
18099
|
var package_default = {
|
|
17843
18100
|
name: "hypermail-mcp",
|
|
17844
|
-
version: "0.7.
|
|
18101
|
+
version: "0.7.9",
|
|
17845
18102
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
17846
18103
|
type: "module",
|
|
17847
18104
|
bin: {
|
|
@@ -17913,193 +18170,6 @@ var package_default = {
|
|
|
17913
18170
|
// src/version.ts
|
|
17914
18171
|
var VERSION = package_default.version;
|
|
17915
18172
|
|
|
17916
|
-
// src/watcher/webhook.ts
|
|
17917
|
-
async function postWebhook(email, config) {
|
|
17918
|
-
if (!config.webhook) return false;
|
|
17919
|
-
const { url, retry } = config.webhook;
|
|
17920
|
-
const maxAttempts = retry.maxAttempts;
|
|
17921
|
-
const baseDelayMs = retry.baseDelayMs;
|
|
17922
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
17923
|
-
if (attempt > 0) {
|
|
17924
|
-
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
17925
|
-
await sleep(delay);
|
|
17926
|
-
}
|
|
17927
|
-
try {
|
|
17928
|
-
const res = await fetch(url, {
|
|
17929
|
-
method: "POST",
|
|
17930
|
-
headers: { "content-type": "application/json" },
|
|
17931
|
-
body: JSON.stringify(email)
|
|
17932
|
-
});
|
|
17933
|
-
if (res.ok) return true;
|
|
17934
|
-
console.error(
|
|
17935
|
-
`[hypermail-watch] webhook POST ${email.id} attempt ${attempt + 1}/${maxAttempts}: HTTP ${res.status}`
|
|
17936
|
-
);
|
|
17937
|
-
} catch (err) {
|
|
17938
|
-
const code = err.code ?? "";
|
|
17939
|
-
console.error(
|
|
17940
|
-
`[hypermail-watch] webhook POST ${email.id} attempt ${attempt + 1}/${maxAttempts}: ${code || String(err)}`
|
|
17941
|
-
);
|
|
17942
|
-
}
|
|
17943
|
-
}
|
|
17944
|
-
console.error(
|
|
17945
|
-
`[hypermail-watch] webhook delivery failed after ${maxAttempts} retries for ${email.id}`
|
|
17946
|
-
);
|
|
17947
|
-
return false;
|
|
17948
|
-
}
|
|
17949
|
-
function sleep(ms) {
|
|
17950
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17951
|
-
}
|
|
17952
|
-
|
|
17953
|
-
// src/watcher/script.ts
|
|
17954
|
-
import { spawn } from "child_process";
|
|
17955
|
-
async function runNotifyCommand(email, config) {
|
|
17956
|
-
if (!config.notifyCommand) return false;
|
|
17957
|
-
const { command, timeoutMs, retry } = config.notifyCommand;
|
|
17958
|
-
const maxAttempts = retry.maxAttempts;
|
|
17959
|
-
const baseDelayMs = retry.baseDelayMs;
|
|
17960
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
17961
|
-
if (attempt > 0) {
|
|
17962
|
-
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
17963
|
-
await sleep2(delay);
|
|
17964
|
-
}
|
|
17965
|
-
try {
|
|
17966
|
-
const ok2 = await spawnWithTimeout(
|
|
17967
|
-
command,
|
|
17968
|
-
JSON.stringify(email),
|
|
17969
|
-
timeoutMs
|
|
17970
|
-
);
|
|
17971
|
-
if (ok2) return true;
|
|
17972
|
-
console.error(
|
|
17973
|
-
`[hypermail-watch] notify command ${email.id} attempt ${attempt + 1}/${maxAttempts}: non-zero exit code`
|
|
17974
|
-
);
|
|
17975
|
-
} catch (err) {
|
|
17976
|
-
console.error(
|
|
17977
|
-
`[hypermail-watch] notify command ${email.id} attempt ${attempt + 1}/${maxAttempts}: ${String(err)}`
|
|
17978
|
-
);
|
|
17979
|
-
}
|
|
17980
|
-
}
|
|
17981
|
-
console.error(
|
|
17982
|
-
`[hypermail-watch] notify command delivery failed after ${maxAttempts} retries for ${email.id}`
|
|
17983
|
-
);
|
|
17984
|
-
return false;
|
|
17985
|
-
}
|
|
17986
|
-
function spawnWithTimeout(command, stdinData, timeoutMs) {
|
|
17987
|
-
return new Promise((resolve) => {
|
|
17988
|
-
const child = spawn(command, {
|
|
17989
|
-
shell: true,
|
|
17990
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
17991
|
-
});
|
|
17992
|
-
let stderr = "";
|
|
17993
|
-
child.stderr?.on("data", (chunk) => {
|
|
17994
|
-
stderr += chunk.toString("utf-8");
|
|
17995
|
-
});
|
|
17996
|
-
const timer = setTimeout(() => {
|
|
17997
|
-
child.kill("SIGTERM");
|
|
17998
|
-
if (stderr) {
|
|
17999
|
-
console.error(`[hypermail-watch] notify command timed out after ${timeoutMs}ms. stderr:
|
|
18000
|
-
${stderr}`);
|
|
18001
|
-
}
|
|
18002
|
-
resolve(false);
|
|
18003
|
-
}, timeoutMs);
|
|
18004
|
-
child.on("close", (code) => {
|
|
18005
|
-
clearTimeout(timer);
|
|
18006
|
-
if (stderr && code !== 0) {
|
|
18007
|
-
console.error(`[hypermail-watch] notify command stderr:
|
|
18008
|
-
${stderr}`);
|
|
18009
|
-
}
|
|
18010
|
-
resolve(code === 0);
|
|
18011
|
-
});
|
|
18012
|
-
child.on("error", (err) => {
|
|
18013
|
-
clearTimeout(timer);
|
|
18014
|
-
console.error(`[hypermail-watch] notify command spawn error: ${err.message}`);
|
|
18015
|
-
resolve(false);
|
|
18016
|
-
});
|
|
18017
|
-
child.stdin?.end(stdinData);
|
|
18018
|
-
});
|
|
18019
|
-
}
|
|
18020
|
-
function sleep2(ms) {
|
|
18021
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18022
|
-
}
|
|
18023
|
-
|
|
18024
|
-
// src/watcher/manager.ts
|
|
18025
|
-
var WatcherManager = class {
|
|
18026
|
-
constructor(store, registry, config) {
|
|
18027
|
-
this.store = store;
|
|
18028
|
-
this.registry = registry;
|
|
18029
|
-
this.config = config;
|
|
18030
|
-
}
|
|
18031
|
-
store;
|
|
18032
|
-
registry;
|
|
18033
|
-
config;
|
|
18034
|
-
intervalId = null;
|
|
18035
|
-
/** Start the poll loop. Fires immediately on the first tick, then every
|
|
18036
|
-
* `pollIntervalSeconds`. Safe to call multiple times — subsequent calls
|
|
18037
|
-
* are no-ops. */
|
|
18038
|
-
start() {
|
|
18039
|
-
if (this.intervalId !== null) return;
|
|
18040
|
-
this.poll();
|
|
18041
|
-
this.intervalId = setInterval(
|
|
18042
|
-
() => this.poll(),
|
|
18043
|
-
this.config.pollIntervalSeconds * 1e3
|
|
18044
|
-
);
|
|
18045
|
-
}
|
|
18046
|
-
/** Stop the poll loop and release the interval. Safe to call when already
|
|
18047
|
-
* stopped. */
|
|
18048
|
-
stop() {
|
|
18049
|
-
if (this.intervalId !== null) {
|
|
18050
|
-
clearInterval(this.intervalId);
|
|
18051
|
-
this.intervalId = null;
|
|
18052
|
-
}
|
|
18053
|
-
}
|
|
18054
|
-
// ── private ──
|
|
18055
|
-
async poll() {
|
|
18056
|
-
const accounts = this.store.listAccounts();
|
|
18057
|
-
for (const acct of accounts) {
|
|
18058
|
-
try {
|
|
18059
|
-
await this.pollAccount(acct.email);
|
|
18060
|
-
} catch (err) {
|
|
18061
|
-
console.error(
|
|
18062
|
-
`[hypermail-watch] poll failed for ${acct.email}:`,
|
|
18063
|
-
err
|
|
18064
|
-
);
|
|
18065
|
-
}
|
|
18066
|
-
}
|
|
18067
|
-
}
|
|
18068
|
-
async pollAccount(email) {
|
|
18069
|
-
const { provider, account } = this.registry.resolveByEmail(email);
|
|
18070
|
-
const result = await provider.listEmails(account, {
|
|
18071
|
-
folder: "inbox",
|
|
18072
|
-
limit: 50
|
|
18073
|
-
});
|
|
18074
|
-
const knownIds = [...account.lastSeenIds ?? []];
|
|
18075
|
-
const newEmails = result.items.filter((e) => !knownIds.includes(e.id));
|
|
18076
|
-
if (newEmails.length === 0) return;
|
|
18077
|
-
for (const summary of newEmails) {
|
|
18078
|
-
try {
|
|
18079
|
-
const full = await provider.readEmail(account, summary.id);
|
|
18080
|
-
await this.emit(full);
|
|
18081
|
-
knownIds.unshift(summary.id);
|
|
18082
|
-
} catch (err) {
|
|
18083
|
-
console.error(
|
|
18084
|
-
`[hypermail-watch] emission failed for ${email}/${summary.id}:`,
|
|
18085
|
-
err
|
|
18086
|
-
);
|
|
18087
|
-
}
|
|
18088
|
-
}
|
|
18089
|
-
const capped = knownIds.slice(0, 200);
|
|
18090
|
-
await this.store.upsertAccount({ ...account, lastSeenIds: capped });
|
|
18091
|
-
}
|
|
18092
|
-
async emit(full) {
|
|
18093
|
-
await postWebhook(full, this.config);
|
|
18094
|
-
runNotifyCommand(full, this.config).catch((err) => {
|
|
18095
|
-
console.error(
|
|
18096
|
-
`[hypermail-watch] notify command unhandled error for ${full.id}:`,
|
|
18097
|
-
err
|
|
18098
|
-
);
|
|
18099
|
-
});
|
|
18100
|
-
}
|
|
18101
|
-
};
|
|
18102
|
-
|
|
18103
18173
|
// src/config/load.ts
|
|
18104
18174
|
var ENV_DATA_DIR = "HYPERMAIL_DATA_DIR";
|
|
18105
18175
|
var ENV_KEY = "HYPERMAIL_KEY";
|
|
@@ -18113,22 +18183,9 @@ var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_OUTLOOK_TENANT_ID";
|
|
|
18113
18183
|
var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_GMAIL_CLIENT_ID";
|
|
18114
18184
|
var ENV_GMAIL_CLIENT_SECRET = "HYPERMAIL_GMAIL_CLIENT_SECRET";
|
|
18115
18185
|
var ENV_GMAIL_REDIRECT_URI = "HYPERMAIL_GMAIL_REDIRECT_URI";
|
|
18116
|
-
var ENV_WATCH_ENABLED = "HYPERMAIL_WATCH_ENABLED";
|
|
18117
|
-
var ENV_WATCH_POLL_SECONDS = "HYPERMAIL_WATCH_POLL_SECONDS";
|
|
18118
|
-
var ENV_WATCH_WEBHOOK_URL = "HYPERMAIL_WATCH_WEBHOOK_URL";
|
|
18119
|
-
var ENV_WATCH_WEBHOOK_RETRY_ATTEMPTS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_ATTEMPTS";
|
|
18120
|
-
var ENV_WATCH_WEBHOOK_RETRY_DELAY_MS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_DELAY_MS";
|
|
18121
|
-
var ENV_WATCH_NOTIFY_COMMAND = "HYPERMAIL_WATCH_NOTIFY_COMMAND";
|
|
18122
|
-
var ENV_WATCH_NOTIFY_TIMEOUT_MS = "HYPERMAIL_WATCH_NOTIFY_TIMEOUT_MS";
|
|
18123
|
-
var ENV_WATCH_NOTIFY_RETRY_ATTEMPTS = "HYPERMAIL_WATCH_NOTIFY_RETRY_ATTEMPTS";
|
|
18124
|
-
var ENV_WATCH_NOTIFY_RETRY_DELAY_MS = "HYPERMAIL_WATCH_NOTIFY_RETRY_DELAY_MS";
|
|
18125
18186
|
var DEFAULT_TRANSPORT = "stdio";
|
|
18126
18187
|
var DEFAULT_HTTP_PORT = 3e3;
|
|
18127
18188
|
var DEFAULT_HTTP_HOST = "127.0.0.1";
|
|
18128
|
-
var DEFAULT_WATCH_POLL_SECONDS = 10;
|
|
18129
|
-
var DEFAULT_RETRY_ATTEMPTS = 5;
|
|
18130
|
-
var DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
18131
|
-
var DEFAULT_NOTIFY_TIMEOUT_MS = 3e4;
|
|
18132
18189
|
function envRaw(name) {
|
|
18133
18190
|
return process.env[name];
|
|
18134
18191
|
}
|
|
@@ -18138,14 +18195,6 @@ function optionalEnvString(name) {
|
|
|
18138
18195
|
const trimmed = value.trim();
|
|
18139
18196
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
18140
18197
|
}
|
|
18141
|
-
function parseBoolEnv(name) {
|
|
18142
|
-
const value = envRaw(name);
|
|
18143
|
-
if (value === void 0) return void 0;
|
|
18144
|
-
const lower = value.trim().toLowerCase();
|
|
18145
|
-
if (lower === "true") return true;
|
|
18146
|
-
if (lower === "false") return false;
|
|
18147
|
-
throw new Error(`${name} must be either "true" or "false"`);
|
|
18148
|
-
}
|
|
18149
18198
|
function parseTransportEnv() {
|
|
18150
18199
|
const value = envRaw(ENV_TRANSPORT);
|
|
18151
18200
|
if (value === void 0) return void 0;
|
|
@@ -18163,15 +18212,6 @@ function parsePositiveInteger(value) {
|
|
|
18163
18212
|
const parsed = Number(trimmed);
|
|
18164
18213
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
18165
18214
|
}
|
|
18166
|
-
function parsePositiveIntegerEnv(name, defaultValue) {
|
|
18167
|
-
const value = envRaw(name);
|
|
18168
|
-
if (value === void 0) return defaultValue;
|
|
18169
|
-
const parsed = parsePositiveInteger(value);
|
|
18170
|
-
if (parsed === void 0) {
|
|
18171
|
-
throw new Error(`${name} must be a positive integer`);
|
|
18172
|
-
}
|
|
18173
|
-
return parsed;
|
|
18174
|
-
}
|
|
18175
18215
|
function parseStringArray(value) {
|
|
18176
18216
|
if (value === void 0) return void 0;
|
|
18177
18217
|
const trimmed = value.trim();
|
|
@@ -18189,17 +18229,6 @@ function validateToolNames(toolNames, envName) {
|
|
|
18189
18229
|
}
|
|
18190
18230
|
}
|
|
18191
18231
|
}
|
|
18192
|
-
function validateWebhookUrl(raw) {
|
|
18193
|
-
try {
|
|
18194
|
-
const url = new URL(raw);
|
|
18195
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
18196
|
-
throw new Error("unsupported protocol");
|
|
18197
|
-
}
|
|
18198
|
-
return raw;
|
|
18199
|
-
} catch {
|
|
18200
|
-
throw new Error(`${ENV_WATCH_WEBHOOK_URL} must be a valid http(s) URL`);
|
|
18201
|
-
}
|
|
18202
|
-
}
|
|
18203
18232
|
function resolveHttpConfig(transport, cliOverrides, warnings) {
|
|
18204
18233
|
const portSource = cliOverrides.port !== void 0 ? "--port" : ENV_HTTP_PORT;
|
|
18205
18234
|
const rawPort = cliOverrides.port ?? envRaw(ENV_HTTP_PORT);
|
|
@@ -18263,68 +18292,12 @@ function resolveProvidersConfig() {
|
|
|
18263
18292
|
}
|
|
18264
18293
|
return providers;
|
|
18265
18294
|
}
|
|
18266
|
-
function resolveRetryConfig(attemptsEnv, delayEnv) {
|
|
18267
|
-
return {
|
|
18268
|
-
maxAttempts: parsePositiveIntegerEnv(attemptsEnv, DEFAULT_RETRY_ATTEMPTS),
|
|
18269
|
-
baseDelayMs: parsePositiveIntegerEnv(delayEnv, DEFAULT_RETRY_DELAY_MS)
|
|
18270
|
-
};
|
|
18271
|
-
}
|
|
18272
|
-
function resolveWatchConfig() {
|
|
18273
|
-
const enabled = parseBoolEnv(ENV_WATCH_ENABLED) ?? false;
|
|
18274
|
-
if (!enabled) return void 0;
|
|
18275
|
-
const pollIntervalSeconds = parsePositiveIntegerEnv(
|
|
18276
|
-
ENV_WATCH_POLL_SECONDS,
|
|
18277
|
-
DEFAULT_WATCH_POLL_SECONDS
|
|
18278
|
-
);
|
|
18279
|
-
const webhookUrl = optionalEnvString(ENV_WATCH_WEBHOOK_URL);
|
|
18280
|
-
let webhook;
|
|
18281
|
-
if (webhookUrl) {
|
|
18282
|
-
webhook = {
|
|
18283
|
-
url: validateWebhookUrl(webhookUrl),
|
|
18284
|
-
retry: resolveRetryConfig(
|
|
18285
|
-
ENV_WATCH_WEBHOOK_RETRY_ATTEMPTS,
|
|
18286
|
-
ENV_WATCH_WEBHOOK_RETRY_DELAY_MS
|
|
18287
|
-
)
|
|
18288
|
-
};
|
|
18289
|
-
}
|
|
18290
|
-
const rawNotifyCommand = envRaw(ENV_WATCH_NOTIFY_COMMAND);
|
|
18291
|
-
let notifyCommand;
|
|
18292
|
-
if (rawNotifyCommand !== void 0) {
|
|
18293
|
-
const command = rawNotifyCommand.trim();
|
|
18294
|
-
if (!command) {
|
|
18295
|
-
throw new Error(`${ENV_WATCH_NOTIFY_COMMAND} must not be empty when watch is enabled`);
|
|
18296
|
-
}
|
|
18297
|
-
notifyCommand = {
|
|
18298
|
-
command,
|
|
18299
|
-
timeoutMs: parsePositiveIntegerEnv(
|
|
18300
|
-
ENV_WATCH_NOTIFY_TIMEOUT_MS,
|
|
18301
|
-
DEFAULT_NOTIFY_TIMEOUT_MS
|
|
18302
|
-
),
|
|
18303
|
-
retry: resolveRetryConfig(
|
|
18304
|
-
ENV_WATCH_NOTIFY_RETRY_ATTEMPTS,
|
|
18305
|
-
ENV_WATCH_NOTIFY_RETRY_DELAY_MS
|
|
18306
|
-
)
|
|
18307
|
-
};
|
|
18308
|
-
}
|
|
18309
|
-
if (!webhook && !notifyCommand) {
|
|
18310
|
-
throw new Error(
|
|
18311
|
-
`${ENV_WATCH_ENABLED}=true requires ${ENV_WATCH_WEBHOOK_URL} or ${ENV_WATCH_NOTIFY_COMMAND}`
|
|
18312
|
-
);
|
|
18313
|
-
}
|
|
18314
|
-
return {
|
|
18315
|
-
enabled: true,
|
|
18316
|
-
pollIntervalSeconds,
|
|
18317
|
-
webhook,
|
|
18318
|
-
notifyCommand
|
|
18319
|
-
};
|
|
18320
|
-
}
|
|
18321
18295
|
function loadConfig(cliOverrides = {}) {
|
|
18322
18296
|
const warnings = [];
|
|
18323
18297
|
const transport = cliOverrides.transport ?? parseTransportEnv() ?? DEFAULT_TRANSPORT;
|
|
18324
18298
|
const http = resolveHttpConfig(transport, cliOverrides, warnings);
|
|
18325
18299
|
const tools = resolveToolsConfig();
|
|
18326
18300
|
const providers = resolveProvidersConfig();
|
|
18327
|
-
const watch = resolveWatchConfig();
|
|
18328
18301
|
const dataDir = cliOverrides.dataDir ?? optionalEnvString(ENV_DATA_DIR);
|
|
18329
18302
|
if (!optionalEnvString(ENV_KEY)) {
|
|
18330
18303
|
warnings.push(
|
|
@@ -18337,8 +18310,7 @@ function loadConfig(cliOverrides = {}) {
|
|
|
18337
18310
|
transport,
|
|
18338
18311
|
http,
|
|
18339
18312
|
tools,
|
|
18340
|
-
providers
|
|
18341
|
-
watch
|
|
18313
|
+
providers
|
|
18342
18314
|
},
|
|
18343
18315
|
warnings
|
|
18344
18316
|
};
|
|
@@ -18353,6 +18325,7 @@ var KNOWN_TOOLS = [
|
|
|
18353
18325
|
"set_account_settings",
|
|
18354
18326
|
"remove_account",
|
|
18355
18327
|
"list_emails",
|
|
18328
|
+
"get_new_emails",
|
|
18356
18329
|
"search_emails",
|
|
18357
18330
|
"read_email",
|
|
18358
18331
|
"read_attachment",
|
|
@@ -18386,14 +18359,6 @@ async function startServer(opts) {
|
|
|
18386
18359
|
const store = await AccountStore.open({ dataDir: config.dataDir });
|
|
18387
18360
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
18388
18361
|
const tools = resolveTools(config);
|
|
18389
|
-
let watcher;
|
|
18390
|
-
if (config.watch?.enabled) {
|
|
18391
|
-
watcher = new WatcherManager(store, registry, config.watch);
|
|
18392
|
-
watcher.start();
|
|
18393
|
-
const stop = () => watcher?.stop();
|
|
18394
|
-
process.on("SIGTERM", stop);
|
|
18395
|
-
process.on("SIGINT", stop);
|
|
18396
|
-
}
|
|
18397
18362
|
const createServer = () => {
|
|
18398
18363
|
const s = new McpServer(
|
|
18399
18364
|
{ name: "hypermail-mcp", version: VERSION },
|
|
@@ -18598,17 +18563,6 @@ Provider environment variables:
|
|
|
18598
18563
|
HYPERMAIL_GMAIL_CLIENT_SECRET
|
|
18599
18564
|
HYPERMAIL_GMAIL_REDIRECT_URI
|
|
18600
18565
|
|
|
18601
|
-
Watcher environment variables:
|
|
18602
|
-
HYPERMAIL_WATCH_ENABLED=true|false
|
|
18603
|
-
HYPERMAIL_WATCH_POLL_SECONDS
|
|
18604
|
-
HYPERMAIL_WATCH_WEBHOOK_URL
|
|
18605
|
-
HYPERMAIL_WATCH_WEBHOOK_RETRY_ATTEMPTS
|
|
18606
|
-
HYPERMAIL_WATCH_WEBHOOK_RETRY_DELAY_MS
|
|
18607
|
-
HYPERMAIL_WATCH_NOTIFY_COMMAND
|
|
18608
|
-
HYPERMAIL_WATCH_NOTIFY_TIMEOUT_MS
|
|
18609
|
-
HYPERMAIL_WATCH_NOTIFY_RETRY_ATTEMPTS
|
|
18610
|
-
HYPERMAIL_WATCH_NOTIFY_RETRY_DELAY_MS
|
|
18611
|
-
|
|
18612
18566
|
Example:
|
|
18613
18567
|
HYPERMAIL_TRANSPORT=http \\
|
|
18614
18568
|
HYPERMAIL_HTTP_PORT=8080 \\
|