@marlinjai/email-mcp 1.4.4 → 1.7.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 +34 -5
- package/dist/index.js +506 -138
- package/dist/index.js.map +3 -3
- package/dist/setup/wizard.js +369 -111
- package/dist/setup/wizard.js.map +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -35,18 +35,62 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
35
35
|
|
|
36
36
|
// src/auth/credential-store.ts
|
|
37
37
|
import crypto from "node:crypto";
|
|
38
|
+
import { execFileSync } from "node:child_process";
|
|
38
39
|
import fs from "node:fs";
|
|
39
40
|
import path from "node:path";
|
|
40
41
|
import os from "node:os";
|
|
41
|
-
function
|
|
42
|
-
|
|
42
|
+
function getStableMachineId() {
|
|
43
|
+
if (cachedMachineId !== void 0) return cachedMachineId;
|
|
44
|
+
cachedMachineId = computeStableMachineId();
|
|
45
|
+
return cachedMachineId;
|
|
43
46
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
47
|
+
function computeStableMachineId() {
|
|
48
|
+
try {
|
|
49
|
+
if (process.platform === "darwin") {
|
|
50
|
+
const out = execFileSync("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
51
|
+
encoding: "utf-8"
|
|
52
|
+
});
|
|
53
|
+
const match = out.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
54
|
+
return match ? match[1] : null;
|
|
55
|
+
}
|
|
56
|
+
if (process.platform === "linux") {
|
|
57
|
+
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
58
|
+
if (fs.existsSync(p)) {
|
|
59
|
+
const id = fs.readFileSync(p, "utf-8").trim();
|
|
60
|
+
if (id) return id;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (process.platform === "win32") {
|
|
66
|
+
const out = execFileSync(
|
|
67
|
+
"reg",
|
|
68
|
+
["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"],
|
|
69
|
+
{ encoding: "utf-8" }
|
|
70
|
+
);
|
|
71
|
+
const match = out.match(/MachineGuid\s+REG_SZ\s+([A-Za-z0-9-]+)/);
|
|
72
|
+
return match ? match[1] : null;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
46
77
|
}
|
|
47
|
-
function
|
|
78
|
+
function getSeedCandidates() {
|
|
79
|
+
const seeds = [];
|
|
80
|
+
const username = os.userInfo().username;
|
|
81
|
+
const envKey = process.env.EMAIL_MCP_KEY;
|
|
82
|
+
if (envKey) seeds.push(`email-mcp:env:${envKey}`);
|
|
83
|
+
const machineId = getStableMachineId();
|
|
84
|
+
if (machineId) seeds.push(`email-mcp:machine:${machineId}:${username}`);
|
|
85
|
+
seeds.push(`email-mcp:${os.hostname()}:${username}`);
|
|
86
|
+
return seeds;
|
|
87
|
+
}
|
|
88
|
+
function deriveKey(seed, salt) {
|
|
89
|
+
return crypto.pbkdf2Sync(seed, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
90
|
+
}
|
|
91
|
+
function encrypt(plaintext, seed) {
|
|
48
92
|
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
49
|
-
const key = deriveKey(salt);
|
|
93
|
+
const key = deriveKey(seed, salt);
|
|
50
94
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
51
95
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
52
96
|
let encrypted = cipher.update(plaintext, "utf-8", "hex");
|
|
@@ -59,16 +103,27 @@ function encrypt(plaintext) {
|
|
|
59
103
|
data: encrypted
|
|
60
104
|
});
|
|
61
105
|
}
|
|
62
|
-
function decrypt(ciphertext) {
|
|
106
|
+
function decrypt(ciphertext, seeds) {
|
|
63
107
|
const { salt, iv, authTag, data } = JSON.parse(ciphertext);
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
let
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
108
|
+
const saltBuf = Buffer.from(salt, "hex");
|
|
109
|
+
const ivBuf = Buffer.from(iv, "hex");
|
|
110
|
+
const authTagBuf = Buffer.from(authTag, "hex");
|
|
111
|
+
let lastError;
|
|
112
|
+
for (const seed of seeds) {
|
|
113
|
+
try {
|
|
114
|
+
const key = deriveKey(seed, saltBuf);
|
|
115
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, ivBuf);
|
|
116
|
+
decipher.setAuthTag(authTagBuf);
|
|
117
|
+
let decrypted = decipher.update(data, "hex", "utf-8");
|
|
118
|
+
decrypted += decipher.final("utf-8");
|
|
119
|
+
return { plaintext: decrypted, seedUsed: seed };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
lastError = err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw lastError ?? new Error("Unable to decrypt credentials");
|
|
125
|
+
}
|
|
126
|
+
var ALGORITHM, KEY_LENGTH, IV_LENGTH, SALT_LENGTH, PBKDF2_ITERATIONS, cachedMachineId, CredentialStore;
|
|
72
127
|
var init_credential_store = __esm({
|
|
73
128
|
"src/auth/credential-store.ts"() {
|
|
74
129
|
"use strict";
|
|
@@ -91,12 +146,20 @@ var init_credential_store = __esm({
|
|
|
91
146
|
return { accounts: {} };
|
|
92
147
|
}
|
|
93
148
|
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
94
|
-
const
|
|
95
|
-
|
|
149
|
+
const seeds = getSeedCandidates();
|
|
150
|
+
const { plaintext, seedUsed } = decrypt(raw, seeds);
|
|
151
|
+
const data = JSON.parse(plaintext);
|
|
152
|
+
if (seedUsed !== seeds[0]) {
|
|
153
|
+
try {
|
|
154
|
+
this.write(data);
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return data;
|
|
96
159
|
}
|
|
97
160
|
write(data) {
|
|
98
161
|
const json2 = JSON.stringify(data);
|
|
99
|
-
const encrypted = encrypt(json2);
|
|
162
|
+
const encrypted = encrypt(json2, getSeedCandidates()[0]);
|
|
100
163
|
fs.writeFileSync(this.filePath, encrypted, { mode: 384 });
|
|
101
164
|
}
|
|
102
165
|
async save(creds) {
|
|
@@ -3314,8 +3377,8 @@ var require_utils = __commonJS({
|
|
|
3314
3377
|
}
|
|
3315
3378
|
return ind;
|
|
3316
3379
|
}
|
|
3317
|
-
function removeDotSegments(
|
|
3318
|
-
let input2 =
|
|
3380
|
+
function removeDotSegments(path4) {
|
|
3381
|
+
let input2 = path4;
|
|
3319
3382
|
const output2 = [];
|
|
3320
3383
|
let nextSlash = -1;
|
|
3321
3384
|
let len = 0;
|
|
@@ -3514,8 +3577,8 @@ var require_schemes = __commonJS({
|
|
|
3514
3577
|
wsComponent.secure = void 0;
|
|
3515
3578
|
}
|
|
3516
3579
|
if (wsComponent.resourceName) {
|
|
3517
|
-
const [
|
|
3518
|
-
wsComponent.path =
|
|
3580
|
+
const [path4, query] = wsComponent.resourceName.split("?");
|
|
3581
|
+
wsComponent.path = path4 && path4 !== "/" ? path4 : void 0;
|
|
3519
3582
|
wsComponent.query = query;
|
|
3520
3583
|
wsComponent.resourceName = void 0;
|
|
3521
3584
|
}
|
|
@@ -6877,12 +6940,12 @@ var require_dist = __commonJS({
|
|
|
6877
6940
|
throw new Error(`Unknown format "${name}"`);
|
|
6878
6941
|
return f;
|
|
6879
6942
|
};
|
|
6880
|
-
function addFormats(ajv, list,
|
|
6943
|
+
function addFormats(ajv, list, fs4, exportName) {
|
|
6881
6944
|
var _a2;
|
|
6882
6945
|
var _b;
|
|
6883
6946
|
(_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
6884
6947
|
for (const f of list)
|
|
6885
|
-
ajv.addFormat(f,
|
|
6948
|
+
ajv.addFormat(f, fs4[f]);
|
|
6886
6949
|
}
|
|
6887
6950
|
module.exports = exports = formatsPlugin;
|
|
6888
6951
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -7119,11 +7182,12 @@ var init_adapter = __esm({
|
|
|
7119
7182
|
const labels = res.data.labels || [];
|
|
7120
7183
|
return labels.map(mapGmailLabel);
|
|
7121
7184
|
}
|
|
7122
|
-
async createFolder(name) {
|
|
7185
|
+
async createFolder(name, parentPath) {
|
|
7123
7186
|
const gmail = this.ensureConnected();
|
|
7187
|
+
const fullName = parentPath ? `${parentPath}/${name}` : name;
|
|
7124
7188
|
const res = await gmail.users.labels.create({
|
|
7125
7189
|
userId: "me",
|
|
7126
|
-
requestBody: { name }
|
|
7190
|
+
requestBody: { name: fullName }
|
|
7127
7191
|
});
|
|
7128
7192
|
return mapGmailLabel(res.data);
|
|
7129
7193
|
}
|
|
@@ -7240,6 +7304,18 @@ var init_adapter = __esm({
|
|
|
7240
7304
|
});
|
|
7241
7305
|
return { id: res.data.id || "" };
|
|
7242
7306
|
}
|
|
7307
|
+
async updateDraft(draftId, params) {
|
|
7308
|
+
const gmail = this.ensureConnected();
|
|
7309
|
+
const raw = this.buildRfc2822(params);
|
|
7310
|
+
const res = await gmail.users.drafts.update({
|
|
7311
|
+
userId: "me",
|
|
7312
|
+
id: draftId,
|
|
7313
|
+
requestBody: {
|
|
7314
|
+
message: { raw }
|
|
7315
|
+
}
|
|
7316
|
+
});
|
|
7317
|
+
return { id: res.data.id || draftId };
|
|
7318
|
+
}
|
|
7243
7319
|
async listDrafts(limit, _offset) {
|
|
7244
7320
|
const gmail = this.ensureConnected();
|
|
7245
7321
|
const res = await gmail.users.drafts.list({
|
|
@@ -7403,6 +7479,34 @@ var init_adapter = __esm({
|
|
|
7403
7479
|
}
|
|
7404
7480
|
return result;
|
|
7405
7481
|
}
|
|
7482
|
+
async batchLabel(emailIds, addLabels, removeLabels) {
|
|
7483
|
+
const gmail = this.ensureConnected();
|
|
7484
|
+
const result = { succeeded: [], failed: [] };
|
|
7485
|
+
const addLabelIds = addLabels ?? [];
|
|
7486
|
+
const removeLabelIds = removeLabels ?? [];
|
|
7487
|
+
if (addLabelIds.length === 0 && removeLabelIds.length === 0) {
|
|
7488
|
+
result.succeeded = [...emailIds];
|
|
7489
|
+
return result;
|
|
7490
|
+
}
|
|
7491
|
+
try {
|
|
7492
|
+
await gmail.users.messages.batchModify({
|
|
7493
|
+
userId: "me",
|
|
7494
|
+
requestBody: { ids: emailIds, addLabelIds, removeLabelIds }
|
|
7495
|
+
});
|
|
7496
|
+
result.succeeded = [...emailIds];
|
|
7497
|
+
} catch (error48) {
|
|
7498
|
+
for (const id of emailIds) {
|
|
7499
|
+
try {
|
|
7500
|
+
if (addLabelIds.length > 0) await this.addLabels(id, addLabelIds);
|
|
7501
|
+
if (removeLabelIds.length > 0) await this.removeLabels(id, removeLabelIds);
|
|
7502
|
+
result.succeeded.push(id);
|
|
7503
|
+
} catch (e) {
|
|
7504
|
+
result.failed.push({ id, error: e.message });
|
|
7505
|
+
}
|
|
7506
|
+
}
|
|
7507
|
+
}
|
|
7508
|
+
return result;
|
|
7509
|
+
}
|
|
7406
7510
|
async getRawMessage(emailId, _sourceFolder) {
|
|
7407
7511
|
const gmail = this.ensureConnected();
|
|
7408
7512
|
const res = await gmail.users.messages.get({
|
|
@@ -7563,18 +7667,37 @@ var init_adapter = __esm({
|
|
|
7563
7667
|
// src/providers/gmail/auth.ts
|
|
7564
7668
|
import { OAuth2Client as OAuth2Client2 } from "google-auth-library";
|
|
7565
7669
|
import crypto2 from "node:crypto";
|
|
7670
|
+
function resolveScopeMode(explicit) {
|
|
7671
|
+
if (explicit) return explicit;
|
|
7672
|
+
return process.env.EMAIL_MCP_GMAIL_SCOPE?.toLowerCase() === "restricted" ? "restricted" : "full";
|
|
7673
|
+
}
|
|
7566
7674
|
var GMAIL_SCOPES, GmailAuth;
|
|
7567
7675
|
var init_auth = __esm({
|
|
7568
7676
|
"src/providers/gmail/auth.ts"() {
|
|
7569
7677
|
"use strict";
|
|
7570
|
-
GMAIL_SCOPES =
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7678
|
+
GMAIL_SCOPES = {
|
|
7679
|
+
full: [
|
|
7680
|
+
// Gmail's maximum-permission scope. Superset of gmail.modify below;
|
|
7681
|
+
// the only capability it adds is immediate, trash-bypassing permanent
|
|
7682
|
+
// deletion (messages.delete / batchDelete with permanent: true).
|
|
7683
|
+
"https://mail.google.com/",
|
|
7684
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
7685
|
+
// Mailbox content access (above) does NOT cover the Settings API —
|
|
7686
|
+
// Google treats them as separate permission domains. Needed for
|
|
7687
|
+
// users.settings.filters.* (email_create_block_rule/list/delete).
|
|
7688
|
+
"https://www.googleapis.com/auth/gmail.settings.basic"
|
|
7689
|
+
],
|
|
7690
|
+
// Drops https://mail.google.com/ so an AI-triggered permanent delete
|
|
7691
|
+
// can't happen: gmail.modify already covers every other read/write
|
|
7692
|
+
// operation this server performs (read, send, label, archive, trash,
|
|
7693
|
+
// drafts, filters). Choosing this mode means deleteEmail(..., permanent:
|
|
7694
|
+
// true) and batchDelete(..., permanent: true) will fail with a Gmail API
|
|
7695
|
+
// 403 instead of succeeding — everything else behaves identically.
|
|
7696
|
+
restricted: [
|
|
7697
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
7698
|
+
"https://www.googleapis.com/auth/gmail.settings.basic"
|
|
7699
|
+
]
|
|
7700
|
+
};
|
|
7578
7701
|
GmailAuth = class {
|
|
7579
7702
|
clientId;
|
|
7580
7703
|
clientSecret;
|
|
@@ -7584,13 +7707,13 @@ var init_auth = __esm({
|
|
|
7584
7707
|
this.clientSecret = clientSecret;
|
|
7585
7708
|
this.oauth2Client = new OAuth2Client2(clientId, clientSecret, redirectUri);
|
|
7586
7709
|
}
|
|
7587
|
-
getAuthUrl(redirectUri) {
|
|
7710
|
+
getAuthUrl(redirectUri, scopeMode) {
|
|
7588
7711
|
const codeVerifier = crypto2.randomBytes(32).toString("base64url");
|
|
7589
7712
|
const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
7590
7713
|
this.oauth2Client = new OAuth2Client2(this.clientId, this.clientSecret, redirectUri);
|
|
7591
7714
|
const url2 = this.oauth2Client.generateAuthUrl({
|
|
7592
7715
|
access_type: "offline",
|
|
7593
|
-
scope: GMAIL_SCOPES,
|
|
7716
|
+
scope: GMAIL_SCOPES[resolveScopeMode(scopeMode)],
|
|
7594
7717
|
prompt: "consent",
|
|
7595
7718
|
code_challenge: codeChallenge,
|
|
7596
7719
|
code_challenge_method: "S256"
|
|
@@ -7805,8 +7928,8 @@ var init_adapter2 = __esm({
|
|
|
7805
7928
|
}
|
|
7806
7929
|
});
|
|
7807
7930
|
const originalApi = client.api.bind(client);
|
|
7808
|
-
client.api = (
|
|
7809
|
-
return originalApi(
|
|
7931
|
+
client.api = (path4) => {
|
|
7932
|
+
return originalApi(path4).header("Prefer", 'IdType="ImmutableId"');
|
|
7810
7933
|
};
|
|
7811
7934
|
this.client = client;
|
|
7812
7935
|
}
|
|
@@ -7947,6 +8070,12 @@ var init_adapter2 = __esm({
|
|
|
7947
8070
|
const result = await client.api("/me/messages").post(message);
|
|
7948
8071
|
return { id: result.id };
|
|
7949
8072
|
}
|
|
8073
|
+
async updateDraft(draftId, params) {
|
|
8074
|
+
const client = this.ensureClient();
|
|
8075
|
+
const message = this.buildGraphMessage(params);
|
|
8076
|
+
await client.api(`/me/messages/${encodeURIComponent(draftId)}`).patch(message);
|
|
8077
|
+
return { id: draftId };
|
|
8078
|
+
}
|
|
7950
8079
|
async listDrafts(limit, offset) {
|
|
7951
8080
|
const client = this.ensureClient();
|
|
7952
8081
|
let request = client.api("/me/mailFolders/drafts/messages");
|
|
@@ -8499,6 +8628,7 @@ var init_adapter3 = __esm({
|
|
|
8499
8628
|
accountId = "";
|
|
8500
8629
|
email = "";
|
|
8501
8630
|
passwordCreds = null;
|
|
8631
|
+
connectingPromise = null;
|
|
8502
8632
|
async connect(credentials) {
|
|
8503
8633
|
if (!credentials.password) {
|
|
8504
8634
|
throw new Error("IMAP adapter requires password credentials");
|
|
@@ -8506,23 +8636,87 @@ var init_adapter3 = __esm({
|
|
|
8506
8636
|
this.accountId = credentials.id;
|
|
8507
8637
|
this.email = credentials.email;
|
|
8508
8638
|
this.passwordCreds = credentials.password;
|
|
8509
|
-
this.
|
|
8510
|
-
host: credentials.password.host,
|
|
8511
|
-
port: credentials.password.port,
|
|
8512
|
-
secure: credentials.password.tls,
|
|
8513
|
-
auth: {
|
|
8514
|
-
user: credentials.email,
|
|
8515
|
-
pass: credentials.password.password
|
|
8516
|
-
},
|
|
8517
|
-
logger: false
|
|
8518
|
-
});
|
|
8519
|
-
await this.client.connect();
|
|
8639
|
+
await this.ensureConnected();
|
|
8520
8640
|
}
|
|
8521
8641
|
async disconnect() {
|
|
8522
8642
|
if (this.client) {
|
|
8523
|
-
|
|
8643
|
+
try {
|
|
8644
|
+
await this.client.logout();
|
|
8645
|
+
} catch {
|
|
8646
|
+
}
|
|
8524
8647
|
this.client = null;
|
|
8525
8648
|
}
|
|
8649
|
+
this.passwordCreds = null;
|
|
8650
|
+
}
|
|
8651
|
+
/**
|
|
8652
|
+
* Builds a fresh ImapFlow client from stored credentials and wires up
|
|
8653
|
+
* close/error listeners so a dropped socket clears `this.client`, forcing
|
|
8654
|
+
* the next `ensureConnected()` call to reconnect.
|
|
8655
|
+
*/
|
|
8656
|
+
buildClient() {
|
|
8657
|
+
if (!this.passwordCreds) {
|
|
8658
|
+
throw new Error("Cannot build IMAP client without stored credentials");
|
|
8659
|
+
}
|
|
8660
|
+
const client = new ImapFlow({
|
|
8661
|
+
host: this.passwordCreds.host,
|
|
8662
|
+
port: this.passwordCreds.port,
|
|
8663
|
+
secure: this.passwordCreds.tls,
|
|
8664
|
+
auth: { user: this.email, pass: this.passwordCreds.password },
|
|
8665
|
+
logger: false
|
|
8666
|
+
});
|
|
8667
|
+
const drop = () => {
|
|
8668
|
+
if (this.client === client) this.client = null;
|
|
8669
|
+
};
|
|
8670
|
+
client.on("close", drop);
|
|
8671
|
+
client.on("error", drop);
|
|
8672
|
+
return client;
|
|
8673
|
+
}
|
|
8674
|
+
/**
|
|
8675
|
+
* Returns true while the underlying ImapFlow socket is healthy.
|
|
8676
|
+
* ImapFlow sets `usable=false` when the connection is closed, in the
|
|
8677
|
+
* middle of logging out, or otherwise unsafe to issue commands on.
|
|
8678
|
+
*/
|
|
8679
|
+
isClientUsable() {
|
|
8680
|
+
return !!this.client && this.client.usable === true;
|
|
8681
|
+
}
|
|
8682
|
+
/**
|
|
8683
|
+
* Guarantees a usable IMAP client and returns it. Reconnects transparently
|
|
8684
|
+
* if the previous client was dropped (idle timeout, server-side disconnect,
|
|
8685
|
+
* network flap). Concurrent callers share a single in-flight reconnect to
|
|
8686
|
+
* avoid duplicate sessions.
|
|
8687
|
+
*
|
|
8688
|
+
* Callers should bind the result to a local `const client` and use that
|
|
8689
|
+
* within the method instead of `this.client`, so a `'close'` event mid-call
|
|
8690
|
+
* does not race the active operation.
|
|
8691
|
+
*/
|
|
8692
|
+
async ensureConnected() {
|
|
8693
|
+
if (this.connectingPromise) {
|
|
8694
|
+
await this.connectingPromise;
|
|
8695
|
+
}
|
|
8696
|
+
if (!this.isClientUsable()) {
|
|
8697
|
+
if (!this.passwordCreds) {
|
|
8698
|
+
throw new Error("Not connected");
|
|
8699
|
+
}
|
|
8700
|
+
this.connectingPromise = (async () => {
|
|
8701
|
+
if (this.client) {
|
|
8702
|
+
try {
|
|
8703
|
+
await this.client.logout();
|
|
8704
|
+
} catch {
|
|
8705
|
+
}
|
|
8706
|
+
this.client = null;
|
|
8707
|
+
}
|
|
8708
|
+
const client = this.buildClient();
|
|
8709
|
+
await client.connect();
|
|
8710
|
+
this.client = client;
|
|
8711
|
+
})().finally(() => {
|
|
8712
|
+
this.connectingPromise = null;
|
|
8713
|
+
});
|
|
8714
|
+
await this.connectingPromise;
|
|
8715
|
+
}
|
|
8716
|
+
if (!this.client) {
|
|
8717
|
+
throw new Error("IMAP client unavailable after reconnect attempt");
|
|
8718
|
+
}
|
|
8719
|
+
return this.client;
|
|
8526
8720
|
}
|
|
8527
8721
|
async testConnection() {
|
|
8528
8722
|
try {
|
|
@@ -8533,14 +8727,14 @@ var init_adapter3 = __esm({
|
|
|
8533
8727
|
}
|
|
8534
8728
|
}
|
|
8535
8729
|
async listFolders() {
|
|
8536
|
-
|
|
8537
|
-
const imapFolders = await
|
|
8730
|
+
const client = await this.ensureConnected();
|
|
8731
|
+
const imapFolders = await client.list();
|
|
8538
8732
|
return imapFolders.map(mapImapFolder);
|
|
8539
8733
|
}
|
|
8540
8734
|
async createFolder(name, parentPath) {
|
|
8541
|
-
|
|
8735
|
+
const client = await this.ensureConnected();
|
|
8542
8736
|
const fullPath = parentPath ? `${parentPath}/${name}` : name;
|
|
8543
|
-
const result = await
|
|
8737
|
+
const result = await client.mailboxCreate(fullPath);
|
|
8544
8738
|
return {
|
|
8545
8739
|
id: result.path,
|
|
8546
8740
|
name: result.name || name,
|
|
@@ -8569,7 +8763,7 @@ var init_adapter3 = __esm({
|
|
|
8569
8763
|
* and matching by name or special-use type.
|
|
8570
8764
|
*/
|
|
8571
8765
|
async resolveFolder(folder) {
|
|
8572
|
-
|
|
8766
|
+
const client = await this.ensureConnected();
|
|
8573
8767
|
const FOLDER_ALIASES = {
|
|
8574
8768
|
junk: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
|
|
8575
8769
|
spam: ["Junk", "Spam", "Bulk Mail", "Junk E-mail", "Junk Email"],
|
|
@@ -8578,7 +8772,7 @@ var init_adapter3 = __esm({
|
|
|
8578
8772
|
drafts: ["Drafts", "Draft"],
|
|
8579
8773
|
archive: ["Archive", "All Mail"]
|
|
8580
8774
|
};
|
|
8581
|
-
const folders = await
|
|
8775
|
+
const folders = await client.list();
|
|
8582
8776
|
const lowerFolder = folder.toLowerCase();
|
|
8583
8777
|
const exactMatch = folders.find(
|
|
8584
8778
|
(f) => f.path === folder || f.path.toLowerCase() === lowerFolder
|
|
@@ -8616,7 +8810,7 @@ var init_adapter3 = __esm({
|
|
|
8616
8810
|
return folder;
|
|
8617
8811
|
}
|
|
8618
8812
|
async search(query) {
|
|
8619
|
-
|
|
8813
|
+
const client = await this.ensureConnected();
|
|
8620
8814
|
const requestedFolder = query.folder || "INBOX";
|
|
8621
8815
|
let folder;
|
|
8622
8816
|
try {
|
|
@@ -8625,17 +8819,25 @@ var init_adapter3 = __esm({
|
|
|
8625
8819
|
folder = requestedFolder;
|
|
8626
8820
|
}
|
|
8627
8821
|
try {
|
|
8628
|
-
await
|
|
8822
|
+
await client.noop();
|
|
8629
8823
|
} catch {
|
|
8630
8824
|
}
|
|
8631
8825
|
let lock;
|
|
8632
8826
|
try {
|
|
8633
|
-
lock = await
|
|
8827
|
+
lock = await client.getMailboxLock(folder);
|
|
8634
8828
|
} catch (error48) {
|
|
8635
8829
|
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
8636
8830
|
}
|
|
8637
8831
|
try {
|
|
8638
|
-
|
|
8832
|
+
let mailboxExists = client.mailbox?.exists ?? 0;
|
|
8833
|
+
if (mailboxExists === 0) {
|
|
8834
|
+
try {
|
|
8835
|
+
const status = await client.status(folder, { messages: true });
|
|
8836
|
+
mailboxExists = status?.messages ?? 0;
|
|
8837
|
+
} catch {
|
|
8838
|
+
mailboxExists = 0;
|
|
8839
|
+
}
|
|
8840
|
+
}
|
|
8639
8841
|
if (mailboxExists === 0) {
|
|
8640
8842
|
return [];
|
|
8641
8843
|
}
|
|
@@ -8643,14 +8845,14 @@ var init_adapter3 = __esm({
|
|
|
8643
8845
|
const hasCriteria = Object.keys(criteria).length > 0;
|
|
8644
8846
|
let allUids = [];
|
|
8645
8847
|
try {
|
|
8646
|
-
const searchResult = await
|
|
8848
|
+
const searchResult = await client.search(
|
|
8647
8849
|
hasCriteria ? criteria : { all: true },
|
|
8648
8850
|
{ uid: true }
|
|
8649
8851
|
);
|
|
8650
8852
|
allUids = Array.isArray(searchResult) ? searchResult : [];
|
|
8651
8853
|
} catch {
|
|
8652
8854
|
try {
|
|
8653
|
-
allUids = await this.collectUidsViaFetch(query);
|
|
8855
|
+
allUids = await this.collectUidsViaFetch(client, query);
|
|
8654
8856
|
} catch {
|
|
8655
8857
|
return [];
|
|
8656
8858
|
}
|
|
@@ -8658,7 +8860,7 @@ var init_adapter3 = __esm({
|
|
|
8658
8860
|
const offset = query.offset || 0;
|
|
8659
8861
|
const slicedUids = query.limit ? allUids.slice(offset, offset + query.limit) : allUids.slice(offset);
|
|
8660
8862
|
if (slicedUids.length === 0) return [];
|
|
8661
|
-
return await this.fetchEmails(slicedUids, folder, query.returnBody);
|
|
8863
|
+
return await this.fetchEmails(client, slicedUids, folder, query.returnBody);
|
|
8662
8864
|
} catch (error48) {
|
|
8663
8865
|
throw formatImapError(error48, `Search failed in folder "${folder}"`);
|
|
8664
8866
|
} finally {
|
|
@@ -8672,13 +8874,12 @@ var init_adapter3 = __esm({
|
|
|
8672
8874
|
* 2. FETCH 1:N using mailbox.exists as explicit range
|
|
8673
8875
|
* 3. Individual sequence number fetches (slowest but most resilient)
|
|
8674
8876
|
*/
|
|
8675
|
-
async collectUidsViaFetch(query) {
|
|
8676
|
-
if (!this.client) return [];
|
|
8877
|
+
async collectUidsViaFetch(client, query) {
|
|
8677
8878
|
const uids = [];
|
|
8678
|
-
const exists =
|
|
8879
|
+
const exists = client.mailbox?.exists ?? 0;
|
|
8679
8880
|
const safeFetch = async (range) => {
|
|
8680
8881
|
try {
|
|
8681
|
-
const messages = await
|
|
8882
|
+
const messages = await client.fetchAll(range, { uid: true, flags: true });
|
|
8682
8883
|
for (const msg of messages) {
|
|
8683
8884
|
if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
|
|
8684
8885
|
if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
|
|
@@ -8698,7 +8899,7 @@ var init_adapter3 = __esm({
|
|
|
8698
8899
|
const count = exists > 0 ? exists : 50;
|
|
8699
8900
|
for (let seq = 1; seq <= count; seq++) {
|
|
8700
8901
|
try {
|
|
8701
|
-
const msgs = await
|
|
8902
|
+
const msgs = await client.fetchAll(String(seq), { uid: true, flags: true });
|
|
8702
8903
|
for (const msg of msgs) {
|
|
8703
8904
|
if (query.unreadOnly && msg.flags?.has("\\Seen")) continue;
|
|
8704
8905
|
if (query.starredOnly && !msg.flags?.has("\\Flagged")) continue;
|
|
@@ -8712,19 +8913,18 @@ var init_adapter3 = __esm({
|
|
|
8712
8913
|
/**
|
|
8713
8914
|
* Fetches email data for a set of UIDs, either with full body or lightweight headers.
|
|
8714
8915
|
*/
|
|
8715
|
-
async fetchEmails(uids, folder, returnBody) {
|
|
8716
|
-
if (!this.client) return [];
|
|
8916
|
+
async fetchEmails(client, uids, folder, returnBody) {
|
|
8717
8917
|
const emails = [];
|
|
8718
8918
|
const fetchOpts = returnBody ? { source: true, uid: true, flags: true } : { envelope: true, uid: true, flags: true, bodyStructure: true };
|
|
8719
8919
|
const uidRange = uids.join(",");
|
|
8720
8920
|
let messages;
|
|
8721
8921
|
try {
|
|
8722
|
-
messages = await
|
|
8922
|
+
messages = await client.fetchAll(uidRange, fetchOpts, { uid: true });
|
|
8723
8923
|
} catch {
|
|
8724
8924
|
messages = [];
|
|
8725
8925
|
for (const uid of uids) {
|
|
8726
8926
|
try {
|
|
8727
|
-
const batch = await
|
|
8927
|
+
const batch = await client.fetchAll(String(uid), fetchOpts, { uid: true });
|
|
8728
8928
|
messages.push(...batch);
|
|
8729
8929
|
} catch {
|
|
8730
8930
|
}
|
|
@@ -8763,17 +8963,17 @@ var init_adapter3 = __esm({
|
|
|
8763
8963
|
return emails;
|
|
8764
8964
|
}
|
|
8765
8965
|
async getEmail(id, folder) {
|
|
8766
|
-
|
|
8966
|
+
const client = await this.ensureConnected();
|
|
8767
8967
|
const targetFolder = folder || "INBOX";
|
|
8768
8968
|
let lock;
|
|
8769
8969
|
try {
|
|
8770
|
-
lock = await
|
|
8970
|
+
lock = await client.getMailboxLock(targetFolder);
|
|
8771
8971
|
} catch (error48) {
|
|
8772
8972
|
throw formatImapError(error48, `Failed to open folder "${targetFolder}"`);
|
|
8773
8973
|
}
|
|
8774
8974
|
try {
|
|
8775
8975
|
const uid = parseInt(id, 10);
|
|
8776
|
-
const msg = await
|
|
8976
|
+
const msg = await client.fetchOne(String(uid), { source: true, uid: true, flags: true }, { uid: true });
|
|
8777
8977
|
if (!msg) throw new Error(`Email ${id} not found`);
|
|
8778
8978
|
const parsed = await simpleParser(msg.source);
|
|
8779
8979
|
parsed.flags = msg.flags;
|
|
@@ -8783,22 +8983,22 @@ var init_adapter3 = __esm({
|
|
|
8783
8983
|
}
|
|
8784
8984
|
}
|
|
8785
8985
|
async getThread(threadId) {
|
|
8786
|
-
|
|
8986
|
+
const client = await this.ensureConnected();
|
|
8787
8987
|
let lock;
|
|
8788
8988
|
try {
|
|
8789
|
-
lock = await
|
|
8989
|
+
lock = await client.getMailboxLock("INBOX");
|
|
8790
8990
|
} catch (error48) {
|
|
8791
8991
|
throw formatImapError(error48, 'Failed to open folder "INBOX"');
|
|
8792
8992
|
}
|
|
8793
8993
|
try {
|
|
8794
|
-
const searchResult = await
|
|
8994
|
+
const searchResult = await client.search(
|
|
8795
8995
|
{ or: [{ header: { "message-id": threadId } }, { header: { references: threadId } }, { header: { "in-reply-to": threadId } }] },
|
|
8796
8996
|
{ uid: true }
|
|
8797
8997
|
);
|
|
8798
8998
|
const uids = Array.isArray(searchResult) ? searchResult : [];
|
|
8799
8999
|
if (uids.length === 0) throw new Error(`Thread ${threadId} not found`);
|
|
8800
9000
|
const messages = [];
|
|
8801
|
-
for await (const msg of
|
|
9001
|
+
for await (const msg of client.fetch(uids, { source: true, uid: true, flags: true })) {
|
|
8802
9002
|
const parsed = await simpleParser(msg.source);
|
|
8803
9003
|
parsed.flags = msg.flags;
|
|
8804
9004
|
messages.push(mapParsedEmail(parsed, "INBOX", this.accountId, msg.uid));
|
|
@@ -8824,15 +9024,15 @@ var init_adapter3 = __esm({
|
|
|
8824
9024
|
}
|
|
8825
9025
|
}
|
|
8826
9026
|
async getAttachment(emailId, attachmentId) {
|
|
8827
|
-
|
|
9027
|
+
const client = await this.ensureConnected();
|
|
8828
9028
|
let lock;
|
|
8829
9029
|
try {
|
|
8830
|
-
lock = await
|
|
9030
|
+
lock = await client.getMailboxLock("INBOX");
|
|
8831
9031
|
} catch (error48) {
|
|
8832
9032
|
throw formatImapError(error48, 'Failed to open folder "INBOX"');
|
|
8833
9033
|
}
|
|
8834
9034
|
try {
|
|
8835
|
-
const msg = await
|
|
9035
|
+
const msg = await client.fetchOne(String(emailId), { source: true, uid: true }, { uid: true });
|
|
8836
9036
|
if (!msg) throw new Error(`Email ${emailId} not found`);
|
|
8837
9037
|
const parsed = await simpleParser(msg.source);
|
|
8838
9038
|
const attachment = (parsed.attachments || []).find(
|
|
@@ -8858,8 +9058,7 @@ var init_adapter3 = __esm({
|
|
|
8858
9058
|
const messageId = await sendViaSmtp(transport, this.email, params);
|
|
8859
9059
|
return { id: messageId };
|
|
8860
9060
|
}
|
|
8861
|
-
|
|
8862
|
-
if (!this.client) throw new Error("Not connected");
|
|
9061
|
+
buildDraftRfc2822(params) {
|
|
8863
9062
|
const lines = [];
|
|
8864
9063
|
lines.push(`From: ${this.email}`);
|
|
8865
9064
|
lines.push(`To: ${params.to.map((c) => c.name ? `"${c.name}" <${c.email}>` : c.email).join(", ")}`);
|
|
@@ -8874,8 +9073,39 @@ var init_adapter3 = __esm({
|
|
|
8874
9073
|
lines.push("Content-Type: text/plain; charset=utf-8");
|
|
8875
9074
|
lines.push("");
|
|
8876
9075
|
lines.push(params.body.text || "");
|
|
8877
|
-
|
|
8878
|
-
|
|
9076
|
+
return lines.join("\r\n");
|
|
9077
|
+
}
|
|
9078
|
+
async createDraft(params) {
|
|
9079
|
+
const client = await this.ensureConnected();
|
|
9080
|
+
const rawMessage = this.buildDraftRfc2822(params);
|
|
9081
|
+
const draftsFolder = await this.resolveFolder("Drafts");
|
|
9082
|
+
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
9083
|
+
if (!result) throw new Error("Failed to append draft");
|
|
9084
|
+
return { id: String(result.uid || result) };
|
|
9085
|
+
}
|
|
9086
|
+
/**
|
|
9087
|
+
* IMAP has no in-place message update — a draft is just a message in the
|
|
9088
|
+
* Drafts folder, and IMAP messages are immutable once appended. This
|
|
9089
|
+
* deletes the old revision and appends a new one, so the returned `id`
|
|
9090
|
+
* is a NEW uid; the old id no longer resolves after this call.
|
|
9091
|
+
*/
|
|
9092
|
+
async updateDraft(draftId, params, sourceFolder) {
|
|
9093
|
+
const client = await this.ensureConnected();
|
|
9094
|
+
const draftsFolder = sourceFolder ? await this.resolveFolder(sourceFolder) : await this.resolveFolder("Drafts");
|
|
9095
|
+
let lock;
|
|
9096
|
+
try {
|
|
9097
|
+
lock = await client.getMailboxLock(draftsFolder);
|
|
9098
|
+
} catch (error48) {
|
|
9099
|
+
throw formatImapError(error48, `Failed to open folder "${draftsFolder}"`);
|
|
9100
|
+
}
|
|
9101
|
+
try {
|
|
9102
|
+
await client.messageDelete(draftId, { uid: true });
|
|
9103
|
+
} finally {
|
|
9104
|
+
lock.release();
|
|
9105
|
+
}
|
|
9106
|
+
const rawMessage = this.buildDraftRfc2822(params);
|
|
9107
|
+
const result = await client.append(draftsFolder, rawMessage, ["\\Draft", "\\Seen"]);
|
|
9108
|
+
if (!result) throw new Error("Failed to append updated draft");
|
|
8879
9109
|
return { id: String(result.uid || result) };
|
|
8880
9110
|
}
|
|
8881
9111
|
async listDrafts(limit, offset) {
|
|
@@ -8886,16 +9116,16 @@ var init_adapter3 = __esm({
|
|
|
8886
9116
|
});
|
|
8887
9117
|
}
|
|
8888
9118
|
async moveEmail(emailId, targetFolder, sourceFolder) {
|
|
8889
|
-
|
|
9119
|
+
const client = await this.ensureConnected();
|
|
8890
9120
|
const folder = sourceFolder || "INBOX";
|
|
8891
9121
|
let lock;
|
|
8892
9122
|
try {
|
|
8893
|
-
lock = await
|
|
9123
|
+
lock = await client.getMailboxLock(folder);
|
|
8894
9124
|
} catch (error48) {
|
|
8895
9125
|
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
8896
9126
|
}
|
|
8897
9127
|
try {
|
|
8898
|
-
await
|
|
9128
|
+
await client.messageMove(emailId, targetFolder, { uid: true });
|
|
8899
9129
|
} finally {
|
|
8900
9130
|
lock.release();
|
|
8901
9131
|
}
|
|
@@ -8925,44 +9155,44 @@ var init_adapter3 = __esm({
|
|
|
8925
9155
|
}
|
|
8926
9156
|
}
|
|
8927
9157
|
async deleteEmail(emailId, permanent, sourceFolder) {
|
|
8928
|
-
|
|
9158
|
+
const client = await this.ensureConnected();
|
|
8929
9159
|
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
8930
9160
|
const trashFolder = await this.resolveFolder("Trash");
|
|
8931
9161
|
let lock;
|
|
8932
9162
|
try {
|
|
8933
|
-
lock = await
|
|
9163
|
+
lock = await client.getMailboxLock(folder);
|
|
8934
9164
|
} catch (error48) {
|
|
8935
9165
|
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
8936
9166
|
}
|
|
8937
9167
|
try {
|
|
8938
9168
|
if (permanent || folder === trashFolder) {
|
|
8939
|
-
await
|
|
9169
|
+
await client.messageDelete(emailId, { uid: true });
|
|
8940
9170
|
} else {
|
|
8941
|
-
await
|
|
9171
|
+
await client.messageMove(emailId, trashFolder, { uid: true });
|
|
8942
9172
|
}
|
|
8943
9173
|
} finally {
|
|
8944
9174
|
lock.release();
|
|
8945
9175
|
}
|
|
8946
9176
|
}
|
|
8947
9177
|
async markEmail(emailId, flags, sourceFolder) {
|
|
8948
|
-
|
|
9178
|
+
const client = await this.ensureConnected();
|
|
8949
9179
|
const folder = sourceFolder || "INBOX";
|
|
8950
9180
|
let lock;
|
|
8951
9181
|
try {
|
|
8952
|
-
lock = await
|
|
9182
|
+
lock = await client.getMailboxLock(folder);
|
|
8953
9183
|
} catch (error48) {
|
|
8954
9184
|
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
8955
9185
|
}
|
|
8956
9186
|
try {
|
|
8957
9187
|
if (flags.read === true) {
|
|
8958
|
-
await
|
|
9188
|
+
await client.messageFlagsAdd(emailId, ["\\Seen"], { uid: true });
|
|
8959
9189
|
} else if (flags.read === false) {
|
|
8960
|
-
await
|
|
9190
|
+
await client.messageFlagsRemove(emailId, ["\\Seen"], { uid: true });
|
|
8961
9191
|
}
|
|
8962
9192
|
if (flags.starred === true || flags.flagged === true) {
|
|
8963
|
-
await
|
|
9193
|
+
await client.messageFlagsAdd(emailId, ["\\Flagged"], { uid: true });
|
|
8964
9194
|
} else if (flags.starred === false || flags.flagged === false) {
|
|
8965
|
-
await
|
|
9195
|
+
await client.messageFlagsRemove(emailId, ["\\Flagged"], { uid: true });
|
|
8966
9196
|
}
|
|
8967
9197
|
} finally {
|
|
8968
9198
|
lock.release();
|
|
@@ -8995,32 +9225,32 @@ var init_adapter3 = __esm({
|
|
|
8995
9225
|
return { id: String(result?.uid ?? "") };
|
|
8996
9226
|
}
|
|
8997
9227
|
async batchDelete(emailIds, permanent, sourceFolder) {
|
|
8998
|
-
|
|
9228
|
+
const client = await this.ensureConnected();
|
|
8999
9229
|
const result = { succeeded: [], failed: [] };
|
|
9000
9230
|
const folder = sourceFolder ? await this.resolveFolder(sourceFolder) : "INBOX";
|
|
9001
9231
|
const trashFolder = await this.resolveFolder("Trash");
|
|
9002
9232
|
const shouldPermanentDelete = permanent || folder === trashFolder;
|
|
9003
9233
|
let lock;
|
|
9004
9234
|
try {
|
|
9005
|
-
lock = await
|
|
9235
|
+
lock = await client.getMailboxLock(folder);
|
|
9006
9236
|
} catch (error48) {
|
|
9007
9237
|
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
9008
9238
|
}
|
|
9009
9239
|
try {
|
|
9010
9240
|
const uidRange = emailIds.join(",");
|
|
9011
9241
|
if (shouldPermanentDelete) {
|
|
9012
|
-
await
|
|
9242
|
+
await client.messageDelete(uidRange, { uid: true });
|
|
9013
9243
|
} else {
|
|
9014
|
-
await
|
|
9244
|
+
await client.messageMove(uidRange, trashFolder, { uid: true });
|
|
9015
9245
|
}
|
|
9016
9246
|
result.succeeded = [...emailIds];
|
|
9017
9247
|
} catch (error48) {
|
|
9018
9248
|
for (const id of emailIds) {
|
|
9019
9249
|
try {
|
|
9020
9250
|
if (shouldPermanentDelete) {
|
|
9021
|
-
await
|
|
9251
|
+
await client.messageDelete(id, { uid: true });
|
|
9022
9252
|
} else {
|
|
9023
|
-
await
|
|
9253
|
+
await client.messageMove(id, trashFolder, { uid: true });
|
|
9024
9254
|
}
|
|
9025
9255
|
result.succeeded.push(id);
|
|
9026
9256
|
} catch (e) {
|
|
@@ -9033,23 +9263,23 @@ var init_adapter3 = __esm({
|
|
|
9033
9263
|
return result;
|
|
9034
9264
|
}
|
|
9035
9265
|
async batchMove(emailIds, targetFolder, sourceFolder) {
|
|
9036
|
-
|
|
9266
|
+
const client = await this.ensureConnected();
|
|
9037
9267
|
const result = { succeeded: [], failed: [] };
|
|
9038
9268
|
const folder = sourceFolder || "INBOX";
|
|
9039
9269
|
let lock;
|
|
9040
9270
|
try {
|
|
9041
|
-
lock = await
|
|
9271
|
+
lock = await client.getMailboxLock(folder);
|
|
9042
9272
|
} catch (error48) {
|
|
9043
9273
|
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
9044
9274
|
}
|
|
9045
9275
|
try {
|
|
9046
9276
|
const uidRange = emailIds.join(",");
|
|
9047
|
-
await
|
|
9277
|
+
await client.messageMove(uidRange, targetFolder, { uid: true });
|
|
9048
9278
|
result.succeeded = [...emailIds];
|
|
9049
9279
|
} catch (error48) {
|
|
9050
9280
|
for (const id of emailIds) {
|
|
9051
9281
|
try {
|
|
9052
|
-
await
|
|
9282
|
+
await client.messageMove(id, targetFolder, { uid: true });
|
|
9053
9283
|
result.succeeded.push(id);
|
|
9054
9284
|
} catch (e) {
|
|
9055
9285
|
result.failed.push({ id, error: e.message });
|
|
@@ -9061,26 +9291,26 @@ var init_adapter3 = __esm({
|
|
|
9061
9291
|
return result;
|
|
9062
9292
|
}
|
|
9063
9293
|
async batchMark(emailIds, flags, sourceFolder) {
|
|
9064
|
-
|
|
9294
|
+
const client = await this.ensureConnected();
|
|
9065
9295
|
const result = { succeeded: [], failed: [] };
|
|
9066
9296
|
const folder = sourceFolder || "INBOX";
|
|
9067
9297
|
let lock;
|
|
9068
9298
|
try {
|
|
9069
|
-
lock = await
|
|
9299
|
+
lock = await client.getMailboxLock(folder);
|
|
9070
9300
|
} catch (error48) {
|
|
9071
9301
|
throw formatImapError(error48, `Failed to open folder "${folder}"`);
|
|
9072
9302
|
}
|
|
9073
9303
|
try {
|
|
9074
9304
|
const uidRange = emailIds.join(",");
|
|
9075
9305
|
if (flags.read === true) {
|
|
9076
|
-
await
|
|
9306
|
+
await client.messageFlagsAdd(uidRange, ["\\Seen"], { uid: true });
|
|
9077
9307
|
} else if (flags.read === false) {
|
|
9078
|
-
await
|
|
9308
|
+
await client.messageFlagsRemove(uidRange, ["\\Seen"], { uid: true });
|
|
9079
9309
|
}
|
|
9080
9310
|
if (flags.starred === true || flags.flagged === true) {
|
|
9081
|
-
await
|
|
9311
|
+
await client.messageFlagsAdd(uidRange, ["\\Flagged"], { uid: true });
|
|
9082
9312
|
} else if (flags.starred === false || flags.flagged === false) {
|
|
9083
|
-
await
|
|
9313
|
+
await client.messageFlagsRemove(uidRange, ["\\Flagged"], { uid: true });
|
|
9084
9314
|
}
|
|
9085
9315
|
result.succeeded = [...emailIds];
|
|
9086
9316
|
} catch (error48) {
|
|
@@ -9133,9 +9363,9 @@ var GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, OUTLOOK_CLIENT_ID;
|
|
|
9133
9363
|
var init_oauth_config = __esm({
|
|
9134
9364
|
"src/oauth-config.ts"() {
|
|
9135
9365
|
"use strict";
|
|
9136
|
-
GMAIL_CLIENT_ID = "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
|
|
9137
|
-
GMAIL_CLIENT_SECRET = "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";
|
|
9138
|
-
OUTLOOK_CLIENT_ID = "76d32225-ef10-4b49-951c-c80ae906595d";
|
|
9366
|
+
GMAIL_CLIENT_ID = process.env.EMAIL_MCP_GMAIL_CLIENT_ID || "1062266096576-lu2ukplhc3lcm0bvheml5i18urptdp4t.apps.googleusercontent.com";
|
|
9367
|
+
GMAIL_CLIENT_SECRET = process.env.EMAIL_MCP_GMAIL_CLIENT_SECRET || "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";
|
|
9368
|
+
OUTLOOK_CLIENT_ID = process.env.EMAIL_MCP_OUTLOOK_CLIENT_ID || "76d32225-ef10-4b49-951c-c80ae906595d";
|
|
9139
9369
|
}
|
|
9140
9370
|
});
|
|
9141
9371
|
|
|
@@ -9541,14 +9771,30 @@ async function testAndSave(creds) {
|
|
|
9541
9771
|
async function promptAccountName(defaultName) {
|
|
9542
9772
|
return askText("Account name:", defaultName);
|
|
9543
9773
|
}
|
|
9544
|
-
async function setupGmail() {
|
|
9774
|
+
async function setupGmail(scopeModeFlag) {
|
|
9545
9775
|
console.log("\n--- Gmail Setup ---");
|
|
9776
|
+
if (process.env.EMAIL_MCP_GMAIL_CLIENT_ID) {
|
|
9777
|
+
console.log("Using your own Google OAuth Client ID (EMAIL_MCP_GMAIL_CLIENT_ID).");
|
|
9778
|
+
}
|
|
9546
9779
|
console.log("A browser window will open for Google authorization.\n");
|
|
9780
|
+
let scopeMode = scopeModeFlag;
|
|
9781
|
+
if (!scopeMode) {
|
|
9782
|
+
scopeMode = await askChoice("Gmail permission scope:", [
|
|
9783
|
+
{
|
|
9784
|
+
name: "Full (default) \u2014 includes permanent delete, bypassing Trash",
|
|
9785
|
+
value: "full"
|
|
9786
|
+
},
|
|
9787
|
+
{
|
|
9788
|
+
name: "Restricted \u2014 read/send/label/archive/trash only, no permanent delete",
|
|
9789
|
+
value: "restricted"
|
|
9790
|
+
}
|
|
9791
|
+
]);
|
|
9792
|
+
}
|
|
9547
9793
|
const server = new OAuthCallbackServer();
|
|
9548
9794
|
const port = await server.start();
|
|
9549
9795
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
9550
9796
|
const gmailAuth = new GmailAuth(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, redirectUri);
|
|
9551
|
-
const { url: url2, codeVerifier } = gmailAuth.getAuthUrl(redirectUri);
|
|
9797
|
+
const { url: url2, codeVerifier } = gmailAuth.getAuthUrl(redirectUri, scopeMode);
|
|
9552
9798
|
console.log("\nOpening browser for Google authorization...");
|
|
9553
9799
|
console.log(`If the browser does not open, visit:
|
|
9554
9800
|
${url2}
|
|
@@ -9747,6 +9993,17 @@ async function main() {
|
|
|
9747
9993
|
await removeAccount(id);
|
|
9748
9994
|
return;
|
|
9749
9995
|
}
|
|
9996
|
+
const scopeIdx = args.indexOf("--scope");
|
|
9997
|
+
let scopeModeFlag;
|
|
9998
|
+
if (scopeIdx !== -1) {
|
|
9999
|
+
const value = args[scopeIdx + 1];
|
|
10000
|
+
if (value === "full" || value === "restricted") {
|
|
10001
|
+
scopeModeFlag = value;
|
|
10002
|
+
} else {
|
|
10003
|
+
console.error("Usage: --scope <full|restricted>");
|
|
10004
|
+
process.exit(1);
|
|
10005
|
+
}
|
|
10006
|
+
}
|
|
9750
10007
|
console.log("@marlinjai/email-mcp Account Setup\n");
|
|
9751
10008
|
let addMore = true;
|
|
9752
10009
|
while (addMore) {
|
|
@@ -9758,7 +10015,7 @@ async function main() {
|
|
|
9758
10015
|
]);
|
|
9759
10016
|
switch (provider) {
|
|
9760
10017
|
case ProviderType.Gmail:
|
|
9761
|
-
await setupGmail();
|
|
10018
|
+
await setupGmail(scopeModeFlag);
|
|
9762
10019
|
break;
|
|
9763
10020
|
case ProviderType.Outlook:
|
|
9764
10021
|
await setupOutlook();
|
|
@@ -10164,8 +10421,8 @@ function getErrorMap() {
|
|
|
10164
10421
|
|
|
10165
10422
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
10166
10423
|
var makeIssue = (params) => {
|
|
10167
|
-
const { data, path:
|
|
10168
|
-
const fullPath = [...
|
|
10424
|
+
const { data, path: path4, errorMaps, issueData } = params;
|
|
10425
|
+
const fullPath = [...path4, ...issueData.path || []];
|
|
10169
10426
|
const fullIssue = {
|
|
10170
10427
|
...issueData,
|
|
10171
10428
|
path: fullPath
|
|
@@ -10280,11 +10537,11 @@ var errorUtil;
|
|
|
10280
10537
|
|
|
10281
10538
|
// node_modules/zod/v3/types.js
|
|
10282
10539
|
var ParseInputLazyPath = class {
|
|
10283
|
-
constructor(parent, value,
|
|
10540
|
+
constructor(parent, value, path4, key) {
|
|
10284
10541
|
this._cachedPath = [];
|
|
10285
10542
|
this.parent = parent;
|
|
10286
10543
|
this.data = value;
|
|
10287
|
-
this._path =
|
|
10544
|
+
this._path = path4;
|
|
10288
10545
|
this._key = key;
|
|
10289
10546
|
}
|
|
10290
10547
|
get path() {
|
|
@@ -14207,10 +14464,10 @@ function mergeDefs(...defs) {
|
|
|
14207
14464
|
function cloneDef(schema) {
|
|
14208
14465
|
return mergeDefs(schema._zod.def);
|
|
14209
14466
|
}
|
|
14210
|
-
function getElementAtPath(obj,
|
|
14211
|
-
if (!
|
|
14467
|
+
function getElementAtPath(obj, path4) {
|
|
14468
|
+
if (!path4)
|
|
14212
14469
|
return obj;
|
|
14213
|
-
return
|
|
14470
|
+
return path4.reduce((acc, key) => acc?.[key], obj);
|
|
14214
14471
|
}
|
|
14215
14472
|
function promiseAllObject(promisesObj) {
|
|
14216
14473
|
const keys = Object.keys(promisesObj);
|
|
@@ -14593,11 +14850,11 @@ function aborted(x, startIndex = 0) {
|
|
|
14593
14850
|
}
|
|
14594
14851
|
return false;
|
|
14595
14852
|
}
|
|
14596
|
-
function prefixIssues(
|
|
14853
|
+
function prefixIssues(path4, issues) {
|
|
14597
14854
|
return issues.map((iss) => {
|
|
14598
14855
|
var _a2;
|
|
14599
14856
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
14600
|
-
iss.path.unshift(
|
|
14857
|
+
iss.path.unshift(path4);
|
|
14601
14858
|
return iss;
|
|
14602
14859
|
});
|
|
14603
14860
|
}
|
|
@@ -14780,7 +15037,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
|
|
|
14780
15037
|
}
|
|
14781
15038
|
function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
14782
15039
|
const result = { errors: [] };
|
|
14783
|
-
const processError = (error49,
|
|
15040
|
+
const processError = (error49, path4 = []) => {
|
|
14784
15041
|
var _a2, _b;
|
|
14785
15042
|
for (const issue2 of error49.issues) {
|
|
14786
15043
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -14790,7 +15047,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
14790
15047
|
} else if (issue2.code === "invalid_element") {
|
|
14791
15048
|
processError({ issues: issue2.issues }, issue2.path);
|
|
14792
15049
|
} else {
|
|
14793
|
-
const fullpath = [...
|
|
15050
|
+
const fullpath = [...path4, ...issue2.path];
|
|
14794
15051
|
if (fullpath.length === 0) {
|
|
14795
15052
|
result.errors.push(mapper(issue2));
|
|
14796
15053
|
continue;
|
|
@@ -14822,8 +15079,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
|
|
|
14822
15079
|
}
|
|
14823
15080
|
function toDotPath(_path) {
|
|
14824
15081
|
const segs = [];
|
|
14825
|
-
const
|
|
14826
|
-
for (const seg of
|
|
15082
|
+
const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
15083
|
+
for (const seg of path4) {
|
|
14827
15084
|
if (typeof seg === "number")
|
|
14828
15085
|
segs.push(`[${seg}]`);
|
|
14829
15086
|
else if (typeof seg === "symbol")
|
|
@@ -27229,13 +27486,13 @@ function resolveRef(ref, ctx) {
|
|
|
27229
27486
|
if (!ref.startsWith("#")) {
|
|
27230
27487
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
27231
27488
|
}
|
|
27232
|
-
const
|
|
27233
|
-
if (
|
|
27489
|
+
const path4 = ref.slice(1).split("/").filter(Boolean);
|
|
27490
|
+
if (path4.length === 0) {
|
|
27234
27491
|
return ctx.rootSchema;
|
|
27235
27492
|
}
|
|
27236
27493
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
27237
|
-
if (
|
|
27238
|
-
const key =
|
|
27494
|
+
if (path4[0] === defsKey) {
|
|
27495
|
+
const key = path4[1];
|
|
27239
27496
|
if (!key || !ctx.defs[key]) {
|
|
27240
27497
|
throw new Error(`Reference not found: ${ref}`);
|
|
27241
27498
|
}
|
|
@@ -33068,9 +33325,28 @@ function registerAccountTools(server, accountManager) {
|
|
|
33068
33325
|
}
|
|
33069
33326
|
|
|
33070
33327
|
// src/tools/reading.ts
|
|
33328
|
+
import fs3 from "node:fs";
|
|
33329
|
+
import os3 from "node:os";
|
|
33330
|
+
import path3 from "node:path";
|
|
33071
33331
|
function jsonResult2(data) {
|
|
33072
33332
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
33073
33333
|
}
|
|
33334
|
+
function getDownloadsBaseDir() {
|
|
33335
|
+
return path3.resolve(
|
|
33336
|
+
process.env.EMAIL_MCP_DOWNLOADS_DIR || path3.join(os3.homedir(), ".email-mcp", "downloads")
|
|
33337
|
+
);
|
|
33338
|
+
}
|
|
33339
|
+
function resolveDownloadPath(outputPath) {
|
|
33340
|
+
const baseDir = getDownloadsBaseDir();
|
|
33341
|
+
const resolved = path3.resolve(baseDir, outputPath);
|
|
33342
|
+
const relative = path3.relative(baseDir, resolved);
|
|
33343
|
+
if (relative.startsWith("..") || path3.isAbsolute(relative)) {
|
|
33344
|
+
throw new Error(
|
|
33345
|
+
`outputPath must resolve inside ${baseDir} (set EMAIL_MCP_DOWNLOADS_DIR to change the base directory)`
|
|
33346
|
+
);
|
|
33347
|
+
}
|
|
33348
|
+
return resolved;
|
|
33349
|
+
}
|
|
33074
33350
|
function stripBodies(emails) {
|
|
33075
33351
|
return emails.map((email3) => ({
|
|
33076
33352
|
...email3,
|
|
@@ -33195,6 +33471,33 @@ function registerReadingTools(server, accountManager) {
|
|
|
33195
33471
|
}
|
|
33196
33472
|
}
|
|
33197
33473
|
);
|
|
33474
|
+
server.tool(
|
|
33475
|
+
"email_save_attachment",
|
|
33476
|
+
`Download an email attachment and save it directly to disk, returning metadata only (no base64 in the response) \u2014 avoids paying the token cost of round-tripping large attachments through the calling model. outputPath is relative to a fixed downloads directory (${getDownloadsBaseDir()}, override with EMAIL_MCP_DOWNLOADS_DIR) and cannot escape it.`,
|
|
33477
|
+
{
|
|
33478
|
+
accountId: external_exports3.string(),
|
|
33479
|
+
emailId: external_exports3.string(),
|
|
33480
|
+
attachmentId: external_exports3.string(),
|
|
33481
|
+
outputPath: external_exports3.string().describe("Relative file path (within the downloads directory) to save the attachment as")
|
|
33482
|
+
},
|
|
33483
|
+
async (args) => {
|
|
33484
|
+
try {
|
|
33485
|
+
const absPath = resolveDownloadPath(args.outputPath);
|
|
33486
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33487
|
+
const { data, meta: meta3 } = await provider.getAttachment(args.emailId, args.attachmentId);
|
|
33488
|
+
fs3.mkdirSync(path3.dirname(absPath), { recursive: true });
|
|
33489
|
+
fs3.writeFileSync(absPath, Buffer.from(data));
|
|
33490
|
+
return jsonResult2({
|
|
33491
|
+
path: absPath,
|
|
33492
|
+
bytes: data.length,
|
|
33493
|
+
filename: meta3.filename,
|
|
33494
|
+
mimeType: meta3.contentType
|
|
33495
|
+
});
|
|
33496
|
+
} catch (error48) {
|
|
33497
|
+
return jsonResult2({ error: error48.message });
|
|
33498
|
+
}
|
|
33499
|
+
}
|
|
33500
|
+
);
|
|
33198
33501
|
}
|
|
33199
33502
|
|
|
33200
33503
|
// src/tools/sending.ts
|
|
@@ -33356,6 +33659,32 @@ function registerSendingTools(server, accountManager) {
|
|
|
33356
33659
|
}
|
|
33357
33660
|
}
|
|
33358
33661
|
);
|
|
33662
|
+
server.tool(
|
|
33663
|
+
"email_draft_update",
|
|
33664
|
+
"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.",
|
|
33665
|
+
{
|
|
33666
|
+
accountId: external_exports3.string(),
|
|
33667
|
+
draftId: external_exports3.string(),
|
|
33668
|
+
to: external_exports3.array(ContactSchema),
|
|
33669
|
+
subject: external_exports3.string(),
|
|
33670
|
+
body: BodySchema,
|
|
33671
|
+
sourceFolder: external_exports3.string().optional().describe("Source folder (required for IMAP/iCloud when the draft is not in the default Drafts folder)")
|
|
33672
|
+
},
|
|
33673
|
+
async (args) => {
|
|
33674
|
+
try {
|
|
33675
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33676
|
+
const params = {
|
|
33677
|
+
to: args.to,
|
|
33678
|
+
subject: args.subject,
|
|
33679
|
+
body: args.body
|
|
33680
|
+
};
|
|
33681
|
+
const result = await provider.updateDraft(args.draftId, params, args.sourceFolder);
|
|
33682
|
+
return jsonResult3(result);
|
|
33683
|
+
} catch (error48) {
|
|
33684
|
+
return jsonResult3({ error: error48.message });
|
|
33685
|
+
}
|
|
33686
|
+
}
|
|
33687
|
+
);
|
|
33359
33688
|
server.tool(
|
|
33360
33689
|
"email_draft_list",
|
|
33361
33690
|
"List email drafts",
|
|
@@ -33543,6 +33872,45 @@ function registerOrganizingTools(server, accountManager) {
|
|
|
33543
33872
|
}
|
|
33544
33873
|
}
|
|
33545
33874
|
);
|
|
33875
|
+
server.tool(
|
|
33876
|
+
"email_batch_label",
|
|
33877
|
+
"Add or remove labels on multiple emails at once (Gmail only). Much faster than individual email_label calls.",
|
|
33878
|
+
{
|
|
33879
|
+
accountId: external_exports3.string(),
|
|
33880
|
+
emailIds: external_exports3.array(external_exports3.string()).min(1).describe("Array of email IDs to label"),
|
|
33881
|
+
addLabels: external_exports3.array(external_exports3.string()).optional(),
|
|
33882
|
+
removeLabels: external_exports3.array(external_exports3.string()).optional()
|
|
33883
|
+
},
|
|
33884
|
+
async (args) => {
|
|
33885
|
+
try {
|
|
33886
|
+
const provider = await accountManager.getProvider(args.accountId);
|
|
33887
|
+
if (!provider.addLabels || !provider.removeLabels) {
|
|
33888
|
+
return jsonResult4({
|
|
33889
|
+
success: false,
|
|
33890
|
+
error: "email_batch_label is only supported on Gmail accounts",
|
|
33891
|
+
supportedProviders: ["gmail"]
|
|
33892
|
+
});
|
|
33893
|
+
}
|
|
33894
|
+
if (provider.batchLabel) {
|
|
33895
|
+
const result2 = await provider.batchLabel(args.emailIds, args.addLabels, args.removeLabels);
|
|
33896
|
+
return jsonResult4({ success: true, ...result2 });
|
|
33897
|
+
}
|
|
33898
|
+
const result = { succeeded: [], failed: [] };
|
|
33899
|
+
for (const id of args.emailIds) {
|
|
33900
|
+
try {
|
|
33901
|
+
if (args.addLabels && args.addLabels.length > 0) await provider.addLabels(id, args.addLabels);
|
|
33902
|
+
if (args.removeLabels && args.removeLabels.length > 0) await provider.removeLabels(id, args.removeLabels);
|
|
33903
|
+
result.succeeded.push(id);
|
|
33904
|
+
} catch (e) {
|
|
33905
|
+
result.failed.push({ id, error: e.message });
|
|
33906
|
+
}
|
|
33907
|
+
}
|
|
33908
|
+
return jsonResult4({ success: true, ...result });
|
|
33909
|
+
} catch (error48) {
|
|
33910
|
+
return jsonResult4({ success: false, error: error48.message });
|
|
33911
|
+
}
|
|
33912
|
+
}
|
|
33913
|
+
);
|
|
33546
33914
|
server.tool(
|
|
33547
33915
|
"email_transfer",
|
|
33548
33916
|
"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.",
|