@marlinjai/email-mcp 1.9.0 → 1.10.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/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,41 @@ 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 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.
7392
+ */
7393
+ async updateDraft(draftId, update) {
7380
7394
  const gmail = this.ensureConnected();
7395
+ let params = update;
7396
+ if (update.threadId === void 0) {
7397
+ const existing = await gmail.users.drafts.get({
7398
+ userId: "me",
7399
+ 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"
7403
+ });
7404
+ 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] };
7410
+ }
7411
+ }
7381
7412
  const raw = await this.buildRaw(params);
7382
7413
  const res = await gmail.users.drafts.update({
7383
7414
  userId: "me",
7384
7415
  id: draftId,
7385
7416
  requestBody: {
7386
- message: { raw }
7417
+ message: { raw, ...this.threadFor(params) }
7387
7418
  }
7388
7419
  });
7389
7420
  return { id: res.data.id || draftId };
@@ -7762,6 +7793,16 @@ var init_adapter = __esm({
7762
7793
  }
7763
7794
  await this.deleteBlockRule(ruleId);
7764
7795
  }
7796
+ /**
7797
+ * Gmail files a message into an existing thread only when the threadId
7798
+ * comes with In-Reply-To and References headers and a subject (which has to
7799
+ * match the thread's; the reply tool builds "Re: <original subject>"). A
7800
+ * threadId without them is left out, and the message starts its own thread.
7801
+ */
7802
+ threadFor(params) {
7803
+ const belongs = params.threadId && params.inReplyTo && params.references?.length && params.subject.trim();
7804
+ return belongs ? { threadId: params.threadId } : {};
7805
+ }
7765
7806
  /**
7766
7807
  * Builds the base64url `raw` payload Gmail expects for send, draft create and
7767
7808
  * draft update: a full MIME message with a text part, an HTML part when one
@@ -8055,12 +8096,41 @@ var init_mapper2 = __esm({
8055
8096
 
8056
8097
  // src/providers/outlook/adapter.ts
8057
8098
  import { Client } from "@microsoft/microsoft-graph-client";
8058
- var OutlookAdapter;
8099
+ function megabytes(bytes) {
8100
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
8101
+ }
8102
+ function assertOutlookAttachmentSizes(attachments) {
8103
+ if (!attachments?.length) return;
8104
+ const limit = megabytes(OUTLOOK_MAX_ATTACHMENT_BYTES);
8105
+ for (const att of attachments) {
8106
+ if (att.content.length > OUTLOOK_MAX_ATTACHMENT_BYTES) {
8107
+ throw new Error(`Outlook accepts attachments up to ${limit} through email-mcp; ${att.filename} is ${megabytes(att.content.length)}. ${UPLOAD_SESSION_HINT}`);
8108
+ }
8109
+ }
8110
+ }
8111
+ function assertOutlookRequestSize(body) {
8112
+ const bytes = Buffer.byteLength(JSON.stringify(body));
8113
+ if (bytes > OUTLOOK_MAX_REQUEST_BYTES) {
8114
+ throw new Error(
8115
+ `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}`
8116
+ );
8117
+ }
8118
+ }
8119
+ var toGraphAttachment, OUTLOOK_MAX_ATTACHMENT_BYTES, OUTLOOK_MAX_REQUEST_BYTES, UPLOAD_SESSION_HINT, OutlookAdapter;
8059
8120
  var init_adapter2 = __esm({
8060
8121
  "src/providers/outlook/adapter.ts"() {
8061
8122
  "use strict";
8062
8123
  init_types();
8063
8124
  init_mapper2();
8125
+ toGraphAttachment = (att) => ({
8126
+ "@odata.type": "#microsoft.graph.fileAttachment",
8127
+ name: att.filename,
8128
+ contentType: att.contentType,
8129
+ contentBytes: att.content.toString("base64")
8130
+ });
8131
+ OUTLOOK_MAX_ATTACHMENT_BYTES = 3 * 1024 * 1024;
8132
+ OUTLOOK_MAX_REQUEST_BYTES = 4 * 1024 * 1024;
8133
+ UPLOAD_SESSION_HINT = "Larger files need an upload session, which email-mcp does not implement for Outlook yet.";
8064
8134
  OutlookAdapter = class _OutlookAdapter {
8065
8135
  providerType = ProviderType.Outlook;
8066
8136
  client = null;
@@ -8079,8 +8149,8 @@ var init_adapter2 = __esm({
8079
8149
  }
8080
8150
  });
8081
8151
  const originalApi = client.api.bind(client);
8082
- client.api = (path4) => {
8083
- return originalApi(path4).header("Prefer", 'IdType="ImmutableId"');
8152
+ client.api = (path5) => {
8153
+ return originalApi(path5).header("Prefer", 'IdType="ImmutableId"');
8084
8154
  };
8085
8155
  this.client = client;
8086
8156
  }
@@ -8200,31 +8270,125 @@ var init_adapter2 = __esm({
8200
8270
  meta: mapGraphAttachment(attachment)
8201
8271
  };
8202
8272
  }
8273
+ /**
8274
+ * Graph only threads a reply (conversationId, subject, quoted history)
8275
+ * through the createReply flow: POST /me/messages and /me/sendMail always
8276
+ * start a new conversation whatever In-Reply-To says. Build the reply draft,
8277
+ * overlay our subject, body, recipients and attachments, and return its id.
8278
+ * Note the body replaces Graph's pre-filled quoted history.
8279
+ *
8280
+ * Each attachment goes up in a request of its own, so the sizes are checked
8281
+ * per file, and before createReply so that a file that is too big leaves no
8282
+ * draft behind. A draft that cannot be filled is deleted again. (A draft
8283
+ * whose send failed is kept, see `sendEmail`.)
8284
+ */
8285
+ async createReplyDraft(params, replyToId) {
8286
+ const client = this.ensureClient();
8287
+ assertOutlookAttachmentSizes(params.attachments);
8288
+ const draft = await client.api(`/me/messages/${encodeURIComponent(replyToId)}/createReply`).post({});
8289
+ try {
8290
+ await this.fillReplyDraft(draft.id, params);
8291
+ } catch (error48) {
8292
+ throw await this.discardDraft(draft.id, error48);
8293
+ }
8294
+ return draft.id;
8295
+ }
8296
+ async fillReplyDraft(draftId, params) {
8297
+ const client = this.ensureClient();
8298
+ const toGraphRecipient = (c) => ({
8299
+ emailAddress: { name: c.name, address: c.email }
8300
+ });
8301
+ const patch = {
8302
+ // Always the caller's subject: Graph pre-fills its own "RE: ..." one.
8303
+ subject: params.subject,
8304
+ body: { contentType: params.body.html ? "html" : "text", content: params.body.html || params.body.text || "" }
8305
+ };
8306
+ if (params.to?.length) patch.toRecipients = params.to.map(toGraphRecipient);
8307
+ if (params.cc !== void 0) patch.ccRecipients = params.cc.map(toGraphRecipient);
8308
+ if (params.bcc !== void 0) patch.bccRecipients = params.bcc.map(toGraphRecipient);
8309
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(patch);
8310
+ for (const att of params.attachments ?? []) {
8311
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post(toGraphAttachment(att));
8312
+ }
8313
+ }
8314
+ /**
8315
+ * Deletes a reply draft that could not be filled, so a failed reply leaves
8316
+ * nothing half-built in Drafts. Returns the error to throw: the original
8317
+ * one, or, when the delete fails as well, the original message with a note
8318
+ * that the draft is still there.
8319
+ */
8320
+ async discardDraft(draftId, cause) {
8321
+ try {
8322
+ await this.ensureClient().api(`/me/messages/${encodeURIComponent(draftId)}`).delete();
8323
+ return cause;
8324
+ } catch (deleteError) {
8325
+ const message = cause instanceof Error ? cause.message : String(cause);
8326
+ return new Error(
8327
+ `${message} (the reply draft ${draftId} could not be removed and was left in Drafts: ${deleteError?.message ?? deleteError})`,
8328
+ { cause }
8329
+ );
8330
+ }
8331
+ }
8203
8332
  async sendEmail(params) {
8204
8333
  const client = this.ensureClient();
8334
+ if (params.replyToGraphId) {
8335
+ const draftId = await this.createReplyDraft(params, params.replyToGraphId);
8336
+ try {
8337
+ await client.api(`/me/messages/${encodeURIComponent(draftId)}/send`).post({});
8338
+ } catch (error48) {
8339
+ throw new Error(
8340
+ `${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)`,
8341
+ { cause: error48 }
8342
+ );
8343
+ }
8344
+ return { id: draftId };
8345
+ }
8346
+ assertOutlookAttachmentSizes(params.attachments);
8205
8347
  const message = this.buildGraphMessage(params);
8206
8348
  const payload = { message };
8207
8349
  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
- }));
8350
+ payload.message.attachments = params.attachments.map(toGraphAttachment);
8214
8351
  }
8352
+ assertOutlookRequestSize(payload);
8215
8353
  await client.api("/me/sendMail").post(payload);
8216
8354
  return { id: `sent-${Date.now()}` };
8217
8355
  }
8218
8356
  async createDraft(params) {
8219
8357
  const client = this.ensureClient();
8358
+ if (params.replyToGraphId) {
8359
+ return { id: await this.createReplyDraft(params, params.replyToGraphId) };
8360
+ }
8361
+ assertOutlookAttachmentSizes(params.attachments);
8220
8362
  const message = this.buildGraphMessage(params);
8363
+ if (params.attachments?.length) {
8364
+ message.attachments = params.attachments.map(toGraphAttachment);
8365
+ }
8366
+ assertOutlookRequestSize(message);
8221
8367
  const result = await client.api("/me/messages").post(message);
8222
8368
  return { id: result.id };
8223
8369
  }
8224
8370
  async updateDraft(draftId, params) {
8225
8371
  const client = this.ensureClient();
8372
+ assertOutlookAttachmentSizes(params.attachments);
8226
8373
  const message = this.buildGraphMessage(params);
8227
8374
  await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
8375
+ if (params.attachments) {
8376
+ const attachmentsPath = `/me/messages/${encodeURIComponent(draftId)}/attachments`;
8377
+ const previousIds = [];
8378
+ let page = await client.api(attachmentsPath).select("id").get();
8379
+ for (; ; ) {
8380
+ previousIds.push(...(page.value || []).map((att) => att.id));
8381
+ const next = page["@odata.nextLink"];
8382
+ if (!next) break;
8383
+ page = await client.api(next).get();
8384
+ }
8385
+ for (const att of params.attachments) {
8386
+ await client.api(attachmentsPath).post(toGraphAttachment(att));
8387
+ }
8388
+ for (const id of previousIds) {
8389
+ await client.api(`${attachmentsPath}/${encodeURIComponent(id)}`).delete();
8390
+ }
8391
+ }
8228
8392
  return { id: draftId };
8229
8393
  }
8230
8394
  async listDrafts(limit, offset) {
@@ -8781,6 +8945,12 @@ function mapImapFolder(imapFolder) {
8781
8945
  unreadCount: imapFolder.status?.unseen
8782
8946
  };
8783
8947
  }
8948
+ function imapAttachmentId(att, index) {
8949
+ return att.contentId || `att-${index}`;
8950
+ }
8951
+ function headerValue(line) {
8952
+ return line.slice(line.indexOf(":") + 1).replace(/\r?\n[ \t]+/g, " ").trim();
8953
+ }
8784
8954
  function mapParsedEmail(parsed, folder, accountId, uid) {
8785
8955
  const mapContact = (addr) => ({
8786
8956
  name: addr.name || void 0,
@@ -8791,7 +8961,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
8791
8961
  return addrs.value.map(mapContact);
8792
8962
  };
8793
8963
  const attachments = (parsed.attachments || []).map((att, i) => ({
8794
- id: att.contentId || `att-${i}`,
8964
+ id: imapAttachmentId(att, i),
8795
8965
  filename: att.filename || `attachment-${i}`,
8796
8966
  contentType: att.contentType || "application/octet-stream",
8797
8967
  size: att.size || 0
@@ -8820,7 +8990,7 @@ function mapParsedEmail(parsed, folder, accountId, uid) {
8820
8990
  flagged: parsed.flags?.has("\\Flagged") || false,
8821
8991
  draft: parsed.flags?.has("\\Draft") || false
8822
8992
  },
8823
- headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, h.line])) : void 0
8993
+ headers: parsed.headerLines ? Object.fromEntries(parsed.headerLines.map((h) => [h.key, headerValue(h.line)])) : void 0
8824
8994
  };
8825
8995
  }
8826
8996
  var SPECIAL_USE_MAP;
@@ -9248,7 +9418,7 @@ var init_adapter3 = __esm({
9248
9418
  }
9249
9419
  async getEmail(id, folder) {
9250
9420
  const client = await this.ensureConnected();
9251
- const targetFolder = folder || "INBOX";
9421
+ const targetFolder = folder ? await this.resolveFolder(folder) : "INBOX";
9252
9422
  let lock;
9253
9423
  try {
9254
9424
  lock = await client.getMailboxLock(targetFolder);
@@ -9267,13 +9437,20 @@ var init_adapter3 = __esm({
9267
9437
  lock.release();
9268
9438
  }
9269
9439
  }
9270
- async getThread(threadId) {
9440
+ /**
9441
+ * IMAP has no thread object: the thread is the set of messages in ONE
9442
+ * folder whose Message-ID, References or In-Reply-To name `threadId`.
9443
+ * Messages of the same conversation in other folders (the own replies in
9444
+ * Sent, for example) are not part of the result.
9445
+ */
9446
+ async getThread(threadId, sourceFolder) {
9271
9447
  const client = await this.ensureConnected();
9448
+ const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
9272
9449
  let lock;
9273
9450
  try {
9274
- lock = await client.getMailboxLock("INBOX");
9451
+ lock = await client.getMailboxLock(folder);
9275
9452
  } catch (error48) {
9276
- throw formatImapError(error48, 'Failed to open folder "INBOX"');
9453
+ throw formatImapError(error48, `Failed to open folder "${folder}"`);
9277
9454
  }
9278
9455
  try {
9279
9456
  const searchResult = await client.search(
@@ -9281,13 +9458,13 @@ var init_adapter3 = __esm({
9281
9458
  { uid: true }
9282
9459
  );
9283
9460
  const uids = Array.isArray(searchResult) ? searchResult : [];
9284
- if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
9461
+ if (uids.length === 0) throw new Error(`Thread ${threadId} not found in "${folder}"`);
9285
9462
  const messages = [];
9286
9463
  for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
9287
9464
  if (!msg.source) continue;
9288
9465
  const parsed = await simpleParser(msg.source);
9289
9466
  parsed.flags = msg.flags;
9290
- messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
9467
+ messages.push(mapParsedEmail(parsed, folder, this.accountId, msg.uid));
9291
9468
  }
9292
9469
  const participantMap = /* @__PURE__ */ new Map();
9293
9470
  for (const msg of messages) {
@@ -9309,27 +9486,29 @@ var init_adapter3 = __esm({
9309
9486
  lock.release();
9310
9487
  }
9311
9488
  }
9312
- async getAttachment(emailId, attachmentId) {
9489
+ async getAttachment(emailId, attachmentId, sourceFolder) {
9313
9490
  const client = await this.ensureConnected();
9491
+ const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
9314
9492
  let lock;
9315
9493
  try {
9316
- lock = await client.getMailboxLock("INBOX");
9494
+ lock = await client.getMailboxLock(folder);
9317
9495
  } catch (error48) {
9318
- throw formatImapError(error48, 'Failed to open folder "INBOX"');
9496
+ throw formatImapError(error48, `Failed to open folder "${folder}"`);
9319
9497
  }
9320
9498
  try {
9321
9499
  const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
9322
- if (!msg) throw new Error(`Email ${emailId} not found`);
9500
+ if (!msg) throw new Error(`Email ${emailId} not found in "${folder}"`);
9323
9501
  if (!msg.source) throw new Error(`Email ${emailId} has no source`);
9324
9502
  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}`);
9503
+ const attachments = parsed.attachments || [];
9504
+ let index = attachments.findIndex((att, i) => imapAttachmentId(att, i) === attachmentId);
9505
+ if (index === -1) index = attachments.findIndex((att) => att.filename === attachmentId);
9506
+ if (index === -1) throw new Error(`Attachment ${attachmentId} not found in email ${emailId}`);
9507
+ const attachment = attachments[index];
9329
9508
  return {
9330
9509
  data: attachment.content,
9331
9510
  meta: {
9332
- id: attachment.contentId || attachmentId,
9511
+ id: imapAttachmentId(attachment, index),
9333
9512
  filename: attachment.filename || "attachment",
9334
9513
  contentType: attachment.contentType || "application/octet-stream",
9335
9514
  size: attachment.size || 0
@@ -9366,18 +9545,34 @@ var init_adapter3 = __esm({
9366
9545
  * Drafts folder, and IMAP messages are immutable once appended. This
9367
9546
  * deletes the old revision and appends a new one, so the returned `id`
9368
9547
  * is a NEW uid; the old id no longer resolves after this call.
9548
+ *
9549
+ * A reply draft keeps its place in the thread: when the caller passes no
9550
+ * `inReplyTo`, the In-Reply-To and References headers of the old revision
9551
+ * are copied into the new one.
9369
9552
  */
9370
9553
  async updateDraft(draftId, params, sourceFolder) {
9371
9554
  const client = await this.ensureConnected();
9372
9555
  const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
9373
- const rawMessage = await this.buildDraftMessage(params);
9374
9556
  let lock;
9375
9557
  try {
9376
9558
  lock = await client.getMailboxLock(draftsFolder);
9377
9559
  } catch (error48) {
9378
9560
  throw formatImapError(error48, `Failed to open folder "${draftsFolder}"`);
9379
9561
  }
9562
+ let rawMessage;
9380
9563
  try {
9564
+ let next = params;
9565
+ if (params.inReplyTo === void 0) {
9566
+ const old = await client.fetchOne(String(draftId), { source: true, uid: true }, { uid: true });
9567
+ if (old && old.source) {
9568
+ 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 };
9572
+ }
9573
+ }
9574
+ }
9575
+ rawMessage = await this.buildDraftMessage(next);
9381
9576
  await client.messageDelete(draftId, { uid: true });
9382
9577
  } finally {
9383
9578
  lock.release();
@@ -9693,6 +9888,11 @@ var init_account_manager = __esm({
9693
9888
  connected: this.providers.has(c.id)
9694
9889
  }));
9695
9890
  }
9891
+ /** The account's own address, or undefined for an unknown account id. */
9892
+ async getAccountEmail(accountId) {
9893
+ const creds = this.credentials.get(accountId) ?? await this.store.get(accountId);
9894
+ return creds?.email;
9895
+ }
9696
9896
  async getProvider(accountId) {
9697
9897
  const existing = this.providers.get(accountId);
9698
9898
  if (existing) {
@@ -10807,8 +11007,8 @@ function getErrorMap() {
10807
11007
 
10808
11008
  // node_modules/zod/v3/helpers/parseUtil.js
10809
11009
  var makeIssue = (params) => {
10810
- const { data, path: path4, errorMaps, issueData } = params;
10811
- const fullPath = [...path4, ...issueData.path || []];
11010
+ const { data, path: path5, errorMaps, issueData } = params;
11011
+ const fullPath = [...path5, ...issueData.path || []];
10812
11012
  const fullIssue = {
10813
11013
  ...issueData,
10814
11014
  path: fullPath
@@ -10923,11 +11123,11 @@ var errorUtil;
10923
11123
 
10924
11124
  // node_modules/zod/v3/types.js
10925
11125
  var ParseInputLazyPath = class {
10926
- constructor(parent, value, path4, key) {
11126
+ constructor(parent, value, path5, key) {
10927
11127
  this._cachedPath = [];
10928
11128
  this.parent = parent;
10929
11129
  this.data = value;
10930
- this._path = path4;
11130
+ this._path = path5;
10931
11131
  this._key = key;
10932
11132
  }
10933
11133
  get path() {
@@ -14850,10 +15050,10 @@ function mergeDefs(...defs) {
14850
15050
  function cloneDef(schema) {
14851
15051
  return mergeDefs(schema._zod.def);
14852
15052
  }
14853
- function getElementAtPath(obj, path4) {
14854
- if (!path4)
15053
+ function getElementAtPath(obj, path5) {
15054
+ if (!path5)
14855
15055
  return obj;
14856
- return path4.reduce((acc, key) => acc?.[key], obj);
15056
+ return path5.reduce((acc, key) => acc?.[key], obj);
14857
15057
  }
14858
15058
  function promiseAllObject(promisesObj) {
14859
15059
  const keys = Object.keys(promisesObj);
@@ -15236,11 +15436,11 @@ function aborted(x, startIndex = 0) {
15236
15436
  }
15237
15437
  return false;
15238
15438
  }
15239
- function prefixIssues(path4, issues) {
15439
+ function prefixIssues(path5, issues) {
15240
15440
  return issues.map((iss) => {
15241
15441
  var _a2;
15242
15442
  (_a2 = iss).path ?? (_a2.path = []);
15243
- iss.path.unshift(path4);
15443
+ iss.path.unshift(path5);
15244
15444
  return iss;
15245
15445
  });
15246
15446
  }
@@ -15423,7 +15623,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
15423
15623
  }
15424
15624
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
15425
15625
  const result = { errors: [] };
15426
- const processError = (error49, path4 = []) => {
15626
+ const processError = (error49, path5 = []) => {
15427
15627
  var _a2, _b;
15428
15628
  for (const issue2 of error49.issues) {
15429
15629
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -15433,7 +15633,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
15433
15633
  } else if (issue2.code === "invalid_element") {
15434
15634
  processError({ issues: issue2.issues }, issue2.path);
15435
15635
  } else {
15436
- const fullpath = [...path4, ...issue2.path];
15636
+ const fullpath = [...path5, ...issue2.path];
15437
15637
  if (fullpath.length === 0) {
15438
15638
  result.errors.push(mapper(issue2));
15439
15639
  continue;
@@ -15465,8 +15665,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
15465
15665
  }
15466
15666
  function toDotPath(_path) {
15467
15667
  const segs = [];
15468
- const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
15469
- for (const seg of path4) {
15668
+ const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
15669
+ for (const seg of path5) {
15470
15670
  if (typeof seg === "number")
15471
15671
  segs.push(`[${seg}]`);
15472
15672
  else if (typeof seg === "symbol")
@@ -27872,13 +28072,13 @@ function resolveRef(ref, ctx) {
27872
28072
  if (!ref.startsWith("#")) {
27873
28073
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
27874
28074
  }
27875
- const path4 = ref.slice(1).split("/").filter(Boolean);
27876
- if (path4.length === 0) {
28075
+ const path5 = ref.slice(1).split("/").filter(Boolean);
28076
+ if (path5.length === 0) {
27877
28077
  return ctx.rootSchema;
27878
28078
  }
27879
28079
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
27880
- if (path4[0] === defsKey) {
27881
- const key = path4[1];
28080
+ if (path5[0] === defsKey) {
28081
+ const key = path5[1];
27882
28082
  if (!key || !ctx.defs[key]) {
27883
28083
  throw new Error(`Reference not found: ${ref}`);
27884
28084
  }
@@ -33807,12 +34007,13 @@ function registerReadingTools(server, accountManager) {
33807
34007
  "Get a single email by ID",
33808
34008
  {
33809
34009
  accountId: external_exports3.string(),
33810
- emailId: external_exports3.string()
34010
+ emailId: external_exports3.string(),
34011
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
33811
34012
  },
33812
34013
  async (args) => {
33813
34014
  try {
33814
34015
  const provider = await accountManager.getProvider(args.accountId);
33815
- const email3 = await provider.getEmail(args.emailId);
34016
+ const email3 = await provider.getEmail(args.emailId, args.sourceFolder);
33816
34017
  return jsonResult2(email3);
33817
34018
  } catch (error48) {
33818
34019
  return jsonResult2({ error: error48.message });
@@ -33824,12 +34025,13 @@ function registerReadingTools(server, accountManager) {
33824
34025
  "Get an email thread by thread ID",
33825
34026
  {
33826
34027
  accountId: external_exports3.string(),
33827
- threadId: external_exports3.string()
34028
+ threadId: external_exports3.string(),
34029
+ 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
34030
  },
33829
34031
  async (args) => {
33830
34032
  try {
33831
34033
  const provider = await accountManager.getProvider(args.accountId);
33832
- const thread = await provider.getThread(args.threadId);
34034
+ const thread = await provider.getThread(args.threadId, args.sourceFolder);
33833
34035
  return jsonResult2(thread);
33834
34036
  } catch (error48) {
33835
34037
  return jsonResult2({ error: error48.message });
@@ -33842,12 +34044,13 @@ function registerReadingTools(server, accountManager) {
33842
34044
  {
33843
34045
  accountId: external_exports3.string(),
33844
34046
  emailId: external_exports3.string(),
33845
- attachmentId: external_exports3.string()
34047
+ attachmentId: external_exports3.string(),
34048
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
33846
34049
  },
33847
34050
  async (args) => {
33848
34051
  try {
33849
34052
  const provider = await accountManager.getProvider(args.accountId);
33850
- const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
34053
+ const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId, args.sourceFolder);
33851
34054
  return jsonResult2({
33852
34055
  data: Buffer.from(data).toString("base64"),
33853
34056
  meta: meta3
@@ -33864,13 +34067,14 @@ function registerReadingTools(server, accountManager) {
33864
34067
  accountId: external_exports3.string(),
33865
34068
  emailId: external_exports3.string(),
33866
34069
  attachmentId: external_exports3.string(),
33867
- outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as")
34070
+ outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as"),
34071
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)")
33868
34072
  },
33869
34073
  async (args) => {
33870
34074
  try {
33871
34075
  const absPath = resolveDownloadPath(args.outputPath);
33872
34076
  const provider = await accountManager.getProvider(args.accountId);
33873
- const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
34077
+ const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId, args.sourceFolder);
33874
34078
  fs3.mkdirSync(path3.dirname(absPath), { recursive: true, mode: 448 });
33875
34079
  fs3.writeFileSync(absPath, Buffer.from(data), { mode: 384 });
33876
34080
  fs3.chmodSync(absPath, 384);
@@ -33887,6 +34091,161 @@ function registerReadingTools(server, accountManager) {
33887
34091
  );
33888
34092
  }
33889
34093
 
34094
+ // src/tools/attachments.ts
34095
+ import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
34096
+ import os4 from "node:os";
34097
+ import path4 from "node:path";
34098
+ var MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024;
34099
+ var ATTACHMENTS_DIR_ENV = "EMAIL_MCP_ATTACHMENTS_DIR";
34100
+ var MIME_BY_EXT = {
34101
+ ".pdf": "application/pdf",
34102
+ ".png": "image/png",
34103
+ ".jpg": "image/jpeg",
34104
+ ".jpeg": "image/jpeg",
34105
+ ".gif": "image/gif",
34106
+ ".webp": "image/webp",
34107
+ ".txt": "text/plain",
34108
+ ".csv": "text/csv",
34109
+ ".html": "text/html",
34110
+ ".json": "application/json",
34111
+ ".zip": "application/zip",
34112
+ ".doc": "application/msword",
34113
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
34114
+ ".xls": "application/vnd.ms-excel",
34115
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
34116
+ ".ics": "text/calendar"
34117
+ };
34118
+ var AttachmentInputSchema = external_exports3.object({
34119
+ path: external_exports3.string().optional().describe(
34120
+ `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.`
34121
+ ),
34122
+ content: external_exports3.string().optional().describe("Base64-encoded file content, used instead of path"),
34123
+ filename: external_exports3.string().optional().describe("File name shown to the recipient. Required with content; defaults to the file name of path"),
34124
+ contentType: external_exports3.string().optional().describe("MIME type. Inferred from the file extension when omitted")
34125
+ }).describe("One attachment: give either path or content (base64) plus filename");
34126
+ var AttachmentsSchema = external_exports3.array(AttachmentInputSchema).optional().describe(`Files to attach. Total size is capped at ${MAX_TOTAL_ATTACHMENT_BYTES / (1024 * 1024)} MB`);
34127
+ function inferContentType(filename) {
34128
+ return MIME_BY_EXT[path4.extname(filename).toLowerCase()] ?? "application/octet-stream";
34129
+ }
34130
+ function megabytes2(bytes) {
34131
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
34132
+ }
34133
+ function isInside(dir, target) {
34134
+ const relative = path4.relative(dir, target);
34135
+ return relative !== "" && !relative.startsWith("..") && !path4.isAbsolute(relative);
34136
+ }
34137
+ function attachmentsDir(env = process.env) {
34138
+ const raw = (env[ATTACHMENTS_DIR_ENV] || "").trim();
34139
+ if (!raw) return null;
34140
+ const expanded = raw === "~" || raw.startsWith("~/") ? path4.join(os4.homedir(), raw.slice(1)) : raw;
34141
+ return path4.resolve(expanded);
34142
+ }
34143
+ 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.`;
34144
+ function planPath(input2, env, dataDir) {
34145
+ const given = input2.path;
34146
+ if (given.trim() === "") throw new Error("Attachment path is empty");
34147
+ const dir = attachmentsDir(env);
34148
+ if (!dir) {
34149
+ throw new Error(`Attaching files by path is switched off: ${ATTACHMENTS_DIR_ENV} is not set. ${HOW_TO_ALLOW}`);
34150
+ }
34151
+ let dirReal;
34152
+ try {
34153
+ dirReal = realpathSync(dir);
34154
+ if (!statSync(dirReal).isDirectory()) throw new Error("not a directory");
34155
+ } catch {
34156
+ throw new Error(`The attachments folder ${dir} (${ATTACHMENTS_DIR_ENV}) does not exist or is not a folder.`);
34157
+ }
34158
+ const candidate = path4.resolve(dir, given);
34159
+ if (!isInside(dir, candidate) && !isInside(dirReal, candidate)) {
34160
+ throw new Error(`Only files inside ${dir} can be attached (${ATTACHMENTS_DIR_ENV}). Put the file there and pass its name.`);
34161
+ }
34162
+ let real;
34163
+ try {
34164
+ real = realpathSync(candidate);
34165
+ } catch {
34166
+ throw new Error(`No such file in the attachments folder: ${path4.relative(dir, candidate)}`);
34167
+ }
34168
+ if (!isInside(dirReal, real)) {
34169
+ throw new Error(`Only files inside ${dir} can be attached (${ATTACHMENTS_DIR_ENV}); ${path4.relative(dir, candidate)} points outside it.`);
34170
+ }
34171
+ let ownDir = null;
34172
+ try {
34173
+ ownDir = realpathSync(dataDir);
34174
+ } catch {
34175
+ ownDir = null;
34176
+ }
34177
+ if (ownDir && path4.dirname(real) === ownDir) {
34178
+ throw new Error("Files in the email-mcp data folder itself cannot be attached.");
34179
+ }
34180
+ const stat = lstatSync(real);
34181
+ if (!stat.isFile()) {
34182
+ throw new Error(`Not a file: ${path4.relative(dir, candidate)}`);
34183
+ }
34184
+ const filename = input2.filename ?? path4.basename(real);
34185
+ return {
34186
+ bytes: stat.size,
34187
+ load: () => ({
34188
+ filename,
34189
+ content: readValidatedFile(real, stat, path4.relative(dir, candidate)),
34190
+ contentType: input2.contentType ?? inferContentType(filename)
34191
+ })
34192
+ };
34193
+ }
34194
+ function readValidatedFile(real, validated, display) {
34195
+ let fd;
34196
+ try {
34197
+ fd = openSync(real, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
34198
+ } catch {
34199
+ throw new Error(`${display} changed while it was being attached; try again.`);
34200
+ }
34201
+ try {
34202
+ const opened = fstatSync(fd);
34203
+ if (!opened.isFile() || opened.dev !== validated.dev || opened.ino !== validated.ino) {
34204
+ throw new Error(`${display} changed while it was being attached; try again.`);
34205
+ }
34206
+ return readFileSync(fd);
34207
+ } finally {
34208
+ closeSync(fd);
34209
+ }
34210
+ }
34211
+ function planContent(input2) {
34212
+ if (!input2.filename) throw new Error("Attachment with content requires a filename");
34213
+ const filename = input2.filename;
34214
+ const compact = input2.content.replace(/\s+/g, "");
34215
+ if (compact === "") throw new Error(`Attachment ${filename} has empty content`);
34216
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(compact) || compact.length % 4 === 1) {
34217
+ throw new Error(`Attachment ${filename}: content is not valid base64`);
34218
+ }
34219
+ const padding = compact.endsWith("==") ? 2 : compact.endsWith("=") ? 1 : 0;
34220
+ return {
34221
+ bytes: Math.floor(compact.length * 3 / 4) - padding,
34222
+ load: () => ({
34223
+ filename,
34224
+ content: Buffer.from(compact, "base64"),
34225
+ contentType: input2.contentType ?? inferContentType(filename)
34226
+ })
34227
+ };
34228
+ }
34229
+ function plan(input2, env, dataDir) {
34230
+ const hasPath = input2.path !== void 0;
34231
+ const hasContent = input2.content !== void 0;
34232
+ if (hasPath && hasContent) throw new Error("Attachment must have either path or content, not both");
34233
+ if (hasPath) return planPath(input2, env, dataDir);
34234
+ if (hasContent) return planContent(input2);
34235
+ throw new Error("Attachment needs a path or content");
34236
+ }
34237
+ function resolveAttachments(inputs, env = process.env, dataDir = path4.join(os4.homedir(), ".email-mcp")) {
34238
+ if (inputs === void 0) return void 0;
34239
+ const planned = inputs.map((input2) => plan(input2, env, dataDir));
34240
+ assertAttachmentTotal(planned.reduce((sum, p) => sum + p.bytes, 0));
34241
+ return planned.map((p) => p.load());
34242
+ }
34243
+ function assertAttachmentTotal(totalBytes) {
34244
+ if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
34245
+ throw new Error(`Attachments total ${megabytes2(totalBytes)}, over the ${megabytes2(MAX_TOTAL_ATTACHMENT_BYTES)} limit`);
34246
+ }
34247
+ }
34248
+
33890
34249
  // src/tools/sending.ts
33891
34250
  var ContactSchema = external_exports3.object({
33892
34251
  email: external_exports3.string(),
@@ -33899,6 +34258,31 @@ var BodySchema = external_exports3.object({
33899
34258
  function jsonResult3(data) {
33900
34259
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
33901
34260
  }
34261
+ function messageIds(headerValue2) {
34262
+ return headerValue2?.match(/<[^<>\s]+>/g) ?? [];
34263
+ }
34264
+ function replyThreading(original) {
34265
+ const [messageId] = messageIds(original.headers?.["message-id"]);
34266
+ if (!messageId) return { threadId: original.threadId, replyToGraphId: original.id };
34267
+ const earlier = messageIds(original.headers?.["references"]).filter((id) => id !== messageId);
34268
+ return {
34269
+ inReplyTo: messageId,
34270
+ references: [...earlier, messageId],
34271
+ threadId: original.threadId,
34272
+ replyToGraphId: original.id
34273
+ };
34274
+ }
34275
+ var sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
34276
+ function withoutDuplicates(to, cc) {
34277
+ const seen = /* @__PURE__ */ new Set();
34278
+ const firstMention = (c) => {
34279
+ const key = c.email.toLowerCase();
34280
+ if (seen.has(key)) return false;
34281
+ seen.add(key);
34282
+ return true;
34283
+ };
34284
+ return { to: to.filter(firstMention), cc: cc?.filter(firstMention) };
34285
+ }
33902
34286
  function registerSendingTools(server, accountManager) {
33903
34287
  server.tool(
33904
34288
  "email_send",
@@ -33909,7 +34293,8 @@ function registerSendingTools(server, accountManager) {
33909
34293
  cc: external_exports3.array(ContactSchema).optional(),
33910
34294
  bcc: external_exports3.array(ContactSchema).optional(),
33911
34295
  subject: external_exports3.string(),
33912
- body: BodySchema
34296
+ body: BodySchema,
34297
+ attachments: AttachmentsSchema
33913
34298
  },
33914
34299
  async (args) => {
33915
34300
  try {
@@ -33919,7 +34304,8 @@ function registerSendingTools(server, accountManager) {
33919
34304
  cc: args.cc,
33920
34305
  bcc: args.bcc,
33921
34306
  subject: args.subject,
33922
- body: args.body
34307
+ body: args.body,
34308
+ attachments: resolveAttachments(args.attachments)
33923
34309
  };
33924
34310
  const result = await provider.sendEmail(params);
33925
34311
  return jsonResult3(result);
@@ -33930,32 +34316,53 @@ function registerSendingTools(server, accountManager) {
33930
34316
  );
33931
34317
  server.tool(
33932
34318
  "email_reply",
33933
- "Reply to an existing email",
34319
+ "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
34320
  {
33935
34321
  accountId: external_exports3.string(),
33936
34322
  emailId: external_exports3.string(),
34323
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when email is not in INBOX)"),
33937
34324
  body: BodySchema,
33938
- replyAll: external_exports3.boolean().optional()
34325
+ replyAll: external_exports3.boolean().optional(),
34326
+ 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"),
34327
+ cc: external_exports3.array(ContactSchema).optional().describe("Cc recipients. Replaces any Cc carried over by replyAll"),
34328
+ bcc: external_exports3.array(ContactSchema).optional(),
34329
+ additionalRecipients: external_exports3.array(ContactSchema).optional().describe("Extra To recipients added to the default or given To (deduplicated by address)"),
34330
+ attachments: AttachmentsSchema
33939
34331
  },
33940
34332
  async (args) => {
33941
34333
  try {
33942
34334
  const provider = await accountManager.getProvider(args.accountId);
33943
- const original = await provider.getEmail(args.emailId);
33944
- const messageId = original.headers?.["message-id"] ?? original.id;
34335
+ const original = await provider.getEmail(args.emailId, args.sourceFolder);
33945
34336
  const subject = original.subject.startsWith("Re: ") ? original.subject : `Re: ${original.subject}`;
33946
34337
  let to = [original.from];
33947
34338
  let cc;
33948
34339
  if (args.replyAll) {
33949
- to = [original.from, ...original.to];
33950
- cc = original.cc;
34340
+ const own = await accountManager.getAccountEmail(args.accountId);
34341
+ const others = (c) => !own || !sameAddress(c.email, own);
34342
+ to = [original.from, ...original.to].filter(others);
34343
+ if (to.length === 0) to = [original.from];
34344
+ cc = original.cc?.filter(others);
34345
+ }
34346
+ if (args.to?.length) to = args.to;
34347
+ if (args.additionalRecipients?.length) {
34348
+ const seen = new Set(to.map((c) => c.email.toLowerCase()));
34349
+ for (const r of args.additionalRecipients) {
34350
+ if (!seen.has(r.email.toLowerCase())) {
34351
+ to = [...to, r];
34352
+ seen.add(r.email.toLowerCase());
34353
+ }
34354
+ }
33951
34355
  }
34356
+ if (args.cc !== void 0) cc = args.cc;
34357
+ if (args.replyAll) ({ to, cc } = withoutDuplicates(to, cc));
33952
34358
  const params = {
33953
34359
  to,
33954
34360
  cc,
34361
+ bcc: args.bcc,
33955
34362
  subject,
33956
34363
  body: args.body,
33957
- inReplyTo: messageId,
33958
- references: [messageId]
34364
+ ...replyThreading(original),
34365
+ attachments: resolveAttachments(args.attachments)
33959
34366
  };
33960
34367
  const result = await provider.sendEmail(params);
33961
34368
  return jsonResult3(result);
@@ -33966,17 +34373,22 @@ function registerSendingTools(server, accountManager) {
33966
34373
  );
33967
34374
  server.tool(
33968
34375
  "email_forward",
33969
- "Forward an email to new recipients",
34376
+ "Forward an email to new recipients. The original message's attachments are left out unless includeOriginalAttachments is true",
33970
34377
  {
33971
34378
  accountId: external_exports3.string(),
33972
34379
  emailId: external_exports3.string(),
33973
34380
  to: external_exports3.array(ContactSchema),
33974
- body: BodySchema.optional()
34381
+ cc: external_exports3.array(ContactSchema).optional(),
34382
+ bcc: external_exports3.array(ContactSchema).optional(),
34383
+ body: BodySchema.optional(),
34384
+ attachments: AttachmentsSchema.describe("Files to attach to the forward. Total size, together with forwarded original attachments, is capped at 25 MB"),
34385
+ 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)")
33975
34387
  },
33976
34388
  async (args) => {
33977
34389
  try {
33978
34390
  const provider = await accountManager.getProvider(args.accountId);
33979
- const original = await provider.getEmail(args.emailId);
34391
+ const original = await provider.getEmail(args.emailId, args.sourceFolder);
33980
34392
  const subject = original.subject.startsWith("Fwd: ") ? original.subject : `Fwd: ${original.subject}`;
33981
34393
  const forwardHeader = [
33982
34394
  "---------- Forwarded message ----------",
@@ -34010,10 +34422,27 @@ function registerSendingTools(server, accountManager) {
34010
34422
  originalHtml
34011
34423
  ].join("");
34012
34424
  }
34425
+ const extra = resolveAttachments(args.attachments) ?? [];
34426
+ const forwarded = [];
34427
+ if (args.includeOriginalAttachments && original.attachments?.length) {
34428
+ const extraBytes = extra.reduce((sum, a) => sum + a.content.length, 0);
34429
+ assertAttachmentTotal(extraBytes + original.attachments.reduce((sum, a) => sum + (a.size || 0), 0));
34430
+ let bytes = extraBytes;
34431
+ for (const att of original.attachments) {
34432
+ const fetched = await provider.getAttachment(original.id, att.id, args.sourceFolder);
34433
+ bytes += fetched.data.length;
34434
+ assertAttachmentTotal(bytes);
34435
+ forwarded.push({ filename: att.filename, content: fetched.data, contentType: att.contentType });
34436
+ }
34437
+ }
34438
+ const attachments = [...forwarded, ...extra];
34013
34439
  const params = {
34014
34440
  to: args.to,
34441
+ cc: args.cc,
34442
+ bcc: args.bcc,
34015
34443
  subject,
34016
- body: { text: forwardedText, html: forwardedHtml }
34444
+ body: { text: forwardedText, html: forwardedHtml },
34445
+ attachments: attachments.length ? attachments : void 0
34017
34446
  };
34018
34447
  const result = await provider.sendEmail(params);
34019
34448
  return jsonResult3(result);
@@ -34024,12 +34453,15 @@ function registerSendingTools(server, accountManager) {
34024
34453
  );
34025
34454
  server.tool(
34026
34455
  "email_draft_create",
34027
- "Create a new email draft",
34456
+ "Create a new email draft. Pass inReplyToEmailId to save it as a reply in that message's thread",
34028
34457
  {
34029
34458
  accountId: external_exports3.string(),
34030
34459
  to: external_exports3.array(ContactSchema),
34031
34460
  subject: external_exports3.string(),
34032
- body: BodySchema
34461
+ body: BodySchema,
34462
+ attachments: AttachmentsSchema,
34463
+ 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")
34033
34465
  },
34034
34466
  async (args) => {
34035
34467
  try {
@@ -34037,8 +34469,12 @@ function registerSendingTools(server, accountManager) {
34037
34469
  const params = {
34038
34470
  to: args.to,
34039
34471
  subject: args.subject,
34040
- body: args.body
34472
+ body: args.body,
34473
+ attachments: resolveAttachments(args.attachments)
34041
34474
  };
34475
+ if (args.inReplyToEmailId) {
34476
+ Object.assign(params, replyThreading(await provider.getEmail(args.inReplyToEmailId, args.inReplyToSourceFolder)));
34477
+ }
34042
34478
  const result = await provider.createDraft(params);
34043
34479
  return jsonResult3(result);
34044
34480
  } catch (error48) {
@@ -34048,13 +34484,16 @@ function registerSendingTools(server, accountManager) {
34048
34484
  );
34049
34485
  server.tool(
34050
34486
  "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.",
34487
+ "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
34488
  {
34053
34489
  accountId: external_exports3.string(),
34054
34490
  draftId: external_exports3.string(),
34055
34491
  to: external_exports3.array(ContactSchema),
34056
34492
  subject: external_exports3.string(),
34057
34493
  body: BodySchema,
34494
+ 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."
34496
+ ),
34058
34497
  sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
34059
34498
  },
34060
34499
  async (args) => {
@@ -34063,7 +34502,8 @@ function registerSendingTools(server, accountManager) {
34063
34502
  const params = {
34064
34503
  to: args.to,
34065
34504
  subject: args.subject,
34066
- body: args.body
34505
+ body: args.body,
34506
+ attachments: resolveAttachments(args.attachments)
34067
34507
  };
34068
34508
  const result = await provider.updateDraft(args.draftId, params, args.sourceFolder);
34069
34509
  return jsonResult3(result);
@@ -34605,7 +35045,7 @@ var FORWARD_ALLOWLIST_ENV = "EMAIL_MCP_FORWARD_ALLOWLIST";
34605
35045
  function forwardAllowlist(env = process.env) {
34606
35046
  return (env[FORWARD_ALLOWLIST_ENV] || "").split(",").map((entry) => entry.trim().toLowerCase()).filter((entry) => entry.length > 0);
34607
35047
  }
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.`;
35048
+ 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
35049
  function withScopeHint(error48) {
34610
35050
  const message = error48?.message || String(error48);
34611
35051
  const status = error48?.code ?? error48?.statusCode ?? error48?.status ?? error48?.response?.status;
@@ -34643,14 +35083,14 @@ function registerForwardingTools(server, accountManager) {
34643
35083
  if (allowed.length === 0) {
34644
35084
  return jsonResult6({
34645
35085
  success: false,
34646
- error: `Forwarding is switched off: ${FORWARD_ALLOWLIST_ENV} is not set. ${HOW_TO_ALLOW}`
35086
+ error: `Forwarding is switched off: ${FORWARD_ALLOWLIST_ENV} is not set. ${HOW_TO_ALLOW2}`
34647
35087
  });
34648
35088
  }
34649
35089
  const forwardTo = args.forwardTo.trim();
34650
35090
  if (!allowed.includes(forwardTo.toLowerCase())) {
34651
35091
  return jsonResult6({
34652
35092
  success: false,
34653
- error: `${forwardTo} is not an allowed forwarding target. ${HOW_TO_ALLOW}`,
35093
+ error: `${forwardTo} is not an allowed forwarding target. ${HOW_TO_ALLOW2}`,
34654
35094
  allowedTargets: allowed
34655
35095
  });
34656
35096
  }
@@ -34716,7 +35156,7 @@ function registerForwardingTools(server, accountManager) {
34716
35156
  }
34717
35157
 
34718
35158
  // src/server.ts
34719
- var SERVER_VERSION = "1.9.0";
35159
+ var SERVER_VERSION = "1.10.0";
34720
35160
  async function createServer(accountManager) {
34721
35161
  const mgr = accountManager ?? new AccountManager();
34722
35162
  const server = new McpServer({