hypermail-mcp 0.7.3 → 0.7.5

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
@@ -170,29 +170,6 @@ var AccountStore = class _AccountStore {
170
170
 
171
171
  // src/providers/outlook/index.ts
172
172
  import { randomUUID as randomUUID2 } from "crypto";
173
- import { writeFileSync } from "fs";
174
- import { tmpdir } from "os";
175
- import { join as pathJoin } from "path";
176
- import { ResponseType } from "@microsoft/microsoft-graph-client";
177
-
178
- // src/providers/shared/inline-images.ts
179
- import { randomUUID } from "crypto";
180
- function parseInlineImages(html) {
181
- const images = [];
182
- const re = /src="data:image\/([\w+]+);base64,([^"]+)"/gi;
183
- const transformed = html.replace(re, (_fullMatch, mimeSubtype, b64) => {
184
- const contentId = `sig-img-${randomUUID()}`;
185
- const ext = mimeSubtype.toLowerCase().replace(/\+/g, "-") === "svg-xml" ? "svg" : mimeSubtype.toLowerCase().replace(/\+/g, "-");
186
- images.push({
187
- cid: contentId,
188
- contentBytes: b64,
189
- contentType: `image/${mimeSubtype}`,
190
- filename: `signature-image.${ext}`
191
- });
192
- return `src="cid:${contentId}"`;
193
- });
194
- return { body: transformed, images };
195
- }
196
173
 
197
174
  // src/providers/outlook/client.ts
198
175
  import "isomorphic-fetch";
@@ -406,7 +383,26 @@ var OutlookClientFactory = class {
406
383
  }
407
384
  };
408
385
 
409
- // src/providers/outlook/index.ts
386
+ // src/providers/shared/inline-images.ts
387
+ import { randomUUID } from "crypto";
388
+ function parseInlineImages(html) {
389
+ const images = [];
390
+ const re = /src="data:image\/([\w+]+);base64,([^"]+)"/gi;
391
+ const transformed = html.replace(re, (_fullMatch, mimeSubtype, b64) => {
392
+ const contentId = `sig-img-${randomUUID()}`;
393
+ const ext = mimeSubtype.toLowerCase().replace(/\+/g, "-") === "svg-xml" ? "svg" : mimeSubtype.toLowerCase().replace(/\+/g, "-");
394
+ images.push({
395
+ cid: contentId,
396
+ contentBytes: b64,
397
+ contentType: `image/${mimeSubtype}`,
398
+ filename: `signature-image.${ext}`
399
+ });
400
+ return `src="cid:${contentId}"`;
401
+ });
402
+ return { body: transformed, images };
403
+ }
404
+
405
+ // src/providers/outlook/helpers.ts
410
406
  function convertInlineImages(body) {
411
407
  const { body: transformed, images } = parseInlineImages(body);
412
408
  const attachments = images.map((img) => ({
@@ -419,6 +415,300 @@ function convertInlineImages(body) {
419
415
  }));
420
416
  return { body: transformed, attachments };
421
417
  }
418
+ function mapFolder(f) {
419
+ return {
420
+ id: f.id,
421
+ displayName: f.displayName,
422
+ parentFolderId: f.parentFolderId,
423
+ childFolderCount: f.childFolderCount,
424
+ totalItemCount: f.totalItemCount,
425
+ unreadItemCount: f.unreadItemCount
426
+ };
427
+ }
428
+ function mapRecipient(r) {
429
+ return {
430
+ name: r.emailAddress?.name,
431
+ address: r.emailAddress?.address ?? ""
432
+ };
433
+ }
434
+ function mapSummary(m, folder) {
435
+ return {
436
+ id: m.id,
437
+ subject: m.subject ?? "",
438
+ from: m.from ? mapRecipient(m.from) : void 0,
439
+ to: (m.toRecipients ?? []).map(mapRecipient),
440
+ receivedAt: m.receivedDateTime,
441
+ preview: m.bodyPreview,
442
+ isRead: m.isRead,
443
+ hasAttachments: m.hasAttachments,
444
+ folder
445
+ };
446
+ }
447
+ function toRecipient(a) {
448
+ return { emailAddress: { name: a.name, address: a.address } };
449
+ }
450
+ function clampLimit(v, dflt, max) {
451
+ if (!v || v <= 0) return dflt;
452
+ return Math.min(v, max);
453
+ }
454
+
455
+ // src/providers/outlook/write-ops.ts
456
+ var THREAD_MARKER = "<!-- hypermail-thread-boundary -->";
457
+ async function buildDraftFromReference(client, createEndpoint, createPayload, converted) {
458
+ const draft = await client.api(createEndpoint).post(createPayload);
459
+ const draftMsg = await client.api(`/me/messages/${draft.id}`).select("body").get();
460
+ const draftBody = draftMsg.body?.content ?? "";
461
+ const draftContentType = draftMsg.body?.contentType ?? "HTML";
462
+ const spacer = '<div style="line-height:12px"><br></div>';
463
+ const prepend = converted.body + spacer + THREAD_MARKER;
464
+ const finalBody = draftBody.includes("<body") ? draftBody.replace(/(<body[^>]*>)/i, `$1${prepend}`) : prepend + draftBody;
465
+ await client.api(`/me/messages/${draft.id}`).patch({
466
+ body: { contentType: draftContentType, content: finalBody }
467
+ });
468
+ for (const att of converted.attachments) {
469
+ await client.api(`/me/messages/${draft.id}/attachments`).post(att);
470
+ }
471
+ return draft.id;
472
+ }
473
+ async function sendOrSave(client, account, msg, mode) {
474
+ const converted = convertInlineImages(msg.body);
475
+ const toRecipients = msg.to.map(toRecipient);
476
+ const ccRecipients = (msg.cc ?? []).map(toRecipient);
477
+ const bccRecipients = (msg.bcc ?? []).map(toRecipient);
478
+ if (msg.forwardMessageId) {
479
+ const draftId = await buildDraftFromReference(
480
+ client,
481
+ `/me/messages/${encodeURIComponent(msg.forwardMessageId)}/createForward`,
482
+ { message: { toRecipients, ccRecipients, bccRecipients }, comment: "" },
483
+ converted
484
+ );
485
+ if (mode === "send") {
486
+ await client.api(`/me/messages/${draftId}/send`).post({});
487
+ }
488
+ return { id: draftId };
489
+ }
490
+ if (msg.inReplyTo) {
491
+ const createEndpoint = msg.replyAll ? `/me/messages/${encodeURIComponent(msg.inReplyTo)}/createReplyAll` : `/me/messages/${encodeURIComponent(msg.inReplyTo)}/createReply`;
492
+ const draftId = await buildDraftFromReference(
493
+ client,
494
+ createEndpoint,
495
+ {},
496
+ converted
497
+ );
498
+ if (mode === "send") {
499
+ await client.api(`/me/messages/${draftId}/send`).post({});
500
+ }
501
+ return { id: draftId };
502
+ }
503
+ const messagePayload = {
504
+ subject: msg.subject,
505
+ body: {
506
+ contentType: msg.isHtml ? "HTML" : "Text",
507
+ content: converted.body
508
+ },
509
+ toRecipients,
510
+ ccRecipients,
511
+ bccRecipients
512
+ };
513
+ const allAttachments = [...converted.attachments];
514
+ if (msg.attachments && msg.attachments.length > 0) {
515
+ for (const att of msg.attachments) {
516
+ allAttachments.push({
517
+ "@odata.type": "#microsoft.graph.fileAttachment",
518
+ name: att.name,
519
+ contentBytes: att.contentBytes,
520
+ contentType: att.contentType ?? "application/octet-stream"
521
+ });
522
+ }
523
+ }
524
+ if (allAttachments.length > 0) {
525
+ messagePayload.attachments = allAttachments;
526
+ }
527
+ if (mode === "send") {
528
+ await client.api("/me/sendMail").post({
529
+ message: messagePayload,
530
+ saveToSentItems: true
531
+ });
532
+ return { id: "" };
533
+ }
534
+ const draft = await client.api("/me/messages").post(messagePayload);
535
+ return { id: draft.id };
536
+ }
537
+ async function updateDraft(client, account, id, update) {
538
+ const payload = {};
539
+ if (update.subject !== void 0) {
540
+ payload.subject = update.subject;
541
+ }
542
+ if (update.to !== void 0) {
543
+ payload.toRecipients = update.to.map(toRecipient);
544
+ }
545
+ if (update.cc !== void 0) {
546
+ payload.ccRecipients = update.cc.map(toRecipient);
547
+ }
548
+ if (update.bcc !== void 0) {
549
+ payload.bccRecipients = update.bcc.map(toRecipient);
550
+ }
551
+ if (update.body !== void 0) {
552
+ const converted = convertInlineImages(update.body);
553
+ payload.body = {
554
+ contentType: update.isHtml ? "HTML" : "Text",
555
+ content: converted.body
556
+ };
557
+ if (converted.attachments.length > 0) {
558
+ payload.attachments = converted.attachments;
559
+ }
560
+ }
561
+ await client.api(`/me/messages/${encodeURIComponent(id)}`).patch(payload);
562
+ return { id };
563
+ }
564
+ async function addAttachmentToDraft(client, account, draftId, name, contentBytes, contentType) {
565
+ const att = await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post({
566
+ "@odata.type": "#microsoft.graph.fileAttachment",
567
+ name,
568
+ contentType: contentType ?? "application/octet-stream",
569
+ contentBytes
570
+ });
571
+ return {
572
+ id: draftId,
573
+ attachment: { id: att.id, name: att.name, contentType: att.contentType }
574
+ };
575
+ }
576
+ async function removeAttachmentFromDraft(client, account, draftId, attachmentId) {
577
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments/${encodeURIComponent(attachmentId)}`).delete();
578
+ }
579
+ async function moveEmail(client, account, id, destinationId) {
580
+ await client.api(`/me/messages/${encodeURIComponent(id)}/move`).post({ destinationId });
581
+ }
582
+ async function sendDraft(client, account, id) {
583
+ await client.api(`/me/messages/${encodeURIComponent(id)}/send`).post({});
584
+ return { id };
585
+ }
586
+ async function markRead(client, account, id, isRead) {
587
+ await client.api(`/me/messages/${encodeURIComponent(id)}`).patch({ isRead });
588
+ }
589
+
590
+ // src/providers/outlook/read-ops.ts
591
+ import { ResponseType } from "@microsoft/microsoft-graph-client";
592
+ import { writeFileSync } from "fs";
593
+ import { tmpdir } from "os";
594
+ import { join as pathJoin } from "path";
595
+ async function listEmails(client, account, opts) {
596
+ const limit = clampLimit(opts.limit, 25, 100);
597
+ const folder = opts.folder ?? "inbox";
598
+ const filterParts = [];
599
+ if (opts.unreadOnly) filterParts.push("isRead eq false");
600
+ let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).top(limit).skip(opts.skip ?? 0).select([
601
+ "id",
602
+ "subject",
603
+ "from",
604
+ "toRecipients",
605
+ "receivedDateTime",
606
+ "bodyPreview",
607
+ "isRead",
608
+ "hasAttachments"
609
+ ].join(",")).orderby("receivedDateTime DESC");
610
+ if (filterParts.length > 0) req = req.filter(filterParts.join(" and "));
611
+ const res = await req.get();
612
+ return {
613
+ items: res.value.map((m) => mapSummary(m, folder)),
614
+ hasMore: !!res["@odata.nextLink"]
615
+ };
616
+ }
617
+ async function searchEmails(client, account, query, opts) {
618
+ const limit = clampLimit(opts.limit, 25, 100);
619
+ const res = await client.api("/me/messages").header("ConsistencyLevel", "eventual").top(limit).search(`"${query.replace(/"/g, '\\"')}"`).select(
620
+ [
621
+ "id",
622
+ "subject",
623
+ "from",
624
+ "toRecipients",
625
+ "receivedDateTime",
626
+ "bodyPreview",
627
+ "isRead",
628
+ "hasAttachments"
629
+ ].join(",")
630
+ ).get();
631
+ return res.value.map((m) => mapSummary(m));
632
+ }
633
+ async function readEmail(client, account, id) {
634
+ const m = await client.api(`/me/messages/${encodeURIComponent(id)}`).select(
635
+ [
636
+ "id",
637
+ "subject",
638
+ "from",
639
+ "toRecipients",
640
+ "ccRecipients",
641
+ "bccRecipients",
642
+ "receivedDateTime",
643
+ "bodyPreview",
644
+ "isRead",
645
+ "hasAttachments",
646
+ "body"
647
+ ].join(",")
648
+ ).get();
649
+ let attachments = void 0;
650
+ if (m.hasAttachments) {
651
+ try {
652
+ const attRes = await client.api(`/me/messages/${encodeURIComponent(id)}/attachments`).select("id,name,contentType,size").get();
653
+ attachments = attRes.value.map((a) => ({
654
+ id: a.id,
655
+ name: a.name,
656
+ contentType: a.contentType,
657
+ size: a.size
658
+ }));
659
+ } catch {
660
+ }
661
+ }
662
+ const summary = mapSummary(m);
663
+ const body = m.body;
664
+ return {
665
+ ...summary,
666
+ cc: (m.ccRecipients ?? []).map(mapRecipient),
667
+ bcc: (m.bccRecipients ?? []).map(mapRecipient),
668
+ bodyText: body?.contentType === "text" ? body.content : void 0,
669
+ bodyHtml: body?.contentType === "html" ? body.content : void 0,
670
+ attachments
671
+ };
672
+ }
673
+ async function readAttachment(client, account, messageId, attachmentId) {
674
+ const att = await client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`).select("name,contentType").get();
675
+ const data = await client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}/$value`).responseType(ResponseType.ARRAYBUFFER).get();
676
+ const outPath = pathJoin(tmpdir(), att.name);
677
+ writeFileSync(outPath, Buffer.from(data));
678
+ return {
679
+ name: att.name,
680
+ contentType: att.contentType,
681
+ path: outPath
682
+ };
683
+ }
684
+
685
+ // src/providers/outlook/folders.ts
686
+ async function listFolders(client, account, opts) {
687
+ const endpoint = opts.parentFolderId ? `/me/mailFolders/${encodeURIComponent(opts.parentFolderId)}/childFolders` : "/me/mailFolders";
688
+ const res = await client.api(endpoint).select([
689
+ "id",
690
+ "displayName",
691
+ "parentFolderId",
692
+ "childFolderCount",
693
+ "totalItemCount",
694
+ "unreadItemCount"
695
+ ].join(",")).get();
696
+ return (res.value ?? []).map(mapFolder);
697
+ }
698
+ async function createFolder(client, account, input) {
699
+ const parentId = input.parentFolderId ?? "msgfolderroot";
700
+ const created = await client.api(`/me/mailFolders/${encodeURIComponent(parentId)}/childFolders`).post({ displayName: input.displayName });
701
+ return mapFolder(created);
702
+ }
703
+ async function renameFolder(client, account, folderId, newName) {
704
+ const updated = await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).patch({ displayName: newName });
705
+ return mapFolder(updated);
706
+ }
707
+ async function deleteFolder(client, account, folderId) {
708
+ await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).delete();
709
+ }
710
+
711
+ // src/providers/outlook/index.ts
422
712
  var OutlookProvider = class {
423
713
  constructor(opts) {
424
714
  this.opts = opts;
@@ -497,301 +787,64 @@ var OutlookProvider = class {
497
787
  }
498
788
  return { status: "pending" };
499
789
  }
500
- // ---------- email ops ----------
790
+ // ---------- email ops (delegated) ----------
501
791
  async listEmails(account, opts) {
502
- const client = this.clients.get(account);
503
- const limit = clampLimit(opts.limit, 25, 100);
504
- const folder = opts.folder ?? "inbox";
505
- const filterParts = [];
506
- if (opts.unreadOnly) filterParts.push("isRead eq false");
507
- let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).top(limit).skip(opts.skip ?? 0).select([
508
- "id",
509
- "subject",
510
- "from",
511
- "toRecipients",
512
- "receivedDateTime",
513
- "bodyPreview",
514
- "isRead",
515
- "hasAttachments"
516
- ].join(",")).orderby("receivedDateTime DESC");
517
- if (filterParts.length > 0) req = req.filter(filterParts.join(" and "));
518
- const res = await req.get();
519
- return {
520
- items: res.value.map((m) => mapSummary(m, folder)),
521
- hasMore: !!res["@odata.nextLink"]
522
- };
792
+ return listEmails(this.clients.get(account), account, opts);
523
793
  }
524
794
  async searchEmails(account, query, opts) {
525
- const client = this.clients.get(account);
526
- const limit = clampLimit(opts.limit, 25, 100);
527
- const res = await client.api("/me/messages").header("ConsistencyLevel", "eventual").top(limit).search(`"${query.replace(/"/g, '\\"')}"`).select(
528
- [
529
- "id",
530
- "subject",
531
- "from",
532
- "toRecipients",
533
- "receivedDateTime",
534
- "bodyPreview",
535
- "isRead",
536
- "hasAttachments"
537
- ].join(",")
538
- ).get();
539
- return res.value.map((m) => mapSummary(m));
795
+ return searchEmails(this.clients.get(account), account, query, opts);
540
796
  }
541
797
  async readEmail(account, id) {
542
- const client = this.clients.get(account);
543
- const m = await client.api(`/me/messages/${encodeURIComponent(id)}`).select(
544
- [
545
- "id",
546
- "subject",
547
- "from",
548
- "toRecipients",
549
- "ccRecipients",
550
- "bccRecipients",
551
- "receivedDateTime",
552
- "bodyPreview",
553
- "isRead",
554
- "hasAttachments",
555
- "body"
556
- ].join(",")
557
- ).get();
558
- let attachments = void 0;
559
- if (m.hasAttachments) {
560
- try {
561
- const attRes = await client.api(`/me/messages/${encodeURIComponent(id)}/attachments`).select("id,name,contentType,size").get();
562
- attachments = attRes.value.map((a) => ({
563
- id: a.id,
564
- name: a.name,
565
- contentType: a.contentType,
566
- size: a.size
567
- }));
568
- } catch {
569
- }
570
- }
571
- const summary = mapSummary(m);
572
- const body = m.body;
573
- return {
574
- ...summary,
575
- cc: (m.ccRecipients ?? []).map(mapRecipient),
576
- bcc: (m.bccRecipients ?? []).map(mapRecipient),
577
- bodyText: body?.contentType === "text" ? body.content : void 0,
578
- bodyHtml: body?.contentType === "html" ? body.content : void 0,
579
- attachments
580
- };
798
+ return readEmail(this.clients.get(account), account, id);
581
799
  }
582
800
  async readAttachment(account, messageId, attachmentId) {
583
- const client = this.clients.get(account);
584
- const att = await client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`).select("name,contentType").get();
585
- const data = await client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}/$value`).responseType(ResponseType.ARRAYBUFFER).get();
586
- const outPath = pathJoin(tmpdir(), att.name);
587
- writeFileSync(outPath, Buffer.from(data));
588
- return {
589
- name: att.name,
590
- contentType: att.contentType,
591
- path: outPath
592
- };
593
- }
594
- // Shared helper — creates a draft from a reference message (forward or
595
- // reply), prepends our composed body before the existing content, and
596
- // attaches inline images. Returns the draft message ID.
597
- async buildDraftFromReference(client, createEndpoint, createPayload, converted) {
598
- const draft = await client.api(createEndpoint).post(createPayload);
599
- const draftMsg = await client.api(`/me/messages/${draft.id}`).select("body").get();
600
- const draftBody = draftMsg.body?.content ?? "";
601
- const draftContentType = draftMsg.body?.contentType ?? "HTML";
602
- const spacer = '<div style="line-height:12px"><br></div>';
603
- const prepend = converted.body + spacer;
604
- const finalBody = draftBody.includes("<body") ? draftBody.replace(/(<body[^>]*>)/i, `$1${prepend}`) : prepend + draftBody;
605
- await client.api(`/me/messages/${draft.id}`).patch({
606
- body: { contentType: draftContentType, content: finalBody }
607
- });
608
- for (const att of converted.attachments) {
609
- await client.api(`/me/messages/${draft.id}/attachments`).post(att);
610
- }
611
- return draft.id;
612
- }
613
- // Shared backend for sendEmail and saveDraft — handles forward, reply, and
614
- // new-message paths. The `mode` controls whether the message is sent
615
- // immediately or saved as a draft.
616
- async sendOrSave(account, msg, mode) {
617
- const client = this.clients.get(account);
618
- const converted = convertInlineImages(msg.body);
619
- const toRecipients = msg.to.map(toRecipient);
620
- const ccRecipients = (msg.cc ?? []).map(toRecipient);
621
- const bccRecipients = (msg.bcc ?? []).map(toRecipient);
622
- if (msg.forwardMessageId) {
623
- const draftId = await this.buildDraftFromReference(
624
- client,
625
- `/me/messages/${encodeURIComponent(msg.forwardMessageId)}/createForward`,
626
- { message: { toRecipients, ccRecipients, bccRecipients }, comment: "" },
627
- converted
628
- );
629
- if (mode === "send") {
630
- await client.api(`/me/messages/${draftId}/send`).post({});
631
- }
632
- return { id: draftId };
633
- }
634
- if (msg.inReplyTo) {
635
- const createEndpoint = msg.replyAll ? `/me/messages/${encodeURIComponent(msg.inReplyTo)}/createReplyAll` : `/me/messages/${encodeURIComponent(msg.inReplyTo)}/createReply`;
636
- const draftId = await this.buildDraftFromReference(
637
- client,
638
- createEndpoint,
639
- {},
640
- converted
641
- );
642
- if (mode === "send") {
643
- await client.api(`/me/messages/${draftId}/send`).post({});
644
- }
645
- return { id: draftId };
646
- }
647
- const messagePayload = {
648
- subject: msg.subject,
649
- body: {
650
- contentType: msg.isHtml ? "HTML" : "Text",
651
- content: converted.body
652
- },
653
- toRecipients,
654
- ccRecipients,
655
- bccRecipients
656
- };
657
- if (converted.attachments.length > 0) {
658
- messagePayload.attachments = converted.attachments;
659
- }
660
- if (mode === "send") {
661
- await client.api("/me/sendMail").post({
662
- message: messagePayload,
663
- saveToSentItems: true
664
- });
665
- return { id: "" };
666
- }
667
- const draft = await client.api("/me/messages").post(messagePayload);
668
- return { id: draft.id };
801
+ return readAttachment(this.clients.get(account), account, messageId, attachmentId);
669
802
  }
670
803
  async sendEmail(account, msg) {
671
- return this.sendOrSave(account, msg, "send");
804
+ return sendOrSave(this.clients.get(account), account, msg, "send");
672
805
  }
673
806
  async saveDraft(account, msg) {
674
- return this.sendOrSave(account, msg, "draft");
807
+ return sendOrSave(this.clients.get(account), account, msg, "draft");
675
808
  }
676
809
  async updateDraft(account, id, update) {
677
- const client = this.clients.get(account);
678
- const payload = {};
679
- if (update.subject !== void 0) {
680
- payload.subject = update.subject;
681
- }
682
- if (update.to !== void 0) {
683
- payload.toRecipients = update.to.map(toRecipient);
684
- }
685
- if (update.cc !== void 0) {
686
- payload.ccRecipients = update.cc.map(toRecipient);
687
- }
688
- if (update.bcc !== void 0) {
689
- payload.bccRecipients = update.bcc.map(toRecipient);
690
- }
691
- if (update.body !== void 0) {
692
- const converted = convertInlineImages(update.body);
693
- payload.body = {
694
- contentType: update.isHtml ? "HTML" : "Text",
695
- content: converted.body
696
- };
697
- if (converted.attachments.length > 0) {
698
- payload.attachments = converted.attachments;
699
- }
700
- }
701
- await client.api(`/me/messages/${encodeURIComponent(id)}`).patch(payload);
702
- return { id };
810
+ return updateDraft(this.clients.get(account), account, id, update);
703
811
  }
704
812
  async moveEmail(account, id, destinationId) {
705
- const client = this.clients.get(account);
706
- await client.api(`/me/messages/${encodeURIComponent(id)}/move`).post({ destinationId });
813
+ return moveEmail(this.clients.get(account), account, id, destinationId);
707
814
  }
708
815
  async sendDraft(account, id) {
709
- const client = this.clients.get(account);
710
- await client.api(`/me/messages/${encodeURIComponent(id)}/send`).post({});
711
- return { id };
816
+ return sendDraft(this.clients.get(account), account, id);
712
817
  }
713
818
  async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
714
- const client = this.clients.get(account);
715
- const att = await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post({
716
- "@odata.type": "#microsoft.graph.fileAttachment",
819
+ return addAttachmentToDraft(
820
+ this.clients.get(account),
821
+ account,
822
+ draftId,
717
823
  name,
718
- contentType: contentType ?? "application/octet-stream",
719
- contentBytes
720
- });
721
- return {
722
- id: draftId,
723
- attachment: { id: att.id, name: att.name, contentType: att.contentType }
724
- };
824
+ contentBytes,
825
+ contentType
826
+ );
827
+ }
828
+ async removeAttachmentFromDraft(account, draftId, attachmentId) {
829
+ return removeAttachmentFromDraft(this.clients.get(account), account, draftId, attachmentId);
725
830
  }
726
831
  async markRead(account, id, isRead) {
727
- const client = this.clients.get(account);
728
- await client.api(`/me/messages/${encodeURIComponent(id)}`).patch({ isRead });
832
+ return markRead(this.clients.get(account), account, id, isRead);
729
833
  }
834
+ // ---------- folder ops (delegated) ----------
730
835
  async listFolders(account, opts) {
731
- const client = this.clients.get(account);
732
- const endpoint = opts.parentFolderId ? `/me/mailFolders/${encodeURIComponent(opts.parentFolderId)}/childFolders` : "/me/mailFolders";
733
- const res = await client.api(endpoint).select([
734
- "id",
735
- "displayName",
736
- "parentFolderId",
737
- "childFolderCount",
738
- "totalItemCount",
739
- "unreadItemCount"
740
- ].join(",")).get();
741
- return (res.value ?? []).map(mapFolder);
836
+ return listFolders(this.clients.get(account), account, opts);
742
837
  }
743
838
  async createFolder(account, input) {
744
- const client = this.clients.get(account);
745
- const parentId = input.parentFolderId ?? "msgfolderroot";
746
- const created = await client.api(`/me/mailFolders/${encodeURIComponent(parentId)}/childFolders`).post({ displayName: input.displayName });
747
- return mapFolder(created);
839
+ return createFolder(this.clients.get(account), account, input);
748
840
  }
749
841
  async renameFolder(account, folderId, newName) {
750
- const client = this.clients.get(account);
751
- const updated = await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).patch({ displayName: newName });
752
- return mapFolder(updated);
842
+ return renameFolder(this.clients.get(account), account, folderId, newName);
753
843
  }
754
844
  async deleteFolder(account, folderId) {
755
- const client = this.clients.get(account);
756
- await client.api(`/me/mailFolders/${encodeURIComponent(folderId)}`).delete();
845
+ return deleteFolder(this.clients.get(account), account, folderId);
757
846
  }
758
847
  };
759
- function mapFolder(f) {
760
- return {
761
- id: f.id,
762
- displayName: f.displayName,
763
- parentFolderId: f.parentFolderId,
764
- childFolderCount: f.childFolderCount,
765
- totalItemCount: f.totalItemCount,
766
- unreadItemCount: f.unreadItemCount
767
- };
768
- }
769
- function mapRecipient(r) {
770
- return {
771
- name: r.emailAddress?.name,
772
- address: r.emailAddress?.address ?? ""
773
- };
774
- }
775
- function mapSummary(m, folder) {
776
- return {
777
- id: m.id,
778
- subject: m.subject ?? "",
779
- from: m.from ? mapRecipient(m.from) : void 0,
780
- to: (m.toRecipients ?? []).map(mapRecipient),
781
- receivedAt: m.receivedDateTime,
782
- preview: m.bodyPreview,
783
- isRead: m.isRead,
784
- hasAttachments: m.hasAttachments,
785
- folder
786
- };
787
- }
788
- function toRecipient(a) {
789
- return { emailAddress: { name: a.name, address: a.address } };
790
- }
791
- function clampLimit(v, dflt, max) {
792
- if (!v || v <= 0) return dflt;
793
- return Math.min(v, max);
794
- }
795
848
 
796
849
  // src/providers/imap/client.ts
797
850
  import { ImapFlow } from "imapflow";
@@ -1000,7 +1053,7 @@ function mapMailboxToListEntry(m) {
1000
1053
  }
1001
1054
 
1002
1055
  // src/providers/imap/read-ops.ts
1003
- async function listEmails(clients, account, opts) {
1056
+ async function listEmails2(clients, account, opts) {
1004
1057
  const client = clients.get(account);
1005
1058
  const folder = resolveFolder(opts.folder ?? "INBOX");
1006
1059
  const limit = clampLimit2(opts.limit, 25, 100);
@@ -1034,7 +1087,7 @@ async function listEmails(clients, account, opts) {
1034
1087
  return { items, hasMore };
1035
1088
  });
1036
1089
  }
1037
- async function searchEmails(clients, account, query, opts) {
1090
+ async function searchEmails2(clients, account, query, opts) {
1038
1091
  const client = clients.get(account);
1039
1092
  const limit = clampLimit2(opts.limit, 25, 100);
1040
1093
  return client.withMailbox("INBOX", async (imap) => {
@@ -1057,7 +1110,7 @@ async function searchEmails(clients, account, query, opts) {
1057
1110
  );
1058
1111
  });
1059
1112
  }
1060
- async function readEmail(clients, account, id) {
1113
+ async function readEmail2(clients, account, id) {
1061
1114
  const client = clients.get(account);
1062
1115
  const { folder, uid } = decodeId(id);
1063
1116
  return client.withMailbox(folder, async (imap) => {
@@ -1112,7 +1165,7 @@ async function readEmail(clients, account, id) {
1112
1165
  };
1113
1166
  });
1114
1167
  }
1115
- async function readAttachment(clients, account, messageId, attachmentId) {
1168
+ async function readAttachment2(clients, account, messageId, attachmentId) {
1116
1169
  const client = clients.get(account);
1117
1170
  const { folder, uid } = decodeId(messageId);
1118
1171
  return client.withMailbox(folder, async (imap) => {
@@ -1146,7 +1199,7 @@ async function readAttachment(clients, account, messageId, attachmentId) {
1146
1199
  };
1147
1200
  });
1148
1201
  }
1149
- async function listFolders(clients, account, opts) {
1202
+ async function listFolders2(clients, account, opts) {
1150
1203
  const client = clients.get(account);
1151
1204
  const imap = await client.getImap();
1152
1205
  const mailboxes = await imap.list({
@@ -1170,9 +1223,7 @@ async function listFolders(clients, account, opts) {
1170
1223
  return results;
1171
1224
  }
1172
1225
 
1173
- // src/providers/imap/write-ops.ts
1174
- import { randomUUID as randomUUID3 } from "crypto";
1175
- import MailComposer from "nodemailer/lib/mail-composer/index.js";
1226
+ // src/providers/imap/account-ops.ts
1176
1227
  async function addAccount(clients, store, input) {
1177
1228
  const cfg = input.config ?? {};
1178
1229
  const host = String(cfg.host ?? "");
@@ -1231,6 +1282,65 @@ function completeAddAccount() {
1231
1282
  error: "IMAP accounts are set up synchronously \u2014 no polling needed. Call add_account with IMAP config to create the account directly."
1232
1283
  };
1233
1284
  }
1285
+
1286
+ // src/providers/imap/write-ops.ts
1287
+ import { randomUUID as randomUUID3 } from "crypto";
1288
+ import MailComposer from "nodemailer/lib/mail-composer/index.js";
1289
+
1290
+ // src/providers/imap/mime-utils.ts
1291
+ function findAttachmentInMime(structure, attachmentId) {
1292
+ if (!structure) return null;
1293
+ if (Array.isArray(structure)) {
1294
+ for (const part of structure) {
1295
+ const result = findAttachmentInMime(part, attachmentId);
1296
+ if (result) return result;
1297
+ }
1298
+ return null;
1299
+ }
1300
+ if (structure.id === attachmentId || structure.partId === attachmentId) {
1301
+ return {
1302
+ filename: structure.disposition?.filename || structure.filename || "attachment",
1303
+ contentType: `${structure.type}/${structure.subtype}`
1304
+ };
1305
+ }
1306
+ if (structure.parts) {
1307
+ return findAttachmentInMime(structure.parts, attachmentId);
1308
+ }
1309
+ return null;
1310
+ }
1311
+ function removeMimePart(source, targetFilename, targetContentType) {
1312
+ const lines = source.split("\r\n");
1313
+ const result = [];
1314
+ let skip = false;
1315
+ let inTargetPart = false;
1316
+ for (let i = 0; i < lines.length; i++) {
1317
+ const line = lines[i];
1318
+ const lowerLine = line.toLowerCase();
1319
+ if (line.startsWith("--")) {
1320
+ if (inTargetPart) {
1321
+ skip = false;
1322
+ inTargetPart = false;
1323
+ }
1324
+ result.push(line);
1325
+ continue;
1326
+ }
1327
+ if (lowerLine.includes("content-disposition:") && lowerLine.includes(`filename="${targetFilename.toLowerCase()}"`)) {
1328
+ inTargetPart = true;
1329
+ skip = true;
1330
+ continue;
1331
+ }
1332
+ if (inTargetPart && lowerLine.includes("content-type:") && lowerLine.includes(targetContentType.toLowerCase())) {
1333
+ skip = true;
1334
+ continue;
1335
+ }
1336
+ if (!skip) {
1337
+ result.push(line);
1338
+ }
1339
+ }
1340
+ return result.join("\r\n");
1341
+ }
1342
+
1343
+ // src/providers/imap/write-ops.ts
1234
1344
  async function sendEmail(clients, account, msg) {
1235
1345
  const client = clients.get(account);
1236
1346
  const transporter = client.getTransporter();
@@ -1301,7 +1411,7 @@ async function saveDraft(clients, account, msg) {
1301
1411
  return { id: encodeId("Drafts", result.uid) };
1302
1412
  });
1303
1413
  }
1304
- async function updateDraft(clients, account, id, update) {
1414
+ async function updateDraft2(clients, account, id, update) {
1305
1415
  const client = clients.get(account);
1306
1416
  const { folder, uid } = decodeId(id);
1307
1417
  return client.withMailbox(folder, async (imap) => {
@@ -1338,7 +1448,7 @@ async function updateDraft(clients, account, id, update) {
1338
1448
  return { id: encodeId(folder, result.uid) };
1339
1449
  });
1340
1450
  }
1341
- async function moveEmail(clients, account, id, destinationId) {
1451
+ async function moveEmail2(clients, account, id, destinationId) {
1342
1452
  const client = clients.get(account);
1343
1453
  const { folder, uid } = decodeId(id);
1344
1454
  const dest = resolveFolder(destinationId);
@@ -1346,7 +1456,7 @@ async function moveEmail(clients, account, id, destinationId) {
1346
1456
  await imap.messageMove(uid, dest, { uid: true });
1347
1457
  });
1348
1458
  }
1349
- async function sendDraft(clients, account, id) {
1459
+ async function sendDraft2(clients, account, id) {
1350
1460
  const client = clients.get(account);
1351
1461
  const { folder, uid } = decodeId(id);
1352
1462
  return client.withMailbox(folder, async (imap) => {
@@ -1368,7 +1478,7 @@ async function sendDraft(clients, account, id) {
1368
1478
  return { id: info.messageId };
1369
1479
  });
1370
1480
  }
1371
- async function addAttachmentToDraft(clients, account, draftId, name, contentBytes, contentType) {
1481
+ async function addAttachmentToDraft2(clients, account, draftId, name, contentBytes, contentType) {
1372
1482
  const client = clients.get(account);
1373
1483
  const { folder, uid } = decodeId(draftId);
1374
1484
  return client.withMailbox(folder, async (imap) => {
@@ -1409,7 +1519,29 @@ async function addAttachmentToDraft(clients, account, draftId, name, contentByte
1409
1519
  };
1410
1520
  });
1411
1521
  }
1412
- async function markRead(clients, account, id, isRead) {
1522
+ async function removeAttachmentFromDraft2(clients, account, draftId, attachmentId) {
1523
+ const client = clients.get(account);
1524
+ const { folder, uid } = decodeId(draftId);
1525
+ return client.withMailbox(folder, async (imap) => {
1526
+ const existing = await imap.fetchOne(
1527
+ uid,
1528
+ { source: true, structure: true },
1529
+ { uid: true }
1530
+ );
1531
+ if (!existing?.source) {
1532
+ throw new Error(`draft not found: ${draftId}`);
1533
+ }
1534
+ const sourceStr = typeof existing.source === "string" ? existing.source : Buffer.from(existing.source).toString("utf-8");
1535
+ const targetInfo = findAttachmentInMime(existing.structure, attachmentId);
1536
+ if (!targetInfo) {
1537
+ throw new Error(`attachment not found: ${attachmentId}`);
1538
+ }
1539
+ const modifiedSource = removeMimePart(sourceStr, targetInfo.filename, targetInfo.contentType);
1540
+ await imap.messageDelete(uid, { uid: true });
1541
+ await imap.append(folder, modifiedSource, ["\\Draft"]);
1542
+ });
1543
+ }
1544
+ async function markRead2(clients, account, id, isRead) {
1413
1545
  const client = clients.get(account);
1414
1546
  const { folder, uid } = decodeId(id);
1415
1547
  return client.withMailbox(folder, async (imap) => {
@@ -1420,7 +1552,46 @@ async function markRead(clients, account, id, isRead) {
1420
1552
  }
1421
1553
  });
1422
1554
  }
1423
- async function createFolder(clients, account, input) {
1555
+ async function buildRawMessage(account, msg, messageId) {
1556
+ const mailOptions = {
1557
+ from: `${account.displayName ?? ""} <${account.email}>`,
1558
+ to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
1559
+ subject: msg.subject,
1560
+ attachDataUrls: true
1561
+ };
1562
+ if (msg.isHtml) {
1563
+ mailOptions.html = msg.body;
1564
+ } else {
1565
+ mailOptions.text = msg.body;
1566
+ }
1567
+ if (msg.cc && msg.cc.length > 0) {
1568
+ mailOptions.cc = msg.cc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1569
+ }
1570
+ if (msg.bcc && msg.bcc.length > 0) {
1571
+ mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1572
+ }
1573
+ if (msg.attachments && msg.attachments.length > 0) {
1574
+ const fileAttachments = msg.attachments.map((att) => ({
1575
+ filename: att.name,
1576
+ content: Buffer.from(att.contentBytes, "base64"),
1577
+ contentType: att.contentType
1578
+ }));
1579
+ mailOptions.attachments = fileAttachments;
1580
+ }
1581
+ if (messageId) {
1582
+ mailOptions.messageId = messageId;
1583
+ }
1584
+ return new Promise((resolve, reject) => {
1585
+ const mc = new MailComposer(mailOptions);
1586
+ mc.compile().build((err, buf) => {
1587
+ if (err) reject(err);
1588
+ else resolve(buf.toString("utf-8"));
1589
+ });
1590
+ });
1591
+ }
1592
+
1593
+ // src/providers/imap/folders.ts
1594
+ async function createFolder2(clients, account, input) {
1424
1595
  const client = clients.get(account);
1425
1596
  const imap = await client.getImap();
1426
1597
  const path3 = input.parentFolderId ? `${input.parentFolderId}/${input.displayName}` : input.displayName;
@@ -1434,7 +1605,7 @@ async function createFolder(clients, account, input) {
1434
1605
  unreadItemCount: 0
1435
1606
  };
1436
1607
  }
1437
- async function renameFolder(clients, account, folderId, newName) {
1608
+ async function renameFolder2(clients, account, folderId, newName) {
1438
1609
  const client = clients.get(account);
1439
1610
  const imap = await client.getImap();
1440
1611
  const lastSep = folderId.lastIndexOf("/");
@@ -1449,40 +1620,11 @@ async function renameFolder(clients, account, folderId, newName) {
1449
1620
  unreadItemCount: 0
1450
1621
  };
1451
1622
  }
1452
- async function deleteFolder(clients, account, folderId) {
1623
+ async function deleteFolder2(clients, account, folderId) {
1453
1624
  const client = clients.get(account);
1454
1625
  const imap = await client.getImap();
1455
1626
  await imap.mailboxDelete(folderId);
1456
1627
  }
1457
- async function buildRawMessage(account, msg, messageId) {
1458
- const mailOptions = {
1459
- from: `${account.displayName ?? ""} <${account.email}>`,
1460
- to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
1461
- subject: msg.subject,
1462
- attachDataUrls: true
1463
- };
1464
- if (msg.isHtml) {
1465
- mailOptions.html = msg.body;
1466
- } else {
1467
- mailOptions.text = msg.body;
1468
- }
1469
- if (msg.cc && msg.cc.length > 0) {
1470
- mailOptions.cc = msg.cc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1471
- }
1472
- if (msg.bcc && msg.bcc.length > 0) {
1473
- mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
1474
- }
1475
- if (messageId) {
1476
- mailOptions.messageId = messageId;
1477
- }
1478
- return new Promise((resolve, reject) => {
1479
- const mc = new MailComposer(mailOptions);
1480
- mc.compile().build((err, buf) => {
1481
- if (err) reject(err);
1482
- else resolve(buf.toString("utf-8"));
1483
- });
1484
- });
1485
- }
1486
1628
 
1487
1629
  // src/providers/imap/index.ts
1488
1630
  var ImapProvider = class {
@@ -1502,16 +1644,16 @@ var ImapProvider = class {
1502
1644
  }
1503
1645
  // ---------- browse ----------
1504
1646
  async listEmails(account, opts) {
1505
- return listEmails(this.clients, account, opts);
1647
+ return listEmails2(this.clients, account, opts);
1506
1648
  }
1507
1649
  async searchEmails(account, query, opts) {
1508
- return searchEmails(this.clients, account, query, opts);
1650
+ return searchEmails2(this.clients, account, query, opts);
1509
1651
  }
1510
1652
  async readEmail(account, id) {
1511
- return readEmail(this.clients, account, id);
1653
+ return readEmail2(this.clients, account, id);
1512
1654
  }
1513
1655
  async readAttachment(account, messageId, attachmentId) {
1514
- return readAttachment(this.clients, account, messageId, attachmentId);
1656
+ return readAttachment2(this.clients, account, messageId, attachmentId);
1515
1657
  }
1516
1658
  // ---------- compose ----------
1517
1659
  async sendEmail(account, msg) {
@@ -1521,33 +1663,36 @@ var ImapProvider = class {
1521
1663
  return saveDraft(this.clients, account, msg);
1522
1664
  }
1523
1665
  async updateDraft(account, id, update) {
1524
- return updateDraft(this.clients, account, id, update);
1666
+ return updateDraft2(this.clients, account, id, update);
1525
1667
  }
1526
1668
  async moveEmail(account, id, destinationId) {
1527
- return moveEmail(this.clients, account, id, destinationId);
1669
+ return moveEmail2(this.clients, account, id, destinationId);
1528
1670
  }
1529
1671
  async sendDraft(account, id) {
1530
- return sendDraft(this.clients, account, id);
1672
+ return sendDraft2(this.clients, account, id);
1531
1673
  }
1532
1674
  async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
1533
- return addAttachmentToDraft(this.clients, account, draftId, name, contentBytes, contentType);
1675
+ return addAttachmentToDraft2(this.clients, account, draftId, name, contentBytes, contentType);
1676
+ }
1677
+ async removeAttachmentFromDraft(account, draftId, attachmentId) {
1678
+ return removeAttachmentFromDraft2(this.clients, account, draftId, attachmentId);
1534
1679
  }
1535
1680
  // ---------- organize ----------
1536
1681
  async markRead(account, id, isRead) {
1537
- return markRead(this.clients, account, id, isRead);
1682
+ return markRead2(this.clients, account, id, isRead);
1538
1683
  }
1539
1684
  // ---------- folders ----------
1540
1685
  async listFolders(account, opts) {
1541
- return listFolders(this.clients, account, opts);
1686
+ return listFolders2(this.clients, account, opts);
1542
1687
  }
1543
1688
  async createFolder(account, input) {
1544
- return createFolder(this.clients, account, input);
1689
+ return createFolder2(this.clients, account, input);
1545
1690
  }
1546
1691
  async renameFolder(account, folderId, newName) {
1547
- return renameFolder(this.clients, account, folderId, newName);
1692
+ return renameFolder2(this.clients, account, folderId, newName);
1548
1693
  }
1549
1694
  async deleteFolder(account, folderId) {
1550
- return deleteFolder(this.clients, account, folderId);
1695
+ return deleteFolder2(this.clients, account, folderId);
1551
1696
  }
1552
1697
  };
1553
1698
 
@@ -1954,6 +2099,17 @@ async function buildRawMessage2(account, msg, messageId) {
1954
2099
  cid: img.cid
1955
2100
  }));
1956
2101
  }
2102
+ if (msg.attachments && msg.attachments.length > 0) {
2103
+ const fileAttachments = msg.attachments.map((att) => ({
2104
+ filename: att.name,
2105
+ content: Buffer.from(att.contentBytes, "base64"),
2106
+ contentType: att.contentType
2107
+ }));
2108
+ mailOptions.attachments = [
2109
+ ...mailOptions.attachments || [],
2110
+ ...fileAttachments
2111
+ ];
2112
+ }
1957
2113
  if (messageId) {
1958
2114
  mailOptions.messageId = messageId;
1959
2115
  }
@@ -1968,7 +2124,7 @@ async function buildRawMessage2(account, msg, messageId) {
1968
2124
  }
1969
2125
 
1970
2126
  // src/providers/gmail/read-ops.ts
1971
- async function listEmails2(clients, account, opts) {
2127
+ async function listEmails3(clients, account, opts) {
1972
2128
  const { gmail } = clients.get(account);
1973
2129
  const limit = clampLimit3(opts.limit, 25, 100);
1974
2130
  const label = resolveLabel(opts.folder ?? "inbox");
@@ -2009,7 +2165,7 @@ async function listEmails2(clients, account, opts) {
2009
2165
  });
2010
2166
  return { items, hasMore };
2011
2167
  }
2012
- async function searchEmails2(clients, account, query, opts) {
2168
+ async function searchEmails3(clients, account, query, opts) {
2013
2169
  const { gmail } = clients.get(account);
2014
2170
  const limit = clampLimit3(opts.limit, 25, 100);
2015
2171
  const res = await gmail.users.messages.list({
@@ -2035,7 +2191,7 @@ async function searchEmails2(clients, account, query, opts) {
2035
2191
  });
2036
2192
  return items;
2037
2193
  }
2038
- async function readEmail2(clients, account, id) {
2194
+ async function readEmail3(clients, account, id) {
2039
2195
  const { gmail } = clients.get(account);
2040
2196
  const res = await gmail.users.messages.get({
2041
2197
  userId: "me",
@@ -2060,7 +2216,7 @@ async function readEmail2(clients, account, id) {
2060
2216
  hasAttachments: attachments.length > 0
2061
2217
  };
2062
2218
  }
2063
- async function readAttachment2(clients, account, messageId, attachmentId) {
2219
+ async function readAttachment3(clients, account, messageId, attachmentId) {
2064
2220
  const { gmail } = clients.get(account);
2065
2221
  const msgRes = await gmail.users.messages.get({
2066
2222
  userId: "me",
@@ -2086,7 +2242,7 @@ async function readAttachment2(clients, account, messageId, attachmentId) {
2086
2242
  writeFileSync2(outPath, buf);
2087
2243
  return { name, contentType, path: outPath };
2088
2244
  }
2089
- async function listFolders2(clients, account, _opts) {
2245
+ async function listFolders3(clients, account, _opts) {
2090
2246
  const { gmail } = clients.get(account);
2091
2247
  const res = await gmail.users.labels.list({ userId: "me" });
2092
2248
  const labels = res.data.labels ?? [];
@@ -2166,7 +2322,7 @@ async function saveDraft2(clients, account, msg) {
2166
2322
  });
2167
2323
  return { id: draftRes.data.message?.id ?? draftRes.data.id ?? "" };
2168
2324
  }
2169
- async function updateDraft2(clients, account, id, update) {
2325
+ async function updateDraft3(clients, account, id, update) {
2170
2326
  const { gmail } = clients.get(account);
2171
2327
  const draftRes = await gmail.users.drafts.get({
2172
2328
  userId: "me",
@@ -2191,6 +2347,7 @@ async function updateDraft2(clients, account, id, update) {
2191
2347
  subject: update.subject ?? origSubject,
2192
2348
  body: update.body ?? "",
2193
2349
  isHtml: update.isHtml,
2350
+ inReplyTo: false,
2194
2351
  cc: existingCc.length > 0 ? existingCc : void 0,
2195
2352
  bcc: existingBcc.length > 0 ? existingBcc : void 0
2196
2353
  });
@@ -2206,7 +2363,7 @@ async function updateDraft2(clients, account, id, update) {
2206
2363
  });
2207
2364
  return { id: updated.data.message?.id ?? updated.data.id ?? id };
2208
2365
  }
2209
- async function moveEmail2(clients, account, id, destinationId) {
2366
+ async function moveEmail3(clients, account, id, destinationId) {
2210
2367
  const { gmail } = clients.get(account);
2211
2368
  const { addLabelIds, removeLabelIds } = resolveLabelsForMove(destinationId);
2212
2369
  await gmail.users.messages.modify({
@@ -2215,7 +2372,7 @@ async function moveEmail2(clients, account, id, destinationId) {
2215
2372
  requestBody: { addLabelIds, removeLabelIds }
2216
2373
  });
2217
2374
  }
2218
- async function sendDraft2(clients, account, id) {
2375
+ async function sendDraft3(clients, account, id) {
2219
2376
  const { gmail } = clients.get(account);
2220
2377
  const res = await gmail.users.drafts.send({
2221
2378
  userId: "me",
@@ -2223,7 +2380,7 @@ async function sendDraft2(clients, account, id) {
2223
2380
  });
2224
2381
  return { id: res.data.id ?? id };
2225
2382
  }
2226
- async function addAttachmentToDraft2(clients, account, draftId, name, contentBytes, contentType) {
2383
+ async function addAttachmentToDraft3(clients, account, draftId, name, contentBytes, contentType) {
2227
2384
  const { gmail } = clients.get(account);
2228
2385
  const draftRes = await gmail.users.drafts.get({
2229
2386
  userId: "me",
@@ -2273,7 +2430,86 @@ async function addAttachmentToDraft2(clients, account, draftId, name, contentByt
2273
2430
  }
2274
2431
  };
2275
2432
  }
2276
- async function markRead2(clients, account, id, isRead) {
2433
+ async function removeAttachmentFromDraft3(clients, account, draftId, attachmentId) {
2434
+ const { gmail } = clients.get(account);
2435
+ const fullDraft = await gmail.users.drafts.get({
2436
+ userId: "me",
2437
+ id: draftId,
2438
+ format: "full"
2439
+ });
2440
+ const message = fullDraft.data.message;
2441
+ if (!message?.payload) {
2442
+ throw new Error(`draft not found: ${draftId}`);
2443
+ }
2444
+ let targetFilename;
2445
+ let targetMimeType;
2446
+ function findAttachment(parts) {
2447
+ if (!parts) return false;
2448
+ for (const part of parts) {
2449
+ if (part.body?.attachmentId === attachmentId) {
2450
+ targetFilename = part.filename ?? void 0;
2451
+ targetMimeType = part.mimeType ?? void 0;
2452
+ return true;
2453
+ }
2454
+ if (part.parts && findAttachment(part.parts)) {
2455
+ return true;
2456
+ }
2457
+ }
2458
+ return false;
2459
+ }
2460
+ if (!findAttachment(message.payload.parts)) {
2461
+ throw new Error(`attachment not found: ${attachmentId}`);
2462
+ }
2463
+ const rawDraft = await gmail.users.drafts.get({
2464
+ userId: "me",
2465
+ id: draftId,
2466
+ format: "raw"
2467
+ });
2468
+ const rawStr = Buffer2.from(
2469
+ rawDraft.data.message.raw.replace(/-/g, "+").replace(/_/g, "/"),
2470
+ "base64"
2471
+ ).toString("utf-8");
2472
+ const newRawStr = removeMimeAttachment(rawStr, targetFilename, targetMimeType);
2473
+ await gmail.users.drafts.update({
2474
+ userId: "me",
2475
+ id: draftId,
2476
+ requestBody: {
2477
+ message: {
2478
+ raw: base64urlEncode(Buffer2.from(newRawStr, "utf-8")),
2479
+ threadId: message.threadId ?? void 0
2480
+ }
2481
+ }
2482
+ });
2483
+ }
2484
+ function removeMimeAttachment(rawMime, targetFilename, targetMimeType) {
2485
+ const boundaryMatch = rawMime.match(/boundary="?([^";]+)"?/i);
2486
+ if (!boundaryMatch) {
2487
+ return rawMime;
2488
+ }
2489
+ const boundary = boundaryMatch[1];
2490
+ const delimiter = `--${boundary}`;
2491
+ const parts = rawMime.split(delimiter);
2492
+ const filtered = parts.filter((part) => {
2493
+ if (!part.trim() || part.trim() === "--") {
2494
+ return true;
2495
+ }
2496
+ const contentDispMatch = targetFilename && part.match(new RegExp(`Content-Disposition:.*?filename="?${escapeRegex(targetFilename)}"?`, "i"));
2497
+ const contentTypeMatch = targetMimeType && part.match(new RegExp(`Content-Type:s*${escapeRegex(targetMimeType)}`, "i"));
2498
+ if (targetFilename && targetMimeType) {
2499
+ return !(contentDispMatch && contentTypeMatch);
2500
+ } else if (targetFilename) {
2501
+ return !contentDispMatch;
2502
+ } else if (targetMimeType) {
2503
+ return !contentTypeMatch;
2504
+ }
2505
+ return true;
2506
+ });
2507
+ return filtered.join(delimiter);
2508
+ }
2509
+ function escapeRegex(str) {
2510
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2511
+ }
2512
+ async function markRead3(clients, account, id, isRead) {
2277
2513
  const { gmail } = clients.get(account);
2278
2514
  await gmail.users.messages.modify({
2279
2515
  userId: "me",
@@ -2284,7 +2520,7 @@ async function markRead2(clients, account, id, isRead) {
2284
2520
  }
2285
2521
  });
2286
2522
  }
2287
- async function createFolder2(clients, account, input) {
2523
+ async function createFolder3(clients, account, input) {
2288
2524
  const { gmail } = clients.get(account);
2289
2525
  const created = await gmail.users.labels.create({
2290
2526
  userId: "me",
@@ -2296,7 +2532,7 @@ async function createFolder2(clients, account, input) {
2296
2532
  });
2297
2533
  return mapFolder2(created.data);
2298
2534
  }
2299
- async function renameFolder2(clients, account, folderId, newName) {
2535
+ async function renameFolder3(clients, account, folderId, newName) {
2300
2536
  const { gmail } = clients.get(account);
2301
2537
  const updated = await gmail.users.labels.patch({
2302
2538
  userId: "me",
@@ -2305,7 +2541,7 @@ async function renameFolder2(clients, account, folderId, newName) {
2305
2541
  });
2306
2542
  return mapFolder2(updated.data);
2307
2543
  }
2308
- async function deleteFolder2(clients, account, folderId) {
2544
+ async function deleteFolder3(clients, account, folderId) {
2309
2545
  const { gmail } = clients.get(account);
2310
2546
  await gmail.users.labels.delete({
2311
2547
  userId: "me",
@@ -2402,16 +2638,16 @@ var GmailProvider = class {
2402
2638
  }
2403
2639
  // ── browse ──
2404
2640
  async listEmails(account, opts) {
2405
- return listEmails2(this.clients, account, opts);
2641
+ return listEmails3(this.clients, account, opts);
2406
2642
  }
2407
2643
  async searchEmails(account, query, opts) {
2408
- return searchEmails2(this.clients, account, query, opts);
2644
+ return searchEmails3(this.clients, account, query, opts);
2409
2645
  }
2410
2646
  async readEmail(account, id) {
2411
- return readEmail2(this.clients, account, id);
2647
+ return readEmail3(this.clients, account, id);
2412
2648
  }
2413
2649
  async readAttachment(account, messageId, attachmentId) {
2414
- return readAttachment2(this.clients, account, messageId, attachmentId);
2650
+ return readAttachment3(this.clients, account, messageId, attachmentId);
2415
2651
  }
2416
2652
  // ── compose ──
2417
2653
  async sendEmail(account, msg) {
@@ -2421,16 +2657,16 @@ var GmailProvider = class {
2421
2657
  return saveDraft2(this.clients, account, msg);
2422
2658
  }
2423
2659
  async updateDraft(account, id, update) {
2424
- return updateDraft2(this.clients, account, id, update);
2660
+ return updateDraft3(this.clients, account, id, update);
2425
2661
  }
2426
2662
  async moveEmail(account, id, destinationId) {
2427
- return moveEmail2(this.clients, account, id, destinationId);
2663
+ return moveEmail3(this.clients, account, id, destinationId);
2428
2664
  }
2429
2665
  async sendDraft(account, id) {
2430
- return sendDraft2(this.clients, account, id);
2666
+ return sendDraft3(this.clients, account, id);
2431
2667
  }
2432
2668
  async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
2433
- return addAttachmentToDraft2(
2669
+ return addAttachmentToDraft3(
2434
2670
  this.clients,
2435
2671
  account,
2436
2672
  draftId,
@@ -2439,22 +2675,25 @@ var GmailProvider = class {
2439
2675
  contentType
2440
2676
  );
2441
2677
  }
2678
+ async removeAttachmentFromDraft(account, draftId, attachmentId) {
2679
+ return removeAttachmentFromDraft3(this.clients, account, draftId, attachmentId);
2680
+ }
2442
2681
  // ── organize ──
2443
2682
  async markRead(account, id, isRead) {
2444
- return markRead2(this.clients, account, id, isRead);
2683
+ return markRead3(this.clients, account, id, isRead);
2445
2684
  }
2446
2685
  // ── folders ──
2447
2686
  async listFolders(account, opts) {
2448
- return listFolders2(this.clients, account, opts);
2687
+ return listFolders3(this.clients, account, opts);
2449
2688
  }
2450
2689
  async createFolder(account, input) {
2451
- return createFolder2(this.clients, account, input);
2690
+ return createFolder3(this.clients, account, input);
2452
2691
  }
2453
2692
  async renameFolder(account, folderId, newName) {
2454
- return renameFolder2(this.clients, account, folderId, newName);
2693
+ return renameFolder3(this.clients, account, folderId, newName);
2455
2694
  }
2456
2695
  async deleteFolder(account, folderId) {
2457
- return deleteFolder2(this.clients, account, folderId);
2696
+ return deleteFolder3(this.clients, account, folderId);
2458
2697
  }
2459
2698
  };
2460
2699
 
@@ -2601,6 +2840,31 @@ function buildStyleAttr(style) {
2601
2840
  if (style.fontColor) parts.push(`color: ${style.fontColor}`);
2602
2841
  return parts.join("; ");
2603
2842
  }
2843
+ function findThreadBoundary(html) {
2844
+ const markerIdx = html.indexOf(THREAD_MARKER);
2845
+ if (markerIdx !== -1) {
2846
+ const before = html.slice(0, markerIdx);
2847
+ let answerEnd = before.lastIndexOf(
2848
+ '<div style="line-height:12px"><br></div>'
2849
+ );
2850
+ if (answerEnd === -1) {
2851
+ const re = /<div\s+style=["'][^"']*line-height\s*:\s*12px[^"']*["']\s*>\s*<br\s*\/?>\s*<\/div>/gi;
2852
+ let m;
2853
+ while ((m = re.exec(before)) !== null) answerEnd = m.index;
2854
+ }
2855
+ return { threadStart: markerIdx, answerEnd: Math.max(answerEnd, 0) };
2856
+ }
2857
+ const spacerRe = /<div\s+style=["'][^"']*line-height\s*:\s*12px[^"']*["']\s*>\s*<br\s*\/?>\s*<\/div>/i;
2858
+ const spacerMatch = spacerRe.exec(html);
2859
+ if (spacerMatch) {
2860
+ return { threadStart: spacerMatch.index, answerEnd: spacerMatch.index };
2861
+ }
2862
+ for (const re of [/id=["']?Signature["']?/i, /<blockquote/i]) {
2863
+ const idx = html.search(re);
2864
+ if (idx !== -1) return { threadStart: idx, answerEnd: idx };
2865
+ }
2866
+ return null;
2867
+ }
2604
2868
  function shouldRegister(name, tools) {
2605
2869
  if (tools.enabledTools) {
2606
2870
  return tools.enabledTools.has(name);
@@ -3305,6 +3569,31 @@ import { readFileSync } from "fs";
3305
3569
  import { basename, extname } from "path";
3306
3570
  function registerComposeTools(server, ctx) {
3307
3571
  const { store, registry, tools } = ctx;
3572
+ const MIME_TYPES = {
3573
+ ".pdf": "application/pdf",
3574
+ ".png": "image/png",
3575
+ ".jpg": "image/jpeg",
3576
+ ".jpeg": "image/jpeg",
3577
+ ".gif": "image/gif",
3578
+ ".svg": "image/svg+xml",
3579
+ ".webp": "image/webp",
3580
+ ".txt": "text/plain",
3581
+ ".html": "text/html",
3582
+ ".css": "text/css",
3583
+ ".csv": "text/csv",
3584
+ ".json": "application/json",
3585
+ ".xml": "application/xml",
3586
+ ".zip": "application/zip",
3587
+ ".gz": "application/gzip",
3588
+ ".tar": "application/x-tar",
3589
+ ".doc": "application/msword",
3590
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
3591
+ ".xls": "application/vnd.ms-excel",
3592
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
3593
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
3594
+ ".mp3": "audio/mpeg",
3595
+ ".mp4": "video/mp4"
3596
+ };
3308
3597
  const sendEmailSchema = z6.object({
3309
3598
  account: z6.string().email(),
3310
3599
  to: z6.array(emailAddrSchema).min(1),
@@ -3318,14 +3607,24 @@ function registerComposeTools(server, ctx) {
3318
3607
  include_signature: z6.boolean().describe(
3319
3608
  "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."
3320
3609
  ),
3321
- inReplyTo: z6.string().optional().describe(
3322
- "Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically."
3610
+ inReplyTo: z6.union([z6.string(), z6.literal(false)]).describe(
3611
+ "Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically. Set to `false` for a new email (not a reply)."
3323
3612
  ),
3324
3613
  replyAll: z6.boolean().default(false).optional().describe(
3325
3614
  "When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
3326
3615
  ),
3327
3616
  forwardMessageId: z6.string().optional().describe(
3328
3617
  "Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
3618
+ ),
3619
+ attachments: z6.array(
3620
+ z6.object({
3621
+ filePath: z6.string().min(1).describe("Absolute path to a local file"),
3622
+ name: z6.string().optional().describe(
3623
+ "Attachment filename. Defaults to the file's basename."
3624
+ )
3625
+ })
3626
+ ).optional().describe(
3627
+ "File attachments to include. The server reads the files from disk and base64-encodes them automatically."
3329
3628
  )
3330
3629
  });
3331
3630
  async function handleSendOrDraft(args, action, resultKey, toolName) {
@@ -3348,6 +3647,18 @@ function registerComposeTools(server, ctx) {
3348
3647
  "inReplyTo and forwardMessageId are mutually exclusive \u2014 use one or the other"
3349
3648
  );
3350
3649
  }
3650
+ let processedAttachments = void 0;
3651
+ if (args.attachments && args.attachments.length > 0) {
3652
+ processedAttachments = args.attachments.map((att) => {
3653
+ const fileData = readFileSync(att.filePath);
3654
+ const ext = extname(att.filePath).toLowerCase();
3655
+ return {
3656
+ name: att.name ?? basename(att.filePath),
3657
+ contentBytes: fileData.toString("base64"),
3658
+ contentType: MIME_TYPES[ext] ?? "application/octet-stream"
3659
+ };
3660
+ });
3661
+ }
3351
3662
  const res = await action(provider, account, {
3352
3663
  to: args.to,
3353
3664
  cc: args.cc,
@@ -3357,7 +3668,8 @@ function registerComposeTools(server, ctx) {
3357
3668
  isHtml: composed.isHtml,
3358
3669
  inReplyTo: args.inReplyTo,
3359
3670
  replyAll: args.replyAll,
3360
- forwardMessageId: args.forwardMessageId
3671
+ forwardMessageId: args.forwardMessageId,
3672
+ attachments: processedAttachments
3361
3673
  });
3362
3674
  const result = { [resultKey]: true, ...res };
3363
3675
  if (toolName === "draft_email" && res.id) {
@@ -3423,6 +3735,19 @@ function registerComposeTools(server, ctx) {
3423
3735
  ),
3424
3736
  include_signature: z6.boolean().optional().describe(
3425
3737
  "Whether to re-apply the account's saved HTML signature to the body. If true, don't include a signature in the body param. Only meaningful when `body` is also provided. Returns an error if true but no signature is configured for this account."
3738
+ ),
3739
+ new_attachments: z6.array(
3740
+ z6.object({
3741
+ filePath: z6.string().min(1).describe("Absolute path to a local file"),
3742
+ name: z6.string().optional().describe(
3743
+ "Attachment filename. Defaults to the file's basename."
3744
+ )
3745
+ })
3746
+ ).optional().describe(
3747
+ "New file attachments to add to the draft. The server reads the files from disk and base64-encodes them automatically."
3748
+ ),
3749
+ remove_attachments: z6.array(z6.string().min(1)).optional().describe(
3750
+ "Attachment IDs to remove from the draft. Get attachment IDs from read_email."
3426
3751
  )
3427
3752
  });
3428
3753
  const editDraftOutputSchema = {
@@ -3461,13 +3786,12 @@ function registerComposeTools(server, ctx) {
3461
3786
  isHtmlPayload = composed.isHtml;
3462
3787
  }
3463
3788
  if (bodyPayload !== void 0) {
3464
- const spacer = '<div style="line-height:12px"><br></div>';
3465
3789
  try {
3466
3790
  const existing = await provider.readEmail(account, a.id);
3467
3791
  const existingHtml = existing.bodyHtml ?? "";
3468
- const spacerIdx = existingHtml.indexOf(spacer);
3469
- if (spacerIdx !== -1) {
3470
- bodyPayload = bodyPayload + existingHtml.slice(spacerIdx);
3792
+ const boundary = findThreadBoundary(existingHtml);
3793
+ if (boundary !== null) {
3794
+ bodyPayload = bodyPayload + existingHtml.slice(boundary.answerEnd);
3471
3795
  }
3472
3796
  } catch {
3473
3797
  }
@@ -3480,11 +3804,38 @@ function registerComposeTools(server, ctx) {
3480
3804
  body: bodyPayload,
3481
3805
  isHtml: isHtmlPayload
3482
3806
  });
3807
+ const newAttachmentIds = [];
3808
+ if (a.new_attachments && a.new_attachments.length > 0) {
3809
+ for (const att of a.new_attachments) {
3810
+ const fileData = readFileSync(att.filePath);
3811
+ const ext = extname(att.filePath).toLowerCase();
3812
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
3813
+ const attRes = await provider.addAttachmentToDraft(
3814
+ account,
3815
+ res.id,
3816
+ att.name ?? basename(att.filePath),
3817
+ fileData.toString("base64"),
3818
+ contentType
3819
+ );
3820
+ newAttachmentIds.push(attRes.attachment.id);
3821
+ }
3822
+ }
3823
+ const removedIds = [];
3824
+ if (a.remove_attachments && a.remove_attachments.length > 0) {
3825
+ for (const attId of a.remove_attachments) {
3826
+ await provider.removeAttachmentFromDraft(
3827
+ account,
3828
+ res.id,
3829
+ attId
3830
+ );
3831
+ removedIds.push(attId);
3832
+ }
3833
+ }
3483
3834
  const draft = await provider.readEmail(account, res.id);
3484
3835
  const result = {
3485
3836
  edited: true,
3486
3837
  id: res.id,
3487
- draftHtml: draft.bodyHtml
3838
+ draftHtml: draft.bodyHtml ?? ""
3488
3839
  };
3489
3840
  return ok(result, result);
3490
3841
  } catch (err) {
@@ -3520,99 +3871,6 @@ function registerComposeTools(server, ctx) {
3520
3871
  }
3521
3872
  );
3522
3873
  }
3523
- const MIME_TYPES = {
3524
- ".pdf": "application/pdf",
3525
- ".png": "image/png",
3526
- ".jpg": "image/jpeg",
3527
- ".jpeg": "image/jpeg",
3528
- ".gif": "image/gif",
3529
- ".svg": "image/svg+xml",
3530
- ".webp": "image/webp",
3531
- ".txt": "text/plain",
3532
- ".html": "text/html",
3533
- ".css": "text/css",
3534
- ".csv": "text/csv",
3535
- ".json": "application/json",
3536
- ".xml": "application/xml",
3537
- ".zip": "application/zip",
3538
- ".gz": "application/gzip",
3539
- ".tar": "application/x-tar",
3540
- ".doc": "application/msword",
3541
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
3542
- ".xls": "application/vnd.ms-excel",
3543
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
3544
- ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
3545
- ".mp3": "audio/mpeg",
3546
- ".mp4": "video/mp4"
3547
- };
3548
- const addAttachmentOutputSchema = {
3549
- attached: z6.literal(true),
3550
- id: z6.string(),
3551
- attachment: z6.object({
3552
- id: z6.string(),
3553
- name: z6.string(),
3554
- contentType: z6.string().optional()
3555
- })
3556
- };
3557
- if (shouldRegister("add_attachment_to_draft", tools)) {
3558
- server.registerTool(
3559
- "add_attachment_to_draft",
3560
- {
3561
- description: "Add a file attachment to an existing draft email by ID. Provide exactly one of `contentBytes` (base64-encoded content) or `filePath` (absolute path to a local file). When using `filePath`, the file is read from disk and base64-encoded automatically, and `name` defaults to the file's basename if not provided. `contentType` is the MIME type (e.g. 'application/pdf'); when using `filePath` it is inferred from the extension if omitted. Disabled in --read-only mode.",
3562
- inputSchema: z6.object({
3563
- account: z6.string().email(),
3564
- id: z6.string().min(1).describe("Draft message ID"),
3565
- name: z6.string().min(1).optional().describe(
3566
- "Attachment filename (e.g. 'report.pdf'). Defaults to the file's basename when `filePath` is used."
3567
- ),
3568
- contentBytes: z6.string().min(1).optional().describe("Base64-encoded file content. Mutually exclusive with `filePath`."),
3569
- filePath: z6.string().min(1).optional().describe(
3570
- "Absolute path to a local file. The file is read and base64-encoded automatically. Mutually exclusive with `contentBytes`."
3571
- ),
3572
- contentType: z6.string().optional().describe("MIME type (e.g. 'application/pdf'). Inferred from `filePath` extension if omitted.")
3573
- }).refine(
3574
- (v) => Boolean(v.contentBytes) !== Boolean(v.filePath),
3575
- { message: "Provide exactly one of `contentBytes` or `filePath`." }
3576
- ),
3577
- outputSchema: addAttachmentOutputSchema
3578
- },
3579
- async (args) => {
3580
- try {
3581
- const { provider, account } = registry.resolveByEmail(args.account);
3582
- let contentBytes;
3583
- let name;
3584
- let contentType = args.contentType;
3585
- if (args.filePath) {
3586
- const fileData = readFileSync(args.filePath);
3587
- contentBytes = fileData.toString("base64");
3588
- name = args.name ?? basename(args.filePath);
3589
- if (!contentType) {
3590
- const ext = extname(args.filePath).toLowerCase();
3591
- contentType = MIME_TYPES[ext] ?? "application/octet-stream";
3592
- }
3593
- } else {
3594
- contentBytes = args.contentBytes;
3595
- name = args.name;
3596
- }
3597
- const res = await provider.addAttachmentToDraft(
3598
- account,
3599
- args.id,
3600
- name,
3601
- contentBytes,
3602
- contentType
3603
- );
3604
- const data = {
3605
- attached: true,
3606
- id: res.id,
3607
- attachment: res.attachment
3608
- };
3609
- return ok(data, data);
3610
- } catch (err) {
3611
- return fail(errMsg(err));
3612
- }
3613
- }
3614
- );
3615
- }
3616
3874
  }
3617
3875
 
3618
3876
  // src/tools/index.ts
@@ -3628,7 +3886,7 @@ function registerTools(server, opts) {
3628
3886
  // package.json
3629
3887
  var package_default = {
3630
3888
  name: "hypermail-mcp",
3631
- version: "0.7.3",
3889
+ version: "0.7.5",
3632
3890
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
3633
3891
  type: "module",
3634
3892
  bin: {
@@ -3689,6 +3947,7 @@ var package_default = {
3689
3947
  "@types/isomorphic-fetch": "^0.0.39",
3690
3948
  "@types/node": "^22.10.2",
3691
3949
  "@types/nodemailer": "^8.0.0",
3950
+ "@types/turndown": "^5.0.6",
3692
3951
  tsup: "^8.3.5",
3693
3952
  typescript: "^5.7.2",
3694
3953
  vitest: "^2.1.8"
@@ -3735,6 +3994,76 @@ function sleep(ms) {
3735
3994
  return new Promise((resolve) => setTimeout(resolve, ms));
3736
3995
  }
3737
3996
 
3997
+ // src/watcher/script.ts
3998
+ import { spawn } from "child_process";
3999
+ async function runScript(email, config) {
4000
+ if (!config.script) return false;
4001
+ const { path: scriptPath, timeoutMs, retry } = config.script;
4002
+ const maxAttempts = retry?.maxAttempts ?? 5;
4003
+ const baseDelayMs = retry?.baseDelayMs ?? 1e3;
4004
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4005
+ if (attempt > 0) {
4006
+ const delay = baseDelayMs * 2 ** (attempt - 1);
4007
+ await sleep2(delay);
4008
+ }
4009
+ try {
4010
+ const ok2 = await spawnWithTimeout(
4011
+ scriptPath,
4012
+ JSON.stringify(email),
4013
+ timeoutMs
4014
+ );
4015
+ if (ok2) return true;
4016
+ console.error(
4017
+ `[hypermail-watch] script ${email.id} attempt ${attempt + 1}/${maxAttempts}: non-zero exit code`
4018
+ );
4019
+ } catch (err) {
4020
+ console.error(
4021
+ `[hypermail-watch] script ${email.id} attempt ${attempt + 1}/${maxAttempts}: ${String(err)}`
4022
+ );
4023
+ }
4024
+ }
4025
+ console.error(
4026
+ `[hypermail-watch] script delivery failed after ${maxAttempts} retries for ${email.id}`
4027
+ );
4028
+ return false;
4029
+ }
4030
+ function spawnWithTimeout(scriptPath, stdinData, timeoutMs) {
4031
+ return new Promise((resolve) => {
4032
+ const child = spawn("node", [scriptPath], {
4033
+ stdio: ["pipe", "pipe", "pipe"]
4034
+ });
4035
+ let stderr = "";
4036
+ child.stderr?.on("data", (chunk) => {
4037
+ stderr += chunk.toString("utf-8");
4038
+ });
4039
+ const timer = setTimeout(() => {
4040
+ child.kill("SIGTERM");
4041
+ if (stderr) {
4042
+ console.error(`[hypermail-watch] script timed out after ${timeoutMs}ms. stderr:
4043
+ ${stderr}`);
4044
+ }
4045
+ resolve(false);
4046
+ }, timeoutMs);
4047
+ child.on("close", (code) => {
4048
+ clearTimeout(timer);
4049
+ if (stderr && code !== 0) {
4050
+ console.error(`[hypermail-watch] script stderr:
4051
+ ${stderr}`);
4052
+ }
4053
+ resolve(code === 0);
4054
+ });
4055
+ child.on("error", (err) => {
4056
+ clearTimeout(timer);
4057
+ console.error(`[hypermail-watch] script spawn error: ${err.message}`);
4058
+ resolve(false);
4059
+ });
4060
+ child.stdin?.end(stdinData);
4061
+ });
4062
+ }
4063
+ function sleep2(ms) {
4064
+ return new Promise((resolve) => setTimeout(resolve, ms));
4065
+ }
4066
+
3738
4067
  // src/watcher/manager.ts
3739
4068
  var WatcherManager = class {
3740
4069
  constructor(store, registry, config) {
@@ -3805,78 +4134,20 @@ var WatcherManager = class {
3805
4134
  }
3806
4135
  async emit(full) {
3807
4136
  await postWebhook(full, this.config);
4137
+ runScript(full, this.config).catch((err) => {
4138
+ console.error(
4139
+ `[hypermail-watch] script unhandled error for ${full.id}:`,
4140
+ err
4141
+ );
4142
+ });
3808
4143
  }
3809
4144
  };
3810
4145
 
3811
4146
  // src/config.ts
3812
- import { readFileSync as readFileSync2 } from "fs";
3813
4147
  import { z as z7 } from "zod";
3814
- var httpConfigSchema = z7.object({
3815
- enabled: z7.boolean().default(false),
3816
- port: z7.number().int().min(1).max(65535).default(3e3),
3817
- host: z7.string().default("127.0.0.1")
3818
- });
3819
- var toolsConfigSchema = z7.object({
3820
- disabled: z7.array(z7.string()).optional(),
3821
- enabled: z7.array(z7.string()).optional()
3822
- });
3823
- var outlookProviderSchema = z7.object({
3824
- clientId: z7.string().optional(),
3825
- tenantId: z7.string().optional()
3826
- });
3827
- var gmailProviderSchema = z7.object({
3828
- clientId: z7.string().optional(),
3829
- clientSecret: z7.string().optional()
3830
- });
3831
- var providersConfigSchema = z7.object({
3832
- outlook: outlookProviderSchema.optional(),
3833
- gmail: gmailProviderSchema.optional()
3834
- });
3835
- var watchConfigSchema = z7.object({
3836
- enabled: z7.boolean().default(false),
3837
- pollIntervalSeconds: z7.number().int().min(10).max(3600).default(10),
3838
- webhook: z7.object({
3839
- url: z7.string(),
3840
- retry: z7.object({
3841
- maxAttempts: z7.number().int().min(1).max(10).default(5),
3842
- baseDelayMs: z7.number().int().min(100).default(1e3)
3843
- }).optional()
3844
- }).optional()
3845
- });
3846
- var rawConfigSchema = z7.object({
3847
- dataDir: z7.string().optional(),
3848
- http: httpConfigSchema.optional(),
3849
- tools: toolsConfigSchema.optional(),
3850
- providers: providersConfigSchema.optional(),
3851
- watch: watchConfigSchema.optional()
3852
- });
3853
- var KNOWN_TOOLS = [
3854
- "list_accounts",
3855
- "add_account",
3856
- "complete_add_account",
3857
- "get_account_settings",
3858
- "set_account_settings",
3859
- "remove_account",
3860
- "list_emails",
3861
- "search_emails",
3862
- "read_email",
3863
- "read_attachment",
3864
- "archive_email",
3865
- "trash_email",
3866
- "move_email",
3867
- "mark_read",
3868
- "mark_unread",
3869
- "list_folders",
3870
- "create_folder",
3871
- "delete_folder",
3872
- "rename_folder",
3873
- "send_email",
3874
- "draft_email",
3875
- "edit_draft",
3876
- "send_draft",
3877
- "add_attachment_to_draft",
3878
- "check_notifications"
3879
- ];
4148
+
4149
+ // src/config/load.ts
4150
+ import { readFileSync as readFileSync2 } from "fs";
3880
4151
  var ENV_HTTP_ENABLED = "HYPERMAIL_HTTP_ENABLED";
3881
4152
  var ENV_HTTP_PORT = "HYPERMAIL_HTTP_PORT";
3882
4153
  var ENV_HTTP_HOST = "HYPERMAIL_HTTP_HOST";
@@ -3891,6 +4162,10 @@ var ENV_WATCH_POLL_INTERVAL = "HYPERMAIL_WATCH_POLL_INTERVAL";
3891
4162
  var ENV_WATCH_WEBHOOK_URL = "HYPERMAIL_WATCH_WEBHOOK_URL";
3892
4163
  var ENV_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS";
3893
4164
  var ENV_WATCH_WEBHOOK_RETRY_BASE_DELAY = "HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS";
4165
+ var ENV_WATCH_SCRIPT_PATH = "HYPERMAIL_WATCH_SCRIPT_PATH";
4166
+ var ENV_WATCH_SCRIPT_TIMEOUT_MS = "HYPERMAIL_WATCH_SCRIPT_TIMEOUT_MS";
4167
+ var ENV_WATCH_SCRIPT_RETRY_MAX_ATTEMPTS = "HYPERMAIL_WATCH_SCRIPT_RETRY_MAX_ATTEMPTS";
4168
+ var ENV_WATCH_SCRIPT_RETRY_BASE_DELAY = "HYPERMAIL_WATCH_SCRIPT_RETRY_BASE_DELAY_MS";
3894
4169
  var LEGACY_MS_CLIENT_ID = "MS_CLIENT_ID";
3895
4170
  var LEGACY_MS_TENANT_ID = "MS_TENANT_ID";
3896
4171
  var LEGACY_GOOGLE_CLIENT_ID = "GOOGLE_CLIENT_ID";
@@ -4007,10 +4282,26 @@ function loadConfig(configPath, cliOverrides = {}) {
4007
4282
  retry: { maxAttempts: retryMaxAttempts, baseDelayMs: retryBaseDelayMs }
4008
4283
  };
4009
4284
  }
4285
+ const scriptPath = parsed.watch?.script?.path ?? process.env[ENV_WATCH_SCRIPT_PATH];
4286
+ let script;
4287
+ if (scriptPath) {
4288
+ const scriptTimeoutMs = parsed.watch?.script?.timeoutMs ?? parseIntSafe(process.env[ENV_WATCH_SCRIPT_TIMEOUT_MS]) ?? 3e4;
4289
+ const scriptRetryMaxAttempts = parsed.watch?.script?.retry?.maxAttempts ?? parseIntSafe(process.env[ENV_WATCH_SCRIPT_RETRY_MAX_ATTEMPTS]) ?? 5;
4290
+ const scriptRetryBaseDelayMs = parsed.watch?.script?.retry?.baseDelayMs ?? parseIntSafe(process.env[ENV_WATCH_SCRIPT_RETRY_BASE_DELAY]) ?? 1e3;
4291
+ script = {
4292
+ path: scriptPath,
4293
+ timeoutMs: scriptTimeoutMs,
4294
+ retry: {
4295
+ maxAttempts: scriptRetryMaxAttempts,
4296
+ baseDelayMs: scriptRetryBaseDelayMs
4297
+ }
4298
+ };
4299
+ }
4010
4300
  watch = {
4011
4301
  enabled: watchEnabledEnv ?? parsed.watch?.enabled ?? false,
4012
4302
  pollIntervalSeconds: parsed.watch?.pollIntervalSeconds ?? parseIntSafe(process.env[ENV_WATCH_POLL_INTERVAL]) ?? 10,
4013
- webhook
4303
+ webhook,
4304
+ script
4014
4305
  };
4015
4306
  }
4016
4307
  return {
@@ -4021,6 +4312,81 @@ function loadConfig(configPath, cliOverrides = {}) {
4021
4312
  watch
4022
4313
  };
4023
4314
  }
4315
+
4316
+ // src/config.ts
4317
+ var httpConfigSchema = z7.object({
4318
+ enabled: z7.boolean().default(false),
4319
+ port: z7.number().int().min(1).max(65535).default(3e3),
4320
+ host: z7.string().default("127.0.0.1")
4321
+ });
4322
+ var toolsConfigSchema = z7.object({
4323
+ disabled: z7.array(z7.string()).optional(),
4324
+ enabled: z7.array(z7.string()).optional()
4325
+ });
4326
+ var outlookProviderSchema = z7.object({
4327
+ clientId: z7.string().optional(),
4328
+ tenantId: z7.string().optional()
4329
+ });
4330
+ var gmailProviderSchema = z7.object({
4331
+ clientId: z7.string().optional(),
4332
+ clientSecret: z7.string().optional()
4333
+ });
4334
+ var providersConfigSchema = z7.object({
4335
+ outlook: outlookProviderSchema.optional(),
4336
+ gmail: gmailProviderSchema.optional()
4337
+ });
4338
+ var watchRetrySchema = z7.object({
4339
+ maxAttempts: z7.number().int().min(1).max(10).default(5),
4340
+ baseDelayMs: z7.number().int().min(100).default(1e3)
4341
+ });
4342
+ var watchWebhookSchema = z7.object({
4343
+ url: z7.string(),
4344
+ retry: watchRetrySchema.optional()
4345
+ });
4346
+ var watchScriptSchema = z7.object({
4347
+ path: z7.string(),
4348
+ timeoutMs: z7.number().int().min(1e3).default(3e4),
4349
+ retry: watchRetrySchema.optional()
4350
+ });
4351
+ var watchConfigSchema = z7.object({
4352
+ enabled: z7.boolean().default(false),
4353
+ pollIntervalSeconds: z7.number().int().min(10).max(3600).default(10),
4354
+ webhook: watchWebhookSchema.optional(),
4355
+ script: watchScriptSchema.optional()
4356
+ });
4357
+ var rawConfigSchema = z7.object({
4358
+ dataDir: z7.string().optional(),
4359
+ http: httpConfigSchema.optional(),
4360
+ tools: toolsConfigSchema.optional(),
4361
+ providers: providersConfigSchema.optional(),
4362
+ watch: watchConfigSchema.optional()
4363
+ });
4364
+ var KNOWN_TOOLS = [
4365
+ "list_accounts",
4366
+ "add_account",
4367
+ "complete_add_account",
4368
+ "get_account_settings",
4369
+ "set_account_settings",
4370
+ "remove_account",
4371
+ "list_emails",
4372
+ "search_emails",
4373
+ "read_email",
4374
+ "read_attachment",
4375
+ "archive_email",
4376
+ "trash_email",
4377
+ "move_email",
4378
+ "mark_read",
4379
+ "mark_unread",
4380
+ "list_folders",
4381
+ "create_folder",
4382
+ "delete_folder",
4383
+ "rename_folder",
4384
+ "send_email",
4385
+ "draft_email",
4386
+ "edit_draft",
4387
+ "send_draft",
4388
+ "check_notifications"
4389
+ ];
4024
4390
  function resolveTools(config) {
4025
4391
  if (!config.tools) {
4026
4392
  return { enabledTools: null, disabledTools: null };