hypermail-mcp 0.7.4 → 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 +37 -17
- package/dist/cli.js +903 -539
- package/dist/cli.js.map +1 -1
- package/package.json +1 -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",
|
|
@@ -2207,7 +2363,7 @@ async function updateDraft2(clients, account, id, update) {
|
|
|
2207
2363
|
});
|
|
2208
2364
|
return { id: updated.data.message?.id ?? updated.data.id ?? id };
|
|
2209
2365
|
}
|
|
2210
|
-
async function
|
|
2366
|
+
async function moveEmail3(clients, account, id, destinationId) {
|
|
2211
2367
|
const { gmail } = clients.get(account);
|
|
2212
2368
|
const { addLabelIds, removeLabelIds } = resolveLabelsForMove(destinationId);
|
|
2213
2369
|
await gmail.users.messages.modify({
|
|
@@ -2216,7 +2372,7 @@ async function moveEmail2(clients, account, id, destinationId) {
|
|
|
2216
2372
|
requestBody: { addLabelIds, removeLabelIds }
|
|
2217
2373
|
});
|
|
2218
2374
|
}
|
|
2219
|
-
async function
|
|
2375
|
+
async function sendDraft3(clients, account, id) {
|
|
2220
2376
|
const { gmail } = clients.get(account);
|
|
2221
2377
|
const res = await gmail.users.drafts.send({
|
|
2222
2378
|
userId: "me",
|
|
@@ -2224,7 +2380,7 @@ async function sendDraft2(clients, account, id) {
|
|
|
2224
2380
|
});
|
|
2225
2381
|
return { id: res.data.id ?? id };
|
|
2226
2382
|
}
|
|
2227
|
-
async function
|
|
2383
|
+
async function addAttachmentToDraft3(clients, account, draftId, name, contentBytes, contentType) {
|
|
2228
2384
|
const { gmail } = clients.get(account);
|
|
2229
2385
|
const draftRes = await gmail.users.drafts.get({
|
|
2230
2386
|
userId: "me",
|
|
@@ -2274,7 +2430,86 @@ async function addAttachmentToDraft2(clients, account, draftId, name, contentByt
|
|
|
2274
2430
|
}
|
|
2275
2431
|
};
|
|
2276
2432
|
}
|
|
2277
|
-
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) {
|
|
2278
2513
|
const { gmail } = clients.get(account);
|
|
2279
2514
|
await gmail.users.messages.modify({
|
|
2280
2515
|
userId: "me",
|
|
@@ -2285,7 +2520,7 @@ async function markRead2(clients, account, id, isRead) {
|
|
|
2285
2520
|
}
|
|
2286
2521
|
});
|
|
2287
2522
|
}
|
|
2288
|
-
async function
|
|
2523
|
+
async function createFolder3(clients, account, input) {
|
|
2289
2524
|
const { gmail } = clients.get(account);
|
|
2290
2525
|
const created = await gmail.users.labels.create({
|
|
2291
2526
|
userId: "me",
|
|
@@ -2297,7 +2532,7 @@ async function createFolder2(clients, account, input) {
|
|
|
2297
2532
|
});
|
|
2298
2533
|
return mapFolder2(created.data);
|
|
2299
2534
|
}
|
|
2300
|
-
async function
|
|
2535
|
+
async function renameFolder3(clients, account, folderId, newName) {
|
|
2301
2536
|
const { gmail } = clients.get(account);
|
|
2302
2537
|
const updated = await gmail.users.labels.patch({
|
|
2303
2538
|
userId: "me",
|
|
@@ -2306,7 +2541,7 @@ async function renameFolder2(clients, account, folderId, newName) {
|
|
|
2306
2541
|
});
|
|
2307
2542
|
return mapFolder2(updated.data);
|
|
2308
2543
|
}
|
|
2309
|
-
async function
|
|
2544
|
+
async function deleteFolder3(clients, account, folderId) {
|
|
2310
2545
|
const { gmail } = clients.get(account);
|
|
2311
2546
|
await gmail.users.labels.delete({
|
|
2312
2547
|
userId: "me",
|
|
@@ -2403,16 +2638,16 @@ var GmailProvider = class {
|
|
|
2403
2638
|
}
|
|
2404
2639
|
// ── browse ──
|
|
2405
2640
|
async listEmails(account, opts) {
|
|
2406
|
-
return
|
|
2641
|
+
return listEmails3(this.clients, account, opts);
|
|
2407
2642
|
}
|
|
2408
2643
|
async searchEmails(account, query, opts) {
|
|
2409
|
-
return
|
|
2644
|
+
return searchEmails3(this.clients, account, query, opts);
|
|
2410
2645
|
}
|
|
2411
2646
|
async readEmail(account, id) {
|
|
2412
|
-
return
|
|
2647
|
+
return readEmail3(this.clients, account, id);
|
|
2413
2648
|
}
|
|
2414
2649
|
async readAttachment(account, messageId, attachmentId) {
|
|
2415
|
-
return
|
|
2650
|
+
return readAttachment3(this.clients, account, messageId, attachmentId);
|
|
2416
2651
|
}
|
|
2417
2652
|
// ── compose ──
|
|
2418
2653
|
async sendEmail(account, msg) {
|
|
@@ -2422,16 +2657,16 @@ var GmailProvider = class {
|
|
|
2422
2657
|
return saveDraft2(this.clients, account, msg);
|
|
2423
2658
|
}
|
|
2424
2659
|
async updateDraft(account, id, update) {
|
|
2425
|
-
return
|
|
2660
|
+
return updateDraft3(this.clients, account, id, update);
|
|
2426
2661
|
}
|
|
2427
2662
|
async moveEmail(account, id, destinationId) {
|
|
2428
|
-
return
|
|
2663
|
+
return moveEmail3(this.clients, account, id, destinationId);
|
|
2429
2664
|
}
|
|
2430
2665
|
async sendDraft(account, id) {
|
|
2431
|
-
return
|
|
2666
|
+
return sendDraft3(this.clients, account, id);
|
|
2432
2667
|
}
|
|
2433
2668
|
async addAttachmentToDraft(account, draftId, name, contentBytes, contentType) {
|
|
2434
|
-
return
|
|
2669
|
+
return addAttachmentToDraft3(
|
|
2435
2670
|
this.clients,
|
|
2436
2671
|
account,
|
|
2437
2672
|
draftId,
|
|
@@ -2440,22 +2675,25 @@ var GmailProvider = class {
|
|
|
2440
2675
|
contentType
|
|
2441
2676
|
);
|
|
2442
2677
|
}
|
|
2678
|
+
async removeAttachmentFromDraft(account, draftId, attachmentId) {
|
|
2679
|
+
return removeAttachmentFromDraft3(this.clients, account, draftId, attachmentId);
|
|
2680
|
+
}
|
|
2443
2681
|
// ── organize ──
|
|
2444
2682
|
async markRead(account, id, isRead) {
|
|
2445
|
-
return
|
|
2683
|
+
return markRead3(this.clients, account, id, isRead);
|
|
2446
2684
|
}
|
|
2447
2685
|
// ── folders ──
|
|
2448
2686
|
async listFolders(account, opts) {
|
|
2449
|
-
return
|
|
2687
|
+
return listFolders3(this.clients, account, opts);
|
|
2450
2688
|
}
|
|
2451
2689
|
async createFolder(account, input) {
|
|
2452
|
-
return
|
|
2690
|
+
return createFolder3(this.clients, account, input);
|
|
2453
2691
|
}
|
|
2454
2692
|
async renameFolder(account, folderId, newName) {
|
|
2455
|
-
return
|
|
2693
|
+
return renameFolder3(this.clients, account, folderId, newName);
|
|
2456
2694
|
}
|
|
2457
2695
|
async deleteFolder(account, folderId) {
|
|
2458
|
-
return
|
|
2696
|
+
return deleteFolder3(this.clients, account, folderId);
|
|
2459
2697
|
}
|
|
2460
2698
|
};
|
|
2461
2699
|
|
|
@@ -2602,6 +2840,31 @@ function buildStyleAttr(style) {
|
|
|
2602
2840
|
if (style.fontColor) parts.push(`color: ${style.fontColor}`);
|
|
2603
2841
|
return parts.join("; ");
|
|
2604
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
|
+
}
|
|
2605
2868
|
function shouldRegister(name, tools) {
|
|
2606
2869
|
if (tools.enabledTools) {
|
|
2607
2870
|
return tools.enabledTools.has(name);
|
|
@@ -3306,6 +3569,31 @@ import { readFileSync } from "fs";
|
|
|
3306
3569
|
import { basename, extname } from "path";
|
|
3307
3570
|
function registerComposeTools(server, ctx) {
|
|
3308
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
|
+
};
|
|
3309
3597
|
const sendEmailSchema = z6.object({
|
|
3310
3598
|
account: z6.string().email(),
|
|
3311
3599
|
to: z6.array(emailAddrSchema).min(1),
|
|
@@ -3327,6 +3615,16 @@ function registerComposeTools(server, ctx) {
|
|
|
3327
3615
|
),
|
|
3328
3616
|
forwardMessageId: z6.string().optional().describe(
|
|
3329
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."
|
|
3330
3628
|
)
|
|
3331
3629
|
});
|
|
3332
3630
|
async function handleSendOrDraft(args, action, resultKey, toolName) {
|
|
@@ -3349,6 +3647,18 @@ function registerComposeTools(server, ctx) {
|
|
|
3349
3647
|
"inReplyTo and forwardMessageId are mutually exclusive \u2014 use one or the other"
|
|
3350
3648
|
);
|
|
3351
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
|
+
}
|
|
3352
3662
|
const res = await action(provider, account, {
|
|
3353
3663
|
to: args.to,
|
|
3354
3664
|
cc: args.cc,
|
|
@@ -3358,7 +3668,8 @@ function registerComposeTools(server, ctx) {
|
|
|
3358
3668
|
isHtml: composed.isHtml,
|
|
3359
3669
|
inReplyTo: args.inReplyTo,
|
|
3360
3670
|
replyAll: args.replyAll,
|
|
3361
|
-
forwardMessageId: args.forwardMessageId
|
|
3671
|
+
forwardMessageId: args.forwardMessageId,
|
|
3672
|
+
attachments: processedAttachments
|
|
3362
3673
|
});
|
|
3363
3674
|
const result = { [resultKey]: true, ...res };
|
|
3364
3675
|
if (toolName === "draft_email" && res.id) {
|
|
@@ -3424,6 +3735,19 @@ function registerComposeTools(server, ctx) {
|
|
|
3424
3735
|
),
|
|
3425
3736
|
include_signature: z6.boolean().optional().describe(
|
|
3426
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."
|
|
3427
3751
|
)
|
|
3428
3752
|
});
|
|
3429
3753
|
const editDraftOutputSchema = {
|
|
@@ -3462,13 +3786,12 @@ function registerComposeTools(server, ctx) {
|
|
|
3462
3786
|
isHtmlPayload = composed.isHtml;
|
|
3463
3787
|
}
|
|
3464
3788
|
if (bodyPayload !== void 0) {
|
|
3465
|
-
const spacer = '<div style="line-height:12px"><br></div>';
|
|
3466
3789
|
try {
|
|
3467
3790
|
const existing = await provider.readEmail(account, a.id);
|
|
3468
3791
|
const existingHtml = existing.bodyHtml ?? "";
|
|
3469
|
-
const
|
|
3470
|
-
if (
|
|
3471
|
-
bodyPayload = bodyPayload + existingHtml.slice(
|
|
3792
|
+
const boundary = findThreadBoundary(existingHtml);
|
|
3793
|
+
if (boundary !== null) {
|
|
3794
|
+
bodyPayload = bodyPayload + existingHtml.slice(boundary.answerEnd);
|
|
3472
3795
|
}
|
|
3473
3796
|
} catch {
|
|
3474
3797
|
}
|
|
@@ -3481,11 +3804,38 @@ function registerComposeTools(server, ctx) {
|
|
|
3481
3804
|
body: bodyPayload,
|
|
3482
3805
|
isHtml: isHtmlPayload
|
|
3483
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
|
+
}
|
|
3484
3834
|
const draft = await provider.readEmail(account, res.id);
|
|
3485
3835
|
const result = {
|
|
3486
3836
|
edited: true,
|
|
3487
3837
|
id: res.id,
|
|
3488
|
-
draftHtml: draft.bodyHtml
|
|
3838
|
+
draftHtml: draft.bodyHtml ?? ""
|
|
3489
3839
|
};
|
|
3490
3840
|
return ok(result, result);
|
|
3491
3841
|
} catch (err) {
|
|
@@ -3521,99 +3871,6 @@ function registerComposeTools(server, ctx) {
|
|
|
3521
3871
|
}
|
|
3522
3872
|
);
|
|
3523
3873
|
}
|
|
3524
|
-
const MIME_TYPES = {
|
|
3525
|
-
".pdf": "application/pdf",
|
|
3526
|
-
".png": "image/png",
|
|
3527
|
-
".jpg": "image/jpeg",
|
|
3528
|
-
".jpeg": "image/jpeg",
|
|
3529
|
-
".gif": "image/gif",
|
|
3530
|
-
".svg": "image/svg+xml",
|
|
3531
|
-
".webp": "image/webp",
|
|
3532
|
-
".txt": "text/plain",
|
|
3533
|
-
".html": "text/html",
|
|
3534
|
-
".css": "text/css",
|
|
3535
|
-
".csv": "text/csv",
|
|
3536
|
-
".json": "application/json",
|
|
3537
|
-
".xml": "application/xml",
|
|
3538
|
-
".zip": "application/zip",
|
|
3539
|
-
".gz": "application/gzip",
|
|
3540
|
-
".tar": "application/x-tar",
|
|
3541
|
-
".doc": "application/msword",
|
|
3542
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
3543
|
-
".xls": "application/vnd.ms-excel",
|
|
3544
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
3545
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
3546
|
-
".mp3": "audio/mpeg",
|
|
3547
|
-
".mp4": "video/mp4"
|
|
3548
|
-
};
|
|
3549
|
-
const addAttachmentOutputSchema = {
|
|
3550
|
-
attached: z6.literal(true),
|
|
3551
|
-
id: z6.string(),
|
|
3552
|
-
attachment: z6.object({
|
|
3553
|
-
id: z6.string(),
|
|
3554
|
-
name: z6.string(),
|
|
3555
|
-
contentType: z6.string().optional()
|
|
3556
|
-
})
|
|
3557
|
-
};
|
|
3558
|
-
if (shouldRegister("add_attachment_to_draft", tools)) {
|
|
3559
|
-
server.registerTool(
|
|
3560
|
-
"add_attachment_to_draft",
|
|
3561
|
-
{
|
|
3562
|
-
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.",
|
|
3563
|
-
inputSchema: z6.object({
|
|
3564
|
-
account: z6.string().email(),
|
|
3565
|
-
id: z6.string().min(1).describe("Draft message ID"),
|
|
3566
|
-
name: z6.string().min(1).optional().describe(
|
|
3567
|
-
"Attachment filename (e.g. 'report.pdf'). Defaults to the file's basename when `filePath` is used."
|
|
3568
|
-
),
|
|
3569
|
-
contentBytes: z6.string().min(1).optional().describe("Base64-encoded file content. Mutually exclusive with `filePath`."),
|
|
3570
|
-
filePath: z6.string().min(1).optional().describe(
|
|
3571
|
-
"Absolute path to a local file. The file is read and base64-encoded automatically. Mutually exclusive with `contentBytes`."
|
|
3572
|
-
),
|
|
3573
|
-
contentType: z6.string().optional().describe("MIME type (e.g. 'application/pdf'). Inferred from `filePath` extension if omitted.")
|
|
3574
|
-
}).refine(
|
|
3575
|
-
(v) => Boolean(v.contentBytes) !== Boolean(v.filePath),
|
|
3576
|
-
{ message: "Provide exactly one of `contentBytes` or `filePath`." }
|
|
3577
|
-
),
|
|
3578
|
-
outputSchema: addAttachmentOutputSchema
|
|
3579
|
-
},
|
|
3580
|
-
async (args) => {
|
|
3581
|
-
try {
|
|
3582
|
-
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3583
|
-
let contentBytes;
|
|
3584
|
-
let name;
|
|
3585
|
-
let contentType = args.contentType;
|
|
3586
|
-
if (args.filePath) {
|
|
3587
|
-
const fileData = readFileSync(args.filePath);
|
|
3588
|
-
contentBytes = fileData.toString("base64");
|
|
3589
|
-
name = args.name ?? basename(args.filePath);
|
|
3590
|
-
if (!contentType) {
|
|
3591
|
-
const ext = extname(args.filePath).toLowerCase();
|
|
3592
|
-
contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
3593
|
-
}
|
|
3594
|
-
} else {
|
|
3595
|
-
contentBytes = args.contentBytes;
|
|
3596
|
-
name = args.name;
|
|
3597
|
-
}
|
|
3598
|
-
const res = await provider.addAttachmentToDraft(
|
|
3599
|
-
account,
|
|
3600
|
-
args.id,
|
|
3601
|
-
name,
|
|
3602
|
-
contentBytes,
|
|
3603
|
-
contentType
|
|
3604
|
-
);
|
|
3605
|
-
const data = {
|
|
3606
|
-
attached: true,
|
|
3607
|
-
id: res.id,
|
|
3608
|
-
attachment: res.attachment
|
|
3609
|
-
};
|
|
3610
|
-
return ok(data, data);
|
|
3611
|
-
} catch (err) {
|
|
3612
|
-
return fail(errMsg(err));
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
);
|
|
3616
|
-
}
|
|
3617
3874
|
}
|
|
3618
3875
|
|
|
3619
3876
|
// src/tools/index.ts
|
|
@@ -3629,7 +3886,7 @@ function registerTools(server, opts) {
|
|
|
3629
3886
|
// package.json
|
|
3630
3887
|
var package_default = {
|
|
3631
3888
|
name: "hypermail-mcp",
|
|
3632
|
-
version: "0.7.
|
|
3889
|
+
version: "0.7.5",
|
|
3633
3890
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
3634
3891
|
type: "module",
|
|
3635
3892
|
bin: {
|
|
@@ -3737,6 +3994,76 @@ function sleep(ms) {
|
|
|
3737
3994
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3738
3995
|
}
|
|
3739
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
|
+
|
|
3740
4067
|
// src/watcher/manager.ts
|
|
3741
4068
|
var WatcherManager = class {
|
|
3742
4069
|
constructor(store, registry, config) {
|
|
@@ -3807,78 +4134,20 @@ var WatcherManager = class {
|
|
|
3807
4134
|
}
|
|
3808
4135
|
async emit(full) {
|
|
3809
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
|
+
});
|
|
3810
4143
|
}
|
|
3811
4144
|
};
|
|
3812
4145
|
|
|
3813
4146
|
// src/config.ts
|
|
3814
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
3815
4147
|
import { z as z7 } from "zod";
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
host: z7.string().default("127.0.0.1")
|
|
3820
|
-
});
|
|
3821
|
-
var toolsConfigSchema = z7.object({
|
|
3822
|
-
disabled: z7.array(z7.string()).optional(),
|
|
3823
|
-
enabled: z7.array(z7.string()).optional()
|
|
3824
|
-
});
|
|
3825
|
-
var outlookProviderSchema = z7.object({
|
|
3826
|
-
clientId: z7.string().optional(),
|
|
3827
|
-
tenantId: z7.string().optional()
|
|
3828
|
-
});
|
|
3829
|
-
var gmailProviderSchema = z7.object({
|
|
3830
|
-
clientId: z7.string().optional(),
|
|
3831
|
-
clientSecret: z7.string().optional()
|
|
3832
|
-
});
|
|
3833
|
-
var providersConfigSchema = z7.object({
|
|
3834
|
-
outlook: outlookProviderSchema.optional(),
|
|
3835
|
-
gmail: gmailProviderSchema.optional()
|
|
3836
|
-
});
|
|
3837
|
-
var watchConfigSchema = z7.object({
|
|
3838
|
-
enabled: z7.boolean().default(false),
|
|
3839
|
-
pollIntervalSeconds: z7.number().int().min(10).max(3600).default(10),
|
|
3840
|
-
webhook: z7.object({
|
|
3841
|
-
url: z7.string(),
|
|
3842
|
-
retry: z7.object({
|
|
3843
|
-
maxAttempts: z7.number().int().min(1).max(10).default(5),
|
|
3844
|
-
baseDelayMs: z7.number().int().min(100).default(1e3)
|
|
3845
|
-
}).optional()
|
|
3846
|
-
}).optional()
|
|
3847
|
-
});
|
|
3848
|
-
var rawConfigSchema = z7.object({
|
|
3849
|
-
dataDir: z7.string().optional(),
|
|
3850
|
-
http: httpConfigSchema.optional(),
|
|
3851
|
-
tools: toolsConfigSchema.optional(),
|
|
3852
|
-
providers: providersConfigSchema.optional(),
|
|
3853
|
-
watch: watchConfigSchema.optional()
|
|
3854
|
-
});
|
|
3855
|
-
var KNOWN_TOOLS = [
|
|
3856
|
-
"list_accounts",
|
|
3857
|
-
"add_account",
|
|
3858
|
-
"complete_add_account",
|
|
3859
|
-
"get_account_settings",
|
|
3860
|
-
"set_account_settings",
|
|
3861
|
-
"remove_account",
|
|
3862
|
-
"list_emails",
|
|
3863
|
-
"search_emails",
|
|
3864
|
-
"read_email",
|
|
3865
|
-
"read_attachment",
|
|
3866
|
-
"archive_email",
|
|
3867
|
-
"trash_email",
|
|
3868
|
-
"move_email",
|
|
3869
|
-
"mark_read",
|
|
3870
|
-
"mark_unread",
|
|
3871
|
-
"list_folders",
|
|
3872
|
-
"create_folder",
|
|
3873
|
-
"delete_folder",
|
|
3874
|
-
"rename_folder",
|
|
3875
|
-
"send_email",
|
|
3876
|
-
"draft_email",
|
|
3877
|
-
"edit_draft",
|
|
3878
|
-
"send_draft",
|
|
3879
|
-
"add_attachment_to_draft",
|
|
3880
|
-
"check_notifications"
|
|
3881
|
-
];
|
|
4148
|
+
|
|
4149
|
+
// src/config/load.ts
|
|
4150
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3882
4151
|
var ENV_HTTP_ENABLED = "HYPERMAIL_HTTP_ENABLED";
|
|
3883
4152
|
var ENV_HTTP_PORT = "HYPERMAIL_HTTP_PORT";
|
|
3884
4153
|
var ENV_HTTP_HOST = "HYPERMAIL_HTTP_HOST";
|
|
@@ -3893,6 +4162,10 @@ var ENV_WATCH_POLL_INTERVAL = "HYPERMAIL_WATCH_POLL_INTERVAL";
|
|
|
3893
4162
|
var ENV_WATCH_WEBHOOK_URL = "HYPERMAIL_WATCH_WEBHOOK_URL";
|
|
3894
4163
|
var ENV_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS";
|
|
3895
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";
|
|
3896
4169
|
var LEGACY_MS_CLIENT_ID = "MS_CLIENT_ID";
|
|
3897
4170
|
var LEGACY_MS_TENANT_ID = "MS_TENANT_ID";
|
|
3898
4171
|
var LEGACY_GOOGLE_CLIENT_ID = "GOOGLE_CLIENT_ID";
|
|
@@ -4009,10 +4282,26 @@ function loadConfig(configPath, cliOverrides = {}) {
|
|
|
4009
4282
|
retry: { maxAttempts: retryMaxAttempts, baseDelayMs: retryBaseDelayMs }
|
|
4010
4283
|
};
|
|
4011
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
|
+
}
|
|
4012
4300
|
watch = {
|
|
4013
4301
|
enabled: watchEnabledEnv ?? parsed.watch?.enabled ?? false,
|
|
4014
4302
|
pollIntervalSeconds: parsed.watch?.pollIntervalSeconds ?? parseIntSafe(process.env[ENV_WATCH_POLL_INTERVAL]) ?? 10,
|
|
4015
|
-
webhook
|
|
4303
|
+
webhook,
|
|
4304
|
+
script
|
|
4016
4305
|
};
|
|
4017
4306
|
}
|
|
4018
4307
|
return {
|
|
@@ -4023,6 +4312,81 @@ function loadConfig(configPath, cliOverrides = {}) {
|
|
|
4023
4312
|
watch
|
|
4024
4313
|
};
|
|
4025
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
|
+
];
|
|
4026
4390
|
function resolveTools(config) {
|
|
4027
4391
|
if (!config.tools) {
|
|
4028
4392
|
return { enabledTools: null, disabledTools: null };
|