@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 CHANGED
@@ -141,7 +141,13 @@ npx -y -p @marlinjai/email-mcp@latest email-mcp-setup
141
141
 
142
142
  ### Sending & Drafts (6)
143
143
 
144
- `email_send`, `email_reply`, `email_forward`, `email_draft_create` and `email_draft_update` take an optional `attachments` list. Each entry is either `{ path }` (a file in the attachments folder, see below) or `{ content, filename }` (base64), with an optional `contentType` (inferred from the extension otherwise). The total is capped at 25 MB; Outlook accepts up to 3 MB per message through this server. On `email_draft_update` a list replaces the draft's files and an empty list removes them.
144
+ `email_send`, `email_reply`, `email_forward`, `email_draft_create` and `email_draft_update` take an optional `attachments` list. Each entry is either `{ path }` (a file in the attachments folder, see below) or `{ content, filename }` (base64), with an optional `contentType` (inferred from the extension otherwise). The total is capped at 25 MB; Outlook accepts up to 3 MB per message through this server. On `email_draft_update` a list replaces the draft's files, an empty list removes them, and no list keeps the files the draft has.
145
+
146
+ **Gmail adds your signature, like Gmail's own editor.** The Gmail API never appends one, so `email_send`, `email_reply`, `email_forward`, `email_draft_create` and `email_draft_update` read the signature of the send-as address the mail goes out from (Gmail settings, read on every call, so a changed signature applies at once) and add it in Gmail's own markup (`gmail_signature`), above any quoted history, plus a plain-text rendering in the text part. A text-only body gains an HTML part for it, as the signature's logo and links only exist as HTML. A body that already holds a signature (a `gmail_signature` block, or a `-- ` line in text) is left alone, so passing a draft's body back to `email_draft_update` never stacks a second one. `signature: false` turns it off for one call. Each of these tools returns a `signature` field saying what happened: `added`, `already_present`, `disabled`, `none_configured`, `unsupported`, or `skipped` with the reason (for example a missing permission); a failed read never blocks the mail or draft.
147
+
148
+ **Outlook, iCloud and IMAP add no signature.** Microsoft Graph has no signature API for Outlook.com accounts (signatures live in the Outlook clients, out of reach for a server), and an IMAP or iCloud server keeps no signature at all. The tools report `unsupported` there instead of inventing one; put the signature in the body when the mail needs it. A signature stored in email-mcp's own account settings was considered and left out on purpose: it would be a second copy that drifts from the one in the mail client, which is exactly how old mail ends up with an outdated signature.
149
+
150
+ **From names you.** The From header carries a display name, not just the address: on Gmail the send-as display name from Gmail's settings, otherwise the account's name in email-mcp (unless that is only a label: a provider name such as "Gmail" or "iCloud", or the address's local part such as "founder"). Gmail often keeps no display name for an account's primary address (its web editor then uses the Google profile name, which this server cannot read); set one under Settings, Accounts, "Send mail as", edit info, and the tools use it. This also holds for a draft sent later from another mail program, which keeps the draft's From. Outlook sets From itself.
145
151
 
146
152
  Replies stay in their thread: `email_reply` sends inside the original conversation on Gmail and Outlook (and sets the `In-Reply-To` and `References` headers everywhere), `email_draft_create` with `inReplyToEmailId` saves a reply draft in that thread, and `email_draft_update` keeps a reply draft there.
147
153
 
package/dist/index.js CHANGED
@@ -7181,6 +7181,16 @@ function normalizeBody(body) {
7181
7181
  function toAddress(contact) {
7182
7182
  return contact.name ? { name: contact.name, address: contact.email } : contact.email;
7183
7183
  }
7184
+ function fromAddress(email3, displayName, accountName) {
7185
+ const own = displayName?.trim();
7186
+ if (own) return { name: own, address: email3 };
7187
+ const fallback = accountName?.trim();
7188
+ const label = (name) => PROVIDER_LABELS.has(name) || name === email3.toLowerCase() || name === email3.toLowerCase().split("@")[0];
7189
+ if (fallback && !label(fallback.toLowerCase())) {
7190
+ return { name: fallback, address: email3 };
7191
+ }
7192
+ return email3;
7193
+ }
7184
7194
  function toAddressList(contacts) {
7185
7195
  return contacts?.length ? contacts.map(toAddress) : void 0;
7186
7196
  }
@@ -7206,9 +7216,11 @@ async function buildMimeMessage(from, params) {
7206
7216
  node.keepBcc = true;
7207
7217
  return node.build();
7208
7218
  }
7219
+ var PROVIDER_LABELS;
7209
7220
  var init_mime = __esm({
7210
7221
  "src/providers/mime.ts"() {
7211
7222
  "use strict";
7223
+ PROVIDER_LABELS = /* @__PURE__ */ new Set(["gmail", "outlook", "icloud", "imap"]);
7212
7224
  }
7213
7225
  });
7214
7226
 
@@ -7228,12 +7240,14 @@ var init_adapter = __esm({
7228
7240
  gmail = null;
7229
7241
  accountId = "";
7230
7242
  email = "";
7243
+ accountName = "";
7231
7244
  async connect(credentials) {
7232
7245
  if (!credentials.oauth) {
7233
7246
  throw new Error("Gmail adapter requires OAuth credentials");
7234
7247
  }
7235
7248
  this.accountId = credentials.id;
7236
7249
  this.email = credentials.email;
7250
+ this.accountName = credentials.name;
7237
7251
  const auth = new OAuth2Client();
7238
7252
  auth.setCredentials({
7239
7253
  access_token: credentials.oauth.access_token,
@@ -7386,27 +7400,39 @@ var init_adapter = __esm({
7386
7400
  return { id: res.data.id || "" };
7387
7401
  }
7388
7402
  /**
7389
- * drafts.update replaces the whole message, so a reply draft would fall out
7390
- * of its thread. When the caller names no thread, the thread and the reply
7391
- * headers of the stored draft are carried over into the new message.
7403
+ * drafts.update replaces the whole message, so what the caller does not
7404
+ * name would be lost: a reply draft would fall out of its thread and a
7405
+ * draft's files would be dropped. So when the caller names no thread, the
7406
+ * thread and the reply headers of the stored draft are carried over, and
7407
+ * when the caller names no attachments, the stored draft's files are. A
7408
+ * given list (an empty one included) replaces the files.
7392
7409
  */
7393
7410
  async updateDraft(draftId, update) {
7394
7411
  const gmail = this.ensureConnected();
7395
7412
  let params = update;
7396
- if (update.threadId === void 0) {
7413
+ const keepThread = update.threadId === void 0;
7414
+ const keepFiles = update.attachments === void 0;
7415
+ if (keepThread || keepFiles) {
7397
7416
  const existing = await gmail.users.drafts.get({
7398
7417
  userId: "me",
7399
7418
  id: draftId,
7400
- // Ids, labels and headers, no body. drafts.get has no way to ask for
7401
- // single headers (messages.get does), so all of them come back.
7402
- format: "metadata"
7419
+ // 'metadata' is ids, labels and headers, no body (drafts.get has no
7420
+ // way to ask for single headers, so all of them come back). The parts
7421
+ // that list the files only come with 'full'.
7422
+ format: keepFiles ? "full" : "metadata"
7403
7423
  });
7404
7424
  const message = existing.data.message;
7405
- const header = (name) => (message?.payload?.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || void 0;
7406
- const inReplyTo = update.inReplyTo ?? header("in-reply-to");
7407
- const references = update.references ?? header("references")?.split(/\s+/).filter(Boolean);
7408
- if (message?.threadId && inReplyTo) {
7409
- params = { ...update, threadId: message.threadId, inReplyTo, references: references?.length ? references : [inReplyTo] };
7425
+ if (keepThread) {
7426
+ const header = (name) => (message?.payload?.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || void 0;
7427
+ const inReplyTo = update.inReplyTo ?? header("in-reply-to");
7428
+ const references = update.references ?? header("references")?.split(/\s+/).filter(Boolean);
7429
+ if (message?.threadId && inReplyTo) {
7430
+ params = { ...params, threadId: message.threadId, inReplyTo, references: references?.length ? references : [inReplyTo] };
7431
+ }
7432
+ }
7433
+ if (keepFiles && message?.id) {
7434
+ const stored = await this.storedAttachments(message.id, message.payload);
7435
+ if (stored.length) params = { ...params, attachments: stored };
7410
7436
  }
7411
7437
  }
7412
7438
  const raw = await this.buildRaw(params);
@@ -7419,6 +7445,43 @@ var init_adapter = __esm({
7419
7445
  });
7420
7446
  return { id: res.data.id || draftId };
7421
7447
  }
7448
+ /**
7449
+ * The signature Gmail's own editor would add: the one of the send-as
7450
+ * address the From header names (`this.email`, see buildRaw), or the
7451
+ * default send-as when that address is not listed. Read on every call, so
7452
+ * a signature changed in Gmail's settings applies to the next mail.
7453
+ */
7454
+ async getSignature() {
7455
+ const entries = await this.sendAsList();
7456
+ const entry = this.ownSendAs(entries) || entries.find((e) => e.isDefault);
7457
+ if (!entry) throw new Error(`Gmail lists no send-as address for ${this.email}`);
7458
+ const html = (entry.signature || "").trim();
7459
+ return { sendAs: entry.sendAsEmail || this.email, html: html || null };
7460
+ }
7461
+ async sendAsList() {
7462
+ const gmail = this.ensureConnected();
7463
+ const res = await gmail.users.settings.sendAs.list({ userId: "me" });
7464
+ return res.data.sendAs || [];
7465
+ }
7466
+ ownSendAs(entries) {
7467
+ const own = this.email.toLowerCase();
7468
+ return entries.find((e) => (e.sendAsEmail || "").toLowerCase() === own);
7469
+ }
7470
+ /**
7471
+ * The From header: the account's address with the display name Gmail
7472
+ * keeps for it as a send-as address. When the settings cannot be read the
7473
+ * mail still goes out, named after the account (see `fromAddress`); the
7474
+ * signature outcome in the tool result already reports that failure.
7475
+ */
7476
+ async from() {
7477
+ let displayName;
7478
+ try {
7479
+ displayName = this.ownSendAs(await this.sendAsList())?.displayName || void 0;
7480
+ } catch {
7481
+ displayName = void 0;
7482
+ }
7483
+ return fromAddress(this.email, displayName, this.accountName);
7484
+ }
7422
7485
  async listDrafts(limit, _offset) {
7423
7486
  const gmail = this.ensureConnected();
7424
7487
  const res = await gmail.users.drafts.list({
@@ -7803,6 +7866,41 @@ var init_adapter = __esm({
7803
7866
  const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
7804
7867
  return belongs ? { threadId: params.threadId } : {};
7805
7868
  }
7869
+ /**
7870
+ * The files of a stored message, with their bytes: every part that has a
7871
+ * file name, at any depth. Large files are referenced by an attachment id
7872
+ * and fetched; small ones can come inline in the part. A fetch that fails
7873
+ * is not swallowed: a draft update built on it would drop that file.
7874
+ */
7875
+ async storedAttachments(messageId, payload) {
7876
+ const gmail = this.ensureConnected();
7877
+ const files = [];
7878
+ const html = (extractBody(payload).html || "").toLowerCase();
7879
+ const visit = async (part) => {
7880
+ if (!part) return;
7881
+ const partHeader = (name) => (part.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || "";
7882
+ const contentId = partHeader("content-id").trim().replace(/^<|>$/g, "").toLowerCase();
7883
+ const embedded = /^\s*inline/i.test(partHeader("content-disposition")) && contentId !== "" && html.includes(`cid:${contentId}`);
7884
+ if (part.filename && !embedded) {
7885
+ let data = part.body?.data;
7886
+ if (!data && part.body?.attachmentId) {
7887
+ const res = await gmail.users.messages.attachments.get({ userId: "me", messageId, id: part.body.attachmentId });
7888
+ data = res.data.data;
7889
+ }
7890
+ if (data === void 0 || data === null) {
7891
+ throw new Error(`Could not read the draft's attachment ${part.filename}; the draft was not changed.`);
7892
+ }
7893
+ files.push({
7894
+ filename: part.filename,
7895
+ content: Buffer.from(data, "base64url"),
7896
+ contentType: part.mimeType || "application/octet-stream"
7897
+ });
7898
+ }
7899
+ for (const child of part.parts || []) await visit(child);
7900
+ };
7901
+ await visit(payload);
7902
+ return files;
7903
+ }
7806
7904
  /**
7807
7905
  * Builds the base64url `raw` payload Gmail expects for send, draft create and
7808
7906
  * draft update: a full MIME message with a text part, an HTML part when one
@@ -7810,7 +7908,7 @@ var init_adapter = __esm({
7810
7908
  * and any attachments. See `buildMimeMessage`.
7811
7909
  */
7812
7910
  async buildRaw(params) {
7813
- const message = await buildMimeMessage(this.email, params);
7911
+ const message = await buildMimeMessage(await this.from(), params);
7814
7912
  return message.toString("base64url");
7815
7913
  }
7816
7914
  };
@@ -9081,6 +9179,7 @@ var init_adapter3 = __esm({
9081
9179
  client = null;
9082
9180
  accountId = "";
9083
9181
  email = "";
9182
+ accountName = "";
9084
9183
  passwordCreds = null;
9085
9184
  connectingPromise = null;
9086
9185
  async connect(credentials) {
@@ -9089,6 +9188,7 @@ var init_adapter3 = __esm({
9089
9188
  }
9090
9189
  this.accountId = credentials.id;
9091
9190
  this.email = credentials.email;
9191
+ this.accountName = credentials.name;
9092
9192
  this.passwordCreds = credentials.password;
9093
9193
  await this.ensureConnected();
9094
9194
  }
@@ -9521,7 +9621,7 @@ var init_adapter3 = __esm({
9521
9621
  async sendEmail(params) {
9522
9622
  if (!this.passwordCreds) throw new Error("No credentials available");
9523
9623
  const transport = createSmtpTransport(this.email, this.passwordCreds);
9524
- const messageId = await sendViaSmtp(transport, this.email, params);
9624
+ const messageId = await sendViaSmtp(transport, this.from(), params);
9525
9625
  return { id: messageId };
9526
9626
  }
9527
9627
  /**
@@ -9530,7 +9630,12 @@ var init_adapter3 = __esm({
9530
9630
  * attachments. See `buildMimeMessage`.
9531
9631
  */
9532
9632
  buildDraftMessage(params) {
9533
- return buildMimeMessage(this.email, params);
9633
+ return buildMimeMessage(this.from(), params);
9634
+ }
9635
+ /** IMAP keeps no display name on the server, so the account's name is the
9636
+ * only one there is (see `fromAddress`). */
9637
+ from() {
9638
+ return fromAddress(this.email, void 0, this.accountName);
9534
9639
  }
9535
9640
  async createDraft(params) {
9536
9641
  const client = await this.ensureConnected();
@@ -9548,7 +9653,11 @@ var init_adapter3 = __esm({
9548
9653
  *
9549
9654
  * A reply draft keeps its place in the thread: when the caller passes no
9550
9655
  * `inReplyTo`, the In-Reply-To and References headers of the old revision
9551
- * are copied into the new one.
9656
+ * are copied into the new one. The same goes for files: when the caller
9657
+ * passes no `attachments`, the old revision's attachments are copied over
9658
+ * (a given list, an empty one included, replaces them). Images embedded in
9659
+ * the old HTML body are not files and are not copied: the new body replaces
9660
+ * the old one.
9552
9661
  */
9553
9662
  async updateDraft(draftId, params, sourceFolder) {
9554
9663
  const client = await this.ensureConnected();
@@ -9562,13 +9671,28 @@ var init_adapter3 = __esm({
9562
9671
  let rawMessage;
9563
9672
  try {
9564
9673
  let next = params;
9565
- if (params.inReplyTo === void 0) {
9674
+ const keepThread = params.inReplyTo === void 0;
9675
+ const keepFiles = params.attachments === void 0;
9676
+ if (keepThread || keepFiles) {
9566
9677
  const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
9567
9678
  if (old && old.source) {
9568
9679
  const parsed = await simpleParser(old.source);
9569
- const references = [].concat(parsed.references ?? []);
9570
- if (parsed.inReplyTo) {
9571
- next = { ...params, inReplyTo: parsed.inReplyTo, references: params.references ?? references };
9680
+ if (keepThread && parsed.inReplyTo) {
9681
+ const references = [].concat(parsed.references ?? []);
9682
+ next = { ...next, inReplyTo: parsed.inReplyTo, references: params.references ?? references };
9683
+ }
9684
+ if (keepFiles) {
9685
+ const html = (parsed.html || "").toLowerCase();
9686
+ const files = (parsed.attachments || []).filter((att) => {
9687
+ if (!att.related) return true;
9688
+ const cid = String(att.cid || att.contentId || "").replace(/^<|>$/g, "").toLowerCase();
9689
+ return !cid || !html.includes(`cid:${cid}`);
9690
+ }).map((att, i) => ({
9691
+ filename: att.filename || `attachment-${i + 1}`,
9692
+ content: att.content,
9693
+ contentType: att.contentType || "application/octet-stream"
9694
+ }));
9695
+ if (files.length) next = { ...next, attachments: files };
9572
9696
  }
9573
9697
  }
9574
9698
  }
@@ -34246,6 +34370,78 @@ function assertAttachmentTotal(totalBytes) {
34246
34370
  }
34247
34371
  }
34248
34372
 
34373
+ // src/tools/signature.ts
34374
+ init_mime();
34375
+ function escapeHtml(text) {
34376
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
34377
+ }
34378
+ function textToHtml(text) {
34379
+ return `<div dir="ltr">${escapeHtml(text).replace(/\r?\n/g, "<br>")}</div>`;
34380
+ }
34381
+ var TEXT_DELIMITER = /^-- \r?$/m;
34382
+ var HTML_QUOTE = /<(?:div|blockquote)\b[^>]*\bclass="[^"]*\bgmail_quote/i;
34383
+ var TEXT_QUOTE = /^(?:-{10} Forwarded message -{10}|On .+ wrote:)\s*$/m;
34384
+ function hasSignature(body) {
34385
+ return /\bgmail_signature\b/.test(body.html ?? "") || TEXT_DELIMITER.test(body.text ?? "");
34386
+ }
34387
+ function renderSignature(signatureHtml) {
34388
+ return {
34389
+ html: `<span class="gmail_signature_prefix">-- </span><br><div dir="ltr" class="gmail_signature" data-smartmail="gmail_signature">${signatureHtml}</div>`,
34390
+ text: `--
34391
+ ${htmlToText(signatureHtml)}`
34392
+ };
34393
+ }
34394
+ function insertBefore(content, quote, block, separator) {
34395
+ const match = quote.exec(content);
34396
+ const at = match ? match.index : content.length;
34397
+ const before = content.slice(0, at).replace(/\s+$/, "");
34398
+ const after = content.slice(at);
34399
+ return [before, block, after].filter(Boolean).join(separator);
34400
+ }
34401
+ function withSignature(body, signatureHtml) {
34402
+ const signature = renderSignature(signatureHtml);
34403
+ const html = body.html ?? (body.text ? textToHtml(body.text) : "");
34404
+ const text = body.text ?? (body.html ? htmlToText(body.html) : "");
34405
+ return {
34406
+ html: insertBefore(html, HTML_QUOTE, `<div>${signature.html}</div>`, "<br>"),
34407
+ text: insertBefore(text, TEXT_QUOTE, signature.text, "\n\n")
34408
+ };
34409
+ }
34410
+ function unsupportedReason(provider) {
34411
+ if (provider === "outlook") {
34412
+ return "Outlook: Microsoft Graph has no signature API for Outlook.com accounts, so no signature was added. Put it in the body if the mail needs one.";
34413
+ }
34414
+ return `${provider === "icloud" ? "iCloud" : "IMAP"}: the server keeps no signature a client can read, so no signature was added. Put it in the body if the mail needs one.`;
34415
+ }
34416
+ async function resolveSignature(provider, body, enabled) {
34417
+ if (enabled === false) return { body, signature: { status: "disabled" } };
34418
+ if (hasSignature(body)) return { body, signature: { status: "already_present" } };
34419
+ if (!provider.getSignature) {
34420
+ return { body, signature: { status: "unsupported", reason: unsupportedReason(provider.providerType) } };
34421
+ }
34422
+ let found;
34423
+ try {
34424
+ found = await provider.getSignature();
34425
+ } catch (error48) {
34426
+ const message = error48?.message || String(error48);
34427
+ return {
34428
+ body,
34429
+ signature: { status: "skipped", reason: `The signature could not be read, so none was added: ${message}` }
34430
+ };
34431
+ }
34432
+ if (!found.html) {
34433
+ return {
34434
+ body,
34435
+ signature: {
34436
+ status: "none_configured",
34437
+ sendAs: found.sendAs,
34438
+ reason: `${found.sendAs} has no signature in Gmail's settings.`
34439
+ }
34440
+ };
34441
+ }
34442
+ return { body: withSignature(body, found.html), signature: { status: "added", sendAs: found.sendAs } };
34443
+ }
34444
+
34249
34445
  // src/tools/sending.ts
34250
34446
  var ContactSchema = external_exports3.object({
34251
34447
  email: external_exports3.string(),
@@ -34255,6 +34451,9 @@ var BodySchema = external_exports3.object({
34255
34451
  text: external_exports3.string().optional(),
34256
34452
  html: external_exports3.string().optional()
34257
34453
  });
34454
+ var SignatureSchema = external_exports3.boolean().optional().describe(
34455
+ "Add the account's signature (default true). Gmail: the signature of the send-as address the mail goes out from, read from Gmail's settings, in Gmail's own markup above any quoted history; a body that already holds one is left alone. Outlook.com, iCloud and IMAP have no signature a client can read, so nothing is added there. The result's `signature` field says what happened"
34456
+ );
34258
34457
  function jsonResult3(data) {
34259
34458
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
34260
34459
  }
@@ -34294,21 +34493,24 @@ function registerSendingTools(server, accountManager) {
34294
34493
  bcc: external_exports3.array(ContactSchema).optional(),
34295
34494
  subject: external_exports3.string(),
34296
34495
  body: BodySchema,
34297
- attachments: AttachmentsSchema
34496
+ attachments: AttachmentsSchema,
34497
+ signature: SignatureSchema
34298
34498
  },
34299
34499
  async (args) => {
34300
34500
  try {
34301
34501
  const provider = await accountManager.getProvider(args.accountId);
34502
+ const attachments = resolveAttachments(args.attachments);
34503
+ const { body, signature } = await resolveSignature(provider, args.body, args.signature);
34302
34504
  const params = {
34303
34505
  to: args.to,
34304
34506
  cc: args.cc,
34305
34507
  bcc: args.bcc,
34306
34508
  subject: args.subject,
34307
- body: args.body,
34308
- attachments: resolveAttachments(args.attachments)
34509
+ body,
34510
+ attachments
34309
34511
  };
34310
34512
  const result = await provider.sendEmail(params);
34311
- return jsonResult3(result);
34513
+ return jsonResult3({ ...result, signature });
34312
34514
  } catch (error48) {
34313
34515
  return jsonResult3({ error: error48.message });
34314
34516
  }
@@ -34327,7 +34529,8 @@ function registerSendingTools(server, accountManager) {
34327
34529
  cc: external_exports3.array(ContactSchema).optional().describe("Cc recipients. Replaces any Cc carried over by replyAll"),
34328
34530
  bcc: external_exports3.array(ContactSchema).optional(),
34329
34531
  additionalRecipients: external_exports3.array(ContactSchema).optional().describe("Extra To recipients added to the default or given To (deduplicated by address)"),
34330
- attachments: AttachmentsSchema
34532
+ attachments: AttachmentsSchema,
34533
+ signature: SignatureSchema
34331
34534
  },
34332
34535
  async (args) => {
34333
34536
  try {
@@ -34355,17 +34558,19 @@ function registerSendingTools(server, accountManager) {
34355
34558
  }
34356
34559
  if (args.cc !== void 0) cc = args.cc;
34357
34560
  if (args.replyAll) ({ to, cc } = withoutDuplicates(to, cc));
34561
+ const attachments = resolveAttachments(args.attachments);
34562
+ const { body, signature } = await resolveSignature(provider, args.body, args.signature);
34358
34563
  const params = {
34359
34564
  to,
34360
34565
  cc,
34361
34566
  bcc: args.bcc,
34362
34567
  subject,
34363
- body: args.body,
34568
+ body,
34364
34569
  ...replyThreading(original),
34365
- attachments: resolveAttachments(args.attachments)
34570
+ attachments
34366
34571
  };
34367
34572
  const result = await provider.sendEmail(params);
34368
- return jsonResult3(result);
34573
+ return jsonResult3({ ...result, signature });
34369
34574
  } catch (error48) {
34370
34575
  return jsonResult3({ error: error48.message });
34371
34576
  }
@@ -34383,12 +34588,14 @@ function registerSendingTools(server, accountManager) {
34383
34588
  body: BodySchema.optional(),
34384
34589
  attachments: AttachmentsSchema.describe("Files to attach to the forward. Total size, together with forwarded original attachments, is capped at 25 MB"),
34385
34590
  includeOriginalAttachments: external_exports3.boolean().optional().describe("Also forward the original message's attachments (default false). Leave it off unless the user asked for the files to go along"),
34386
- sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
34591
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)"),
34592
+ signature: SignatureSchema
34387
34593
  },
34388
34594
  async (args) => {
34389
34595
  try {
34390
34596
  const provider = await accountManager.getProvider(args.accountId);
34391
34597
  const original = await provider.getEmail(args.emailId, args.sourceFolder);
34598
+ const { body: note, signature } = await resolveSignature(provider, args.body ?? {}, args.signature);
34392
34599
  const subject = original.subject.startsWith("Fwd: ") ? original.subject : `Fwd: ${original.subject}`;
34393
34600
  const forwardHeader = [
34394
34601
  "---------- Forwarded message ----------",
@@ -34398,7 +34605,7 @@ function registerSendingTools(server, accountManager) {
34398
34605
  `To: ${original.to.map((c) => c.name ? `${c.name} <${c.email}>` : c.email).join(", ")}`,
34399
34606
  ""
34400
34607
  ].join("\n");
34401
- const additionalText = args.body?.text ?? "";
34608
+ const additionalText = note.text ?? "";
34402
34609
  const originalText = original.body.text ?? "";
34403
34610
  const forwardedText = [
34404
34611
  additionalText,
@@ -34407,19 +34614,22 @@ function registerSendingTools(server, accountManager) {
34407
34614
  originalText
34408
34615
  ].join("");
34409
34616
  let forwardedHtml;
34410
- if (original.body.html || args.body?.html) {
34411
- const additionalHtml = args.body?.html ?? "";
34412
- const originalHtml = original.body.html ?? "";
34617
+ if (original.body.html || note.html) {
34618
+ const additionalHtml = note.html ?? "";
34619
+ const originalHtml = original.body.html ?? textToHtml(original.body.text ?? "");
34620
+ const contact = (c) => escapeHtml(c.name ? `${c.name} <${c.email}>` : c.email);
34413
34621
  forwardedHtml = [
34414
34622
  additionalHtml,
34415
34623
  "<br/><hr/>",
34624
+ '<div class="gmail_quote">',
34416
34625
  `<b>---------- Forwarded message ----------</b><br/>`,
34417
- `From: ${original.from.name ? `${original.from.name} &lt;${original.from.email}&gt;` : original.from.email}<br/>`,
34418
- `Date: ${original.date}<br/>`,
34419
- `Subject: ${original.subject}<br/>`,
34420
- `To: ${original.to.map((c) => c.name ? `${c.name} &lt;${c.email}&gt;` : c.email).join(", ")}<br/>`,
34626
+ `From: ${contact(original.from)}<br/>`,
34627
+ `Date: ${escapeHtml(original.date)}<br/>`,
34628
+ `Subject: ${escapeHtml(original.subject)}<br/>`,
34629
+ `To: ${original.to.map(contact).join(", ")}<br/>`,
34421
34630
  "<br/>",
34422
- originalHtml
34631
+ originalHtml,
34632
+ "</div>"
34423
34633
  ].join("");
34424
34634
  }
34425
34635
  const extra = resolveAttachments(args.attachments) ?? [];
@@ -34445,7 +34655,7 @@ function registerSendingTools(server, accountManager) {
34445
34655
  attachments: attachments.length ? attachments : void 0
34446
34656
  };
34447
34657
  const result = await provider.sendEmail(params);
34448
- return jsonResult3(result);
34658
+ return jsonResult3({ ...result, signature });
34449
34659
  } catch (error48) {
34450
34660
  return jsonResult3({ error: error48.message });
34451
34661
  }
@@ -34461,22 +34671,24 @@ function registerSendingTools(server, accountManager) {
34461
34671
  body: BodySchema,
34462
34672
  attachments: AttachmentsSchema,
34463
34673
  inReplyToEmailId: external_exports3.string().optional().describe("Id of the email this draft replies to. The draft joins that thread (Gmail threadId, Outlook createReply, In-Reply-To/References headers elsewhere)"),
34464
- inReplyToSourceFolder: external_exports3.string().optional().describe("Folder of the email named in inReplyToEmailId (required for IMAP/iCloud when that email is not in INBOX). The draft itself is saved to Drafts")
34674
+ inReplyToSourceFolder: external_exports3.string().optional().describe("Folder of the email named in inReplyToEmailId (required for IMAP/iCloud when that email is not in INBOX). The draft itself is saved to Drafts"),
34675
+ signature: SignatureSchema
34465
34676
  },
34466
34677
  async (args) => {
34467
34678
  try {
34468
34679
  const provider = await accountManager.getProvider(args.accountId);
34680
+ const attachments = resolveAttachments(args.attachments);
34681
+ const threading = args.inReplyToEmailId ? replyThreading(await provider.getEmail(args.inReplyToEmailId, args.inReplyToSourceFolder)) : {};
34682
+ const { body, signature } = await resolveSignature(provider, args.body, args.signature);
34469
34683
  const params = {
34470
34684
  to: args.to,
34471
34685
  subject: args.subject,
34472
- body: args.body,
34473
- attachments: resolveAttachments(args.attachments)
34686
+ body,
34687
+ attachments,
34688
+ ...threading
34474
34689
  };
34475
- if (args.inReplyToEmailId) {
34476
- Object.assign(params, replyThreading(await provider.getEmail(args.inReplyToEmailId, args.inReplyToSourceFolder)));
34477
- }
34478
34690
  const result = await provider.createDraft(params);
34479
- return jsonResult3(result);
34691
+ return jsonResult3({ ...result, signature });
34480
34692
  } catch (error48) {
34481
34693
  return jsonResult3({ error: error48.message });
34482
34694
  }
@@ -34492,21 +34704,26 @@ function registerSendingTools(server, accountManager) {
34492
34704
  subject: external_exports3.string(),
34493
34705
  body: BodySchema,
34494
34706
  attachments: AttachmentsSchema.describe(
34495
- "Files to attach. When given, they replace the draft's current attachments; an empty list removes them all. When omitted, Outlook keeps the existing files but Gmail and IMAP drop them (those providers rewrite the whole message), so pass them again to keep them."
34707
+ "Files to attach. When given, they replace the draft's current attachments; an empty list removes them all. When omitted, the draft keeps the files it has."
34496
34708
  ),
34497
- sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
34709
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)"),
34710
+ signature: SignatureSchema.describe(
34711
+ "Add the account's signature (default true), as on email_draft_create. The body given here replaces the draft's body, so the signature is added to it once; a body that already holds one (passed back from the draft) is left alone, so repeated updates never stack a second one"
34712
+ )
34498
34713
  },
34499
34714
  async (args) => {
34500
34715
  try {
34501
34716
  const provider = await accountManager.getProvider(args.accountId);
34717
+ const attachments = resolveAttachments(args.attachments);
34718
+ const { body, signature } = await resolveSignature(provider, args.body, args.signature);
34502
34719
  const params = {
34503
34720
  to: args.to,
34504
34721
  subject: args.subject,
34505
- body: args.body,
34506
- attachments: resolveAttachments(args.attachments)
34722
+ body,
34723
+ attachments
34507
34724
  };
34508
34725
  const result = await provider.updateDraft(args.draftId, params, args.sourceFolder);
34509
- return jsonResult3(result);
34726
+ return jsonResult3({ ...result, signature });
34510
34727
  } catch (error48) {
34511
34728
  return jsonResult3({ error: error48.message });
34512
34729
  }
@@ -35156,7 +35373,7 @@ function registerForwardingTools(server, accountManager) {
35156
35373
  }
35157
35374
 
35158
35375
  // src/server.ts
35159
- var SERVER_VERSION = "1.10.0";
35376
+ var SERVER_VERSION = "1.11.0";
35160
35377
  async function createServer(accountManager) {
35161
35378
  const mgr = accountManager ?? new AccountManager();
35162
35379
  const server = new McpServer({