hypermail-mcp 0.7.14 → 0.7.16

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/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() {
@@ -15250,6 +15264,17 @@ function resolveTrashMailbox(mailboxes) {
15250
15264
  }
15251
15265
  return "Trash";
15252
15266
  }
15267
+ function resolveDraftMailbox(mailboxes) {
15268
+ for (const mailbox of mailboxes) {
15269
+ const specialUse = mailbox.specialUse?.toLowerCase();
15270
+ if (specialUse === "\\drafts") return mailbox.path;
15271
+ const flags = mailbox.flags ? Array.from(mailbox.flags) : [];
15272
+ if (flags.some((flag) => flag.toLowerCase() === "\\drafts")) {
15273
+ return mailbox.path;
15274
+ }
15275
+ }
15276
+ return "Drafts";
15277
+ }
15253
15278
  function clampLimit2(v, dflt, max) {
15254
15279
  if (!v || v <= 0) return dflt;
15255
15280
  return Math.min(v, max);
@@ -15473,10 +15498,11 @@ async function readAttachment2(clients, account, messageId, attachmentId) {
15473
15498
  }
15474
15499
  async function listFolders2(clients, account, opts) {
15475
15500
  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
- });
15501
+ const mailboxes = await client.run(
15502
+ (imap) => imap.list({
15503
+ statusQuery: { messages: true, unseen: true, uidNext: true }
15504
+ })
15505
+ );
15480
15506
  let results = mailboxes.map(mapMailboxToListEntry);
15481
15507
  if (opts.parentFolderId) {
15482
15508
  const parentPath = opts.parentFolderId;
@@ -15615,59 +15641,11 @@ function removeMimePart(source, targetFilename, targetContentType) {
15615
15641
  async function sendEmail(clients, account, msg) {
15616
15642
  const client = clients.get(account);
15617
15643
  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
- }
15644
+ const mailOptions = await buildMailOptions(client, account, msg);
15667
15645
  const info = await transporter.sendMail(mailOptions);
15668
15646
  try {
15669
- const rawMsg = await buildRawMessage(account, msg, info.messageId);
15670
- await client.withMailbox("Sent", async (imap) => {
15647
+ const rawMsg = await buildRawMessage(client, account, msg, info.messageId);
15648
+ await client.run(async (imap) => {
15671
15649
  await imap.append("Sent", rawMsg, ["\\Seen"]);
15672
15650
  });
15673
15651
  } catch {
@@ -15676,48 +15654,58 @@ async function sendEmail(clients, account, msg) {
15676
15654
  }
15677
15655
  async function saveDraft(clients, account, msg) {
15678
15656
  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
- });
15657
+ const rawMsg = await buildRawMessage(client, account, msg);
15658
+ let folder = "Drafts";
15659
+ try {
15660
+ const result = await client.run(async (imap) => {
15661
+ folder = resolveDraftMailbox(await imap.list());
15662
+ return appendDraft(imap, folder, rawMsg);
15663
+ });
15664
+ return { id: encodeId(folder, appendUid(result, folder)) };
15665
+ } catch (err) {
15666
+ throw imapOperationError(`failed to save IMAP draft to ${folder}`, err);
15667
+ }
15684
15668
  }
15685
15669
  async function updateDraft2(clients, account, id, update) {
15686
15670
  const client = clients.get(account);
15687
15671
  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;
15672
+ try {
15673
+ return await client.withMailbox(folder, async (imap) => {
15674
+ const existing = await imap.fetchOne(
15675
+ uid,
15676
+ { source: true, envelope: true },
15677
+ { uid: true }
15678
+ );
15679
+ if (!existing?.source) {
15680
+ throw new Error(`draft not found: ${id}`);
15681
+ }
15682
+ const origSubject = existing.envelope ? existing.envelope.subject ?? "" : "";
15683
+ const updatedMsg = {
15684
+ from: `${account.displayName ?? ""} <${account.email}>`,
15685
+ subject: update.subject ?? origSubject,
15686
+ attachDataUrls: true
15687
+ };
15688
+ if (update.body !== void 0) {
15689
+ if (update.isHtml) {
15690
+ updatedMsg.html = update.body;
15691
+ } else {
15692
+ updatedMsg.text = update.body;
15693
+ }
15708
15694
  }
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);
15695
+ const raw = await new Promise((resolve, reject) => {
15696
+ const mc = new MailComposer(updatedMsg);
15697
+ mc.compile().build((err, buf) => {
15698
+ if (err) reject(err);
15699
+ else resolve(buf);
15700
+ });
15715
15701
  });
15702
+ const result = await appendDraft(imap, folder, raw.toString("utf-8"));
15703
+ await imap.messageDelete(uid, { uid: true });
15704
+ return { id: encodeId(folder, appendUid(result, folder)) };
15716
15705
  });
15717
- await imap.messageDelete(uid, { uid: true });
15718
- const result = await imap.append(folder, raw, ["\\Draft"]);
15719
- return { id: encodeId(folder, result.uid) };
15720
- });
15706
+ } catch (err) {
15707
+ throw imapOperationError(`failed to update IMAP draft ${id}`, err);
15708
+ }
15721
15709
  }
15722
15710
  async function moveEmail2(clients, account, id, destinationId) {
15723
15711
  if (isTrashFolderAlias(destinationId)) {
@@ -15733,9 +15721,8 @@ async function moveEmail2(clients, account, id, destinationId) {
15733
15721
  async function trashEmail(clients, account, id) {
15734
15722
  const client = clients.get(account);
15735
15723
  const { folder, uid } = decodeId(id);
15736
- const imap = await client.getImap();
15737
- const dest = resolveTrashMailbox(
15738
- await imap.list()
15724
+ const dest = await client.run(
15725
+ async (imap) => resolveTrashMailbox(await imap.list())
15739
15726
  );
15740
15727
  return client.withMailbox(folder, async (lockedImap) => {
15741
15728
  await lockedImap.messageMove(uid, dest, { uid: true });
@@ -15766,43 +15753,47 @@ async function sendDraft2(clients, account, id) {
15766
15753
  async function addAttachmentToDraft2(clients, account, draftId, name, contentBytes, contentType) {
15767
15754
  const client = clients.get(account);
15768
15755
  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);
15756
+ try {
15757
+ return await client.withMailbox(folder, async (imap) => {
15758
+ const existing = await imap.fetchOne(
15759
+ uid,
15760
+ { source: true },
15761
+ { uid: true }
15762
+ );
15763
+ if (!existing?.source) {
15764
+ throw new Error(`draft not found: ${draftId}`);
15765
+ }
15766
+ const sourceStr = typeof existing.source === "string" ? existing.source : Buffer.from(existing.source).toString("utf-8");
15767
+ const built = await new Promise((resolve, reject) => {
15768
+ const mc = new MailComposer({
15769
+ raw: sourceStr,
15770
+ attachments: [
15771
+ {
15772
+ filename: name,
15773
+ content: Buffer.from(contentBytes, "base64"),
15774
+ contentType: contentType ?? "application/octet-stream"
15775
+ }
15776
+ ]
15777
+ });
15778
+ mc.compile().build((err, buf) => {
15779
+ if (err) reject(err);
15780
+ else resolve(buf);
15781
+ });
15793
15782
  });
15783
+ const result = await appendDraft(imap, folder, built.toString("utf-8"));
15784
+ await imap.messageDelete(uid, { uid: true });
15785
+ return {
15786
+ id: encodeId(folder, appendUid(result, folder)),
15787
+ attachment: {
15788
+ id: randomUUID3(),
15789
+ name,
15790
+ contentType: contentType ?? "application/octet-stream"
15791
+ }
15792
+ };
15794
15793
  });
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
- });
15794
+ } catch (err) {
15795
+ throw imapOperationError(`failed to add attachment to IMAP draft ${draftId}`, err);
15796
+ }
15806
15797
  }
15807
15798
  async function removeAttachmentFromDraft2(clients, account, draftId, attachmentId) {
15808
15799
  const client = clients.get(account);
@@ -15837,7 +15828,7 @@ async function markRead2(clients, account, id, isRead) {
15837
15828
  }
15838
15829
  });
15839
15830
  }
15840
- async function buildRawMessage(account, msg, messageId) {
15831
+ async function buildMailOptions(client, account, msg, messageId) {
15841
15832
  const mailOptions = {
15842
15833
  from: `${account.displayName ?? ""} <${account.email}>`,
15843
15834
  to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
@@ -15856,16 +15847,55 @@ async function buildRawMessage(account, msg, messageId) {
15856
15847
  mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
15857
15848
  }
15858
15849
  if (msg.attachments && msg.attachments.length > 0) {
15859
- const fileAttachments = msg.attachments.map((att) => ({
15850
+ mailOptions.attachments = msg.attachments.map((att) => ({
15860
15851
  filename: att.name,
15861
15852
  content: Buffer.from(att.contentBytes, "base64"),
15862
15853
  contentType: att.contentType
15863
15854
  }));
15864
- mailOptions.attachments = fileAttachments;
15865
15855
  }
15866
15856
  if (messageId) {
15867
15857
  mailOptions.messageId = messageId;
15868
15858
  }
15859
+ await applyReferenceMessage(client, mailOptions, msg);
15860
+ return mailOptions;
15861
+ }
15862
+ async function applyReferenceMessage(client, mailOptions, msg) {
15863
+ if (!msg.inReplyTo && !msg.forwardMessageId) return;
15864
+ const refId = msg.inReplyTo || msg.forwardMessageId;
15865
+ if (!refId) return;
15866
+ try {
15867
+ const { folder: refFolder, uid: refUid } = decodeId(refId);
15868
+ const refMsg = await client.withMailbox(refFolder, async (imap) => {
15869
+ return imap.fetchOne(
15870
+ refUid,
15871
+ { envelope: true, source: true },
15872
+ { uid: true }
15873
+ );
15874
+ });
15875
+ if (refMsg?.envelope) {
15876
+ const env = refMsg.envelope;
15877
+ if (msg.inReplyTo && env.messageId && !msg.forwardMessageId) {
15878
+ mailOptions.inReplyTo = env.messageId;
15879
+ mailOptions.references = env.messageId;
15880
+ }
15881
+ }
15882
+ if (msg.forwardMessageId && refMsg?.source) {
15883
+ const sourceStr = typeof refMsg.source === "string" ? refMsg.source : Buffer.from(refMsg.source).toString("utf-8");
15884
+ 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>";
15885
+ if (mailOptions.html) {
15886
+ mailOptions.html = `${mailOptions.html}${divider}`;
15887
+ } else if (mailOptions.text) {
15888
+ mailOptions.text = `${mailOptions.text}
15889
+
15890
+ ---------- Forwarded message ---------
15891
+ ${sourceStr}`;
15892
+ }
15893
+ }
15894
+ } catch {
15895
+ }
15896
+ }
15897
+ async function buildRawMessage(client, account, msg, messageId) {
15898
+ const mailOptions = await buildMailOptions(client, account, msg, messageId);
15869
15899
  return new Promise((resolve, reject) => {
15870
15900
  const mc = new MailComposer(mailOptions);
15871
15901
  mc.compile().build((err, buf) => {
@@ -15874,6 +15904,48 @@ async function buildRawMessage(account, msg, messageId) {
15874
15904
  });
15875
15905
  });
15876
15906
  }
15907
+ async function appendDraft(imap, folder, rawMsg) {
15908
+ try {
15909
+ return await imap.append(folder, rawMsg, ["\\Draft"]);
15910
+ } catch (err) {
15911
+ if (!isImapCommandFailure(err)) throw err;
15912
+ return imap.append(folder, rawMsg);
15913
+ }
15914
+ }
15915
+ function appendUid(result, folder) {
15916
+ if (!result || typeof result !== "object") {
15917
+ throw new Error(`IMAP append to ${folder} did not return a UID`);
15918
+ }
15919
+ const uid = Number(result.uid);
15920
+ if (Number.isFinite(uid) && uid > 0) return uid;
15921
+ throw new Error(`IMAP append to ${folder} did not return a UID`);
15922
+ }
15923
+ function isImapCommandFailure(err) {
15924
+ const e = err;
15925
+ return typeof e.responseStatus === "string" || typeof e.message === "string" && e.message.includes("Command failed");
15926
+ }
15927
+ function imapOperationError(message, err) {
15928
+ const detail = formatImapError(err);
15929
+ return new Error(`${message}: ${detail}`, { cause: err });
15930
+ }
15931
+ function formatImapError(err) {
15932
+ const e = err;
15933
+ const parts = [];
15934
+ const message = err instanceof Error ? err.message : String(err);
15935
+ if (message) parts.push(message);
15936
+ for (const key of ["responseStatus", "responseText", "serverResponseCode", "response"]) {
15937
+ const value = e[key];
15938
+ if (value !== void 0 && value !== null) {
15939
+ parts.push(`${key}=${safeErrorValue(value)}`);
15940
+ }
15941
+ }
15942
+ return parts.join("; ");
15943
+ }
15944
+ function safeErrorValue(value) {
15945
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
15946
+ const text = raw ?? String(value);
15947
+ return text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
15948
+ }
15877
15949
 
15878
15950
  // src/providers/imap/folders.ts
15879
15951
  async function createFolder2(clients, account, input) {
@@ -17190,11 +17262,29 @@ var accountFullOutputSchema = z2.object({
17190
17262
  email: z2.string(),
17191
17263
  provider: providerIdEnum,
17192
17264
  displayName: z2.string().optional(),
17193
- tokens: z2.record(z2.string(), z2.unknown()),
17194
17265
  addedAt: z2.string(),
17195
17266
  signature: z2.string().optional(),
17196
17267
  style: styleOutputSchema.optional()
17197
17268
  });
17269
+ function publicAccount(account) {
17270
+ return {
17271
+ email: account.email,
17272
+ provider: account.provider,
17273
+ displayName: account.displayName,
17274
+ addedAt: account.addedAt,
17275
+ signature: account.signature,
17276
+ style: account.style
17277
+ };
17278
+ }
17279
+ function publicAccountResult(result) {
17280
+ if (typeof result !== "object" || result === null) {
17281
+ return { result };
17282
+ }
17283
+ const record2 = result;
17284
+ const account = record2.account;
17285
+ if (!account) return record2;
17286
+ return { ...record2, account: publicAccount(account) };
17287
+ }
17198
17288
  var emailSummaryOutputSchema = z2.object({
17199
17289
  id: z2.string(),
17200
17290
  subject: z2.string(),
@@ -17220,8 +17310,17 @@ var folderInfoOutputSchema = z2.object({
17220
17310
  totalItemCount: z2.number(),
17221
17311
  unreadItemCount: z2.number()
17222
17312
  });
17313
+ 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;
17314
+ function isSuspiciousPlainTextHtml(body) {
17315
+ return body.trim() !== "" && /\r\n|\r|\n/.test(body) && !meaningfulHtmlTagRe.test(body);
17316
+ }
17223
17317
  function composeBody(input) {
17224
17318
  const { body, format, signature, style, includeSignature } = input;
17319
+ if (format === "html" && isSuspiciousPlainTextHtml(body)) {
17320
+ throw new Error(
17321
+ '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>.'
17322
+ );
17323
+ }
17225
17324
  const htmlBody = format === "markdown" ? markdownToHtml(body) : body;
17226
17325
  const hasSignature = includeSignature && !!signature;
17227
17326
  const hasStyle = !!(style && (style.fontFamily || style.fontSize || style.fontColor));
@@ -17332,7 +17431,8 @@ function registerAccountTools(server, ctx) {
17332
17431
  email: args.email,
17333
17432
  config: args.config
17334
17433
  });
17335
- return ok(res, res);
17434
+ const data = publicAccountResult(res);
17435
+ return ok(data, data);
17336
17436
  } catch (err) {
17337
17437
  return fail(errMsg(err));
17338
17438
  }
@@ -17377,7 +17477,8 @@ function registerAccountTools(server, ctx) {
17377
17477
  code: args.code,
17378
17478
  state: args.state
17379
17479
  });
17380
- return ok(res, res);
17480
+ const data = publicAccountResult(res);
17481
+ return ok(data, data);
17381
17482
  } catch (err) {
17382
17483
  return fail(errMsg(err));
17383
17484
  }
@@ -18377,7 +18478,7 @@ var sendEmailSchema = z8.object({
18377
18478
  subject: z8.string(),
18378
18479
  body: z8.string(),
18379
18480
  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."
18481
+ "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
18482
  ),
18382
18483
  include_signature: z8.boolean().describe(
18383
18484
  "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 +18518,7 @@ var editDraftSchema = z8.object({
18417
18518
  "Deprecated alias for `new_text`. Body-only full replacement is not supported; provide `old_text` with this field."
18418
18519
  ),
18419
18520
  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."
18521
+ "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
18522
  ),
18422
18523
  include_signature: z8.boolean().optional().describe(
18423
18524
  "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."
@@ -18435,10 +18536,21 @@ var editDraftSchema = z8.object({
18435
18536
 
18436
18537
  // src/tools/compose.ts
18437
18538
  function registerComposeTools(server, ctx) {
18438
- const { store, registry, tools } = ctx;
18539
+ const { store, registry, tools, logger } = ctx;
18439
18540
  async function handleSendOrDraft(args, action, resultKey, toolName) {
18440
18541
  try {
18441
18542
  const { provider, account } = registry.resolveByEmail(args.account);
18543
+ logger?.debug("compose", "start", {
18544
+ tool: toolName,
18545
+ account: account.email,
18546
+ provider: provider.id,
18547
+ toCount: args.to.length,
18548
+ ccCount: args.cc?.length ?? 0,
18549
+ bccCount: args.bcc?.length ?? 0,
18550
+ hasReply: !!args.inReplyTo,
18551
+ hasForward: !!args.forwardMessageId,
18552
+ attachmentCount: args.attachments?.length ?? 0
18553
+ });
18442
18554
  if (args.include_signature && !account.signature) {
18443
18555
  return fail(
18444
18556
  "include_signature is true but no signature is configured for this account. Set up a signature first with set_account_settings."
@@ -18451,6 +18563,14 @@ function registerComposeTools(server, ctx) {
18451
18563
  style: account.style,
18452
18564
  includeSignature: args.include_signature
18453
18565
  });
18566
+ logger?.debug("compose", "composed", {
18567
+ tool: toolName,
18568
+ account: account.email,
18569
+ provider: provider.id,
18570
+ format: args.format,
18571
+ isHtml: composed.isHtml,
18572
+ includeSignature: args.include_signature
18573
+ });
18454
18574
  if (args.inReplyTo && args.forwardMessageId) {
18455
18575
  return fail(
18456
18576
  "inReplyTo and forwardMessageId are mutually exclusive \u2014 use one or the other"
@@ -18468,6 +18588,12 @@ function registerComposeTools(server, ctx) {
18468
18588
  };
18469
18589
  });
18470
18590
  }
18591
+ logger?.debug("compose", "attachmentsProcessed", {
18592
+ tool: toolName,
18593
+ account: account.email,
18594
+ provider: provider.id,
18595
+ attachmentCount: processedAttachments?.length ?? 0
18596
+ });
18471
18597
  const res = await action(provider, account, {
18472
18598
  to: args.to,
18473
18599
  cc: args.cc,
@@ -18480,13 +18606,42 @@ function registerComposeTools(server, ctx) {
18480
18606
  forwardMessageId: args.forwardMessageId,
18481
18607
  attachments: processedAttachments
18482
18608
  });
18609
+ logger?.debug("compose", "providerActionSuccess", {
18610
+ tool: toolName,
18611
+ account: account.email,
18612
+ provider: provider.id,
18613
+ hasId: !!res.id
18614
+ });
18483
18615
  const result = { [resultKey]: true, ...res };
18484
18616
  if (toolName === "draft_email" && res.id) {
18485
- const draft = await provider.readEmail(account, res.id);
18486
- result.draftHtml = draft.bodyHtml;
18617
+ try {
18618
+ const draft = await provider.readEmail(account, res.id);
18619
+ result.draftHtml = draft.bodyHtml;
18620
+ logger?.debug("compose", "draftReadbackSuccess", {
18621
+ tool: toolName,
18622
+ account: account.email,
18623
+ provider: provider.id,
18624
+ hasDraftHtml: draft.bodyHtml !== void 0
18625
+ });
18626
+ } catch (readErr) {
18627
+ const message = errMsg(readErr);
18628
+ result.warning = "Draft was created, but reading it back for draftHtml failed. Use read_email with the returned id to inspect it, or continue with send_draft if appropriate.";
18629
+ result.draftReadbackError = message;
18630
+ logger?.debug("compose", "draftReadbackError", {
18631
+ tool: toolName,
18632
+ account: account.email,
18633
+ provider: provider.id,
18634
+ message
18635
+ });
18636
+ }
18487
18637
  }
18488
18638
  return ok(result, result);
18489
18639
  } catch (err) {
18640
+ logger?.debug("compose", "error", {
18641
+ tool: toolName,
18642
+ account: args.account,
18643
+ message: errMsg(err)
18644
+ });
18490
18645
  return fail(errMsg(err));
18491
18646
  }
18492
18647
  }
@@ -18513,7 +18668,9 @@ function registerComposeTools(server, ctx) {
18513
18668
  const draftEmailOutputSchema = {
18514
18669
  draft: z9.literal(true),
18515
18670
  id: z9.string(),
18516
- draftHtml: z9.string().optional()
18671
+ draftHtml: z9.string().optional(),
18672
+ warning: z9.string().optional(),
18673
+ draftReadbackError: z9.string().optional()
18517
18674
  };
18518
18675
  if (shouldRegister("draft_email", tools)) {
18519
18676
  server.registerTool(
@@ -18710,13 +18867,13 @@ function registerTools(server, opts) {
18710
18867
  registerBrowseTools(server, { store, registry, tools, logger });
18711
18868
  registerFolderTools(server, { registry, tools });
18712
18869
  registerOrganizeTools(server, { registry, tools });
18713
- registerComposeTools(server, { store, registry, tools });
18870
+ registerComposeTools(server, { store, registry, tools, logger });
18714
18871
  }
18715
18872
 
18716
18873
  // package.json
18717
18874
  var package_default = {
18718
18875
  name: "hypermail-mcp",
18719
- version: "0.7.14",
18876
+ version: "0.7.16",
18720
18877
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
18721
18878
  type: "module",
18722
18879
  bin: {
@@ -18726,7 +18883,8 @@ var package_default = {
18726
18883
  files: [
18727
18884
  "dist",
18728
18885
  "README.md",
18729
- "LICENSE"
18886
+ "LICENSE",
18887
+ "examples"
18730
18888
  ],
18731
18889
  scripts: {
18732
18890
  build: "tsup",
@@ -19122,10 +19280,16 @@ function parseArgs(argv) {
19122
19280
  if (argv.length > 1) {
19123
19281
  throw new Error(`Unknown argument for generate-key: ${argv[1]}`);
19124
19282
  }
19125
- return { command: "generate-key", help: false, overrides: {} };
19283
+ return {
19284
+ command: "generate-key",
19285
+ help: false,
19286
+ version: false,
19287
+ overrides: {}
19288
+ };
19126
19289
  }
19127
19290
  const out = {
19128
19291
  help: false,
19292
+ version: false,
19129
19293
  overrides: {}
19130
19294
  };
19131
19295
  for (let i = 0; i < argv.length; i++) {
@@ -19156,6 +19320,9 @@ function parseArgs(argv) {
19156
19320
  case "--help":
19157
19321
  out.help = true;
19158
19322
  break;
19323
+ case "--version":
19324
+ out.version = true;
19325
+ break;
19159
19326
  default:
19160
19327
  if (arg.startsWith("-")) {
19161
19328
  throw new Error(`Unknown option: ${arg}`);
@@ -19180,6 +19347,7 @@ Options:
19180
19347
  --port <n> HTTP port (default: 3000)
19181
19348
  --host <addr> HTTP bind address (default: 127.0.0.1)
19182
19349
  --data-dir <path> Where to store the encrypted accounts file
19350
+ --version Show package version
19183
19351
  -h, --help Show this help
19184
19352
 
19185
19353
  Configuration:
@@ -19223,6 +19391,11 @@ async function main() {
19223
19391
  process.stdout.write(helpText());
19224
19392
  return;
19225
19393
  }
19394
+ if (opts.version) {
19395
+ process.stdout.write(`${VERSION}
19396
+ `);
19397
+ return;
19398
+ }
19226
19399
  const { config, warnings } = loadConfig(opts.overrides);
19227
19400
  for (const warning of warnings) {
19228
19401
  process.stderr.write(`[hypermail-mcp] warning: ${warning}