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/README.md +42 -17
- package/dist/cli.js +907 -541
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
804
|
+
return sendOrSave(this.clients.get(account), account, msg, "send");
|
|
672
805
|
}
|
|
673
806
|
async saveDraft(account, msg) {
|
|
674
|
-
return this.
|
|
807
|
+
return sendOrSave(this.clients.get(account), account, msg, "draft");
|
|
675
808
|
}
|
|
676
809
|
async updateDraft(account, id, update) {
|
|
677
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
819
|
+
return addAttachmentToDraft(
|
|
820
|
+
this.clients.get(account),
|
|
821
|
+
account,
|
|
822
|
+
draftId,
|
|
717
823
|
name,
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1647
|
+
return listEmails2(this.clients, account, opts);
|
|
1506
1648
|
}
|
|
1507
1649
|
async searchEmails(account, query, opts) {
|
|
1508
|
-
return
|
|
1650
|
+
return searchEmails2(this.clients, account, query, opts);
|
|
1509
1651
|
}
|
|
1510
1652
|
async readEmail(account, id) {
|
|
1511
|
-
return
|
|
1653
|
+
return readEmail2(this.clients, account, id);
|
|
1512
1654
|
}
|
|
1513
1655
|
async readAttachment(account, messageId, attachmentId) {
|
|
1514
|
-
return
|
|
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
|
|
1666
|
+
return updateDraft2(this.clients, account, id, update);
|
|
1525
1667
|
}
|
|
1526
1668
|
async moveEmail(account, id, destinationId) {
|
|
1527
|
-
return
|
|
1669
|
+
return moveEmail2(this.clients, account, id, destinationId);
|
|
1528
1670
|
}
|
|
1529
1671
|
async sendDraft(account, id) {
|
|
1530
|
-
return
|
|
1672
|
+
return sendDraft2(this.clients, account, id);
|
|
1531
1673
|
}
|
|
1532
1674
|
async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
|
|
1533
|
-
return
|
|
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
|
|
1682
|
+
return markRead2(this.clients, account, id, isRead);
|
|
1538
1683
|
}
|
|
1539
1684
|
// ---------- folders ----------
|
|
1540
1685
|
async listFolders(account, opts) {
|
|
1541
|
-
return
|
|
1686
|
+
return listFolders2(this.clients, account, opts);
|
|
1542
1687
|
}
|
|
1543
1688
|
async createFolder(account, input) {
|
|
1544
|
-
return
|
|
1689
|
+
return createFolder2(this.clients, account, input);
|
|
1545
1690
|
}
|
|
1546
1691
|
async renameFolder(account, folderId, newName) {
|
|
1547
|
-
return
|
|
1692
|
+
return renameFolder2(this.clients, account, folderId, newName);
|
|
1548
1693
|
}
|
|
1549
1694
|
async deleteFolder(account, folderId) {
|
|
1550
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2641
|
+
return listEmails3(this.clients, account, opts);
|
|
2406
2642
|
}
|
|
2407
2643
|
async searchEmails(account, query, opts) {
|
|
2408
|
-
return
|
|
2644
|
+
return searchEmails3(this.clients, account, query, opts);
|
|
2409
2645
|
}
|
|
2410
2646
|
async readEmail(account, id) {
|
|
2411
|
-
return
|
|
2647
|
+
return readEmail3(this.clients, account, id);
|
|
2412
2648
|
}
|
|
2413
2649
|
async readAttachment(account, messageId, attachmentId) {
|
|
2414
|
-
return
|
|
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
|
|
2660
|
+
return updateDraft3(this.clients, account, id, update);
|
|
2425
2661
|
}
|
|
2426
2662
|
async moveEmail(account, id, destinationId) {
|
|
2427
|
-
return
|
|
2663
|
+
return moveEmail3(this.clients, account, id, destinationId);
|
|
2428
2664
|
}
|
|
2429
2665
|
async sendDraft(account, id) {
|
|
2430
|
-
return
|
|
2666
|
+
return sendDraft3(this.clients, account, id);
|
|
2431
2667
|
}
|
|
2432
2668
|
async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
|
|
2433
|
-
return
|
|
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
|
|
2683
|
+
return markRead3(this.clients, account, id, isRead);
|
|
2445
2684
|
}
|
|
2446
2685
|
// ── folders ──
|
|
2447
2686
|
async listFolders(account, opts) {
|
|
2448
|
-
return
|
|
2687
|
+
return listFolders3(this.clients, account, opts);
|
|
2449
2688
|
}
|
|
2450
2689
|
async createFolder(account, input) {
|
|
2451
|
-
return
|
|
2690
|
+
return createFolder3(this.clients, account, input);
|
|
2452
2691
|
}
|
|
2453
2692
|
async renameFolder(account, folderId, newName) {
|
|
2454
|
-
return
|
|
2693
|
+
return renameFolder3(this.clients, account, folderId, newName);
|
|
2455
2694
|
}
|
|
2456
2695
|
async deleteFolder(account, folderId) {
|
|
2457
|
-
return
|
|
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().
|
|
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
|
|
3469
|
-
if (
|
|
3470
|
-
bodyPayload = bodyPayload + existingHtml.slice(
|
|
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.
|
|
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
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
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 };
|