@marlinjai/email-mcp 1.2.8 → 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 +3 -2
- package/dist/index.js +179 -11
- package/dist/index.js.map +2 -2
- package/dist/setup/wizard.js +119 -11
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
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 (
|
|
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 (
|
|
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
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
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
|
|
7155
|
+
for (const id of windowIds) {
|
|
7143
7156
|
const full = await gmail.users.messages.get({
|
|
7144
7157
|
userId: "me",
|
|
7145
|
-
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) {
|
|
@@ -8707,6 +8789,32 @@ var init_adapter3 = __esm({
|
|
|
8707
8789
|
lock.release();
|
|
8708
8790
|
}
|
|
8709
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
|
+
}
|
|
8710
8818
|
async batchDelete(emailIds, permanent, sourceFolder) {
|
|
8711
8819
|
if (!this.client) throw new Error("Not connected");
|
|
8712
8820
|
const result = { succeeded: [], failed: [] };
|
|
@@ -33227,6 +33335,66 @@ function registerOrganizingTools(server, accountManager) {
|
|
|
33227
33335
|
}
|
|
33228
33336
|
}
|
|
33229
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
|
+
);
|
|
33230
33398
|
server.tool(
|
|
33231
33399
|
"email_label",
|
|
33232
33400
|
"Add or remove labels on an email (Gmail only)",
|