@marlinjai/email-mcp 1.9.0 → 1.10.1

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/dist/index.js CHANGED
@@ -3396,8 +3396,8 @@ var require_utils = __commonJS({
3396
3396
  }
3397
3397
  return ind;
3398
3398
  }
3399
- function removeDotSegments(path4) {
3400
- let input2 = path4;
3399
+ function removeDotSegments(path5) {
3400
+ let input2 = path5;
3401
3401
  const output2 = [];
3402
3402
  let nextSlash = -1;
3403
3403
  let len = 0;
@@ -3596,8 +3596,8 @@ var require_schemes = __commonJS({
3596
3596
  wsComponent.secure = void 0;
3597
3597
  }
3598
3598
  if (wsComponent.resourceName) {
3599
- const [path4, query] = wsComponent.resourceName.split("?");
3600
- wsComponent.path = path4 && path4 !== "/" ? path4 : void 0;
3599
+ const [path5, query] = wsComponent.resourceName.split("?");
3600
+ wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
3601
3601
  wsComponent.query = query;
3602
3602
  wsComponent.resourceName = void 0;
3603
3603
  }
@@ -7068,6 +7068,14 @@ function extractAttachments(payload) {
7068
7068
  }
7069
7069
  return attachments;
7070
7070
  }
7071
+ function threadingHeaders(headers) {
7072
+ const picked = {};
7073
+ for (const name of ["Message-ID", "In-Reply-To", "References"]) {
7074
+ const value = getHeader(headers, name);
7075
+ if (value) picked[name.toLowerCase()] = value;
7076
+ }
7077
+ return Object.keys(picked).length > 0 ? picked : void 0;
7078
+ }
7071
7079
  function mapGmailMessage(message, accountId) {
7072
7080
  const headers = message.payload?.headers || [];
7073
7081
  const labelIds = message.labelIds || [];
@@ -7102,6 +7110,7 @@ function mapGmailMessage(message, accountId) {
7102
7110
  snippet: message.snippet,
7103
7111
  attachments,
7104
7112
  labels: labelIds,
7113
+ headers: threadingHeaders(headers),
7105
7114
  flags: {
7106
7115
  read: !labelIds.includes("UNREAD"),
7107
7116
  starred: labelIds.includes("STARRED"),
@@ -7358,7 +7367,7 @@ var init_adapter = __esm({
7358
7367
  const raw = await this.buildRaw(params);
7359
7368
  const res = await gmail.users.messages.send({
7360
7369
  userId: "me",
7361
- requestBody: { raw }
7370
+ requestBody: { raw, ...this.threadFor(params) }
7362
7371
  });
7363
7372
  return {
7364
7373
  id: res.data.id || "",
@@ -7371,19 +7380,53 @@ var init_adapter = __esm({
7371
7380
  const res = await gmail.users.drafts.create({
7372
7381
  userId: "me",
7373
7382
  requestBody: {
7374
- message: { raw }
7383
+ message: { raw, ...this.threadFor(params) }
7375
7384
  }
7376
7385
  });
7377
7386
  return { id: res.data.id || "" };
7378
7387
  }
7379
- async updateDraft(draftId, params) {
7388
+ /**
7389
+ * drafts.update replaces the whole message, so what the caller does not
7390
+ * name would be lost: a reply draft would fall out of its thread and a
7391
+ * draft's files would be dropped. So when the caller names no thread, the
7392
+ * thread and the reply headers of the stored draft are carried over, and
7393
+ * when the caller names no attachments, the stored draft's files are. A
7394
+ * given list (an empty one included) replaces the files.
7395
+ */
7396
+ async updateDraft(draftId, update) {
7380
7397
  const gmail = this.ensureConnected();
7398
+ let params = update;
7399
+ const keepThread = update.threadId === void 0;
7400
+ const keepFiles = update.attachments === void 0;
7401
+ if (keepThread || keepFiles) {
7402
+ const existing = await gmail.users.drafts.get({
7403
+ userId: "me",
7404
+ id: draftId,
7405
+ // 'metadata' is ids, labels and headers, no body (drafts.get has no
7406
+ // way to ask for single headers, so all of them come back). The parts
7407
+ // that list the files only come with 'full'.
7408
+ format: keepFiles ? "full" : "metadata"
7409
+ });
7410
+ const message = existing.data.message;
7411
+ if (keepThread) {
7412
+ const header = (name) => (message?.payload?.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || void 0;
7413
+ const inReplyTo = update.inReplyTo ?? header("in-reply-to");
7414
+ const references = update.references ?? header("references")?.split(/\s+/).filter(Boolean);
7415
+ if (message?.threadId && inReplyTo) {
7416
+ params = { ...params, threadId: message.threadId, inReplyTo, references: references?.length ? references : [inReplyTo] };
7417
+ }
7418
+ }
7419
+ if (keepFiles && message?.id) {
7420
+ const stored = await this.storedAttachments(message.id, message.payload);
7421
+ if (stored.length) params = { ...params, attachments: stored };
7422
+ }
7423
+ }
7381
7424
  const raw = await this.buildRaw(params);
7382
7425
  const res = await gmail.users.drafts.update({
7383
7426
  userId: "me",
7384
7427
  id: draftId,
7385
7428
  requestBody: {
7386
- message: { raw }
7429
+ message: { raw, ...this.threadFor(params) }
7387
7430
  }
7388
7431
  });
7389
7432
  return { id: res.data.id || draftId };
@@ -7762,6 +7805,51 @@ var init_adapter = __esm({
7762
7805
  }
7763
7806
  await this.deleteBlockRule(ruleId);
7764
7807
  }
7808
+ /**
7809
+ * Gmail files a message into an existing thread only when the threadId
7810
+ * comes with In-Reply-To and References headers and a subject (which has to
7811
+ * match the thread's; the reply tool builds "Re: <original subject>"). A
7812
+ * threadId without them is left out, and the message starts its own thread.
7813
+ */
7814
+ threadFor(params) {
7815
+ const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
7816
+ return belongs ? { threadId: params.threadId } : {};
7817
+ }
7818
+ /**
7819
+ * The files of a stored message, with their bytes: every part that has a
7820
+ * file name, at any depth. Large files are referenced by an attachment id
7821
+ * and fetched; small ones can come inline in the part. A fetch that fails
7822
+ * is not swallowed: a draft update built on it would drop that file.
7823
+ */
7824
+ async storedAttachments(messageId, payload) {
7825
+ const gmail = this.ensureConnected();
7826
+ const files = [];
7827
+ const html = (extractBody(payload).html || "").toLowerCase();
7828
+ const visit = async (part) => {
7829
+ if (!part) return;
7830
+ const partHeader = (name) => (part.headers || []).find((h) => (h.name || "").toLowerCase() === name)?.value || "";
7831
+ const contentId = partHeader("content-id").trim().replace(/^<|>$/g, "").toLowerCase();
7832
+ const embedded = /^\s*inline/i.test(partHeader("content-disposition")) && contentId !== "" && html.includes(`cid:${contentId}`);
7833
+ if (part.filename && !embedded) {
7834
+ let data = part.body?.data;
7835
+ if (!data && part.body?.attachmentId) {
7836
+ const res = await gmail.users.messages.attachments.get({ userId: "me", messageId, id: part.body.attachmentId });
7837
+ data = res.data.data;
7838
+ }
7839
+ if (data === void 0 || data === null) {
7840
+ throw new Error(`Could not read the draft's attachment ${part.filename}; the draft was not changed.`);
7841
+ }
7842
+ files.push({
7843
+ filename: part.filename,
7844
+ content: Buffer.from(data, "base64url"),
7845
+ contentType: part.mimeType || "application/octet-stream"
7846
+ });
7847
+ }
7848
+ for (const child of part.parts || []) await visit(child);
7849
+ };
7850
+ await visit(payload);
7851
+ return files;
7852
+ }
7765
7853
  /**
7766
7854
  * Builds the base64url `raw` payload Gmail expects for send, draft create and
7767
7855
  * draft update: a full MIME message with a text part, an HTML part when one
@@ -8055,12 +8143,41 @@ var init_mapper2 = __esm({
8055
8143
 
8056
8144
  // src/providers/outlook/adapter.ts
8057
8145
  import { Client } from "@microsoft/microsoft-graph-client";
8058
- var OutlookAdapter;
8146
+ function megabytes(bytes) {
8147
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8148
+ }
8149
+ function assertOutlookAttachmentSizes(attachments) {
8150
+ if (!attachments?.length) return;
8151
+ const limit = megabytes(OUTLOOK_MAX_ATTACHMENT_BYTES);
8152
+ for (const att of attachments) {
8153
+ if (att.content.length > OUTLOOK_MAX_ATTACHMENT_BYTES) {
8154
+ throw new Error(`Outlook accepts attachments up to ${limit} through email-mcp; ${att.filename} is ${megabytes(att.content.length)}. ${UPLOAD_SESSION_HINT}`);
8155
+ }
8156
+ }
8157
+ }
8158
+ function assertOutlookRequestSize(body) {
8159
+ const bytes = Buffer.byteLength(JSON.stringify(body));
8160
+ if (bytes > OUTLOOK_MAX_REQUEST_BYTES) {
8161
+ throw new Error(
8162
+ `Outlook accepts requests up to ${megabytes(OUTLOOK_MAX_REQUEST_BYTES)} through email-mcp; this message is ${megabytes(bytes)} once its attachments are base64-encoded. ${UPLOAD_SESSION_HINT}`
8163
+ );
8164
+ }
8165
+ }
8166
+ var toGraphAttachment, OUTLOOK_MAX_ATTACHMENT_BYTES, OUTLOOK_MAX_REQUEST_BYTES, UPLOAD_SESSION_HINT, OutlookAdapter;
8059
8167
  var init_adapter2 = __esm({
8060
8168
  "src/providers/outlook/adapter.ts"() {
8061
8169
  "use strict";
8062
8170
  init_types();
8063
8171
  init_mapper2();
8172
+ toGraphAttachment = (att) => ({
8173
+ "@odata.type": "#microsoft.graph.fileAttachment",
8174
+ name: att.filename,
8175
+ contentType: att.contentType,
8176
+ contentBytes: att.content.toString("base64")
8177
+ });
8178
+ OUTLOOK_MAX_ATTACHMENT_BYTES = 3 * 1024 * 1024;
8179
+ OUTLOOK_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
8180
+ UPLOAD_SESSION_HINT = "Larger files need an upload session, which email-mcp does not implement for Outlook yet.";
8064
8181
  OutlookAdapter = class _OutlookAdapter {
8065
8182
  providerType = ProviderType.Outlook;
8066
8183
  client = null;
@@ -8079,8 +8196,8 @@ var init_adapter2 = __esm({
8079
8196
  }
8080
8197
  });
8081
8198
  const originalApi = client.api.bind(client);
8082
- client.api = (path4) => {
8083
- return originalApi(path4).header("Prefer", 'IdType="ImmutableId"');
8199
+ client.api = (path5) => {
8200
+ return originalApi(path5).header("Prefer", 'IdType="ImmutableId"');
8084
8201
  };
8085
8202
  this.client = client;
8086
8203
  }
@@ -8200,31 +8317,125 @@ var init_adapter2 = __esm({
8200
8317
  meta: mapGraphAttachment(attachment)
8201
8318
  };
8202
8319
  }
8320
+ /**
8321
+ * Graph only threads a reply (conversationId, subject, quoted history)
8322
+ * through the createReply flow: POST /me/messages and /me/sendMail always
8323
+ * start a new conversation whatever In-Reply-To says. Build the reply draft,
8324
+ * overlay our subject, body, recipients and attachments, and return its id.
8325
+ * Note the body replaces Graph's pre-filled quoted history.
8326
+ *
8327
+ * Each attachment goes up in a request of its own, so the sizes are checked
8328
+ * per file, and before createReply so that a file that is too big leaves no
8329
+ * draft behind. A draft that cannot be filled is deleted again. (A draft
8330
+ * whose send failed is kept, see `sendEmail`.)
8331
+ */
8332
+ async createReplyDraft(params, replyToId) {
8333
+ const client = this.ensureClient();
8334
+ assertOutlookAttachmentSizes(params.attachments);
8335
+ const draft = await client.api(`/me/messages/${encodeURIComponent(replyToId)}/createReply`).post({});
8336
+ try {
8337
+ await this.fillReplyDraft(draft.id, params);
8338
+ } catch (error48) {
8339
+ throw await this.discardDraft(draft.id, error48);
8340
+ }
8341
+ return draft.id;
8342
+ }
8343
+ async fillReplyDraft(draftId, params) {
8344
+ const client = this.ensureClient();
8345
+ const toGraphRecipient = (c) => ({
8346
+ emailAddress: { name: c.name, address: c.email }
8347
+ });
8348
+ const patch = {
8349
+ // Always the caller's subject: Graph pre-fills its own "RE: ..." one.
8350
+ subject: params.subject,
8351
+ body: { contentType: params.body.html ? "html" : "text", content: params.body.html || params.body.text || "" }
8352
+ };
8353
+ if (params.to?.length) patch.toRecipients = params.to.map(toGraphRecipient);
8354
+ if (params.cc !== void 0) patch.ccRecipients = params.cc.map(toGraphRecipient);
8355
+ if (params.bcc !== void 0) patch.bccRecipients = params.bcc.map(toGraphRecipient);
8356
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(patch);
8357
+ for (const att of params.attachments ?? []) {
8358
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post(toGraphAttachment(att));
8359
+ }
8360
+ }
8361
+ /**
8362
+ * Deletes a reply draft that could not be filled, so a failed reply leaves
8363
+ * nothing half-built in Drafts. Returns the error to throw: the original
8364
+ * one, or, when the delete fails as well, the original message with a note
8365
+ * that the draft is still there.
8366
+ */
8367
+ async discardDraft(draftId, cause) {
8368
+ try {
8369
+ await this.ensureClient().api(`/me/messages/${encodeURIComponent(draftId)}`).delete();
8370
+ return cause;
8371
+ } catch (deleteError) {
8372
+ const message = cause instanceof Error ? cause.message : String(cause);
8373
+ return new Error(
8374
+ `${message} (the reply draft ${draftId} could not be removed and was left in Drafts: ${deleteError?.message ?? deleteError})`,
8375
+ { cause }
8376
+ );
8377
+ }
8378
+ }
8203
8379
  async sendEmail(params) {
8204
8380
  const client = this.ensureClient();
8381
+ if (params.replyToGraphId) {
8382
+ const draftId = await this.createReplyDraft(params, params.replyToGraphId);
8383
+ try {
8384
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}/send`).post({});
8385
+ } catch (error48) {
8386
+ throw new Error(
8387
+ `${error48?.message ?? error48} (the reply was saved as draft ${draftId} in Drafts and was not confirmed as sent; check Sent Items before sending it again)`,
8388
+ { cause: error48 }
8389
+ );
8390
+ }
8391
+ return { id: draftId };
8392
+ }
8393
+ assertOutlookAttachmentSizes(params.attachments);
8205
8394
  const message = this.buildGraphMessage(params);
8206
8395
  const payload = { message };
8207
8396
  if (params.attachments?.length) {
8208
- payload.message.attachments = params.attachments.map((att) => ({
8209
- "@odata.type": "#microsoft.graph.fileAttachment",
8210
- name: att.filename,
8211
- contentType: att.contentType,
8212
- contentBytes: att.content.toString("base64")
8213
- }));
8397
+ payload.message.attachments = params.attachments.map(toGraphAttachment);
8214
8398
  }
8399
+ assertOutlookRequestSize(payload);
8215
8400
  await client.api("/me/sendMail").post(payload);
8216
8401
  return { id: `sent-${Date.now()}` };
8217
8402
  }
8218
8403
  async createDraft(params) {
8219
8404
  const client = this.ensureClient();
8405
+ if (params.replyToGraphId) {
8406
+ return { id: await this.createReplyDraft(params, params.replyToGraphId) };
8407
+ }
8408
+ assertOutlookAttachmentSizes(params.attachments);
8220
8409
  const message = this.buildGraphMessage(params);
8410
+ if (params.attachments?.length) {
8411
+ message.attachments = params.attachments.map(toGraphAttachment);
8412
+ }
8413
+ assertOutlookRequestSize(message);
8221
8414
  const result = await client.api("/me/messages").post(message);
8222
8415
  return { id: result.id };
8223
8416
  }
8224
8417
  async updateDraft(draftId, params) {
8225
8418
  const client = this.ensureClient();
8419
+ assertOutlookAttachmentSizes(params.attachments);
8226
8420
  const message = this.buildGraphMessage(params);
8227
8421
  await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
8422
+ if (params.attachments) {
8423
+ const attachmentsPath = `/me/messages/${encodeURIComponent(draftId)}/attachments`;
8424
+ const previousIds = [];
8425
+ let page = await client.api(attachmentsPath).select("id").get();
8426
+ for (; ; ) {
8427
+ previousIds.push(...(page.value || []).map((att) => att.id));
8428
+ const next = page["@odata.nextLink"];
8429
+ if (!next) break;
8430
+ page = await client.api(next).get();
8431
+ }
8432
+ for (const att of params.attachments) {
8433
+ await client.api(attachmentsPath).post(toGraphAttachment(att));
8434
+ }
8435
+ for (const id of previousIds) {
8436
+ await client.api(`${attachmentsPath}/${encodeURIComponent(id)}`).delete();
8437
+ }
8438
+ }
8228
8439
  return { id: draftId };
8229
8440
  }
8230
8441
  async listDrafts(limit, offset) {
@@ -8781,6 +8992,12 @@ function mapImapFolder(imapFolder) {
8781
8992
  unreadCount: imapFolder.status?.unseen
8782
8993
  };
8783
8994
  }
8995
+ function imapAttachmentId(att, index) {
8996
+ return att.contentId || `att-${index}`;
8997
+ }
8998
+ function headerValue(line) {
8999
+ return line.slice(line.indexOf(":") + 1).replace(/\r?\n[ \t]+/g, " ").trim();
9000
+ }
8784
9001
  function mapParsedEmail(parsed, folder, accountId, uid) {
8785
9002
  const mapContact = (addr) => ({
8786
9003
  name: addr.name || void 0,
@@ -8791,7 +9008,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
8791
9008
  return addrs.value.map(mapContact);
8792
9009
  };
8793
9010
  const attachments = (parsed.attachments || []).map((att, i) => ({
8794
- id: att.contentId || `att-${i}`,
9011
+ id: imapAttachmentId(att, i),
8795
9012
  filename: att.filename || `attachment-${i}`,
8796
9013
  contentType: att.contentType || "application/octet-stream",
8797
9014
  size: att.size || 0
@@ -8820,7 +9037,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
8820
9037
  flagged: parsed.flags?.has("\\Flagged") || false,
8821
9038
  draft: parsed.flags?.has("\\Draft") || false
8822
9039
  },
8823
- headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, h.line])) : void 0
9040
+ headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, headerValue(h.line)])) : void 0
8824
9041
  };
8825
9042
  }
8826
9043
  var SPECIAL_USE_MAP;
@@ -9248,7 +9465,7 @@ var init_adapter3 = __esm({
9248
9465
  }
9249
9466
  async getEmail(id, folder) {
9250
9467
  const client = await this.ensureConnected();
9251
- const targetFolder = folder || "INBOX";
9468
+ const targetFolder = folder ? await this.resolveFolder(folder) : "INBOX";
9252
9469
  let lock;
9253
9470
  try {
9254
9471
  lock = await client.getMailboxLock(targetFolder);
@@ -9267,13 +9484,20 @@ var init_adapter3 = __esm({
9267
9484
  lock.release();
9268
9485
  }
9269
9486
  }
9270
- async getThread(threadId) {
9487
+ /**
9488
+ * IMAP has no thread object: the thread is the set of messages in ONE
9489
+ * folder whose Message-ID, References or In-Reply-To name `threadId`.
9490
+ * Messages of the same conversation in other folders (the own replies in
9491
+ * Sent, for example) are not part of the result.
9492
+ */
9493
+ async getThread(threadId, sourceFolder) {
9271
9494
  const client = await this.ensureConnected();
9495
+ const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
9272
9496
  let lock;
9273
9497
  try {
9274
- lock = await client.getMailboxLock("INBOX");
9498
+ lock = await client.getMailboxLock(folder);
9275
9499
  } catch (error48) {
9276
- throw formatImapError(error48, 'Failed to open folder "INBOX"');
9500
+ throw formatImapError(error48, `Failed to open folder "${folder}"`);
9277
9501
  }
9278
9502
  try {
9279
9503
  const searchResult = await client.search(
@@ -9281,13 +9505,13 @@ var init_adapter3 = __esm({
9281
9505
  { uid: true }
9282
9506
  );
9283
9507
  const uids = Array.isArray(searchResult) ? searchResult : [];
9284
- if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
9508
+ if (uids.length === 0) throw new Error(`Thread ${threadId} not found in "${folder}"`);
9285
9509
  const messages = [];
9286
9510
  for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
9287
9511
  if (!msg.source) continue;
9288
9512
  const parsed = await simpleParser(msg.source);
9289
9513
  parsed.flags = msg.flags;
9290
- messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
9514
+ messages.push(mapParsedEmail(parsed, folder, this.accountId, msg.uid));
9291
9515
  }
9292
9516
  const participantMap = /* @__PURE__ */ new Map();
9293
9517
  for (const msg of messages) {
@@ -9309,27 +9533,29 @@ var init_adapter3 = __esm({
9309
9533
  lock.release();
9310
9534
  }
9311
9535
  }
9312
- async getAttachment(emailId, attachmentId) {
9536
+ async getAttachment(emailId, attachmentId, sourceFolder) {
9313
9537
  const client = await this.ensureConnected();
9538
+ const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
9314
9539
  let lock;
9315
9540
  try {
9316
- lock = await client.getMailboxLock("INBOX");
9541
+ lock = await client.getMailboxLock(folder);
9317
9542
  } catch (error48) {
9318
- throw formatImapError(error48, 'Failed to open folder "INBOX"');
9543
+ throw formatImapError(error48, `Failed to open folder "${folder}"`);
9319
9544
  }
9320
9545
  try {
9321
9546
  const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
9322
- if (!msg) throw new Error(`Email ${emailId} not found`);
9547
+ if (!msg) throw new Error(`Email ${emailId} not found in "${folder}"`);
9323
9548
  if (!msg.source) throw new Error(`Email ${emailId} has no source`);
9324
9549
  const parsed = await simpleParser(msg.source);
9325
- const attachment = (parsed.attachments || []).find(
9326
- (att) => att.contentId === attachmentId || att.filename === attachmentId
9327
- );
9328
- if (!attachment) throw new Error(`Attachment ${attachmentId} not found in email ${emailId}`);
9550
+ const attachments = parsed.attachments || [];
9551
+ let index = attachments.findIndex((att, i) => imapAttachmentId(att, i) === attachmentId);
9552
+ if (index === -1) index = attachments.findIndex((att) => att.filename === attachmentId);
9553
+ if (index === -1) throw new Error(`Attachment ${attachmentId} not found in email ${emailId}`);
9554
+ const attachment = attachments[index];
9329
9555
  return {
9330
9556
  data: attachment.content,
9331
9557
  meta: {
9332
- id: attachment.contentId || attachmentId,
9558
+ id: imapAttachmentId(attachment, index),
9333
9559
  filename: attachment.filename || "attachment",
9334
9560
  contentType: attachment.contentType || "application/octet-stream",
9335
9561
  size: attachment.size || 0
@@ -9366,18 +9592,53 @@ var init_adapter3 = __esm({
9366
9592
  * Drafts folder, and IMAP messages are immutable once appended. This
9367
9593
  * deletes the old revision and appends a new one, so the returned `id`
9368
9594
  * is a NEW uid; the old id no longer resolves after this call.
9595
+ *
9596
+ * A reply draft keeps its place in the thread: when the caller passes no
9597
+ * `inReplyTo`, the In-Reply-To and References headers of the old revision
9598
+ * are copied into the new one. The same goes for files: when the caller
9599
+ * passes no `attachments`, the old revision's attachments are copied over
9600
+ * (a given list, an empty one included, replaces them). Images embedded in
9601
+ * the old HTML body are not files and are not copied: the new body replaces
9602
+ * the old one.
9369
9603
  */
9370
9604
  async updateDraft(draftId, params, sourceFolder) {
9371
9605
  const client = await this.ensureConnected();
9372
9606
  const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
9373
- const rawMessage = await this.buildDraftMessage(params);
9374
9607
  let lock;
9375
9608
  try {
9376
9609
  lock = await client.getMailboxLock(draftsFolder);
9377
9610
  } catch (error48) {
9378
9611
  throw formatImapError(error48, `Failed to open folder "${draftsFolder}"`);
9379
9612
  }
9613
+ let rawMessage;
9380
9614
  try {
9615
+ let next = params;
9616
+ const keepThread = params.inReplyTo === void 0;
9617
+ const keepFiles = params.attachments === void 0;
9618
+ if (keepThread || keepFiles) {
9619
+ const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
9620
+ if (old && old.source) {
9621
+ const parsed = await simpleParser(old.source);
9622
+ if (keepThread && parsed.inReplyTo) {
9623
+ const references = [].concat(parsed.references ?? []);
9624
+ next = { ...next, inReplyTo: parsed.inReplyTo, references: params.references ?? references };
9625
+ }
9626
+ if (keepFiles) {
9627
+ const html = (parsed.html || "").toLowerCase();
9628
+ const files = (parsed.attachments || []).filter((att) => {
9629
+ if (!att.related) return true;
9630
+ const cid = String(att.cid || att.contentId || "").replace(/^<|>$/g, "").toLowerCase();
9631
+ return !cid || !html.includes(`cid:${cid}`);
9632
+ }).map((att, i) => ({
9633
+ filename: att.filename || `attachment-${i + 1}`,
9634
+ content: att.content,
9635
+ contentType: att.contentType || "application/octet-stream"
9636
+ }));
9637
+ if (files.length) next = { ...next, attachments: files };
9638
+ }
9639
+ }
9640
+ }
9641
+ rawMessage = await this.buildDraftMessage(next);
9381
9642
  await client.messageDelete(draftId, { uid: true });
9382
9643
  } finally {
9383
9644
  lock.release();
@@ -9693,6 +9954,11 @@ var init_account_manager = __esm({
9693
9954
  connected: this.providers.has(c.id)
9694
9955
  }));
9695
9956
  }
9957
+ /** The account's own address, or undefined for an unknown account id. */
9958
+ async getAccountEmail(accountId) {
9959
+ const creds = this.credentials.get(accountId) ?? await this.store.get(accountId);
9960
+ return creds?.email;
9961
+ }
9696
9962
  async getProvider(accountId) {
9697
9963
  const existing = this.providers.get(accountId);
9698
9964
  if (existing) {
@@ -10807,8 +11073,8 @@ function getErrorMap() {
10807
11073
 
10808
11074
  // node_modules/zod/v3/helpers/parseUtil.js
10809
11075
  var makeIssue = (params) => {
10810
- const { data, path: path4, errorMaps, issueData } = params;
10811
- const fullPath = [...path4, ...issueData.path || []];
11076
+ const { data, path: path5, errorMaps, issueData } = params;
11077
+ const fullPath = [...path5, ...issueData.path || []];
10812
11078
  const fullIssue = {
10813
11079
  ...issueData,
10814
11080
  path: fullPath
@@ -10923,11 +11189,11 @@ var errorUtil;
10923
11189
 
10924
11190
  // node_modules/zod/v3/types.js
10925
11191
  var ParseInputLazyPath = class {
10926
- constructor(parent, value, path4, key) {
11192
+ constructor(parent, value, path5, key) {
10927
11193
  this._cachedPath = [];
10928
11194
  this.parent = parent;
10929
11195
  this.data = value;
10930
- this._path = path4;
11196
+ this._path = path5;
10931
11197
  this._key = key;
10932
11198
  }
10933
11199
  get path() {
@@ -14850,10 +15116,10 @@ function mergeDefs(...defs) {
14850
15116
  function cloneDef(schema) {
14851
15117
  return mergeDefs(schema._zod.def);
14852
15118
  }
14853
- function getElementAtPath(obj, path4) {
14854
- if (!path4)
15119
+ function getElementAtPath(obj, path5) {
15120
+ if (!path5)
14855
15121
  return obj;
14856
- return path4.reduce((acc, key) => acc?.[key], obj);
15122
+ return path5.reduce((acc, key) => acc?.[key], obj);
14857
15123
  }
14858
15124
  function promiseAllObject(promisesObj) {
14859
15125
  const keys = Object.keys(promisesObj);
@@ -15236,11 +15502,11 @@ function aborted(x, startIndex = 0) {
15236
15502
  }
15237
15503
  return false;
15238
15504
  }
15239
- function prefixIssues(path4, issues) {
15505
+ function prefixIssues(path5, issues) {
15240
15506
  return issues.map((iss) => {
15241
15507
  var _a2;
15242
15508
  (_a2 = iss).path ?? (_a2.path = []);
15243
- iss.path.unshift(path4);
15509
+ iss.path.unshift(path5);
15244
15510
  return iss;
15245
15511
  });
15246
15512
  }
@@ -15423,7 +15689,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
15423
15689
  }
15424
15690
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
15425
15691
  const result = { errors: [] };
15426
- const processError = (error49, path4 = []) => {
15692
+ const processError = (error49, path5 = []) => {
15427
15693
  var _a2, _b;
15428
15694
  for (const issue2 of error49.issues) {
15429
15695
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -15433,7 +15699,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
15433
15699
  } else if (issue2.code === "invalid_element") {
15434
15700
  processError({ issues: issue2.issues }, issue2.path);
15435
15701
  } else {
15436
- const fullpath = [...path4, ...issue2.path];
15702
+ const fullpath = [...path5, ...issue2.path];
15437
15703
  if (fullpath.length === 0) {
15438
15704
  result.errors.push(mapper(issue2));
15439
15705
  continue;
@@ -15465,8 +15731,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
15465
15731
  }
15466
15732
  function toDotPath(_path) {
15467
15733
  const segs = [];
15468
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
15469
- for (const seg of path4) {
15734
+ const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
15735
+ for (const seg of path5) {
15470
15736
  if (typeof seg === "number")
15471
15737
  segs.push(`[${seg}]`);
15472
15738
  else if (typeof seg === "symbol")
@@ -27872,13 +28138,13 @@ function resolveRef(ref, ctx) {
27872
28138
  if (!ref.startsWith("#")) {
27873
28139
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
27874
28140
  }
27875
- const path4 = ref.slice(1).split("/").filter(Boolean);
27876
- if (path4.length === 0) {
28141
+ const path5 = ref.slice(1).split("/").filter(Boolean);
28142
+ if (path5.length === 0) {
27877
28143
  return ctx.rootSchema;
27878
28144
  }
27879
28145
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
27880
- if (path4[0] === defsKey) {
27881
- const key = path4[1];
28146
+ if (path5[0] === defsKey) {
28147
+ const key = path5[1];
27882
28148
  if (!key || !ctx.defs[key]) {
27883
28149
  throw new Error(`Reference not found: ${ref}`);
27884
28150
  }
@@ -33807,12 +34073,13 @@ function registerReadingTools(server, accountManager) {
33807
34073
  "Get a single email by ID",
33808
34074
  {
33809
34075
  accountId: external_exports3.string(),
33810
- emailId: external_exports3.string()
34076
+ emailId: external_exports3.string(),
34077
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
33811
34078
  },
33812
34079
  async (args) => {
33813
34080
  try {
33814
34081
  const provider = await accountManager.getProvider(args.accountId);
33815
- const email3 = await provider.getEmail(args.emailId);
34082
+ const email3 = await provider.getEmail(args.emailId, args.sourceFolder);
33816
34083
  return jsonResult2(email3);
33817
34084
  } catch (error48) {
33818
34085
  return jsonResult2({ error: error48.message });
@@ -33824,12 +34091,13 @@ function registerReadingTools(server, accountManager) {
33824
34091
  "Get an email thread by thread ID",
33825
34092
  {
33826
34093
  accountId: external_exports3.string(),
33827
- threadId: external_exports3.string()
34094
+ threadId: external_exports3.string(),
34095
+ sourceFolder: external_exports3.string().optional().describe("Folder to look for the thread in on IMAP/iCloud, where a thread is searched in one folder (default INBOX). Gmail and Outlook ignore it")
33828
34096
  },
33829
34097
  async (args) => {
33830
34098
  try {
33831
34099
  const provider = await accountManager.getProvider(args.accountId);
33832
- const thread = await provider.getThread(args.threadId);
34100
+ const thread = await provider.getThread(args.threadId, args.sourceFolder);
33833
34101
  return jsonResult2(thread);
33834
34102
  } catch (error48) {
33835
34103
  return jsonResult2({ error: error48.message });
@@ -33842,12 +34110,13 @@ function registerReadingTools(server, accountManager) {
33842
34110
  {
33843
34111
  accountId: external_exports3.string(),
33844
34112
  emailId: external_exports3.string(),
33845
- attachmentId: external_exports3.string()
34113
+ attachmentId: external_exports3.string(),
34114
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
33846
34115
  },
33847
34116
  async (args) => {
33848
34117
  try {
33849
34118
  const provider = await accountManager.getProvider(args.accountId);
33850
- const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
34119
+ const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId, args.sourceFolder);
33851
34120
  return jsonResult2({
33852
34121
  data: Buffer.from(data).toString("base64"),
33853
34122
  meta: meta3
@@ -33864,13 +34133,14 @@ function registerReadingTools(server, accountManager) {
33864
34133
  accountId: external_exports3.string(),
33865
34134
  emailId: external_exports3.string(),
33866
34135
  attachmentId: external_exports3.string(),
33867
- outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as")
34136
+ outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as"),
34137
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
33868
34138
  },
33869
34139
  async (args) => {
33870
34140
  try {
33871
34141
  const absPath = resolveDownloadPath(args.outputPath);
33872
34142
  const provider = await accountManager.getProvider(args.accountId);
33873
- const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
34143
+ const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId, args.sourceFolder);
33874
34144
  fs3.mkdirSync(path3.dirname(absPath), { recursive: true, mode: 448 });
33875
34145
  fs3.writeFileSync(absPath, Buffer.from(data), { mode: 384 });
33876
34146
  fs3.chmodSync(absPath, 384);
@@ -33887,6 +34157,161 @@ function registerReadingTools(server, accountManager) {
33887
34157
  );
33888
34158
  }
33889
34159
 
34160
+ // src/tools/attachments.ts
34161
+ import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
34162
+ import os4 from "node:os";
34163
+ import path4 from "node:path";
34164
+ var MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024;
34165
+ var ATTACHMENTS_DIR_ENV = "EMAIL_MCP_ATTACHMENTS_DIR";
34166
+ var MIME_BY_EXT = {
34167
+ ".pdf": "application/pdf",
34168
+ ".png": "image/png",
34169
+ ".jpg": "image/jpeg",
34170
+ ".jpeg": "image/jpeg",
34171
+ ".gif": "image/gif",
34172
+ ".webp": "image/webp",
34173
+ ".txt": "text/plain",
34174
+ ".csv": "text/csv",
34175
+ ".html": "text/html",
34176
+ ".json": "application/json",
34177
+ ".zip": "application/zip",
34178
+ ".doc": "application/msword",
34179
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
34180
+ ".xls": "application/vnd.ms-excel",
34181
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
34182
+ ".ics": "text/calendar"
34183
+ };
34184
+ var AttachmentInputSchema = external_exports3.object({
34185
+ path: external_exports3.string().optional().describe(
34186
+ `A file inside the folder the user set in ${ATTACHMENTS_DIR_ENV}: its name, a path relative to that folder, or an absolute path inside it. Files anywhere else are refused, and path attachments are off when that variable is not set.`
34187
+ ),
34188
+ content: external_exports3.string().optional().describe("Base64-encoded file content, used instead of path"),
34189
+ filename: external_exports3.string().optional().describe("File name shown to the recipient. Required with content; defaults to the file name of path"),
34190
+ contentType: external_exports3.string().optional().describe("MIME type. Inferred from the file extension when omitted")
34191
+ }).describe("One attachment: give either path or content (base64) plus filename");
34192
+ var AttachmentsSchema = external_exports3.array(AttachmentInputSchema).optional().describe(`Files to attach. Total size is capped at ${MAX_TOTAL_ATTACHMENT_BYTES / (1024 * 1024)} MB`);
34193
+ function inferContentType(filename) {
34194
+ return MIME_BY_EXT[path4.extname(filename).toLowerCase()] ?? "application/octet-stream";
34195
+ }
34196
+ function megabytes2(bytes) {
34197
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
34198
+ }
34199
+ function isInside(dir, target) {
34200
+ const relative = path4.relative(dir, target);
34201
+ return relative !== "" && !relative.startsWith("..") && !path4.isAbsolute(relative);
34202
+ }
34203
+ function attachmentsDir(env = process.env) {
34204
+ const raw = (env[ATTACHMENTS_DIR_ENV] || "").trim();
34205
+ if (!raw) return null;
34206
+ const expanded = raw === "~" || raw.startsWith("~/") ? path4.join(os4.homedir(), raw.slice(1)) : raw;
34207
+ return path4.resolve(expanded);
34208
+ }
34209
+ var HOW_TO_ALLOW = `Set ${ATTACHMENTS_DIR_ENV} to the one folder files may be attached from (in the environment of the email-mcp server, in the MCP client configuration), put the file there and restart the client. Content passed directly as base64 with a filename works without it.`;
34210
+ function planPath(input2, env, dataDir) {
34211
+ const given = input2.path;
34212
+ if (given.trim() === "") throw new Error("Attachment path is empty");
34213
+ const dir = attachmentsDir(env);
34214
+ if (!dir) {
34215
+ throw new Error(`Attaching files by path is switched off: ${ATTACHMENTS_DIR_ENV} is not set. ${HOW_TO_ALLOW}`);
34216
+ }
34217
+ let dirReal;
34218
+ try {
34219
+ dirReal = realpathSync(dir);
34220
+ if (!statSync(dirReal).isDirectory()) throw new Error("not a directory");
34221
+ } catch {
34222
+ throw new Error(`The attachments folder ${dir} (${ATTACHMENTS_DIR_ENV}) does not exist or is not a folder.`);
34223
+ }
34224
+ const candidate = path4.resolve(dir, given);
34225
+ if (!isInside(dir, candidate) && !isInside(dirReal, candidate)) {
34226
+ throw new Error(`Only files inside ${dir} can be attached (${ATTACHMENTS_DIR_ENV}). Put the file there and pass its name.`);
34227
+ }
34228
+ let real;
34229
+ try {
34230
+ real = realpathSync(candidate);
34231
+ } catch {
34232
+ throw new Error(`No such file in the attachments folder: ${path4.relative(dir, candidate)}`);
34233
+ }
34234
+ if (!isInside(dirReal, real)) {
34235
+ throw new Error(`Only files inside ${dir} can be attached (${ATTACHMENTS_DIR_ENV}); ${path4.relative(dir, candidate)} points outside it.`);
34236
+ }
34237
+ let ownDir = null;
34238
+ try {
34239
+ ownDir = realpathSync(dataDir);
34240
+ } catch {
34241
+ ownDir = null;
34242
+ }
34243
+ if (ownDir && path4.dirname(real) === ownDir) {
34244
+ throw new Error("Files in the email-mcp data folder itself cannot be attached.");
34245
+ }
34246
+ const stat = lstatSync(real);
34247
+ if (!stat.isFile()) {
34248
+ throw new Error(`Not a file: ${path4.relative(dir, candidate)}`);
34249
+ }
34250
+ const filename = input2.filename ?? path4.basename(real);
34251
+ return {
34252
+ bytes: stat.size,
34253
+ load: () => ({
34254
+ filename,
34255
+ content: readValidatedFile(real, stat, path4.relative(dir, candidate)),
34256
+ contentType: input2.contentType ?? inferContentType(filename)
34257
+ })
34258
+ };
34259
+ }
34260
+ function readValidatedFile(real, validated, display) {
34261
+ let fd;
34262
+ try {
34263
+ fd = openSync(real, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
34264
+ } catch {
34265
+ throw new Error(`${display} changed while it was being attached; try again.`);
34266
+ }
34267
+ try {
34268
+ const opened = fstatSync(fd);
34269
+ if (!opened.isFile() || opened.dev !== validated.dev || opened.ino !== validated.ino) {
34270
+ throw new Error(`${display} changed while it was being attached; try again.`);
34271
+ }
34272
+ return readFileSync(fd);
34273
+ } finally {
34274
+ closeSync(fd);
34275
+ }
34276
+ }
34277
+ function planContent(input2) {
34278
+ if (!input2.filename) throw new Error("Attachment with content requires a filename");
34279
+ const filename = input2.filename;
34280
+ const compact = input2.content.replace(/\s+/g, "");
34281
+ if (compact === "") throw new Error(`Attachment ${filename} has empty content`);
34282
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) {
34283
+ throw new Error(`Attachment ${filename}: content is not valid base64`);
34284
+ }
34285
+ const padding = compact.endsWith("==") ? 2 : compact.endsWith("=") ? 1 : 0;
34286
+ return {
34287
+ bytes: Math.floor(compact.length * 3 / 4) - padding,
34288
+ load: () => ({
34289
+ filename,
34290
+ content: Buffer.from(compact, "base64"),
34291
+ contentType: input2.contentType ?? inferContentType(filename)
34292
+ })
34293
+ };
34294
+ }
34295
+ function plan(input2, env, dataDir) {
34296
+ const hasPath = input2.path !== void 0;
34297
+ const hasContent = input2.content !== void 0;
34298
+ if (hasPath && hasContent) throw new Error("Attachment must have either path or content, not both");
34299
+ if (hasPath) return planPath(input2, env, dataDir);
34300
+ if (hasContent) return planContent(input2);
34301
+ throw new Error("Attachment needs a path or content");
34302
+ }
34303
+ function resolveAttachments(inputs, env = process.env, dataDir = path4.join(os4.homedir(), ".email-mcp")) {
34304
+ if (inputs === void 0) return void 0;
34305
+ const planned = inputs.map((input2) => plan(input2, env, dataDir));
34306
+ assertAttachmentTotal(planned.reduce((sum, p) => sum + p.bytes, 0));
34307
+ return planned.map((p) => p.load());
34308
+ }
34309
+ function assertAttachmentTotal(totalBytes) {
34310
+ if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
34311
+ throw new Error(`Attachments total ${megabytes2(totalBytes)}, over the ${megabytes2(MAX_TOTAL_ATTACHMENT_BYTES)} limit`);
34312
+ }
34313
+ }
34314
+
33890
34315
  // src/tools/sending.ts
33891
34316
  var ContactSchema = external_exports3.object({
33892
34317
  email: external_exports3.string(),
@@ -33899,6 +34324,31 @@ var BodySchema = external_exports3.object({
33899
34324
  function jsonResult3(data) {
33900
34325
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
33901
34326
  }
34327
+ function messageIds(headerValue2) {
34328
+ return headerValue2?.match(/<[^<>\s]+>/g) ?? [];
34329
+ }
34330
+ function replyThreading(original) {
34331
+ const [messageId] = messageIds(original.headers?.["message-id"]);
34332
+ if (!messageId) return { threadId: original.threadId, replyToGraphId: original.id };
34333
+ const earlier = messageIds(original.headers?.["references"]).filter((id) => id !== messageId);
34334
+ return {
34335
+ inReplyTo: messageId,
34336
+ references: [...earlier, messageId],
34337
+ threadId: original.threadId,
34338
+ replyToGraphId: original.id
34339
+ };
34340
+ }
34341
+ var sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
34342
+ function withoutDuplicates(to, cc) {
34343
+ const seen = /* @__PURE__ */ new Set();
34344
+ const firstMention = (c) => {
34345
+ const key = c.email.toLowerCase();
34346
+ if (seen.has(key)) return false;
34347
+ seen.add(key);
34348
+ return true;
34349
+ };
34350
+ return { to: to.filter(firstMention), cc: cc?.filter(firstMention) };
34351
+ }
33902
34352
  function registerSendingTools(server, accountManager) {
33903
34353
  server.tool(
33904
34354
  "email_send",
@@ -33909,7 +34359,8 @@ function registerSendingTools(server, accountManager) {
33909
34359
  cc: external_exports3.array(ContactSchema).optional(),
33910
34360
  bcc: external_exports3.array(ContactSchema).optional(),
33911
34361
  subject: external_exports3.string(),
33912
- body: BodySchema
34362
+ body: BodySchema,
34363
+ attachments: AttachmentsSchema
33913
34364
  },
33914
34365
  async (args) => {
33915
34366
  try {
@@ -33919,7 +34370,8 @@ function registerSendingTools(server, accountManager) {
33919
34370
  cc: args.cc,
33920
34371
  bcc: args.bcc,
33921
34372
  subject: args.subject,
33922
- body: args.body
34373
+ body: args.body,
34374
+ attachments: resolveAttachments(args.attachments)
33923
34375
  };
33924
34376
  const result = await provider.sendEmail(params);
33925
34377
  return jsonResult3(result);
@@ -33930,32 +34382,53 @@ function registerSendingTools(server, accountManager) {
33930
34382
  );
33931
34383
  server.tool(
33932
34384
  "email_reply",
33933
- "Reply to an existing email",
34385
+ "Reply to an existing email, in its thread. With replyAll the reply goes to the original sender and all To and Cc recipients, except the account's own address",
33934
34386
  {
33935
34387
  accountId: external_exports3.string(),
33936
34388
  emailId: external_exports3.string(),
34389
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)"),
33937
34390
  body: BodySchema,
33938
- replyAll: external_exports3.boolean().optional()
34391
+ replyAll: external_exports3.boolean().optional(),
34392
+ to: external_exports3.array(ContactSchema).optional().describe("Replace the default To (the original sender). Use it when the original was sent by you, so the reply goes to the real correspondents instead of back to yourself"),
34393
+ cc: external_exports3.array(ContactSchema).optional().describe("Cc recipients. Replaces any Cc carried over by replyAll"),
34394
+ bcc: external_exports3.array(ContactSchema).optional(),
34395
+ additionalRecipients: external_exports3.array(ContactSchema).optional().describe("Extra To recipients added to the default or given To (deduplicated by address)"),
34396
+ attachments: AttachmentsSchema
33939
34397
  },
33940
34398
  async (args) => {
33941
34399
  try {
33942
34400
  const provider = await accountManager.getProvider(args.accountId);
33943
- const original = await provider.getEmail(args.emailId);
33944
- const messageId = original.headers?.["message-id"] ?? original.id;
34401
+ const original = await provider.getEmail(args.emailId, args.sourceFolder);
33945
34402
  const subject = original.subject.startsWith("Re: ") ? original.subject : `Re: ${original.subject}`;
33946
34403
  let to = [original.from];
33947
34404
  let cc;
33948
34405
  if (args.replyAll) {
33949
- to = [original.from, ...original.to];
33950
- cc = original.cc;
34406
+ const own = await accountManager.getAccountEmail(args.accountId);
34407
+ const others = (c) => !own || !sameAddress(c.email, own);
34408
+ to = [original.from, ...original.to].filter(others);
34409
+ if (to.length === 0) to = [original.from];
34410
+ cc = original.cc?.filter(others);
34411
+ }
34412
+ if (args.to?.length) to = args.to;
34413
+ if (args.additionalRecipients?.length) {
34414
+ const seen = new Set(to.map((c) => c.email.toLowerCase()));
34415
+ for (const r of args.additionalRecipients) {
34416
+ if (!seen.has(r.email.toLowerCase())) {
34417
+ to = [...to, r];
34418
+ seen.add(r.email.toLowerCase());
34419
+ }
34420
+ }
33951
34421
  }
34422
+ if (args.cc !== void 0) cc = args.cc;
34423
+ if (args.replyAll) ({ to, cc } = withoutDuplicates(to, cc));
33952
34424
  const params = {
33953
34425
  to,
33954
34426
  cc,
34427
+ bcc: args.bcc,
33955
34428
  subject,
33956
34429
  body: args.body,
33957
- inReplyTo: messageId,
33958
- references: [messageId]
34430
+ ...replyThreading(original),
34431
+ attachments: resolveAttachments(args.attachments)
33959
34432
  };
33960
34433
  const result = await provider.sendEmail(params);
33961
34434
  return jsonResult3(result);
@@ -33966,17 +34439,22 @@ function registerSendingTools(server, accountManager) {
33966
34439
  );
33967
34440
  server.tool(
33968
34441
  "email_forward",
33969
- "Forward an email to new recipients",
34442
+ "Forward an email to new recipients. The original message's attachments are left out unless includeOriginalAttachments is true",
33970
34443
  {
33971
34444
  accountId: external_exports3.string(),
33972
34445
  emailId: external_exports3.string(),
33973
34446
  to: external_exports3.array(ContactSchema),
33974
- body: BodySchema.optional()
34447
+ cc: external_exports3.array(ContactSchema).optional(),
34448
+ bcc: external_exports3.array(ContactSchema).optional(),
34449
+ body: BodySchema.optional(),
34450
+ attachments: AttachmentsSchema.describe("Files to attach to the forward. Total size, together with forwarded original attachments, is capped at 25 MB"),
34451
+ 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"),
34452
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
33975
34453
  },
33976
34454
  async (args) => {
33977
34455
  try {
33978
34456
  const provider = await accountManager.getProvider(args.accountId);
33979
- const original = await provider.getEmail(args.emailId);
34457
+ const original = await provider.getEmail(args.emailId, args.sourceFolder);
33980
34458
  const subject = original.subject.startsWith("Fwd: ") ? original.subject : `Fwd: ${original.subject}`;
33981
34459
  const forwardHeader = [
33982
34460
  "---------- Forwarded message ----------",
@@ -34010,10 +34488,27 @@ function registerSendingTools(server, accountManager) {
34010
34488
  originalHtml
34011
34489
  ].join("");
34012
34490
  }
34491
+ const extra = resolveAttachments(args.attachments) ?? [];
34492
+ const forwarded = [];
34493
+ if (args.includeOriginalAttachments && original.attachments?.length) {
34494
+ const extraBytes = extra.reduce((sum, a) => sum + a.content.length, 0);
34495
+ assertAttachmentTotal(extraBytes + original.attachments.reduce((sum, a) => sum + (a.size || 0), 0));
34496
+ let bytes = extraBytes;
34497
+ for (const att of original.attachments) {
34498
+ const fetched = await provider.getAttachment(original.id, att.id, args.sourceFolder);
34499
+ bytes += fetched.data.length;
34500
+ assertAttachmentTotal(bytes);
34501
+ forwarded.push({ filename: att.filename, content: fetched.data, contentType: att.contentType });
34502
+ }
34503
+ }
34504
+ const attachments = [...forwarded, ...extra];
34013
34505
  const params = {
34014
34506
  to: args.to,
34507
+ cc: args.cc,
34508
+ bcc: args.bcc,
34015
34509
  subject,
34016
- body: { text: forwardedText, html: forwardedHtml }
34510
+ body: { text: forwardedText, html: forwardedHtml },
34511
+ attachments: attachments.length ? attachments : void 0
34017
34512
  };
34018
34513
  const result = await provider.sendEmail(params);
34019
34514
  return jsonResult3(result);
@@ -34024,12 +34519,15 @@ function registerSendingTools(server, accountManager) {
34024
34519
  );
34025
34520
  server.tool(
34026
34521
  "email_draft_create",
34027
- "Create a new email draft",
34522
+ "Create a new email draft. Pass inReplyToEmailId to save it as a reply in that message's thread",
34028
34523
  {
34029
34524
  accountId: external_exports3.string(),
34030
34525
  to: external_exports3.array(ContactSchema),
34031
34526
  subject: external_exports3.string(),
34032
- body: BodySchema
34527
+ body: BodySchema,
34528
+ attachments: AttachmentsSchema,
34529
+ 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)"),
34530
+ 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")
34033
34531
  },
34034
34532
  async (args) => {
34035
34533
  try {
@@ -34037,8 +34535,12 @@ function registerSendingTools(server, accountManager) {
34037
34535
  const params = {
34038
34536
  to: args.to,
34039
34537
  subject: args.subject,
34040
- body: args.body
34538
+ body: args.body,
34539
+ attachments: resolveAttachments(args.attachments)
34041
34540
  };
34541
+ if (args.inReplyToEmailId) {
34542
+ Object.assign(params, replyThreading(await provider.getEmail(args.inReplyToEmailId, args.inReplyToSourceFolder)));
34543
+ }
34042
34544
  const result = await provider.createDraft(params);
34043
34545
  return jsonResult3(result);
34044
34546
  } catch (error48) {
@@ -34048,13 +34550,16 @@ function registerSendingTools(server, accountManager) {
34048
34550
  );
34049
34551
  server.tool(
34050
34552
  "email_draft_update",
34051
- "Update an existing email draft in place (to/subject/body). On Gmail and Outlook the draft id is unchanged. On iCloud/generic IMAP there is no in-place update \u2014 the old draft is deleted and a new one appended \u2014 so the returned id is a NEW id and the old one no longer resolves; always use the id this call returns, not the one you passed in.",
34553
+ "Update an existing email draft in place (to/subject/body). A reply draft stays in its thread. On Gmail and Outlook the draft id is unchanged. On iCloud/generic IMAP there is no in-place update (the old draft is deleted and a new one appended), so the returned id is a NEW id and the old one no longer resolves; always use the id this call returns, not the one you passed in.",
34052
34554
  {
34053
34555
  accountId: external_exports3.string(),
34054
34556
  draftId: external_exports3.string(),
34055
34557
  to: external_exports3.array(ContactSchema),
34056
34558
  subject: external_exports3.string(),
34057
34559
  body: BodySchema,
34560
+ attachments: AttachmentsSchema.describe(
34561
+ "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."
34562
+ ),
34058
34563
  sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
34059
34564
  },
34060
34565
  async (args) => {
@@ -34063,7 +34568,8 @@ function registerSendingTools(server, accountManager) {
34063
34568
  const params = {
34064
34569
  to: args.to,
34065
34570
  subject: args.subject,
34066
- body: args.body
34571
+ body: args.body,
34572
+ attachments: resolveAttachments(args.attachments)
34067
34573
  };
34068
34574
  const result = await provider.updateDraft(args.draftId, params, args.sourceFolder);
34069
34575
  return jsonResult3(result);
@@ -34605,7 +35111,7 @@ var FORWARD_ALLOWLIST_ENV = "EMAIL_MCP_FORWARD_ALLOWLIST";
34605
35111
  function forwardAllowlist(env = process.env) {
34606
35112
  return (env[FORWARD_ALLOWLIST_ENV] || "").split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
34607
35113
  }
34608
- var HOW_TO_ALLOW = `Forwarding targets are set by the user, not by the assistant: add the address to ${FORWARD_ALLOWLIST_ENV} (comma-separated) in the environment of the email-mcp server, in the MCP client configuration, then restart the client.`;
35114
+ var HOW_TO_ALLOW2 = `Forwarding targets are set by the user, not by the assistant: add the address to ${FORWARD_ALLOWLIST_ENV} (comma-separated) in the environment of the email-mcp server, in the MCP client configuration, then restart the client.`;
34609
35115
  function withScopeHint(error48) {
34610
35116
  const message = error48?.message || String(error48);
34611
35117
  const status = error48?.code ?? error48?.statusCode ?? error48?.status ?? error48?.response?.status;
@@ -34643,14 +35149,14 @@ function registerForwardingTools(server, accountManager) {
34643
35149
  if (allowed.length === 0) {
34644
35150
  return jsonResult6({
34645
35151
  success: false,
34646
- error: `Forwarding is switched off: ${FORWARD_ALLOWLIST_ENV} is not set. ${HOW_TO_ALLOW}`
35152
+ error: `Forwarding is switched off: ${FORWARD_ALLOWLIST_ENV} is not set. ${HOW_TO_ALLOW2}`
34647
35153
  });
34648
35154
  }
34649
35155
  const forwardTo = args.forwardTo.trim();
34650
35156
  if (!allowed.includes(forwardTo.toLowerCase())) {
34651
35157
  return jsonResult6({
34652
35158
  success: false,
34653
- error: `${forwardTo} is not an allowed forwarding target. ${HOW_TO_ALLOW}`,
35159
+ error: `${forwardTo} is not an allowed forwarding target. ${HOW_TO_ALLOW2}`,
34654
35160
  allowedTargets: allowed
34655
35161
  });
34656
35162
  }
@@ -34716,7 +35222,7 @@ function registerForwardingTools(server, accountManager) {
34716
35222
  }
34717
35223
 
34718
35224
  // src/server.ts
34719
- var SERVER_VERSION = "1.9.0";
35225
+ var SERVER_VERSION = "1.10.1";
34720
35226
  async function createServer(accountManager) {
34721
35227
  const mgr = accountManager ?? new AccountManager();
34722
35228
  const server = new McpServer({