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