@marlinjai/email-mcp 1.2.7 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,7 +103,7 @@ npx @marlinjai/email-mcp setup
103
103
  # Optionally enter SMTP host and port for sending
104
104
  ```
105
105
 
106
- ## Available Tools (24)
106
+ ## Available Tools (25)
107
107
 
108
108
  ### Account Management (4)
109
109
 
@@ -134,11 +134,12 @@ npx @marlinjai/email-mcp setup
134
134
  | `email_draft_create` | Save a draft without sending |
135
135
  | `email_draft_list` | List all drafts |
136
136
 
137
- ### Organization (7)
137
+ ### Organization (8)
138
138
 
139
139
  | Tool | Description |
140
140
  |------|-------------|
141
141
  | `email_move` | Move an email to a different folder. Supports `sourceFolder` for IMAP/iCloud |
142
+ | `email_transfer` | Move or copy emails **between accounts**, preserving the original message (sender, date, threading) via raw MIME transfer. `deleteAfter=true` trashes the source only after a confirmed import (safe cross-account move) |
142
143
  | `email_delete` | Delete an email (trash or permanent). Supports `sourceFolder` for IMAP/iCloud |
143
144
  | `email_mark` | Mark as read/unread, starred, or flagged. Supports `sourceFolder` for IMAP/iCloud |
144
145
  | `email_label` | Add/remove labels (Gmail only) |
package/dist/index.js CHANGED
@@ -7070,6 +7070,7 @@ var init_mapper = __esm({
7070
7070
  });
7071
7071
 
7072
7072
  // src/providers/gmail/adapter.ts
7073
+ import { Readable } from "node:stream";
7073
7074
  import { google } from "googleapis";
7074
7075
  import { OAuth2Client } from "google-auth-library";
7075
7076
  var GmailAdapter;
@@ -7129,20 +7130,32 @@ var init_adapter = __esm({
7129
7130
  async search(query) {
7130
7131
  const gmail = this.ensureConnected();
7131
7132
  const q = buildGmailQuery(query);
7132
- const params = {
7133
- userId: "me",
7134
- maxResults: query.limit || 20
7135
- };
7136
- if (q) params.q = q;
7137
- if (query.folder) params.labelIds = [query.folder];
7138
- const res = await gmail.users.messages.list(params);
7139
- const messages = res.data.messages || [];
7140
- if (messages.length === 0) return [];
7133
+ const limit = query.limit || 20;
7134
+ const offset = query.offset || 0;
7135
+ const needed = offset + limit;
7136
+ const ids = [];
7137
+ let pageToken;
7138
+ do {
7139
+ const params = {
7140
+ userId: "me",
7141
+ maxResults: Math.min(500, needed - ids.length)
7142
+ };
7143
+ if (q) params.q = q;
7144
+ if (query.folder) params.labelIds = [query.folder];
7145
+ if (pageToken) params.pageToken = pageToken;
7146
+ const res = await gmail.users.messages.list(params);
7147
+ for (const msg of res.data.messages || []) {
7148
+ if (msg.id) ids.push(msg.id);
7149
+ }
7150
+ pageToken = res.data.nextPageToken || void 0;
7151
+ } while (pageToken && ids.length < needed);
7152
+ const windowIds = ids.slice(offset, offset + limit);
7153
+ if (windowIds.length === 0) return [];
7141
7154
  const emails = [];
7142
- for (const msg of messages) {
7155
+ for (const id of windowIds) {
7143
7156
  const full = await gmail.users.messages.get({
7144
7157
  userId: "me",
7145
- id: msg.id,
7158
+ id,
7146
7159
  format: "full"
7147
7160
  });
7148
7161
  emails.push(mapGmailMessage(full.data, this.accountId));
@@ -7390,6 +7403,40 @@ var init_adapter = __esm({
7390
7403
  }
7391
7404
  return result;
7392
7405
  }
7406
+ async getRawMessage(emailId, _sourceFolder) {
7407
+ const gmail = this.ensureConnected();
7408
+ const res = await gmail.users.messages.get({
7409
+ userId: "me",
7410
+ id: emailId,
7411
+ format: "raw"
7412
+ });
7413
+ return Buffer.from(res.data.raw || "", "base64url");
7414
+ }
7415
+ async appendRawMessage(raw, targetFolder, flags) {
7416
+ const gmail = this.ensureConnected();
7417
+ const labelIds = [];
7418
+ labelIds.push(targetFolder ? await this.resolveLabelId(targetFolder) : "INBOX");
7419
+ if (flags?.read === false) labelIds.push("UNREAD");
7420
+ if (flags?.starred === true) labelIds.push("STARRED");
7421
+ const res = await gmail.users.messages.insert({
7422
+ userId: "me",
7423
+ internalDateSource: "dateHeader",
7424
+ requestBody: { labelIds },
7425
+ media: { mimeType: "message/rfc822", body: Readable.from(raw) }
7426
+ });
7427
+ return { id: res.data.id || "" };
7428
+ }
7429
+ /** Resolve a Gmail label name or id to a label id (accepts either). */
7430
+ async resolveLabelId(nameOrId) {
7431
+ const gmail = this.ensureConnected();
7432
+ const res = await gmail.users.labels.list({ userId: "me" });
7433
+ const labels = res.data.labels || [];
7434
+ const byId = labels.find((l) => l.id === nameOrId);
7435
+ if (byId?.id) return byId.id;
7436
+ const byName = labels.find((l) => (l.name || "").toLowerCase() === nameOrId.toLowerCase());
7437
+ if (byName?.id) return byName.id;
7438
+ return nameOrId;
7439
+ }
7393
7440
  async addLabels(emailId, labels) {
7394
7441
  const gmail = this.ensureConnected();
7395
7442
  await gmail.users.messages.modify({
@@ -7848,6 +7895,41 @@ var init_adapter2 = __esm({
7848
7895
  destinationId
7849
7896
  });
7850
7897
  }
7898
+ async getRawMessage(emailId, _sourceFolder) {
7899
+ this.ensureClient();
7900
+ const res = await fetch(
7901
+ `https://graph.microsoft.com/v1.0/me/messages/${encodeURIComponent(emailId)}/$value`,
7902
+ { headers: { Authorization: `Bearer ${this.accessToken}` } }
7903
+ );
7904
+ if (!res.ok) {
7905
+ throw new Error(`Graph $value fetch failed for ${emailId}: HTTP ${res.status}`);
7906
+ }
7907
+ return Buffer.from(await res.arrayBuffer());
7908
+ }
7909
+ async appendRawMessage(raw, targetFolder, flags) {
7910
+ this.ensureClient();
7911
+ const endpoint = targetFolder ? `https://graph.microsoft.com/v1.0/me/mailFolders/${encodeURIComponent(await this.resolveFolder(targetFolder))}/messages` : "https://graph.microsoft.com/v1.0/me/messages";
7912
+ const res = await fetch(endpoint, {
7913
+ method: "POST",
7914
+ headers: {
7915
+ Authorization: `Bearer ${this.accessToken}`,
7916
+ "Content-Type": "text/plain"
7917
+ },
7918
+ body: raw.toString("base64")
7919
+ });
7920
+ if (!res.ok) {
7921
+ const detail = await res.text().catch(() => "");
7922
+ throw new Error(`Graph MIME import failed: HTTP ${res.status} ${detail.slice(0, 200)}`);
7923
+ }
7924
+ const data = await res.json().catch(() => ({}));
7925
+ if (flags && data.id && (flags.read !== void 0 || flags.starred !== void 0)) {
7926
+ try {
7927
+ await this.markEmail(data.id, flags);
7928
+ } catch {
7929
+ }
7930
+ }
7931
+ return { id: data.id || "" };
7932
+ }
7851
7933
  async deleteEmail(emailId, permanent, _sourceFolder) {
7852
7934
  const client = this.ensureClient();
7853
7935
  if (permanent) {
@@ -8399,14 +8481,7 @@ var init_adapter3 = __esm({
8399
8481
  }
8400
8482
  try {
8401
8483
  const mailboxExists = this.client.mailbox?.exists ?? 0;
8402
- let statusCount = 0;
8403
- try {
8404
- const status = await this.client.status(folder, { messages: true });
8405
- statusCount = status.messages ?? 0;
8406
- } catch {
8407
- }
8408
- const effectiveCount = Math.max(mailboxExists, statusCount);
8409
- if (effectiveCount === 0) {
8484
+ if (mailboxExists === 0) {
8410
8485
  return [];
8411
8486
  }
8412
8487
  const criteria = this.buildSearchCriteria(query);
@@ -8419,7 +8494,11 @@ var init_adapter3 = __esm({
8419
8494
  );
8420
8495
  allUids = Array.isArray(searchResult) ? searchResult : [];
8421
8496
  } catch {
8422
- allUids = await this.collectUidsViaFetch(query);
8497
+ try {
8498
+ allUids = await this.collectUidsViaFetch(query);
8499
+ } catch {
8500
+ return [];
8501
+ }
8423
8502
  }
8424
8503
  const offset = query.offset || 0;
8425
8504
  const slicedUids = query.limit ? allUids.slice(offset, offset + query.limit) : allUids.slice(offset);
@@ -8481,19 +8560,27 @@ var init_adapter3 = __esm({
8481
8560
  async fetchEmails(uids, folder, returnBody) {
8482
8561
  if (!this.client) return [];
8483
8562
  const emails = [];
8484
- if (returnBody) {
8485
- for await (const msg of this.client.fetch(uids, { source: true, uid: true, flags: true })) {
8563
+ const fetchOpts = returnBody ? { source: true, uid: true, flags: true } : { envelope: true, uid: true, flags: true, bodyStructure: true };
8564
+ const uidRange = uids.join(",");
8565
+ let messages;
8566
+ try {
8567
+ messages = await this.client.fetchAll(uidRange, fetchOpts, { uid: true });
8568
+ } catch {
8569
+ messages = [];
8570
+ for (const uid of uids) {
8571
+ try {
8572
+ const batch = await this.client.fetchAll(String(uid), fetchOpts, { uid: true });
8573
+ messages.push(...batch);
8574
+ } catch {
8575
+ }
8576
+ }
8577
+ }
8578
+ for (const msg of messages) {
8579
+ if (returnBody) {
8486
8580
  const parsed = await simpleParser(msg.source);
8487
8581
  parsed.flags = msg.flags;
8488
8582
  emails.push(mapParsedEmail(parsed, folder, this.accountId, msg.uid));
8489
- }
8490
- } else {
8491
- for await (const msg of this.client.fetch(uids, {
8492
- envelope: true,
8493
- uid: true,
8494
- flags: true,
8495
- bodyStructure: true
8496
- })) {
8583
+ } else {
8497
8584
  const env = msg.envelope;
8498
8585
  emails.push({
8499
8586
  id: String(msg.uid),
@@ -8702,6 +8789,32 @@ var init_adapter3 = __esm({
8702
8789
  lock.release();
8703
8790
  }
8704
8791
  }
8792
+ async getRawMessage(emailId, sourceFolder) {
8793
+ if (!this.client) throw new Error("Not connected");
8794
+ const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
8795
+ let lock;
8796
+ try {
8797
+ lock = await this.client.getMailboxLock(folder);
8798
+ } catch (error48) {
8799
+ throw formatImapError(error48, `Failed to open folder "${folder}"`);
8800
+ }
8801
+ try {
8802
+ const msg = await this.client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
8803
+ if (!msg || !msg.source) throw new Error(`Email ${emailId} not found in "${folder}"`);
8804
+ return msg.source;
8805
+ } finally {
8806
+ lock.release();
8807
+ }
8808
+ }
8809
+ async appendRawMessage(raw, targetFolder, flags) {
8810
+ if (!this.client) throw new Error("Not connected");
8811
+ const folder = targetFolder ? await this.resolveFolder(targetFolder) : "INBOX";
8812
+ const imapFlags = [];
8813
+ if (flags?.read) imapFlags.push("\\Seen");
8814
+ if (flags?.starred) imapFlags.push("\\Flagged");
8815
+ const result = await this.client.append(folder, raw, imapFlags);
8816
+ return { id: String(result?.uid ?? "") };
8817
+ }
8705
8818
  async batchDelete(emailIds, permanent, sourceFolder) {
8706
8819
  if (!this.client) throw new Error("Not connected");
8707
8820
  const result = { succeeded: [], failed: [] };
@@ -33222,6 +33335,66 @@ function registerOrganizingTools(server, accountManager) {
33222
33335
  }
33223
33336
  }
33224
33337
  );
33338
+ server.tool(
33339
+ "email_transfer",
33340
+ "Move or copy emails BETWEEN accounts, preserving the original message intact (sender, date, threading) by transferring the raw MIME. Fetches each message from the source account and imports it into the target account. With deleteAfter=true the source copy is sent to trash only after a confirmed import, making this a safe cross-account move.",
33341
+ {
33342
+ sourceAccountId: external_exports3.string().describe("Account to read messages from"),
33343
+ targetAccountId: external_exports3.string().describe("Account to import messages into"),
33344
+ emailIds: external_exports3.array(external_exports3.string()).min(1).describe("Source email IDs to transfer"),
33345
+ targetFolder: external_exports3.string().optional().describe("Destination folder/label in the target account (Gmail label id or name; IMAP/Outlook folder name). Defaults to INBOX."),
33346
+ sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when emails are not in INBOX)"),
33347
+ deleteAfter: external_exports3.boolean().optional().describe("Send the source copy to trash after a successful import. Default false (copy, leaving the original in place)."),
33348
+ markRead: external_exports3.boolean().optional().describe("Mark imported messages as read in the target. Default: provider default (usually unread).")
33349
+ },
33350
+ async (args) => {
33351
+ try {
33352
+ const source = await accountManager.getProvider(args.sourceAccountId);
33353
+ const target = await accountManager.getProvider(args.targetAccountId);
33354
+ if (!source.getRawMessage) {
33355
+ return jsonResult4({ success: false, error: "Source provider does not support raw message export" });
33356
+ }
33357
+ if (!target.appendRawMessage) {
33358
+ return jsonResult4({ success: false, error: "Target provider does not support raw message import" });
33359
+ }
33360
+ const flags = args.markRead !== void 0 ? { read: args.markRead } : void 0;
33361
+ const transferred = [];
33362
+ const failed = [];
33363
+ const deleted = [];
33364
+ const deleteFailed = [];
33365
+ for (const id of args.emailIds) {
33366
+ let targetId;
33367
+ try {
33368
+ const raw = await source.getRawMessage(id, args.sourceFolder);
33369
+ const res = await target.appendRawMessage(raw, args.targetFolder, flags);
33370
+ targetId = res.id;
33371
+ transferred.push({ sourceId: id, targetId });
33372
+ } catch (e) {
33373
+ failed.push({ id, error: e.message });
33374
+ continue;
33375
+ }
33376
+ if (args.deleteAfter) {
33377
+ try {
33378
+ await source.deleteEmail(id, false, args.sourceFolder);
33379
+ deleted.push(id);
33380
+ } catch (e) {
33381
+ deleteFailed.push({ id, error: e.message });
33382
+ }
33383
+ }
33384
+ }
33385
+ return jsonResult4({
33386
+ success: true,
33387
+ transferred: transferred.length,
33388
+ deleted: deleted.length,
33389
+ ...failed.length ? { failedCount: failed.length } : {},
33390
+ ...deleteFailed.length ? { deleteFailedCount: deleteFailed.length } : {},
33391
+ details: { transferred, failed, deleted, deleteFailed }
33392
+ });
33393
+ } catch (error48) {
33394
+ return jsonResult4({ success: false, error: error48.message });
33395
+ }
33396
+ }
33397
+ );
33225
33398
  server.tool(
33226
33399
  "email_label",
33227
33400
  "Add or remove labels on an email (Gmail only)",