hypermail-mcp 0.7.14 → 0.7.15

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,16 @@
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.15** — Hardened IMAP draft creation: drafts now append directly to
7
+ > the Drafts mailbox, retry without the `\\Draft` flag when an IMAP server
8
+ > rejects flagged APPEND, and preserve IMAP reply/forward context in drafts the
9
+ > same way `send_email` does. IMAP operations for a single account are serialized
10
+ > to avoid shared-connection races, and IMAP write failures now include safe
11
+ > server response details instead of only `Command failed`. Account setup
12
+ > responses no longer expose stored provider tokens/passwords. Also rejects
13
+ > multiline plain text passed as `format: "html"`, and adds the Hermes
14
+ > `get_new_emails` poller example.
15
+ >
6
16
  > **v0.7.14** — Added opt-in `HYPERMAIL_DEBUG` structured stderr logs with
7
17
  > redaction for server startup, account-store locking/checkpoint writes, and
8
18
  > `get_new_emails` candidate/claim flow. Hardened Outlook `edit_draft` body
@@ -260,8 +270,8 @@ account store.
260
270
  | Tool | Inputs | Notes |
261
271
  | --- | --- | --- |
262
272
  | `list_accounts` | — | Returns registered emails + provider, no secrets. |
263
- | `add_account` | `provider`, `email?`, `config?` | Starts the provider add flow. Outlook returns a device code; Gmail returns an OAuth URL. Returns `{handle, verification:{type, userCode, verificationUri, expiresAt, message}}`. |
264
- | `complete_add_account` | `provider`, `handle`, `authorizationResponse?`, `code?`, `state?` | Returns `pending` / `ready` / `expired` / `error`. Gmail accepts a pasted final redirected URL or raw code/state for remote-safe completion. |
273
+ | `add_account` | `provider`, `email?`, `config?` | Starts the provider add flow. Outlook returns a device code; Gmail returns an OAuth URL. Ready account responses include public metadata only and never return stored tokens/passwords. Returns `{handle, verification:{type, userCode, verificationUri, expiresAt, message}}` for pending flows. |
274
+ | `complete_add_account` | `provider`, `handle`, `authorizationResponse?`, `code?`, `state?` | Returns `pending` / `ready` / `expired` / `error`. Ready account responses include public metadata only and never return stored tokens/passwords. Gmail accepts a pasted final redirected URL or raw code/state for remote-safe completion. |
265
275
  | `get_account_settings` | `account` | Get signature (HTML) and style preferences for an account. |
266
276
  | `set_account_settings` | `account`, `signature?`, `signaturePath?`, `style?` | Set signature HTML (inline or via file path) and font preferences. |
267
277
  | `remove_account` | `email` | Deletes tokens for the account. |
@@ -273,9 +283,9 @@ account store.
273
283
  | `archive_email` | `account`, `id` | Move a message to the Archive folder. |
274
284
  | `trash_email` | `account`, `id` | Move a message to Deleted Items (trash). |
275
285
  | `move_email` | `account`, `id`, `destination` | Move to any folder by well-known name (`inbox`, `drafts`, etc.) or custom folder ID. |
276
- | `send_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?`, `attachments?` | Send an email. `format` (`"html"` or `"markdown"`) controls body format — Markdown is converted to HTML via `marked`. Appends signature when `include_signature` is true. `inReplyTo` sends as threaded reply; `forwardMessageId` sends as forward. `inReplyTo` is required — set to `false` for new emails. `attachments` is an optional array of `{filePath, name?}` — files are read from disk and encoded automatically. |
286
+ | `send_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?`, `attachments?` | Send an email. `format` (`"html"` or `"markdown"`) controls body format — Markdown is converted to HTML via `marked`; multiline plain text with `format: "html"` is rejected, so use `"markdown"` for paragraphs or add tags like `<p>`/`<br>`. Appends signature when `include_signature` is true. `inReplyTo` sends as threaded reply; `forwardMessageId` sends as forward. `inReplyTo` is required — set to `false` for new emails. `attachments` is an optional array of `{filePath, name?}` — files are read from disk and encoded automatically. |
277
287
  | `draft_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?`, `attachments?` | Save as draft instead of sending. Same params as `send_email` including `attachments`. Returns the draft message ID and HTML body (`draftHtml`). `inReplyTo` is required — set to `false` for new emails. |
278
- | `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `old_text?`, `new_text?`, `body?`, `format?`, `include_signature?`, `new_attachments?`, `remove_attachments?` | Edit an existing draft by ID. Body edits require exact selected-section replacement: copy `old_text` from the current draft HTML (`draftHtml` or `read_email` with `format: "html"`) and provide `new_text`; the match must occur exactly once, and unselected content such as reply/forward history is preserved. Deprecated `body` is only an alias for `new_text` when `old_text` is also provided; body-only full replacement is rejected. Body edits are re-read after saving; if the updated body is not observable after retries, the tool returns an error instead of reporting success. `new_attachments` adds files (`{filePath, name?}[]`); `remove_attachments` removes by attachment ID (`string[]`). Returns the updated draft ID, HTML body (`draftHtml`), and attachment metadata. |
288
+ | `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `old_text?`, `new_text?`, `body?`, `format?`, `include_signature?`, `new_attachments?`, `remove_attachments?` | Edit an existing draft by ID. Body edits require exact selected-section replacement: copy `old_text` from the current draft HTML (`draftHtml` or `read_email` with `format: "html"`) and provide `new_text`; the match must occur exactly once, and unselected content such as reply/forward history is preserved. Deprecated `body` is only an alias for `new_text` when `old_text` is also provided; body-only full replacement is rejected. Multiline plain-text replacements with `format: "html"` are rejected; use `"markdown"` for paragraphs or add tags like `<p>`/`<br>`. Body edits are re-read after saving; if the updated body is not observable after retries, the tool returns an error instead of reporting success. `new_attachments` adds files (`{filePath, name?}[]`); `remove_attachments` removes by attachment ID (`string[]`). Returns the updated draft ID, HTML body (`draftHtml`), and attachment metadata. |
279
289
  | `send_draft` | `account`, `id` | Send an existing draft email by ID. Use with draft IDs returned by `draft_email` or `edit_draft`. |
280
290
  | `list_folders` | `account`, `parentFolderId?` | List available mail folders. Returns top-level folders by default, or children of `parentFolderId`. |
281
291
  | `create_folder` | `account`, `displayName`, `parentFolderId?` | Create a new mail folder under root (default) or the given parent. |
@@ -307,6 +317,9 @@ tool on their own schedule, for example every 30–60 seconds.
307
317
  All-account calls return partial failures as `errors: [{ account, message }]`
308
318
  and still return successful accounts' emails.
309
319
 
320
+ See [`examples/hermes/`](examples/hermes/) for a Hermes scheduler integration
321
+ that polls this tool and hands new-email payloads to a Hermes agent.
322
+
310
323
  ## Add-account flows
311
324
 
312
325
  ### Outlook
package/dist/cli.js CHANGED
@@ -15127,6 +15127,7 @@ var ImapClient = class {
15127
15127
  imap = null;
15128
15128
  transporter = null;
15129
15129
  connecting = null;
15130
+ imapQueue = Promise.resolve();
15130
15131
  /** Get (or create) the ImapFlow instance. */
15131
15132
  async getImap() {
15132
15133
  if (this.imap) return this.imap;
@@ -15164,18 +15165,31 @@ var ImapClient = class {
15164
15165
  });
15165
15166
  return this.transporter;
15166
15167
  }
15168
+ /** Run an IMAP operation serially on this account's shared connection. */
15169
+ async run(fn) {
15170
+ const run = this.imapQueue.catch(() => void 0).then(async () => {
15171
+ const imap = await this.getImap();
15172
+ return fn(imap);
15173
+ });
15174
+ this.imapQueue = run.then(
15175
+ () => void 0,
15176
+ () => void 0
15177
+ );
15178
+ return run;
15179
+ }
15167
15180
  /**
15168
15181
  * Acquire a mailbox lock and run `fn` with the mailbox selected.
15169
15182
  * Releases the lock automatically after `fn` completes.
15170
15183
  */
15171
15184
  async withMailbox(mailbox, fn) {
15172
- const imap = await this.getImap();
15173
- const lock = await imap.getMailboxLock(mailbox);
15174
- try {
15175
- return await fn(imap);
15176
- } finally {
15177
- lock.release();
15178
- }
15185
+ return this.run(async (imap) => {
15186
+ const lock = await imap.getMailboxLock(mailbox);
15187
+ try {
15188
+ return await fn(imap);
15189
+ } finally {
15190
+ lock.release();
15191
+ }
15192
+ });
15179
15193
  }
15180
15194
  /** Disconnect IMAP and close the SMTP pool. */
15181
15195
  async disconnect() {
@@ -15473,10 +15487,11 @@ async function readAttachment2(clients, account, messageId, attachmentId) {
15473
15487
  }
15474
15488
  async function listFolders2(clients, account, opts) {
15475
15489
  const client = clients.get(account);
15476
- const imap = await client.getImap();
15477
- const mailboxes = await imap.list({
15478
- statusQuery: { messages: true, unseen: true, uidNext: true }
15479
- });
15490
+ const mailboxes = await client.run(
15491
+ (imap) => imap.list({
15492
+ statusQuery: { messages: true, unseen: true, uidNext: true }
15493
+ })
15494
+ );
15480
15495
  let results = mailboxes.map(mapMailboxToListEntry);
15481
15496
  if (opts.parentFolderId) {
15482
15497
  const parentPath = opts.parentFolderId;
@@ -15615,59 +15630,11 @@ function removeMimePart(source, targetFilename, targetContentType) {
15615
15630
  async function sendEmail(clients, account, msg) {
15616
15631
  const client = clients.get(account);
15617
15632
  const transporter = client.getTransporter();
15618
- const mailOptions = {
15619
- from: `${account.displayName ?? ""} <${account.email}>`,
15620
- to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
15621
- subject: msg.subject
15622
- };
15623
- if (msg.isHtml) {
15624
- mailOptions.html = msg.body;
15625
- } else {
15626
- mailOptions.text = msg.body;
15627
- }
15628
- mailOptions.attachDataUrls = true;
15629
- if (msg.cc && msg.cc.length > 0) {
15630
- mailOptions.cc = msg.cc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
15631
- }
15632
- if (msg.bcc && msg.bcc.length > 0) {
15633
- mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
15634
- }
15635
- if (msg.inReplyTo || msg.forwardMessageId) {
15636
- const refId = msg.inReplyTo ?? msg.forwardMessageId;
15637
- if (refId) {
15638
- try {
15639
- const { folder: refFolder, uid: refUid } = decodeId(refId);
15640
- const refMsg = await client.withMailbox(refFolder, async (imap) => {
15641
- return imap.fetchOne(
15642
- refUid,
15643
- { envelope: true, source: true },
15644
- { uid: true }
15645
- );
15646
- });
15647
- if (refMsg?.envelope) {
15648
- const env = refMsg.envelope;
15649
- if (msg.inReplyTo && env.messageId && !msg.forwardMessageId) {
15650
- mailOptions.inReplyTo = env.messageId;
15651
- mailOptions.references = env.messageId;
15652
- }
15653
- }
15654
- if (msg.forwardMessageId && refMsg?.source) {
15655
- const sourceStr = typeof refMsg.source === "string" ? refMsg.source : Buffer.from(refMsg.source).toString("utf-8");
15656
- const divider = '\n\n<div style="line-height:12px"><br></div>\n\n<div style="border-left:2px solid #ccc; padding-left:8px; margin-left:0; color:#666">\n---------- Forwarded message ---------<br>' + sourceStr + "\n</div>";
15657
- if (mailOptions.html) {
15658
- mailOptions.html += divider;
15659
- } else if (mailOptions.text) {
15660
- mailOptions.text += "\n\n---------- Forwarded message ---------\n" + sourceStr;
15661
- }
15662
- }
15663
- } catch {
15664
- }
15665
- }
15666
- }
15633
+ const mailOptions = await buildMailOptions(client, account, msg);
15667
15634
  const info = await transporter.sendMail(mailOptions);
15668
15635
  try {
15669
- const rawMsg = await buildRawMessage(account, msg, info.messageId);
15670
- await client.withMailbox("Sent", async (imap) => {
15636
+ const rawMsg = await buildRawMessage(client, account, msg, info.messageId);
15637
+ await client.run(async (imap) => {
15671
15638
  await imap.append("Sent", rawMsg, ["\\Seen"]);
15672
15639
  });
15673
15640
  } catch {
@@ -15676,48 +15643,55 @@ async function sendEmail(clients, account, msg) {
15676
15643
  }
15677
15644
  async function saveDraft(clients, account, msg) {
15678
15645
  const client = clients.get(account);
15679
- const rawMsg = await buildRawMessage(account, msg);
15680
- return client.withMailbox("Drafts", async (imap) => {
15681
- const result = await imap.append("Drafts", rawMsg, ["\\Draft"]);
15682
- return { id: encodeId("Drafts", result.uid) };
15683
- });
15646
+ const folder = "Drafts";
15647
+ const rawMsg = await buildRawMessage(client, account, msg);
15648
+ try {
15649
+ const result = await client.run((imap) => appendDraft(imap, folder, rawMsg));
15650
+ return { id: encodeId(folder, appendUid(result, folder)) };
15651
+ } catch (err) {
15652
+ throw imapOperationError(`failed to save IMAP draft to ${folder}`, err);
15653
+ }
15684
15654
  }
15685
15655
  async function updateDraft2(clients, account, id, update) {
15686
15656
  const client = clients.get(account);
15687
15657
  const { folder, uid } = decodeId(id);
15688
- return client.withMailbox(folder, async (imap) => {
15689
- const existing = await imap.fetchOne(
15690
- uid,
15691
- { source: true, envelope: true },
15692
- { uid: true }
15693
- );
15694
- if (!existing?.source) {
15695
- throw new Error(`draft not found: ${id}`);
15696
- }
15697
- const origSubject = existing.envelope ? existing.envelope.subject ?? "" : "";
15698
- const updatedMsg = {
15699
- from: `${account.displayName ?? ""} <${account.email}>`,
15700
- subject: update.subject ?? origSubject,
15701
- attachDataUrls: true
15702
- };
15703
- if (update.body !== void 0) {
15704
- if (update.isHtml) {
15705
- updatedMsg.html = update.body;
15706
- } else {
15707
- updatedMsg.text = update.body;
15658
+ try {
15659
+ return await client.withMailbox(folder, async (imap) => {
15660
+ const existing = await imap.fetchOne(
15661
+ uid,
15662
+ { source: true, envelope: true },
15663
+ { uid: true }
15664
+ );
15665
+ if (!existing?.source) {
15666
+ throw new Error(`draft not found: ${id}`);
15667
+ }
15668
+ const origSubject = existing.envelope ? existing.envelope.subject ?? "" : "";
15669
+ const updatedMsg = {
15670
+ from: `${account.displayName ?? ""} <${account.email}>`,
15671
+ subject: update.subject ?? origSubject,
15672
+ attachDataUrls: true
15673
+ };
15674
+ if (update.body !== void 0) {
15675
+ if (update.isHtml) {
15676
+ updatedMsg.html = update.body;
15677
+ } else {
15678
+ updatedMsg.text = update.body;
15679
+ }
15708
15680
  }
15709
- }
15710
- const raw = await new Promise((resolve, reject) => {
15711
- const mc = new MailComposer(updatedMsg);
15712
- mc.compile().build((err, buf) => {
15713
- if (err) reject(err);
15714
- else resolve(buf);
15681
+ const raw = await new Promise((resolve, reject) => {
15682
+ const mc = new MailComposer(updatedMsg);
15683
+ mc.compile().build((err, buf) => {
15684
+ if (err) reject(err);
15685
+ else resolve(buf);
15686
+ });
15715
15687
  });
15688
+ const result = await appendDraft(imap, folder, raw.toString("utf-8"));
15689
+ await imap.messageDelete(uid, { uid: true });
15690
+ return { id: encodeId(folder, appendUid(result, folder)) };
15716
15691
  });
15717
- await imap.messageDelete(uid, { uid: true });
15718
- const result = await imap.append(folder, raw, ["\\Draft"]);
15719
- return { id: encodeId(folder, result.uid) };
15720
- });
15692
+ } catch (err) {
15693
+ throw imapOperationError(`failed to update IMAP draft ${id}`, err);
15694
+ }
15721
15695
  }
15722
15696
  async function moveEmail2(clients, account, id, destinationId) {
15723
15697
  if (isTrashFolderAlias(destinationId)) {
@@ -15733,9 +15707,8 @@ async function moveEmail2(clients, account, id, destinationId) {
15733
15707
  async function trashEmail(clients, account, id) {
15734
15708
  const client = clients.get(account);
15735
15709
  const { folder, uid } = decodeId(id);
15736
- const imap = await client.getImap();
15737
- const dest = resolveTrashMailbox(
15738
- await imap.list()
15710
+ const dest = await client.run(
15711
+ async (imap) => resolveTrashMailbox(await imap.list())
15739
15712
  );
15740
15713
  return client.withMailbox(folder, async (lockedImap) => {
15741
15714
  await lockedImap.messageMove(uid, dest, { uid: true });
@@ -15766,43 +15739,47 @@ async function sendDraft2(clients, account, id) {
15766
15739
  async function addAttachmentToDraft2(clients, account, draftId, name, contentBytes, contentType) {
15767
15740
  const client = clients.get(account);
15768
15741
  const { folder, uid } = decodeId(draftId);
15769
- return client.withMailbox(folder, async (imap) => {
15770
- const existing = await imap.fetchOne(
15771
- uid,
15772
- { source: true },
15773
- { uid: true }
15774
- );
15775
- if (!existing?.source) {
15776
- throw new Error(`draft not found: ${draftId}`);
15777
- }
15778
- const sourceStr = typeof existing.source === "string" ? existing.source : Buffer.from(existing.source).toString("utf-8");
15779
- const built = await new Promise((resolve, reject) => {
15780
- const mc = new MailComposer({
15781
- raw: sourceStr,
15782
- attachments: [
15783
- {
15784
- filename: name,
15785
- content: Buffer.from(contentBytes, "base64"),
15786
- contentType: contentType ?? "application/octet-stream"
15787
- }
15788
- ]
15789
- });
15790
- mc.compile().build((err, buf) => {
15791
- if (err) reject(err);
15792
- else resolve(buf);
15742
+ try {
15743
+ return await client.withMailbox(folder, async (imap) => {
15744
+ const existing = await imap.fetchOne(
15745
+ uid,
15746
+ { source: true },
15747
+ { uid: true }
15748
+ );
15749
+ if (!existing?.source) {
15750
+ throw new Error(`draft not found: ${draftId}`);
15751
+ }
15752
+ const sourceStr = typeof existing.source === "string" ? existing.source : Buffer.from(existing.source).toString("utf-8");
15753
+ const built = await new Promise((resolve, reject) => {
15754
+ const mc = new MailComposer({
15755
+ raw: sourceStr,
15756
+ attachments: [
15757
+ {
15758
+ filename: name,
15759
+ content: Buffer.from(contentBytes, "base64"),
15760
+ contentType: contentType ?? "application/octet-stream"
15761
+ }
15762
+ ]
15763
+ });
15764
+ mc.compile().build((err, buf) => {
15765
+ if (err) reject(err);
15766
+ else resolve(buf);
15767
+ });
15793
15768
  });
15769
+ const result = await appendDraft(imap, folder, built.toString("utf-8"));
15770
+ await imap.messageDelete(uid, { uid: true });
15771
+ return {
15772
+ id: encodeId(folder, appendUid(result, folder)),
15773
+ attachment: {
15774
+ id: randomUUID3(),
15775
+ name,
15776
+ contentType: contentType ?? "application/octet-stream"
15777
+ }
15778
+ };
15794
15779
  });
15795
- await imap.messageDelete(uid, { uid: true });
15796
- const result = await imap.append(folder, built, ["\\Draft"]);
15797
- return {
15798
- id: encodeId(folder, result.uid),
15799
- attachment: {
15800
- id: randomUUID3(),
15801
- name,
15802
- contentType: contentType ?? "application/octet-stream"
15803
- }
15804
- };
15805
- });
15780
+ } catch (err) {
15781
+ throw imapOperationError(`failed to add attachment to IMAP draft ${draftId}`, err);
15782
+ }
15806
15783
  }
15807
15784
  async function removeAttachmentFromDraft2(clients, account, draftId, attachmentId) {
15808
15785
  const client = clients.get(account);
@@ -15837,7 +15814,7 @@ async function markRead2(clients, account, id, isRead) {
15837
15814
  }
15838
15815
  });
15839
15816
  }
15840
- async function buildRawMessage(account, msg, messageId) {
15817
+ async function buildMailOptions(client, account, msg, messageId) {
15841
15818
  const mailOptions = {
15842
15819
  from: `${account.displayName ?? ""} <${account.email}>`,
15843
15820
  to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
@@ -15856,16 +15833,55 @@ async function buildRawMessage(account, msg, messageId) {
15856
15833
  mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
15857
15834
  }
15858
15835
  if (msg.attachments && msg.attachments.length > 0) {
15859
- const fileAttachments = msg.attachments.map((att) => ({
15836
+ mailOptions.attachments = msg.attachments.map((att) => ({
15860
15837
  filename: att.name,
15861
15838
  content: Buffer.from(att.contentBytes, "base64"),
15862
15839
  contentType: att.contentType
15863
15840
  }));
15864
- mailOptions.attachments = fileAttachments;
15865
15841
  }
15866
15842
  if (messageId) {
15867
15843
  mailOptions.messageId = messageId;
15868
15844
  }
15845
+ await applyReferenceMessage(client, mailOptions, msg);
15846
+ return mailOptions;
15847
+ }
15848
+ async function applyReferenceMessage(client, mailOptions, msg) {
15849
+ if (!msg.inReplyTo && !msg.forwardMessageId) return;
15850
+ const refId = msg.inReplyTo || msg.forwardMessageId;
15851
+ if (!refId) return;
15852
+ try {
15853
+ const { folder: refFolder, uid: refUid } = decodeId(refId);
15854
+ const refMsg = await client.withMailbox(refFolder, async (imap) => {
15855
+ return imap.fetchOne(
15856
+ refUid,
15857
+ { envelope: true, source: true },
15858
+ { uid: true }
15859
+ );
15860
+ });
15861
+ if (refMsg?.envelope) {
15862
+ const env = refMsg.envelope;
15863
+ if (msg.inReplyTo && env.messageId && !msg.forwardMessageId) {
15864
+ mailOptions.inReplyTo = env.messageId;
15865
+ mailOptions.references = env.messageId;
15866
+ }
15867
+ }
15868
+ if (msg.forwardMessageId && refMsg?.source) {
15869
+ const sourceStr = typeof refMsg.source === "string" ? refMsg.source : Buffer.from(refMsg.source).toString("utf-8");
15870
+ const divider = '\n\n<div style="line-height:12px"><br></div>\n\n<div style="border-left:2px solid #ccc; padding-left:8px; margin-left:0; color:#666">\n---------- Forwarded message ---------<br>' + sourceStr + "\n</div>";
15871
+ if (mailOptions.html) {
15872
+ mailOptions.html = `${mailOptions.html}${divider}`;
15873
+ } else if (mailOptions.text) {
15874
+ mailOptions.text = `${mailOptions.text}
15875
+
15876
+ ---------- Forwarded message ---------
15877
+ ${sourceStr}`;
15878
+ }
15879
+ }
15880
+ } catch {
15881
+ }
15882
+ }
15883
+ async function buildRawMessage(client, account, msg, messageId) {
15884
+ const mailOptions = await buildMailOptions(client, account, msg, messageId);
15869
15885
  return new Promise((resolve, reject) => {
15870
15886
  const mc = new MailComposer(mailOptions);
15871
15887
  mc.compile().build((err, buf) => {
@@ -15874,6 +15890,48 @@ async function buildRawMessage(account, msg, messageId) {
15874
15890
  });
15875
15891
  });
15876
15892
  }
15893
+ async function appendDraft(imap, folder, rawMsg) {
15894
+ try {
15895
+ return await imap.append(folder, rawMsg, ["\\Draft"]);
15896
+ } catch (err) {
15897
+ if (!isImapCommandFailure(err)) throw err;
15898
+ return imap.append(folder, rawMsg);
15899
+ }
15900
+ }
15901
+ function appendUid(result, folder) {
15902
+ if (!result || typeof result !== "object") {
15903
+ throw new Error(`IMAP append to ${folder} did not return a UID`);
15904
+ }
15905
+ const uid = Number(result.uid);
15906
+ if (Number.isFinite(uid) && uid > 0) return uid;
15907
+ throw new Error(`IMAP append to ${folder} did not return a UID`);
15908
+ }
15909
+ function isImapCommandFailure(err) {
15910
+ const e = err;
15911
+ return typeof e.responseStatus === "string" || typeof e.message === "string" && e.message.includes("Command failed");
15912
+ }
15913
+ function imapOperationError(message, err) {
15914
+ const detail = formatImapError(err);
15915
+ return new Error(`${message}: ${detail}`, { cause: err });
15916
+ }
15917
+ function formatImapError(err) {
15918
+ const e = err;
15919
+ const parts = [];
15920
+ const message = err instanceof Error ? err.message : String(err);
15921
+ if (message) parts.push(message);
15922
+ for (const key of ["responseStatus", "responseText", "serverResponseCode", "response"]) {
15923
+ const value = e[key];
15924
+ if (value !== void 0 && value !== null) {
15925
+ parts.push(`${key}=${safeErrorValue(value)}`);
15926
+ }
15927
+ }
15928
+ return parts.join("; ");
15929
+ }
15930
+ function safeErrorValue(value) {
15931
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
15932
+ const text = raw ?? String(value);
15933
+ return text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
15934
+ }
15877
15935
 
15878
15936
  // src/providers/imap/folders.ts
15879
15937
  async function createFolder2(clients, account, input) {
@@ -17190,11 +17248,29 @@ var accountFullOutputSchema = z2.object({
17190
17248
  email: z2.string(),
17191
17249
  provider: providerIdEnum,
17192
17250
  displayName: z2.string().optional(),
17193
- tokens: z2.record(z2.string(), z2.unknown()),
17194
17251
  addedAt: z2.string(),
17195
17252
  signature: z2.string().optional(),
17196
17253
  style: styleOutputSchema.optional()
17197
17254
  });
17255
+ function publicAccount(account) {
17256
+ return {
17257
+ email: account.email,
17258
+ provider: account.provider,
17259
+ displayName: account.displayName,
17260
+ addedAt: account.addedAt,
17261
+ signature: account.signature,
17262
+ style: account.style
17263
+ };
17264
+ }
17265
+ function publicAccountResult(result) {
17266
+ if (typeof result !== "object" || result === null) {
17267
+ return { result };
17268
+ }
17269
+ const record2 = result;
17270
+ const account = record2.account;
17271
+ if (!account) return record2;
17272
+ return { ...record2, account: publicAccount(account) };
17273
+ }
17198
17274
  var emailSummaryOutputSchema = z2.object({
17199
17275
  id: z2.string(),
17200
17276
  subject: z2.string(),
@@ -17220,8 +17296,17 @@ var folderInfoOutputSchema = z2.object({
17220
17296
  totalItemCount: z2.number(),
17221
17297
  unreadItemCount: z2.number()
17222
17298
  });
17299
+ var meaningfulHtmlTagRe = /<\/?(?:p|div|br|blockquote|table|thead|tbody|tfoot|tr|td|th|ul|ol|li|a|span|font|b|strong|em|i|u|img|hr|pre|h[1-6])\b/i;
17300
+ function isSuspiciousPlainTextHtml(body) {
17301
+ return body.trim() !== "" && /\r\n|\r|\n/.test(body) && !meaningfulHtmlTagRe.test(body);
17302
+ }
17223
17303
  function composeBody(input) {
17224
17304
  const { body, format, signature, style, includeSignature } = input;
17305
+ if (format === "html" && isSuspiciousPlainTextHtml(body)) {
17306
+ throw new Error(
17307
+ 'format: "html" requires valid HTML for multiline bodies. Use format: "markdown" for plain text with paragraphs, or add HTML tags such as <p> or <br>.'
17308
+ );
17309
+ }
17225
17310
  const htmlBody = format === "markdown" ? markdownToHtml(body) : body;
17226
17311
  const hasSignature = includeSignature && !!signature;
17227
17312
  const hasStyle = !!(style && (style.fontFamily || style.fontSize || style.fontColor));
@@ -17332,7 +17417,8 @@ function registerAccountTools(server, ctx) {
17332
17417
  email: args.email,
17333
17418
  config: args.config
17334
17419
  });
17335
- return ok(res, res);
17420
+ const data = publicAccountResult(res);
17421
+ return ok(data, data);
17336
17422
  } catch (err) {
17337
17423
  return fail(errMsg(err));
17338
17424
  }
@@ -17377,7 +17463,8 @@ function registerAccountTools(server, ctx) {
17377
17463
  code: args.code,
17378
17464
  state: args.state
17379
17465
  });
17380
- return ok(res, res);
17466
+ const data = publicAccountResult(res);
17467
+ return ok(data, data);
17381
17468
  } catch (err) {
17382
17469
  return fail(errMsg(err));
17383
17470
  }
@@ -18377,7 +18464,7 @@ var sendEmailSchema = z8.object({
18377
18464
  subject: z8.string(),
18378
18465
  body: z8.string(),
18379
18466
  format: z8.enum(["html", "markdown"]).describe(
18380
- "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."
18467
+ "Body format. 'html' sends the body as-is (must be valid HTML); multiline plain text with format='html' is rejected, so use 'markdown' for paragraphs or add tags like <p>/<br>. 'markdown' converts the body from Markdown to HTML for clean rendering on the recipient side."
18381
18468
  ),
18382
18469
  include_signature: z8.boolean().describe(
18383
18470
  "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."
@@ -18417,7 +18504,7 @@ var editDraftSchema = z8.object({
18417
18504
  "Deprecated alias for `new_text`. Body-only full replacement is not supported; provide `old_text` with this field."
18418
18505
  ),
18419
18506
  format: z8.enum(["html", "markdown"]).optional().describe(
18420
- "Replacement format. Only meaningful when `new_text` or deprecated `body` is also provided. 'html' inserts the replacement as-is (must be valid HTML). 'markdown' converts the replacement from Markdown to HTML."
18507
+ "Replacement format. Only meaningful when `new_text` or deprecated `body` is also provided. 'html' inserts the replacement as-is (must be valid HTML); multiline plain text with format='html' is rejected, so use 'markdown' for paragraphs or add tags like <p>/<br>. 'markdown' converts the replacement from Markdown to HTML."
18421
18508
  ),
18422
18509
  include_signature: z8.boolean().optional().describe(
18423
18510
  "Whether to append the account's saved HTML signature to the replacement section. If true, don't include a signature in `new_text`/`body`. Only meaningful when replacement content is provided. Returns an error if true but no signature is configured for this account."
@@ -18716,7 +18803,7 @@ function registerTools(server, opts) {
18716
18803
  // package.json
18717
18804
  var package_default = {
18718
18805
  name: "hypermail-mcp",
18719
- version: "0.7.14",
18806
+ version: "0.7.15",
18720
18807
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
18721
18808
  type: "module",
18722
18809
  bin: {
@@ -18726,7 +18813,8 @@ var package_default = {
18726
18813
  files: [
18727
18814
  "dist",
18728
18815
  "README.md",
18729
- "LICENSE"
18816
+ "LICENSE",
18817
+ "examples"
18730
18818
  ],
18731
18819
  scripts: {
18732
18820
  build: "tsup",