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 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, errorMessage, refs) {
8628
+ function addErrorMessage(res, key, errorMessage2, refs) {
8629
8629
  if (!refs?.errorMessages)
8630
8630
  return;
8631
- if (errorMessage) {
8631
+ if (errorMessage2) {
8632
8632
  res.errorMessage = {
8633
8633
  ...res.errorMessage,
8634
- [key]: errorMessage
8634
+ [key]: errorMessage2
8635
8635
  };
8636
8636
  }
8637
8637
  }
8638
- function setResponseValueAndErrors(res, key, value, errorMessage, refs) {
8638
+ function setResponseValueAndErrors(res, key, value, errorMessage2, refs) {
8639
8639
  res[key] = value;
8640
- addErrorMessage(res, key, errorMessage, refs);
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 errorMessage = message;
9959
- const error = new McpError(errorMessage.error.code, errorMessage.error.message, errorMessage.error.data);
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 errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
11260
- throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
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 errorMessage = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
11268
- throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
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 errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
11275
- throw new McpError(ErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage}`);
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(errorMessage) {
11770
+ createToolError(errorMessage2) {
11771
11771
  return {
11772
11772
  content: [
11773
11773
  {
11774
11774
  type: "text",
11775
- text: errorMessage
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 errorMessage = getParseErrorMessage(error);
11794
- throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`);
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 errorMessage = getParseErrorMessage(error);
11819
- throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`);
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 errorMessage = getParseErrorMessage(error);
12032
- throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`);
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 true;
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 true;
14948
+ return id;
14928
14949
  } catch (err) {
14929
- if (isStaleMessageIdError(err)) return false;
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
- const message2 = await client.api(`/me/messages/${encodeURIComponent(id)}`).header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).select(FULL_MESSAGE_SELECT).get();
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 message = await client.api(`/me/messages/${encodeURIComponent(id)}`).select(FULL_MESSAGE_SELECT).get();
14941
- return { message, useImmutableIds: false };
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 req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).top(limit).skip(opts.skip ?? 0).select(MESSAGE_SELECT).orderby("receivedDateTime DESC");
14965
- if (filterParts.length > 0) req = req.filter(filterParts.join(" and "));
14966
- const res = await req.get();
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 readable = await probeSearchResult(client, summary.id);
14979
- return readable ? summary : { ...summary, stale: true };
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.connecting = this.imap.connect().catch((err) => {
15242
- this.imap = null;
15243
- this.connecting = null;
15244
- throw err;
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
- return run;
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 = new ImapClientFactory();
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 collectCandidatesForAccount(store, provider, account, logger);
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 hydrateAndAdvance(store, provider, result.account, selected2, logger);
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 collectCandidatesForAccount(store, provider, account, logger);
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 hydrateAndAdvance(store, provider, account, accountCandidates, logger)
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.18",
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(