hypermail-mcp 0.7.18 → 0.7.20
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 +28 -4
- package/dist/cli.js +293 -66
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
A **Model Context Protocol** server that lets an agent operate any of the user's
|
|
4
4
|
inboxes through a single, unified tool surface.
|
|
5
5
|
|
|
6
|
+
> **v0.7.20** — Hardened long-running IMAP and new-email polling paths:
|
|
7
|
+
> IMAP connections now use explicit connection/greeting/socket/operation
|
|
8
|
+
> timeouts, reset stuck connections, and report post-TLS authentication
|
|
9
|
+
> failures with account-specific remediation guidance. `get_new_emails` now
|
|
10
|
+
> applies per-account timeouts during candidate collection and hydration so one
|
|
11
|
+
> hanging account does not block all-account polling; partial timeout failures
|
|
12
|
+
> are returned alongside successful accounts' emails.
|
|
13
|
+
>
|
|
14
|
+
> **v0.7.19** — Hardened Outlook ID handling when Microsoft Graph `$search`
|
|
15
|
+
> returns IDs that `/me/messages/{id}` rejects as malformed: `search_emails`
|
|
16
|
+
> now returns a translated readable immutable ID when available, and
|
|
17
|
+
> `read_email` retries malformed/stale IDs through `translateExchangeIds`
|
|
18
|
+
> before failing. `list_emails` also falls back from localized folder display
|
|
19
|
+
> names such as `Éléments envoyés` to the matching Graph folder ID.
|
|
20
|
+
>
|
|
6
21
|
> **v0.7.18** — `search_emails` can now omit `account` to search all
|
|
7
22
|
> registered accounts in parallel, returning account-annotated results plus
|
|
8
23
|
> per-account errors without discarding successful matches. All-account
|
|
@@ -295,10 +310,10 @@ account store.
|
|
|
295
310
|
| `get_account_settings` | `account` | Get signature (HTML) and style preferences for an account. |
|
|
296
311
|
| `set_account_settings` | `account`, `signature?`, `signaturePath?`, `style?` | Set signature HTML (inline or via file path) and font preferences. |
|
|
297
312
|
| `remove_account` | `email` | Deletes tokens for the account. |
|
|
298
|
-
| `list_emails` | `account`, `folder?`, `limit?`, `unreadOnly?`, `skip?` | Defaults: folder=`inbox`, limit=25. Supports pagination via `skip` — response includes `hasMore`. |
|
|
313
|
+
| `list_emails` | `account`, `folder?`, `limit?`, `unreadOnly?`, `skip?` | Defaults: folder=`inbox`, limit=25. Supports pagination via `skip` — response includes `hasMore`. Outlook accepts well-known folder names, folder IDs, and falls back from localized display names to matching folder IDs. |
|
|
299
314
|
| `get_new_emails` | `account?`, `limit?` | Pull new inbox emails not previously returned by this tool. `limit` defaults to 10 and is global when `account` is omitted. Returns full markdown bodies with attachment metadata; bodies may be truncated. |
|
|
300
|
-
| `search_emails` | `account?`, `query`, `limit?` | Search one account when `account` is provided, or omit it to search all registered accounts in parallel. Returns account-annotated summaries and partial per-account errors. Uses KQL on Outlook. |
|
|
301
|
-
| `read_email` | `account`, `id`, `format?` | Returns full body + recipients + attachment metadata. `format`: `markdown` (default), `html`, or `text`. |
|
|
315
|
+
| `search_emails` | `account?`, `query`, `limit?` | Search one account when `account` is provided, or omit it to search all registered accounts in parallel. Returns account-annotated summaries and partial per-account errors. Uses KQL on Outlook, and normalizes malformed Outlook search-result IDs to readable immutable IDs when possible. |
|
|
316
|
+
| `read_email` | `account`, `id`, `format?` | Returns full body + recipients + attachment metadata. `format`: `markdown` (default), `html`, or `text`. Outlook retries malformed/stale IDs through Graph ID translation before failing. |
|
|
302
317
|
| `read_attachment` | `account`, `messageId`, `attachmentId` | Download an attachment to a temporary file and return its path. |
|
|
303
318
|
| `archive_email` | `account`, `id` | Move a message to the Archive folder. |
|
|
304
319
|
| `trash_email` | `account`, `id` | Move a message to Deleted Items (trash). |
|
|
@@ -337,7 +352,9 @@ tool on their own schedule, for example every 30–60 seconds.
|
|
|
337
352
|
- `limit: 0` can initialize/check state without fetching message bodies.
|
|
338
353
|
|
|
339
354
|
All-account calls return partial failures as `errors: [{ account, message }]`
|
|
340
|
-
and still return successful accounts' emails.
|
|
355
|
+
and still return successful accounts' emails. Pollers should continue processing
|
|
356
|
+
returned emails when `errors` is non-empty, and use those entries to notify or
|
|
357
|
+
log which accounts are failing.
|
|
341
358
|
|
|
342
359
|
See [`examples/hermes/`](examples/hermes/) for a Hermes scheduler integration
|
|
343
360
|
that polls this tool and hands new-email payloads to a Hermes agent.
|
|
@@ -366,6 +383,13 @@ that polls this tool and hands new-email payloads to a Hermes agent.
|
|
|
366
383
|
it returns `{ "status": "ready", "account": {...} }`.
|
|
367
384
|
5. From then on, any tool can be called with `account: "<that-email>"`.
|
|
368
385
|
|
|
386
|
+
### IMAP
|
|
387
|
+
|
|
388
|
+
IMAP accounts are added synchronously with host/user/password settings. If the
|
|
389
|
+
server accepts TCP/TLS but closes during login, Hypermail reports an IMAP
|
|
390
|
+
authentication failure. Check the mailbox password or app-password, confirm IMAP
|
|
391
|
+
access is enabled by the provider, then re-add or update the account.
|
|
392
|
+
|
|
369
393
|
### Gmail
|
|
370
394
|
|
|
371
395
|
Gmail uses Google OAuth 2.0, matching the official Gmail MCP model. Google's
|
package/dist/cli.js
CHANGED
|
@@ -8625,19 +8625,19 @@ var getRefs = (options) => {
|
|
|
8625
8625
|
};
|
|
8626
8626
|
|
|
8627
8627
|
// node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@4.4.3/node_modules/zod-to-json-schema/dist/esm/errorMessages.js
|
|
8628
|
-
function addErrorMessage(res, key,
|
|
8628
|
+
function addErrorMessage(res, key, errorMessage2, refs) {
|
|
8629
8629
|
if (!refs?.errorMessages)
|
|
8630
8630
|
return;
|
|
8631
|
-
if (
|
|
8631
|
+
if (errorMessage2) {
|
|
8632
8632
|
res.errorMessage = {
|
|
8633
8633
|
...res.errorMessage,
|
|
8634
|
-
[key]:
|
|
8634
|
+
[key]: errorMessage2
|
|
8635
8635
|
};
|
|
8636
8636
|
}
|
|
8637
8637
|
}
|
|
8638
|
-
function setResponseValueAndErrors(res, key, value,
|
|
8638
|
+
function setResponseValueAndErrors(res, key, value, errorMessage2, refs) {
|
|
8639
8639
|
res[key] = value;
|
|
8640
|
-
addErrorMessage(res, key,
|
|
8640
|
+
addErrorMessage(res, key, errorMessage2, refs);
|
|
8641
8641
|
}
|
|
8642
8642
|
|
|
8643
8643
|
// node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@4.4.3/node_modules/zod-to-json-schema/dist/esm/getRelativePath.js
|
|
@@ -9955,8 +9955,8 @@ var Protocol = class {
|
|
|
9955
9955
|
if (queuedMessage.type === "response") {
|
|
9956
9956
|
resolver(message);
|
|
9957
9957
|
} else {
|
|
9958
|
-
const
|
|
9959
|
-
const error = new McpError(
|
|
9958
|
+
const errorMessage2 = message;
|
|
9959
|
+
const error = new McpError(errorMessage2.error.code, errorMessage2.error.message, errorMessage2.error.data);
|
|
9960
9960
|
resolver(error);
|
|
9961
9961
|
}
|
|
9962
9962
|
} else {
|
|
@@ -11256,23 +11256,23 @@ var Server = class extends Protocol {
|
|
|
11256
11256
|
const wrappedHandler = async (request, extra) => {
|
|
11257
11257
|
const validatedRequest = safeParse2(CallToolRequestSchema, request);
|
|
11258
11258
|
if (!validatedRequest.success) {
|
|
11259
|
-
const
|
|
11260
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${
|
|
11259
|
+
const errorMessage2 = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
|
|
11260
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage2}`);
|
|
11261
11261
|
}
|
|
11262
11262
|
const { params } = validatedRequest.data;
|
|
11263
11263
|
const result = await Promise.resolve(handler(request, extra));
|
|
11264
11264
|
if (params.task) {
|
|
11265
11265
|
const taskValidationResult = safeParse2(CreateTaskResultSchema, result);
|
|
11266
11266
|
if (!taskValidationResult.success) {
|
|
11267
|
-
const
|
|
11268
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${
|
|
11267
|
+
const errorMessage2 = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
|
|
11268
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage2}`);
|
|
11269
11269
|
}
|
|
11270
11270
|
return taskValidationResult.data;
|
|
11271
11271
|
}
|
|
11272
11272
|
const validationResult = safeParse2(CallToolResultSchema, result);
|
|
11273
11273
|
if (!validationResult.success) {
|
|
11274
|
-
const
|
|
11275
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${
|
|
11274
|
+
const errorMessage2 = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
|
|
11275
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage2}`);
|
|
11276
11276
|
}
|
|
11277
11277
|
return validationResult.data;
|
|
11278
11278
|
};
|
|
@@ -11767,12 +11767,12 @@ var McpServer = class {
|
|
|
11767
11767
|
* @param errorMessage - The error message.
|
|
11768
11768
|
* @returns The tool error result.
|
|
11769
11769
|
*/
|
|
11770
|
-
createToolError(
|
|
11770
|
+
createToolError(errorMessage2) {
|
|
11771
11771
|
return {
|
|
11772
11772
|
content: [
|
|
11773
11773
|
{
|
|
11774
11774
|
type: "text",
|
|
11775
|
-
text:
|
|
11775
|
+
text: errorMessage2
|
|
11776
11776
|
}
|
|
11777
11777
|
],
|
|
11778
11778
|
isError: true
|
|
@@ -11790,8 +11790,8 @@ var McpServer = class {
|
|
|
11790
11790
|
const parseResult = await safeParseAsync2(schemaToParse, args);
|
|
11791
11791
|
if (!parseResult.success) {
|
|
11792
11792
|
const error = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
11793
|
-
const
|
|
11794
|
-
throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${
|
|
11793
|
+
const errorMessage2 = getParseErrorMessage(error);
|
|
11794
|
+
throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage2}`);
|
|
11795
11795
|
}
|
|
11796
11796
|
return parseResult.data;
|
|
11797
11797
|
}
|
|
@@ -11815,8 +11815,8 @@ var McpServer = class {
|
|
|
11815
11815
|
const parseResult = await safeParseAsync2(outputObj, result.structuredContent);
|
|
11816
11816
|
if (!parseResult.success) {
|
|
11817
11817
|
const error = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
11818
|
-
const
|
|
11819
|
-
throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${
|
|
11818
|
+
const errorMessage2 = getParseErrorMessage(error);
|
|
11819
|
+
throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage2}`);
|
|
11820
11820
|
}
|
|
11821
11821
|
}
|
|
11822
11822
|
/**
|
|
@@ -12028,8 +12028,8 @@ var McpServer = class {
|
|
|
12028
12028
|
const parseResult = await safeParseAsync2(argsObj, request.params.arguments);
|
|
12029
12029
|
if (!parseResult.success) {
|
|
12030
12030
|
const error = "error" in parseResult ? parseResult.error : "Unknown error";
|
|
12031
|
-
const
|
|
12032
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${
|
|
12031
|
+
const errorMessage2 = getParseErrorMessage(error);
|
|
12032
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage2}`);
|
|
12033
12033
|
}
|
|
12034
12034
|
const args = parseResult.data;
|
|
12035
12035
|
const cb = prompt.callback;
|
|
@@ -14870,6 +14870,10 @@ var FULL_MESSAGE_SELECT = [
|
|
|
14870
14870
|
"hasAttachments",
|
|
14871
14871
|
"body"
|
|
14872
14872
|
].join(",");
|
|
14873
|
+
var EXCHANGE_ID_TRANSLATION_SOURCE_TYPES = [
|
|
14874
|
+
"ewsId",
|
|
14875
|
+
"restId"
|
|
14876
|
+
];
|
|
14873
14877
|
function graphErrorFields(err) {
|
|
14874
14878
|
if (typeof err !== "object" || err === null) {
|
|
14875
14879
|
return [String(err)];
|
|
@@ -14910,6 +14914,23 @@ function isStaleMessageIdError(err) {
|
|
|
14910
14914
|
return normalized.includes("erroritemnotfound") || normalized.includes("invalidid") || normalized.includes("object was not found in the store") || normalized.includes("not found in the store");
|
|
14911
14915
|
});
|
|
14912
14916
|
}
|
|
14917
|
+
async function translateMessageIdToImmutable(client, id) {
|
|
14918
|
+
for (const sourceIdType of EXCHANGE_ID_TRANSLATION_SOURCE_TYPES) {
|
|
14919
|
+
try {
|
|
14920
|
+
const res = await client.api("/me/translateExchangeIds").post({
|
|
14921
|
+
inputIds: [id],
|
|
14922
|
+
sourceIdType,
|
|
14923
|
+
targetIdType: "restImmutableEntryId"
|
|
14924
|
+
});
|
|
14925
|
+
const targetId = res.value?.find((item) => item.targetId)?.targetId;
|
|
14926
|
+
if (typeof targetId === "string" && targetId !== "" && targetId !== id) {
|
|
14927
|
+
return targetId;
|
|
14928
|
+
}
|
|
14929
|
+
} catch {
|
|
14930
|
+
}
|
|
14931
|
+
}
|
|
14932
|
+
return void 0;
|
|
14933
|
+
}
|
|
14913
14934
|
async function probeMessage(client, id, useImmutableIds) {
|
|
14914
14935
|
let req = client.api(`/me/messages/${encodeURIComponent(id)}`).select("id");
|
|
14915
14936
|
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
@@ -14918,27 +14939,58 @@ async function probeMessage(client, id, useImmutableIds) {
|
|
|
14918
14939
|
async function probeSearchResult(client, id) {
|
|
14919
14940
|
try {
|
|
14920
14941
|
await probeMessage(client, id, true);
|
|
14921
|
-
return
|
|
14942
|
+
return id;
|
|
14922
14943
|
} catch (err) {
|
|
14923
14944
|
if (!isStaleMessageIdError(err)) throw err;
|
|
14924
14945
|
}
|
|
14925
14946
|
try {
|
|
14926
14947
|
await probeMessage(client, id, false);
|
|
14927
|
-
return
|
|
14948
|
+
return id;
|
|
14928
14949
|
} catch (err) {
|
|
14929
|
-
if (isStaleMessageIdError(err))
|
|
14930
|
-
throw err;
|
|
14950
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14931
14951
|
}
|
|
14952
|
+
const translatedId = await translateMessageIdToImmutable(client, id);
|
|
14953
|
+
if (!translatedId) return void 0;
|
|
14954
|
+
try {
|
|
14955
|
+
await probeMessage(client, translatedId, true);
|
|
14956
|
+
return translatedId;
|
|
14957
|
+
} catch (err) {
|
|
14958
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14959
|
+
return void 0;
|
|
14960
|
+
}
|
|
14961
|
+
}
|
|
14962
|
+
async function getMessageById(client, id, useImmutableIds) {
|
|
14963
|
+
let req = client.api(`/me/messages/${encodeURIComponent(id)}`).select(FULL_MESSAGE_SELECT);
|
|
14964
|
+
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
14965
|
+
return await req.get();
|
|
14932
14966
|
}
|
|
14933
14967
|
async function getMessage(client, id) {
|
|
14968
|
+
let lastStaleError;
|
|
14969
|
+
try {
|
|
14970
|
+
return { message: await getMessageById(client, id, true), useImmutableIds: true };
|
|
14971
|
+
} catch (err) {
|
|
14972
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14973
|
+
lastStaleError = err;
|
|
14974
|
+
}
|
|
14934
14975
|
try {
|
|
14935
|
-
|
|
14936
|
-
return { message: message2, useImmutableIds: true };
|
|
14976
|
+
return { message: await getMessageById(client, id, false), useImmutableIds: false };
|
|
14937
14977
|
} catch (err) {
|
|
14938
14978
|
if (!isStaleMessageIdError(err)) throw err;
|
|
14979
|
+
lastStaleError = err;
|
|
14939
14980
|
}
|
|
14940
|
-
const
|
|
14941
|
-
|
|
14981
|
+
const translatedId = await translateMessageIdToImmutable(client, id);
|
|
14982
|
+
if (translatedId) {
|
|
14983
|
+
try {
|
|
14984
|
+
return {
|
|
14985
|
+
message: await getMessageById(client, translatedId, true),
|
|
14986
|
+
useImmutableIds: true
|
|
14987
|
+
};
|
|
14988
|
+
} catch (err) {
|
|
14989
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14990
|
+
lastStaleError = err;
|
|
14991
|
+
}
|
|
14992
|
+
}
|
|
14993
|
+
throw lastStaleError ?? new Error("Message not found");
|
|
14942
14994
|
}
|
|
14943
14995
|
async function listAttachments(client, messageId, useImmutableIds) {
|
|
14944
14996
|
let req = client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments`).select("id,name,contentType,size");
|
|
@@ -14956,14 +15008,36 @@ async function getAttachmentValue(client, messageId, attachmentId, useImmutableI
|
|
|
14956
15008
|
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
14957
15009
|
return await req.get();
|
|
14958
15010
|
}
|
|
15011
|
+
async function listMessagePage(client, folder, limit, skip, filterParts) {
|
|
15012
|
+
let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).top(limit).skip(skip).select(MESSAGE_SELECT).orderby("receivedDateTime DESC");
|
|
15013
|
+
if (filterParts.length > 0) req = req.filter(filterParts.join(" and "));
|
|
15014
|
+
return await req.get();
|
|
15015
|
+
}
|
|
15016
|
+
function normalizeFolderDisplayName(value) {
|
|
15017
|
+
return value.trim().normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase();
|
|
15018
|
+
}
|
|
15019
|
+
async function resolveFolderIdByDisplayName(client, displayName) {
|
|
15020
|
+
const wanted = normalizeFolderDisplayName(displayName);
|
|
15021
|
+
if (!wanted) return void 0;
|
|
15022
|
+
const res = await client.api("/me/mailFolders").top(100).select("id,displayName").get();
|
|
15023
|
+
return (res.value ?? []).find(
|
|
15024
|
+
(folder) => normalizeFolderDisplayName(folder.displayName) === wanted
|
|
15025
|
+
)?.id;
|
|
15026
|
+
}
|
|
14959
15027
|
async function listEmails(client, account, opts) {
|
|
14960
15028
|
const limit = clampLimit(opts.limit, 25, 100);
|
|
14961
15029
|
const folder = opts.folder ?? "inbox";
|
|
14962
15030
|
const filterParts = [];
|
|
14963
15031
|
if (opts.unreadOnly) filterParts.push("isRead eq false");
|
|
14964
|
-
let
|
|
14965
|
-
|
|
14966
|
-
|
|
15032
|
+
let res;
|
|
15033
|
+
try {
|
|
15034
|
+
res = await listMessagePage(client, folder, limit, opts.skip ?? 0, filterParts);
|
|
15035
|
+
} catch (err) {
|
|
15036
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
15037
|
+
const resolvedFolderId = await resolveFolderIdByDisplayName(client, folder);
|
|
15038
|
+
if (!resolvedFolderId || resolvedFolderId === folder) throw err;
|
|
15039
|
+
res = await listMessagePage(client, resolvedFolderId, limit, opts.skip ?? 0, filterParts);
|
|
15040
|
+
}
|
|
14967
15041
|
return {
|
|
14968
15042
|
items: res.value.map((m) => mapSummary(m, folder)),
|
|
14969
15043
|
hasMore: !!res["@odata.nextLink"]
|
|
@@ -14975,8 +15049,8 @@ async function searchEmails(client, account, query, opts) {
|
|
|
14975
15049
|
const summaries = res.value.map((m) => mapSummary(m));
|
|
14976
15050
|
return Promise.all(
|
|
14977
15051
|
summaries.map(async (summary) => {
|
|
14978
|
-
const
|
|
14979
|
-
return
|
|
15052
|
+
const readableId = await probeSearchResult(client, summary.id);
|
|
15053
|
+
return readableId ? { ...summary, id: readableId } : { ...summary, stale: true };
|
|
14980
15054
|
})
|
|
14981
15055
|
);
|
|
14982
15056
|
}
|
|
@@ -15202,6 +15276,10 @@ var OutlookProvider = class {
|
|
|
15202
15276
|
// src/providers/imap/client.ts
|
|
15203
15277
|
import { ImapFlow } from "imapflow";
|
|
15204
15278
|
import nodemailer from "nodemailer";
|
|
15279
|
+
var IMAP_CONNECTION_TIMEOUT_MS = 15e3;
|
|
15280
|
+
var IMAP_GREETING_TIMEOUT_MS = 15e3;
|
|
15281
|
+
var IMAP_SOCKET_TIMEOUT_MS = 45e3;
|
|
15282
|
+
var IMAP_OPERATION_TIMEOUT_MS = 45e3;
|
|
15205
15283
|
function isImapTokens(obj) {
|
|
15206
15284
|
if (typeof obj !== "object" || obj === null) return false;
|
|
15207
15285
|
const o = obj;
|
|
@@ -15216,10 +15294,12 @@ function extractTokens(account) {
|
|
|
15216
15294
|
return account.tokens;
|
|
15217
15295
|
}
|
|
15218
15296
|
var ImapClient = class {
|
|
15219
|
-
constructor(tokens) {
|
|
15297
|
+
constructor(tokens, opts = {}) {
|
|
15220
15298
|
this.tokens = tokens;
|
|
15299
|
+
this.opts = opts;
|
|
15221
15300
|
}
|
|
15222
15301
|
tokens;
|
|
15302
|
+
opts;
|
|
15223
15303
|
imap = null;
|
|
15224
15304
|
transporter = null;
|
|
15225
15305
|
connecting = null;
|
|
@@ -15235,16 +15315,28 @@ var ImapClient = class {
|
|
|
15235
15315
|
user: this.tokens.user,
|
|
15236
15316
|
pass: this.tokens.password
|
|
15237
15317
|
},
|
|
15238
|
-
logger: false
|
|
15318
|
+
logger: false,
|
|
15319
|
+
connectionTimeout: IMAP_CONNECTION_TIMEOUT_MS,
|
|
15320
|
+
greetingTimeout: IMAP_GREETING_TIMEOUT_MS,
|
|
15321
|
+
socketTimeout: IMAP_SOCKET_TIMEOUT_MS
|
|
15239
15322
|
});
|
|
15240
15323
|
if (!this.connecting) {
|
|
15241
|
-
this.
|
|
15242
|
-
|
|
15243
|
-
this.
|
|
15244
|
-
|
|
15324
|
+
this.log("connectStart");
|
|
15325
|
+
this.connecting = this.withOperationTimeout("connect", this.imap.connect()).then(() => {
|
|
15326
|
+
this.log("connectEnd");
|
|
15327
|
+
}).catch((err) => {
|
|
15328
|
+
const normalized = this.normalizeConnectError(err);
|
|
15329
|
+
this.log("connectError", {
|
|
15330
|
+
message: normalized.message,
|
|
15331
|
+
code: imapErrorCode(err) ?? null,
|
|
15332
|
+
authenticationFailed: hasAuthenticationFailedFlag(err)
|
|
15333
|
+
});
|
|
15334
|
+
this.resetImap();
|
|
15335
|
+
throw normalized;
|
|
15245
15336
|
});
|
|
15246
15337
|
}
|
|
15247
15338
|
await this.connecting;
|
|
15339
|
+
if (!this.imap) throw new Error("IMAP connection unavailable after connect");
|
|
15248
15340
|
return this.imap;
|
|
15249
15341
|
}
|
|
15250
15342
|
/** Get (or create) a nodemailer SMTP transporter. */
|
|
@@ -15271,7 +15363,17 @@ var ImapClient = class {
|
|
|
15271
15363
|
() => void 0,
|
|
15272
15364
|
() => void 0
|
|
15273
15365
|
);
|
|
15274
|
-
|
|
15366
|
+
try {
|
|
15367
|
+
return await this.withOperationTimeout("operation", run, () => {
|
|
15368
|
+
this.imapQueue = Promise.resolve();
|
|
15369
|
+
this.resetImap();
|
|
15370
|
+
});
|
|
15371
|
+
} catch (err) {
|
|
15372
|
+
if (err instanceof ImapTimeoutError) {
|
|
15373
|
+
this.log("operationTimeout", { message: err.message });
|
|
15374
|
+
}
|
|
15375
|
+
throw err;
|
|
15376
|
+
}
|
|
15275
15377
|
}
|
|
15276
15378
|
/**
|
|
15277
15379
|
* Acquire a mailbox lock and run `fn` with the mailbox selected.
|
|
@@ -15302,15 +15404,101 @@ var ImapClient = class {
|
|
|
15302
15404
|
this.transporter = null;
|
|
15303
15405
|
}
|
|
15304
15406
|
}
|
|
15407
|
+
resetImap() {
|
|
15408
|
+
const imap = this.imap;
|
|
15409
|
+
this.imap = null;
|
|
15410
|
+
this.connecting = null;
|
|
15411
|
+
if (imap) {
|
|
15412
|
+
imap.logout().catch(() => {
|
|
15413
|
+
});
|
|
15414
|
+
}
|
|
15415
|
+
}
|
|
15416
|
+
async withOperationTimeout(operation, promise, onTimeout) {
|
|
15417
|
+
let timer;
|
|
15418
|
+
const timeout = new Promise((_, reject) => {
|
|
15419
|
+
timer = setTimeout(() => {
|
|
15420
|
+
onTimeout?.();
|
|
15421
|
+
reject(new ImapTimeoutError(this.timeoutMessage(operation)));
|
|
15422
|
+
}, IMAP_OPERATION_TIMEOUT_MS);
|
|
15423
|
+
});
|
|
15424
|
+
try {
|
|
15425
|
+
return await Promise.race([promise, timeout]);
|
|
15426
|
+
} finally {
|
|
15427
|
+
if (timer) clearTimeout(timer);
|
|
15428
|
+
}
|
|
15429
|
+
}
|
|
15430
|
+
timeoutMessage(operation) {
|
|
15431
|
+
return `IMAP ${operation} timed out after ${IMAP_OPERATION_TIMEOUT_MS}ms for account ${this.accountLabel()} (${this.tokens.host}:${this.tokens.port})`;
|
|
15432
|
+
}
|
|
15433
|
+
accountLabel() {
|
|
15434
|
+
return this.opts.account ?? this.tokens.user;
|
|
15435
|
+
}
|
|
15436
|
+
normalizeConnectError(err) {
|
|
15437
|
+
if (!isImapAuthenticationError(err)) {
|
|
15438
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
15439
|
+
}
|
|
15440
|
+
const code = imapErrorCode(err);
|
|
15441
|
+
const codeSuffix = code ? ` Provider error code: ${code}.` : "";
|
|
15442
|
+
return new Error(
|
|
15443
|
+
`IMAP authentication failed for account ${this.accountLabel()} (${this.tokens.host}:${this.tokens.port}). Verify the password/app-password and IMAP access policy, then re-add or update the account.` + codeSuffix,
|
|
15444
|
+
{ cause: err }
|
|
15445
|
+
);
|
|
15446
|
+
}
|
|
15447
|
+
log(event, fields = {}) {
|
|
15448
|
+
(this.opts.logger ?? noopLogger).debug("imap", event, {
|
|
15449
|
+
account: this.accountLabel(),
|
|
15450
|
+
host: this.tokens.host,
|
|
15451
|
+
port: this.tokens.port,
|
|
15452
|
+
...fields
|
|
15453
|
+
});
|
|
15454
|
+
}
|
|
15455
|
+
};
|
|
15456
|
+
var ImapTimeoutError = class extends Error {
|
|
15457
|
+
constructor(message) {
|
|
15458
|
+
super(message);
|
|
15459
|
+
this.name = "ImapTimeoutError";
|
|
15460
|
+
}
|
|
15305
15461
|
};
|
|
15462
|
+
function errorMessage(err) {
|
|
15463
|
+
return err instanceof Error ? err.message : String(err);
|
|
15464
|
+
}
|
|
15465
|
+
function isImapAuthenticationError(err) {
|
|
15466
|
+
if (hasAuthenticationFailedFlag(err)) return true;
|
|
15467
|
+
const code = imapErrorCode(err);
|
|
15468
|
+
if (code === "ClosedAfterConnectTLS") return true;
|
|
15469
|
+
const message = errorMessage(err).toLowerCase();
|
|
15470
|
+
return message.includes("authentication failed") || message.includes("invalid credentials") || message.includes("login failed");
|
|
15471
|
+
}
|
|
15472
|
+
function hasAuthenticationFailedFlag(err) {
|
|
15473
|
+
return readErrorField(err, "authenticationFailed") === true;
|
|
15474
|
+
}
|
|
15475
|
+
function imapErrorCode(err) {
|
|
15476
|
+
const direct = readErrorField(err, "code");
|
|
15477
|
+
if (typeof direct === "string" && direct !== "NoConnection") return direct;
|
|
15478
|
+
const nested = readErrorField(readErrorField(err, "error"), "code");
|
|
15479
|
+
if (typeof nested === "string") return nested;
|
|
15480
|
+
if (typeof direct === "string") return direct;
|
|
15481
|
+
return void 0;
|
|
15482
|
+
}
|
|
15483
|
+
function readErrorField(err, field) {
|
|
15484
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
15485
|
+
return err[field];
|
|
15486
|
+
}
|
|
15306
15487
|
var ImapClientFactory = class {
|
|
15488
|
+
constructor(logger = noopLogger) {
|
|
15489
|
+
this.logger = logger;
|
|
15490
|
+
}
|
|
15491
|
+
logger;
|
|
15307
15492
|
cache = /* @__PURE__ */ new Map();
|
|
15308
15493
|
get(account) {
|
|
15309
15494
|
const key = account.email.toLowerCase();
|
|
15310
15495
|
const existing = this.cache.get(key);
|
|
15311
15496
|
if (existing) return existing;
|
|
15312
15497
|
const tokens = extractTokens(account);
|
|
15313
|
-
const client = new ImapClient(tokens
|
|
15498
|
+
const client = new ImapClient(tokens, {
|
|
15499
|
+
account: account.email,
|
|
15500
|
+
logger: this.logger
|
|
15501
|
+
});
|
|
15314
15502
|
this.cache.set(key, client);
|
|
15315
15503
|
return client;
|
|
15316
15504
|
}
|
|
@@ -16081,12 +16269,13 @@ async function deleteFolder2(clients, account, folderId) {
|
|
|
16081
16269
|
|
|
16082
16270
|
// src/providers/imap/index.ts
|
|
16083
16271
|
var ImapProvider = class {
|
|
16084
|
-
constructor(store) {
|
|
16272
|
+
constructor(store, logger = noopLogger) {
|
|
16085
16273
|
this.store = store;
|
|
16274
|
+
this.clients = new ImapClientFactory(logger);
|
|
16086
16275
|
}
|
|
16087
16276
|
store;
|
|
16088
16277
|
id = "imap";
|
|
16089
|
-
clients
|
|
16278
|
+
clients;
|
|
16090
16279
|
// ---------- account lifecycle ----------
|
|
16091
16280
|
async addAccount(input) {
|
|
16092
16281
|
if (!this.store) throw new Error("IMAP provider requires an AccountStore");
|
|
@@ -17271,7 +17460,7 @@ function buildRegistry(opts) {
|
|
|
17271
17460
|
clientId: outlookCfg?.clientId,
|
|
17272
17461
|
tenantId: outlookCfg?.tenantId
|
|
17273
17462
|
}));
|
|
17274
|
-
providers.set("imap", new ImapProvider(opts.store));
|
|
17463
|
+
providers.set("imap", new ImapProvider(opts.store, opts.logger));
|
|
17275
17464
|
const gmailCfg = opts.providers?.gmail;
|
|
17276
17465
|
providers.set("gmail", new GmailProvider({
|
|
17277
17466
|
store: opts.store,
|
|
@@ -17746,10 +17935,46 @@ function selectBody(msg, format) {
|
|
|
17746
17935
|
|
|
17747
17936
|
// src/tools/new-emails.ts
|
|
17748
17937
|
import { z as z4 } from "zod";
|
|
17938
|
+
|
|
17939
|
+
// src/tools/new-emails-timeout.ts
|
|
17940
|
+
var ACCOUNT_POLL_TIMEOUT_MS = 45e3;
|
|
17941
|
+
var MISSING_RECEIVED_AT = "1970-01-01T00:00:00.000Z";
|
|
17942
|
+
function effectiveReceivedAt(receivedAt) {
|
|
17943
|
+
return normalizeTimestamp2(receivedAt) ?? MISSING_RECEIVED_AT;
|
|
17944
|
+
}
|
|
17945
|
+
function normalizeTimestamp2(value) {
|
|
17946
|
+
if (!value) return null;
|
|
17947
|
+
const ms = Date.parse(value);
|
|
17948
|
+
if (!Number.isFinite(ms)) return null;
|
|
17949
|
+
return new Date(ms).toISOString();
|
|
17950
|
+
}
|
|
17951
|
+
function compareTimestamp2(a, b) {
|
|
17952
|
+
if (a < b) return -1;
|
|
17953
|
+
if (a > b) return 1;
|
|
17954
|
+
return 0;
|
|
17955
|
+
}
|
|
17956
|
+
async function withAccountPollTimeout(account, operation, promise) {
|
|
17957
|
+
let timer;
|
|
17958
|
+
const timeout = new Promise((_, reject) => {
|
|
17959
|
+
timer = setTimeout(() => {
|
|
17960
|
+
reject(
|
|
17961
|
+
new Error(
|
|
17962
|
+
`${operation} timed out after ${ACCOUNT_POLL_TIMEOUT_MS}ms for account ${account}`
|
|
17963
|
+
)
|
|
17964
|
+
);
|
|
17965
|
+
}, ACCOUNT_POLL_TIMEOUT_MS);
|
|
17966
|
+
});
|
|
17967
|
+
try {
|
|
17968
|
+
return await Promise.race([promise, timeout]);
|
|
17969
|
+
} finally {
|
|
17970
|
+
if (timer) clearTimeout(timer);
|
|
17971
|
+
}
|
|
17972
|
+
}
|
|
17973
|
+
|
|
17974
|
+
// src/tools/new-emails.ts
|
|
17749
17975
|
var DEFAULT_LIMIT = 10;
|
|
17750
17976
|
var BODY_LIMIT = 2e4;
|
|
17751
17977
|
var PAGE_SIZE = 100;
|
|
17752
|
-
var MISSING_RECEIVED_AT = "1970-01-01T00:00:00.000Z";
|
|
17753
17978
|
function registerNewEmailTool(server, ctx) {
|
|
17754
17979
|
const { store, registry, tools, logger = noopLogger } = ctx;
|
|
17755
17980
|
if (!shouldRegister("get_new_emails", tools)) return;
|
|
@@ -17800,7 +18025,11 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17800
18025
|
if (args.account) {
|
|
17801
18026
|
try {
|
|
17802
18027
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
17803
|
-
const result = await
|
|
18028
|
+
const result = await withAccountPollTimeout(
|
|
18029
|
+
account.email,
|
|
18030
|
+
"collect new-email candidates",
|
|
18031
|
+
collectCandidatesForAccount(store, provider, account, logger)
|
|
18032
|
+
);
|
|
17804
18033
|
const selected2 = oldestCandidatesFirst(result.candidates).slice(0, limit);
|
|
17805
18034
|
logger.debug("get-new-emails", "selected", {
|
|
17806
18035
|
account: result.account.email,
|
|
@@ -17810,7 +18039,11 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17810
18039
|
selectedReceivedAt: selected2.map((candidate) => candidate.timestamp),
|
|
17811
18040
|
limit
|
|
17812
18041
|
});
|
|
17813
|
-
const emails2 = limit === 0 ? [] : await
|
|
18042
|
+
const emails2 = limit === 0 ? [] : await withAccountPollTimeout(
|
|
18043
|
+
result.account.email,
|
|
18044
|
+
"hydrate new emails",
|
|
18045
|
+
hydrateAndAdvance(store, provider, result.account, selected2, logger)
|
|
18046
|
+
);
|
|
17814
18047
|
const data2 = { count: emails2.length, emails: emails2, errors: [] };
|
|
17815
18048
|
logger.debug("get-new-emails", "end", {
|
|
17816
18049
|
account: result.account.email,
|
|
@@ -17843,7 +18076,11 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17843
18076
|
accounts.map(async (stored) => {
|
|
17844
18077
|
try {
|
|
17845
18078
|
const { provider, account } = registry.resolveByEmail(stored.email);
|
|
17846
|
-
const result = await
|
|
18079
|
+
const result = await withAccountPollTimeout(
|
|
18080
|
+
account.email,
|
|
18081
|
+
"collect new-email candidates",
|
|
18082
|
+
collectCandidatesForAccount(store, provider, account, logger)
|
|
18083
|
+
);
|
|
17847
18084
|
return {
|
|
17848
18085
|
status: "ok",
|
|
17849
18086
|
account: result.account,
|
|
@@ -17895,7 +18132,11 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17895
18132
|
try {
|
|
17896
18133
|
return {
|
|
17897
18134
|
status: "ok",
|
|
17898
|
-
emails: await
|
|
18135
|
+
emails: await withAccountPollTimeout(
|
|
18136
|
+
email,
|
|
18137
|
+
"hydrate new emails",
|
|
18138
|
+
hydrateAndAdvance(store, provider, account, accountCandidates, logger)
|
|
18139
|
+
)
|
|
17899
18140
|
};
|
|
17900
18141
|
} catch (err) {
|
|
17901
18142
|
const message = errMsg(err);
|
|
@@ -18064,15 +18305,6 @@ function normalizeCheckpoint2(checkpoint) {
|
|
|
18064
18305
|
deliveredIdsAtReceivedAt: checkpoint?.deliveredIdsAtReceivedAt ?? []
|
|
18065
18306
|
};
|
|
18066
18307
|
}
|
|
18067
|
-
function effectiveReceivedAt(receivedAt) {
|
|
18068
|
-
return normalizeTimestamp2(receivedAt) ?? MISSING_RECEIVED_AT;
|
|
18069
|
-
}
|
|
18070
|
-
function normalizeTimestamp2(value) {
|
|
18071
|
-
if (!value) return null;
|
|
18072
|
-
const ms = Date.parse(value);
|
|
18073
|
-
if (!Number.isFinite(ms)) return null;
|
|
18074
|
-
return new Date(ms).toISOString();
|
|
18075
|
-
}
|
|
18076
18308
|
function oldestCandidatesFirst(items) {
|
|
18077
18309
|
return [...items].sort((a, b) => {
|
|
18078
18310
|
const byTimestamp = compareTimestamp2(a.timestamp, b.timestamp);
|
|
@@ -18088,11 +18320,6 @@ function compareNewEmailOutputOldestFirst(a, b) {
|
|
|
18088
18320
|
if (byTimestamp !== 0) return byTimestamp;
|
|
18089
18321
|
return a.id.localeCompare(b.id);
|
|
18090
18322
|
}
|
|
18091
|
-
function compareTimestamp2(a, b) {
|
|
18092
|
-
if (a < b) return -1;
|
|
18093
|
-
if (a > b) return 1;
|
|
18094
|
-
return 0;
|
|
18095
|
-
}
|
|
18096
18323
|
|
|
18097
18324
|
// src/tools/browse.ts
|
|
18098
18325
|
function registerBrowseTools(server, ctx) {
|
|
@@ -19064,7 +19291,7 @@ function registerTools(server, opts) {
|
|
|
19064
19291
|
// package.json
|
|
19065
19292
|
var package_default = {
|
|
19066
19293
|
name: "hypermail-mcp",
|
|
19067
|
-
version: "0.7.
|
|
19294
|
+
version: "0.7.20",
|
|
19068
19295
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
19069
19296
|
type: "module",
|
|
19070
19297
|
bin: {
|
|
@@ -19345,7 +19572,7 @@ async function startServer(opts) {
|
|
|
19345
19572
|
toolsDisabled: config.tools?.disabled ?? null
|
|
19346
19573
|
});
|
|
19347
19574
|
const store = await AccountStore.open({ dataDir, logger });
|
|
19348
|
-
const registry = buildRegistry({ store, providers: config.providers });
|
|
19575
|
+
const registry = buildRegistry({ store, providers: config.providers, logger });
|
|
19349
19576
|
const tools = resolveTools(config);
|
|
19350
19577
|
const createServer = () => {
|
|
19351
19578
|
const s = new McpServer(
|