@marlinjai/email-mcp 1.10.0 → 1.11.0
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 +7 -1
- package/dist/index.js +269 -52
- package/dist/index.js.map +4 -4
- package/dist/setup/wizard.js +143 -20
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/dist/setup/wizard.js
CHANGED
|
@@ -922,6 +922,17 @@ function normalizeBody(body) {
|
|
|
922
922
|
function toAddress(contact) {
|
|
923
923
|
return contact.name ? { name: contact.name, address: contact.email } : contact.email;
|
|
924
924
|
}
|
|
925
|
+
var PROVIDER_LABELS = /* @__PURE__ */ new Set(["gmail", "outlook", "icloud", "imap"]);
|
|
926
|
+
function fromAddress(email, displayName, accountName) {
|
|
927
|
+
const own = displayName?.trim();
|
|
928
|
+
if (own) return { name: own, address: email };
|
|
929
|
+
const fallback = accountName?.trim();
|
|
930
|
+
const label = (name) => PROVIDER_LABELS.has(name) || name === email.toLowerCase() || name === email.toLowerCase().split("@")[0];
|
|
931
|
+
if (fallback && !label(fallback.toLowerCase())) {
|
|
932
|
+
return { name: fallback, address: email };
|
|
933
|
+
}
|
|
934
|
+
return email;
|
|
935
|
+
}
|
|
925
936
|
function toAddressList(contacts) {
|
|
926
937
|
return contacts?.length ? contacts.map(toAddress) : void 0;
|
|
927
938
|
}
|
|
@@ -954,12 +965,14 @@ var GmailAdapter = class {
|
|
|
954
965
|
gmail = null;
|
|
955
966
|
accountId = "";
|
|
956
967
|
email = "";
|
|
968
|
+
accountName = "";
|
|
957
969
|
async connect(credentials) {
|
|
958
970
|
if (!credentials.oauth) {
|
|
959
971
|
throw new Error("Gmail adapter requires OAuth credentials");
|
|
960
972
|
}
|
|
961
973
|
this.accountId = credentials.id;
|
|
962
974
|
this.email = credentials.email;
|
|
975
|
+
this.accountName = credentials.name;
|
|
963
976
|
const auth = new OAuth2Client2();
|
|
964
977
|
auth.setCredentials({
|
|
965
978
|
access_token: credentials.oauth.access_token,
|
|
@@ -1112,27 +1125,39 @@ var GmailAdapter = class {
|
|
|
1112
1125
|
return { id: res.data.id || "" };
|
|
1113
1126
|
}
|
|
1114
1127
|
/**
|
|
1115
|
-
* drafts.update replaces the whole message, so
|
|
1116
|
-
*
|
|
1117
|
-
*
|
|
1128
|
+
* drafts.update replaces the whole message, so what the caller does not
|
|
1129
|
+
* name would be lost: a reply draft would fall out of its thread and a
|
|
1130
|
+
* draft's files would be dropped. So when the caller names no thread, the
|
|
1131
|
+
* thread and the reply headers of the stored draft are carried over, and
|
|
1132
|
+
* when the caller names no attachments, the stored draft's files are. A
|
|
1133
|
+
* given list (an empty one included) replaces the files.
|
|
1118
1134
|
*/
|
|
1119
1135
|
async updateDraft(draftId, update) {
|
|
1120
1136
|
const gmail = this.ensureConnected();
|
|
1121
1137
|
let params = update;
|
|
1122
|
-
|
|
1138
|
+
const keepThread = update.threadId === void 0;
|
|
1139
|
+
const keepFiles = update.attachments === void 0;
|
|
1140
|
+
if (keepThread || keepFiles) {
|
|
1123
1141
|
const existing = await gmail.users.drafts.get({
|
|
1124
1142
|
userId: "me",
|
|
1125
1143
|
id: draftId,
|
|
1126
|
-
//
|
|
1127
|
-
// single headers
|
|
1128
|
-
|
|
1144
|
+
// 'metadata' is ids, labels and headers, no body (drafts.get has no
|
|
1145
|
+
// way to ask for single headers, so all of them come back). The parts
|
|
1146
|
+
// that list the files only come with 'full'.
|
|
1147
|
+
format: keepFiles ? "full" : "metadata"
|
|
1129
1148
|
});
|
|
1130
1149
|
const message = existing.data.message;
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1150
|
+
if (keepThread) {
|
|
1151
|
+
const header = (name) => (message?.payload?.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || void 0;
|
|
1152
|
+
const inReplyTo = update.inReplyTo ?? header("in-reply-to");
|
|
1153
|
+
const references = update.references ?? header("references")?.split(/\s+/).filter(Boolean);
|
|
1154
|
+
if (message?.threadId && inReplyTo) {
|
|
1155
|
+
params = { ...params, threadId: message.threadId, inReplyTo, references: references?.length ? references : [inReplyTo] };
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (keepFiles && message?.id) {
|
|
1159
|
+
const stored = await this.storedAttachments(message.id, message.payload);
|
|
1160
|
+
if (stored.length) params = { ...params, attachments: stored };
|
|
1136
1161
|
}
|
|
1137
1162
|
}
|
|
1138
1163
|
const raw = await this.buildRaw(params);
|
|
@@ -1145,6 +1170,43 @@ var GmailAdapter = class {
|
|
|
1145
1170
|
});
|
|
1146
1171
|
return { id: res.data.id || draftId };
|
|
1147
1172
|
}
|
|
1173
|
+
/**
|
|
1174
|
+
* The signature Gmail's own editor would add: the one of the send-as
|
|
1175
|
+
* address the From header names (`this.email`, see buildRaw), or the
|
|
1176
|
+
* default send-as when that address is not listed. Read on every call, so
|
|
1177
|
+
* a signature changed in Gmail's settings applies to the next mail.
|
|
1178
|
+
*/
|
|
1179
|
+
async getSignature() {
|
|
1180
|
+
const entries = await this.sendAsList();
|
|
1181
|
+
const entry = this.ownSendAs(entries) || entries.find((e) => e.isDefault);
|
|
1182
|
+
if (!entry) throw new Error(`Gmail lists no send-as address for ${this.email}`);
|
|
1183
|
+
const html = (entry.signature || "").trim();
|
|
1184
|
+
return { sendAs: entry.sendAsEmail || this.email, html: html || null };
|
|
1185
|
+
}
|
|
1186
|
+
async sendAsList() {
|
|
1187
|
+
const gmail = this.ensureConnected();
|
|
1188
|
+
const res = await gmail.users.settings.sendAs.list({ userId: "me" });
|
|
1189
|
+
return res.data.sendAs || [];
|
|
1190
|
+
}
|
|
1191
|
+
ownSendAs(entries) {
|
|
1192
|
+
const own = this.email.toLowerCase();
|
|
1193
|
+
return entries.find((e) => (e.sendAsEmail || "").toLowerCase() === own);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* The From header: the account's address with the display name Gmail
|
|
1197
|
+
* keeps for it as a send-as address. When the settings cannot be read the
|
|
1198
|
+
* mail still goes out, named after the account (see `fromAddress`); the
|
|
1199
|
+
* signature outcome in the tool result already reports that failure.
|
|
1200
|
+
*/
|
|
1201
|
+
async from() {
|
|
1202
|
+
let displayName;
|
|
1203
|
+
try {
|
|
1204
|
+
displayName = this.ownSendAs(await this.sendAsList())?.displayName || void 0;
|
|
1205
|
+
} catch {
|
|
1206
|
+
displayName = void 0;
|
|
1207
|
+
}
|
|
1208
|
+
return fromAddress(this.email, displayName, this.accountName);
|
|
1209
|
+
}
|
|
1148
1210
|
async listDrafts(limit, _offset) {
|
|
1149
1211
|
const gmail = this.ensureConnected();
|
|
1150
1212
|
const res = await gmail.users.drafts.list({
|
|
@@ -1529,6 +1591,41 @@ var GmailAdapter = class {
|
|
|
1529
1591
|
const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
|
|
1530
1592
|
return belongs ? { threadId: params.threadId } : {};
|
|
1531
1593
|
}
|
|
1594
|
+
/**
|
|
1595
|
+
* The files of a stored message, with their bytes: every part that has a
|
|
1596
|
+
* file name, at any depth. Large files are referenced by an attachment id
|
|
1597
|
+
* and fetched; small ones can come inline in the part. A fetch that fails
|
|
1598
|
+
* is not swallowed: a draft update built on it would drop that file.
|
|
1599
|
+
*/
|
|
1600
|
+
async storedAttachments(messageId, payload) {
|
|
1601
|
+
const gmail = this.ensureConnected();
|
|
1602
|
+
const files = [];
|
|
1603
|
+
const html = (extractBody(payload).html || "").toLowerCase();
|
|
1604
|
+
const visit = async (part) => {
|
|
1605
|
+
if (!part) return;
|
|
1606
|
+
const partHeader = (name) => (part.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || "";
|
|
1607
|
+
const contentId = partHeader("content-id").trim().replace(/^<|>$/g, "").toLowerCase();
|
|
1608
|
+
const embedded = /^\s*inline/i.test(partHeader("content-disposition")) && contentId !== "" && html.includes(`cid:${contentId}`);
|
|
1609
|
+
if (part.filename && !embedded) {
|
|
1610
|
+
let data = part.body?.data;
|
|
1611
|
+
if (!data && part.body?.attachmentId) {
|
|
1612
|
+
const res = await gmail.users.messages.attachments.get({ userId: "me", messageId, id: part.body.attachmentId });
|
|
1613
|
+
data = res.data.data;
|
|
1614
|
+
}
|
|
1615
|
+
if (data === void 0 || data === null) {
|
|
1616
|
+
throw new Error(`Could not read the draft's attachment ${part.filename}; the draft was not changed.`);
|
|
1617
|
+
}
|
|
1618
|
+
files.push({
|
|
1619
|
+
filename: part.filename,
|
|
1620
|
+
content: Buffer.from(data, "base64url"),
|
|
1621
|
+
contentType: part.mimeType || "application/octet-stream"
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
for (const child of part.parts || []) await visit(child);
|
|
1625
|
+
};
|
|
1626
|
+
await visit(payload);
|
|
1627
|
+
return files;
|
|
1628
|
+
}
|
|
1532
1629
|
/**
|
|
1533
1630
|
* Builds the base64url `raw` payload Gmail expects for send, draft create and
|
|
1534
1631
|
* draft update: a full MIME message with a text part, an HTML part when one
|
|
@@ -1536,7 +1633,7 @@ var GmailAdapter = class {
|
|
|
1536
1633
|
* and any attachments. See `buildMimeMessage`.
|
|
1537
1634
|
*/
|
|
1538
1635
|
async buildRaw(params) {
|
|
1539
|
-
const message = await buildMimeMessage(this.
|
|
1636
|
+
const message = await buildMimeMessage(await this.from(), params);
|
|
1540
1637
|
return message.toString("base64url");
|
|
1541
1638
|
}
|
|
1542
1639
|
};
|
|
@@ -2451,6 +2548,7 @@ var ImapAdapter = class {
|
|
|
2451
2548
|
client = null;
|
|
2452
2549
|
accountId = "";
|
|
2453
2550
|
email = "";
|
|
2551
|
+
accountName = "";
|
|
2454
2552
|
passwordCreds = null;
|
|
2455
2553
|
connectingPromise = null;
|
|
2456
2554
|
async connect(credentials) {
|
|
@@ -2459,6 +2557,7 @@ var ImapAdapter = class {
|
|
|
2459
2557
|
}
|
|
2460
2558
|
this.accountId = credentials.id;
|
|
2461
2559
|
this.email = credentials.email;
|
|
2560
|
+
this.accountName = credentials.name;
|
|
2462
2561
|
this.passwordCreds = credentials.password;
|
|
2463
2562
|
await this.ensureConnected();
|
|
2464
2563
|
}
|
|
@@ -2891,7 +2990,7 @@ var ImapAdapter = class {
|
|
|
2891
2990
|
async sendEmail(params) {
|
|
2892
2991
|
if (!this.passwordCreds) throw new Error("No credentials available");
|
|
2893
2992
|
const transport = createSmtpTransport(this.email, this.passwordCreds);
|
|
2894
|
-
const messageId = await sendViaSmtp(transport, this.
|
|
2993
|
+
const messageId = await sendViaSmtp(transport, this.from(), params);
|
|
2895
2994
|
return { id: messageId };
|
|
2896
2995
|
}
|
|
2897
2996
|
/**
|
|
@@ -2900,7 +2999,12 @@ var ImapAdapter = class {
|
|
|
2900
2999
|
* attachments. See `buildMimeMessage`.
|
|
2901
3000
|
*/
|
|
2902
3001
|
buildDraftMessage(params) {
|
|
2903
|
-
return buildMimeMessage(this.
|
|
3002
|
+
return buildMimeMessage(this.from(), params);
|
|
3003
|
+
}
|
|
3004
|
+
/** IMAP keeps no display name on the server, so the account's name is the
|
|
3005
|
+
* only one there is (see `fromAddress`). */
|
|
3006
|
+
from() {
|
|
3007
|
+
return fromAddress(this.email, void 0, this.accountName);
|
|
2904
3008
|
}
|
|
2905
3009
|
async createDraft(params) {
|
|
2906
3010
|
const client = await this.ensureConnected();
|
|
@@ -2918,7 +3022,11 @@ var ImapAdapter = class {
|
|
|
2918
3022
|
*
|
|
2919
3023
|
* A reply draft keeps its place in the thread: when the caller passes no
|
|
2920
3024
|
* `inReplyTo`, the In-Reply-To and References headers of the old revision
|
|
2921
|
-
* are copied into the new one.
|
|
3025
|
+
* are copied into the new one. The same goes for files: when the caller
|
|
3026
|
+
* passes no `attachments`, the old revision's attachments are copied over
|
|
3027
|
+
* (a given list, an empty one included, replaces them). Images embedded in
|
|
3028
|
+
* the old HTML body are not files and are not copied: the new body replaces
|
|
3029
|
+
* the old one.
|
|
2922
3030
|
*/
|
|
2923
3031
|
async updateDraft(draftId, params, sourceFolder) {
|
|
2924
3032
|
const client = await this.ensureConnected();
|
|
@@ -2932,13 +3040,28 @@ var ImapAdapter = class {
|
|
|
2932
3040
|
let rawMessage;
|
|
2933
3041
|
try {
|
|
2934
3042
|
let next = params;
|
|
2935
|
-
|
|
3043
|
+
const keepThread = params.inReplyTo === void 0;
|
|
3044
|
+
const keepFiles = params.attachments === void 0;
|
|
3045
|
+
if (keepThread || keepFiles) {
|
|
2936
3046
|
const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
|
|
2937
3047
|
if (old && old.source) {
|
|
2938
3048
|
const parsed = await simpleParser(old.source);
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
next = { ...
|
|
3049
|
+
if (keepThread && parsed.inReplyTo) {
|
|
3050
|
+
const references = [].concat(parsed.references ?? []);
|
|
3051
|
+
next = { ...next, inReplyTo: parsed.inReplyTo, references: params.references ?? references };
|
|
3052
|
+
}
|
|
3053
|
+
if (keepFiles) {
|
|
3054
|
+
const html = (parsed.html || "").toLowerCase();
|
|
3055
|
+
const files = (parsed.attachments || []).filter((att) => {
|
|
3056
|
+
if (!att.related) return true;
|
|
3057
|
+
const cid = String(att.cid || att.contentId || "").replace(/^<|>$/g, "").toLowerCase();
|
|
3058
|
+
return !cid || !html.includes(`cid:${cid}`);
|
|
3059
|
+
}).map((att, i) => ({
|
|
3060
|
+
filename: att.filename || `attachment-${i + 1}`,
|
|
3061
|
+
content: att.content,
|
|
3062
|
+
contentType: att.contentType || "application/octet-stream"
|
|
3063
|
+
}));
|
|
3064
|
+
if (files.length) next = { ...next, attachments: files };
|
|
2942
3065
|
}
|
|
2943
3066
|
}
|
|
2944
3067
|
}
|