hypermail-mcp 0.7.13 → 0.7.15
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 +26 -4
- package/dist/cli.js +689 -253
- package/dist/cli.js.map +1 -1
- package/examples/hermes/README.md +65 -0
- package/examples/hermes/hypermail_new_email_poller.py +172 -0
- package/examples/hermes/jobs.example.json +21 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -13954,21 +13954,79 @@ async function tryKeytarSet(key) {
|
|
|
13954
13954
|
}
|
|
13955
13955
|
}
|
|
13956
13956
|
|
|
13957
|
+
// src/logger.ts
|
|
13958
|
+
var noopLogger = {
|
|
13959
|
+
debug: () => void 0
|
|
13960
|
+
};
|
|
13961
|
+
var REDACTED = "[redacted]";
|
|
13962
|
+
var SENSITIVE_FIELD = /token|secret|password|credential|key|body|content/i;
|
|
13963
|
+
function createLogger(opts) {
|
|
13964
|
+
if (!opts.enabled) return noopLogger;
|
|
13965
|
+
const write = opts.write ?? ((line) => process.stderr.write(line));
|
|
13966
|
+
return {
|
|
13967
|
+
debug(component, event, fields = {}) {
|
|
13968
|
+
try {
|
|
13969
|
+
const payload = {
|
|
13970
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13971
|
+
pid: process.pid,
|
|
13972
|
+
component,
|
|
13973
|
+
event,
|
|
13974
|
+
...sanitizeFields(fields)
|
|
13975
|
+
};
|
|
13976
|
+
write(`[hypermail-mcp] debug ${JSON.stringify(payload)}
|
|
13977
|
+
`);
|
|
13978
|
+
} catch {
|
|
13979
|
+
}
|
|
13980
|
+
}
|
|
13981
|
+
};
|
|
13982
|
+
}
|
|
13983
|
+
function sanitizeFields(fields) {
|
|
13984
|
+
const sanitized = {};
|
|
13985
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
13986
|
+
sanitized[key] = sanitizeValue(key, value, 0);
|
|
13987
|
+
}
|
|
13988
|
+
return sanitized;
|
|
13989
|
+
}
|
|
13990
|
+
function sanitizeValue(key, value, depth) {
|
|
13991
|
+
if (SENSITIVE_FIELD.test(key)) return REDACTED;
|
|
13992
|
+
if (value === null || value === void 0) return value;
|
|
13993
|
+
const valueType = typeof value;
|
|
13994
|
+
if (valueType === "string" || valueType === "number" || valueType === "boolean") {
|
|
13995
|
+
return value;
|
|
13996
|
+
}
|
|
13997
|
+
if (value instanceof Date) return value.toISOString();
|
|
13998
|
+
if (Array.isArray(value)) {
|
|
13999
|
+
if (depth >= 2) return `[array:${value.length}]`;
|
|
14000
|
+
return value.map((item) => sanitizeValue(key, item, depth + 1));
|
|
14001
|
+
}
|
|
14002
|
+
if (valueType === "object") {
|
|
14003
|
+
if (depth >= 2) return "[object]";
|
|
14004
|
+
const out = {};
|
|
14005
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
14006
|
+
out[childKey] = sanitizeValue(childKey, childValue, depth + 1);
|
|
14007
|
+
}
|
|
14008
|
+
return out;
|
|
14009
|
+
}
|
|
14010
|
+
return String(value);
|
|
14011
|
+
}
|
|
14012
|
+
|
|
13957
14013
|
// src/store/account-store.ts
|
|
13958
14014
|
var FILE_NAME = "accounts.json.enc";
|
|
13959
14015
|
var LOCK_STALE_MS = 3e4;
|
|
13960
14016
|
var LOCK_TIMEOUT_MS = 1e4;
|
|
13961
14017
|
var LOCK_RETRY_MS = 25;
|
|
13962
14018
|
var AccountStore = class _AccountStore {
|
|
13963
|
-
constructor(filePath, key, data) {
|
|
14019
|
+
constructor(filePath, key, data, logger) {
|
|
13964
14020
|
this.filePath = filePath;
|
|
13965
14021
|
this.key = key;
|
|
13966
14022
|
this.data = data;
|
|
14023
|
+
this.logger = logger;
|
|
13967
14024
|
this.lockPath = `${filePath}.lock`;
|
|
13968
14025
|
}
|
|
13969
14026
|
filePath;
|
|
13970
14027
|
key;
|
|
13971
14028
|
data;
|
|
14029
|
+
logger;
|
|
13972
14030
|
writeLocks = /* @__PURE__ */ new Map();
|
|
13973
14031
|
lockPath;
|
|
13974
14032
|
static async open(opts = {}) {
|
|
@@ -13987,7 +14045,12 @@ var AccountStore = class _AccountStore {
|
|
|
13987
14045
|
throw err;
|
|
13988
14046
|
}
|
|
13989
14047
|
}
|
|
13990
|
-
|
|
14048
|
+
const logger = opts.logger ?? noopLogger;
|
|
14049
|
+
logger.debug("account-store", "open", {
|
|
14050
|
+
filePath,
|
|
14051
|
+
accountCount: data.accounts.length
|
|
14052
|
+
});
|
|
14053
|
+
return new _AccountStore(filePath, key, data, logger);
|
|
13991
14054
|
}
|
|
13992
14055
|
listAccounts() {
|
|
13993
14056
|
return this.data.accounts.map((a) => ({ ...a }));
|
|
@@ -14011,6 +14074,13 @@ var AccountStore = class _AccountStore {
|
|
|
14011
14074
|
else delete next.newEmailCheckpoint;
|
|
14012
14075
|
if (idx >= 0) data.accounts[idx] = next;
|
|
14013
14076
|
else data.accounts.push(next);
|
|
14077
|
+
this.logger.debug("account-store", "upsertAccount", {
|
|
14078
|
+
email: norm,
|
|
14079
|
+
existed: idx >= 0,
|
|
14080
|
+
checkpointReceivedAt: mergedCheckpoint?.receivedAt ?? null,
|
|
14081
|
+
deliveredIdCount: mergedCheckpoint?.deliveredIdsAtReceivedAt?.length ?? 0,
|
|
14082
|
+
changed: true
|
|
14083
|
+
});
|
|
14014
14084
|
return { result: { ...next }, changed: true };
|
|
14015
14085
|
}));
|
|
14016
14086
|
}
|
|
@@ -14018,10 +14088,25 @@ var AccountStore = class _AccountStore {
|
|
|
14018
14088
|
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14019
14089
|
const norm = email.trim().toLowerCase();
|
|
14020
14090
|
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
14021
|
-
if (idx < 0)
|
|
14091
|
+
if (idx < 0) {
|
|
14092
|
+
this.logger.debug("account-store", "updateTokens", {
|
|
14093
|
+
email: norm,
|
|
14094
|
+
found: false,
|
|
14095
|
+
changed: false
|
|
14096
|
+
});
|
|
14097
|
+
return { result: void 0, changed: false };
|
|
14098
|
+
}
|
|
14022
14099
|
const current = data.accounts[idx];
|
|
14023
14100
|
const next = { ...current, tokens };
|
|
14024
14101
|
data.accounts[idx] = next;
|
|
14102
|
+
this.logger.debug("account-store", "updateTokens", {
|
|
14103
|
+
email: norm,
|
|
14104
|
+
found: true,
|
|
14105
|
+
fieldCount: Object.keys(tokens).length,
|
|
14106
|
+
storedCheckpointReceivedAt: current.newEmailCheckpoint?.receivedAt ?? null,
|
|
14107
|
+
storedDeliveredIdCount: current.newEmailCheckpoint?.deliveredIdsAtReceivedAt?.length ?? 0,
|
|
14108
|
+
changed: true
|
|
14109
|
+
});
|
|
14025
14110
|
return { result: { ...next }, changed: true };
|
|
14026
14111
|
}));
|
|
14027
14112
|
}
|
|
@@ -14029,18 +14114,45 @@ var AccountStore = class _AccountStore {
|
|
|
14029
14114
|
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14030
14115
|
const norm = email.trim().toLowerCase();
|
|
14031
14116
|
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
14032
|
-
if (idx < 0)
|
|
14117
|
+
if (idx < 0) {
|
|
14118
|
+
this.logger.debug("account-store", "updateNewEmailCheckpoint", {
|
|
14119
|
+
email: norm,
|
|
14120
|
+
found: false,
|
|
14121
|
+
incomingReceivedAt: checkpoint.receivedAt,
|
|
14122
|
+
incomingDeliveredIdCount: checkpoint.deliveredIdsAtReceivedAt?.length ?? 0,
|
|
14123
|
+
changed: false
|
|
14124
|
+
});
|
|
14125
|
+
return { result: void 0, changed: false };
|
|
14126
|
+
}
|
|
14033
14127
|
const current = data.accounts[idx];
|
|
14034
14128
|
const mergedCheckpoint = mergeNewEmailCheckpoints(
|
|
14035
14129
|
current.newEmailCheckpoint,
|
|
14036
14130
|
checkpoint
|
|
14037
14131
|
);
|
|
14038
|
-
if (!mergedCheckpoint)
|
|
14132
|
+
if (!mergedCheckpoint) {
|
|
14133
|
+
this.logger.debug("account-store", "updateNewEmailCheckpoint", {
|
|
14134
|
+
email: norm,
|
|
14135
|
+
found: true,
|
|
14136
|
+
incomingReceivedAt: checkpoint.receivedAt,
|
|
14137
|
+
incomingDeliveredIdCount: checkpoint.deliveredIdsAtReceivedAt?.length ?? 0,
|
|
14138
|
+
changed: false
|
|
14139
|
+
});
|
|
14140
|
+
return { result: { ...current }, changed: false };
|
|
14141
|
+
}
|
|
14039
14142
|
const next = {
|
|
14040
14143
|
...current,
|
|
14041
14144
|
newEmailCheckpoint: mergedCheckpoint
|
|
14042
14145
|
};
|
|
14043
14146
|
data.accounts[idx] = next;
|
|
14147
|
+
this.logger.debug("account-store", "updateNewEmailCheckpoint", {
|
|
14148
|
+
email: norm,
|
|
14149
|
+
found: true,
|
|
14150
|
+
currentReceivedAt: current.newEmailCheckpoint?.receivedAt ?? null,
|
|
14151
|
+
incomingReceivedAt: checkpoint.receivedAt,
|
|
14152
|
+
mergedReceivedAt: mergedCheckpoint.receivedAt,
|
|
14153
|
+
mergedDeliveredIdCount: mergedCheckpoint.deliveredIdsAtReceivedAt?.length ?? 0,
|
|
14154
|
+
changed: true
|
|
14155
|
+
});
|
|
14044
14156
|
return { result: { ...next }, changed: true };
|
|
14045
14157
|
}));
|
|
14046
14158
|
}
|
|
@@ -14048,7 +14160,17 @@ var AccountStore = class _AccountStore {
|
|
|
14048
14160
|
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14049
14161
|
const norm = email.trim().toLowerCase();
|
|
14050
14162
|
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
14051
|
-
if (idx < 0 || candidates.length === 0)
|
|
14163
|
+
if (idx < 0 || candidates.length === 0) {
|
|
14164
|
+
this.logger.debug("account-store", "claimNewEmails", {
|
|
14165
|
+
email: norm,
|
|
14166
|
+
found: idx >= 0,
|
|
14167
|
+
candidateCount: candidates.length,
|
|
14168
|
+
candidateIds: candidates.map((candidate) => candidate.summaryId),
|
|
14169
|
+
claimedCount: 0,
|
|
14170
|
+
changed: false
|
|
14171
|
+
});
|
|
14172
|
+
return { result: [], changed: false };
|
|
14173
|
+
}
|
|
14052
14174
|
const account = data.accounts[idx];
|
|
14053
14175
|
let checkpoint = normalizeCheckpoint(account.newEmailCheckpoint);
|
|
14054
14176
|
const claimed = [];
|
|
@@ -14069,12 +14191,36 @@ var AccountStore = class _AccountStore {
|
|
|
14069
14191
|
deliveredIdsAtReceivedAt: ids
|
|
14070
14192
|
});
|
|
14071
14193
|
}
|
|
14072
|
-
if (claimed.length === 0 || !checkpoint)
|
|
14194
|
+
if (claimed.length === 0 || !checkpoint) {
|
|
14195
|
+
this.logger.debug("account-store", "claimNewEmails", {
|
|
14196
|
+
email: norm,
|
|
14197
|
+
found: true,
|
|
14198
|
+
candidateCount: candidates.length,
|
|
14199
|
+
candidateIds: candidates.map((candidate) => candidate.summaryId),
|
|
14200
|
+
claimedCount: claimed.length,
|
|
14201
|
+
claimedIds: claimed,
|
|
14202
|
+
checkpointReceivedAt: checkpoint?.receivedAt ?? null,
|
|
14203
|
+
deliveredIdCount: checkpoint?.deliveredIdsAtReceivedAt?.length ?? 0,
|
|
14204
|
+
changed: false
|
|
14205
|
+
});
|
|
14206
|
+
return { result: claimed, changed: false };
|
|
14207
|
+
}
|
|
14073
14208
|
const next = {
|
|
14074
14209
|
...account,
|
|
14075
14210
|
newEmailCheckpoint: checkpoint
|
|
14076
14211
|
};
|
|
14077
14212
|
data.accounts[idx] = next;
|
|
14213
|
+
this.logger.debug("account-store", "claimNewEmails", {
|
|
14214
|
+
email: norm,
|
|
14215
|
+
found: true,
|
|
14216
|
+
candidateCount: candidates.length,
|
|
14217
|
+
candidateIds: candidates.map((candidate) => candidate.summaryId),
|
|
14218
|
+
claimedCount: claimed.length,
|
|
14219
|
+
claimedIds: claimed,
|
|
14220
|
+
checkpointReceivedAt: checkpoint.receivedAt,
|
|
14221
|
+
deliveredIdCount: checkpoint.deliveredIdsAtReceivedAt?.length ?? 0,
|
|
14222
|
+
changed: true
|
|
14223
|
+
});
|
|
14078
14224
|
return { result: claimed, changed: true };
|
|
14079
14225
|
}));
|
|
14080
14226
|
}
|
|
@@ -14083,7 +14229,12 @@ var AccountStore = class _AccountStore {
|
|
|
14083
14229
|
const norm = email.trim().toLowerCase();
|
|
14084
14230
|
const before = data.accounts.length;
|
|
14085
14231
|
data.accounts = data.accounts.filter((a) => a.email.toLowerCase() !== norm);
|
|
14086
|
-
|
|
14232
|
+
const changed = data.accounts.length !== before;
|
|
14233
|
+
this.logger.debug("account-store", "removeAccount", {
|
|
14234
|
+
email: norm,
|
|
14235
|
+
changed
|
|
14236
|
+
});
|
|
14237
|
+
return { result: changed, changed };
|
|
14087
14238
|
}));
|
|
14088
14239
|
}
|
|
14089
14240
|
async runSerial(email, task) {
|
|
@@ -14125,6 +14276,10 @@ var AccountStore = class _AccountStore {
|
|
|
14125
14276
|
async flush() {
|
|
14126
14277
|
const buf = encrypt(this.data, this.key);
|
|
14127
14278
|
await writeAtomic(this.filePath, buf);
|
|
14279
|
+
this.logger.debug("account-store", "flush", {
|
|
14280
|
+
filePath: this.filePath,
|
|
14281
|
+
accountCount: this.data.accounts.length
|
|
14282
|
+
});
|
|
14128
14283
|
}
|
|
14129
14284
|
async withFileLock(task) {
|
|
14130
14285
|
await this.acquireFileLock();
|
|
@@ -14132,10 +14287,14 @@ var AccountStore = class _AccountStore {
|
|
|
14132
14287
|
return await task();
|
|
14133
14288
|
} finally {
|
|
14134
14289
|
await fs2.rm(this.lockPath, { recursive: true, force: true });
|
|
14290
|
+
this.logger.debug("account-store", "lockReleased", {
|
|
14291
|
+
lockPath: this.lockPath
|
|
14292
|
+
});
|
|
14135
14293
|
}
|
|
14136
14294
|
}
|
|
14137
14295
|
async acquireFileLock() {
|
|
14138
14296
|
const startedAt = Date.now();
|
|
14297
|
+
let loggedWait = false;
|
|
14139
14298
|
while (true) {
|
|
14140
14299
|
try {
|
|
14141
14300
|
await fs2.mkdir(this.lockPath, { mode: 448 });
|
|
@@ -14146,11 +14305,25 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
|
14146
14305
|
`,
|
|
14147
14306
|
{ mode: 384 }
|
|
14148
14307
|
);
|
|
14308
|
+
this.logger.debug("account-store", "lockAcquired", {
|
|
14309
|
+
lockPath: this.lockPath,
|
|
14310
|
+
waitedMs: Date.now() - startedAt
|
|
14311
|
+
});
|
|
14149
14312
|
return;
|
|
14150
14313
|
} catch (err) {
|
|
14151
14314
|
if (err.code !== "EEXIST") throw err;
|
|
14315
|
+
if (!loggedWait) {
|
|
14316
|
+
this.logger.debug("account-store", "lockWait", {
|
|
14317
|
+
lockPath: this.lockPath
|
|
14318
|
+
});
|
|
14319
|
+
loggedWait = true;
|
|
14320
|
+
}
|
|
14152
14321
|
await this.removeStaleLock();
|
|
14153
14322
|
if (Date.now() - startedAt > LOCK_TIMEOUT_MS) {
|
|
14323
|
+
this.logger.debug("account-store", "lockTimeout", {
|
|
14324
|
+
lockPath: this.lockPath,
|
|
14325
|
+
waitedMs: Date.now() - startedAt
|
|
14326
|
+
});
|
|
14154
14327
|
throw new Error(`timed out waiting for account store lock: ${this.lockPath}`);
|
|
14155
14328
|
}
|
|
14156
14329
|
await delay(LOCK_RETRY_MS);
|
|
@@ -14162,6 +14335,10 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
|
14162
14335
|
const stat = await fs2.stat(this.lockPath);
|
|
14163
14336
|
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
14164
14337
|
await fs2.rm(this.lockPath, { recursive: true, force: true });
|
|
14338
|
+
this.logger.debug("account-store", "staleLockRemoved", {
|
|
14339
|
+
lockPath: this.lockPath,
|
|
14340
|
+
staleMs: Date.now() - stat.mtimeMs
|
|
14341
|
+
});
|
|
14165
14342
|
}
|
|
14166
14343
|
} catch (err) {
|
|
14167
14344
|
if (err.code !== "ENOENT") throw err;
|
|
@@ -14635,8 +14812,8 @@ async function updateDraft(client, account, id, update) {
|
|
|
14635
14812
|
payload.attachments = converted.attachments;
|
|
14636
14813
|
}
|
|
14637
14814
|
}
|
|
14638
|
-
await client.api(`/me/messages/${encodeURIComponent(id)}`).patch(payload);
|
|
14639
|
-
return { id };
|
|
14815
|
+
const updated = await client.api(`/me/messages/${encodeURIComponent(id)}`).header("Prefer", "return=representation").patch(payload);
|
|
14816
|
+
return { id: updated?.id ?? id };
|
|
14640
14817
|
}
|
|
14641
14818
|
async function addAttachmentToDraft(client, account, draftId, name, contentBytes, contentType) {
|
|
14642
14819
|
const att = await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post({
|
|
@@ -14950,6 +15127,7 @@ var ImapClient = class {
|
|
|
14950
15127
|
imap = null;
|
|
14951
15128
|
transporter = null;
|
|
14952
15129
|
connecting = null;
|
|
15130
|
+
imapQueue = Promise.resolve();
|
|
14953
15131
|
/** Get (or create) the ImapFlow instance. */
|
|
14954
15132
|
async getImap() {
|
|
14955
15133
|
if (this.imap) return this.imap;
|
|
@@ -14987,18 +15165,31 @@ var ImapClient = class {
|
|
|
14987
15165
|
});
|
|
14988
15166
|
return this.transporter;
|
|
14989
15167
|
}
|
|
15168
|
+
/** Run an IMAP operation serially on this account's shared connection. */
|
|
15169
|
+
async run(fn) {
|
|
15170
|
+
const run = this.imapQueue.catch(() => void 0).then(async () => {
|
|
15171
|
+
const imap = await this.getImap();
|
|
15172
|
+
return fn(imap);
|
|
15173
|
+
});
|
|
15174
|
+
this.imapQueue = run.then(
|
|
15175
|
+
() => void 0,
|
|
15176
|
+
() => void 0
|
|
15177
|
+
);
|
|
15178
|
+
return run;
|
|
15179
|
+
}
|
|
14990
15180
|
/**
|
|
14991
15181
|
* Acquire a mailbox lock and run `fn` with the mailbox selected.
|
|
14992
15182
|
* Releases the lock automatically after `fn` completes.
|
|
14993
15183
|
*/
|
|
14994
15184
|
async withMailbox(mailbox, fn) {
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15185
|
+
return this.run(async (imap) => {
|
|
15186
|
+
const lock = await imap.getMailboxLock(mailbox);
|
|
15187
|
+
try {
|
|
15188
|
+
return await fn(imap);
|
|
15189
|
+
} finally {
|
|
15190
|
+
lock.release();
|
|
15191
|
+
}
|
|
15192
|
+
});
|
|
15002
15193
|
}
|
|
15003
15194
|
/** Disconnect IMAP and close the SMTP pool. */
|
|
15004
15195
|
async disconnect() {
|
|
@@ -15296,10 +15487,11 @@ async function readAttachment2(clients, account, messageId, attachmentId) {
|
|
|
15296
15487
|
}
|
|
15297
15488
|
async function listFolders2(clients, account, opts) {
|
|
15298
15489
|
const client = clients.get(account);
|
|
15299
|
-
const
|
|
15300
|
-
|
|
15301
|
-
|
|
15302
|
-
|
|
15490
|
+
const mailboxes = await client.run(
|
|
15491
|
+
(imap) => imap.list({
|
|
15492
|
+
statusQuery: { messages: true, unseen: true, uidNext: true }
|
|
15493
|
+
})
|
|
15494
|
+
);
|
|
15303
15495
|
let results = mailboxes.map(mapMailboxToListEntry);
|
|
15304
15496
|
if (opts.parentFolderId) {
|
|
15305
15497
|
const parentPath = opts.parentFolderId;
|
|
@@ -15438,59 +15630,11 @@ function removeMimePart(source, targetFilename, targetContentType) {
|
|
|
15438
15630
|
async function sendEmail(clients, account, msg) {
|
|
15439
15631
|
const client = clients.get(account);
|
|
15440
15632
|
const transporter = client.getTransporter();
|
|
15441
|
-
const mailOptions =
|
|
15442
|
-
from: `${account.displayName ?? ""} <${account.email}>`,
|
|
15443
|
-
to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
|
|
15444
|
-
subject: msg.subject
|
|
15445
|
-
};
|
|
15446
|
-
if (msg.isHtml) {
|
|
15447
|
-
mailOptions.html = msg.body;
|
|
15448
|
-
} else {
|
|
15449
|
-
mailOptions.text = msg.body;
|
|
15450
|
-
}
|
|
15451
|
-
mailOptions.attachDataUrls = true;
|
|
15452
|
-
if (msg.cc && msg.cc.length > 0) {
|
|
15453
|
-
mailOptions.cc = msg.cc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
|
|
15454
|
-
}
|
|
15455
|
-
if (msg.bcc && msg.bcc.length > 0) {
|
|
15456
|
-
mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
|
|
15457
|
-
}
|
|
15458
|
-
if (msg.inReplyTo || msg.forwardMessageId) {
|
|
15459
|
-
const refId = msg.inReplyTo ?? msg.forwardMessageId;
|
|
15460
|
-
if (refId) {
|
|
15461
|
-
try {
|
|
15462
|
-
const { folder: refFolder, uid: refUid } = decodeId(refId);
|
|
15463
|
-
const refMsg = await client.withMailbox(refFolder, async (imap) => {
|
|
15464
|
-
return imap.fetchOne(
|
|
15465
|
-
refUid,
|
|
15466
|
-
{ envelope: true, source: true },
|
|
15467
|
-
{ uid: true }
|
|
15468
|
-
);
|
|
15469
|
-
});
|
|
15470
|
-
if (refMsg?.envelope) {
|
|
15471
|
-
const env = refMsg.envelope;
|
|
15472
|
-
if (msg.inReplyTo && env.messageId && !msg.forwardMessageId) {
|
|
15473
|
-
mailOptions.inReplyTo = env.messageId;
|
|
15474
|
-
mailOptions.references = env.messageId;
|
|
15475
|
-
}
|
|
15476
|
-
}
|
|
15477
|
-
if (msg.forwardMessageId && refMsg?.source) {
|
|
15478
|
-
const sourceStr = typeof refMsg.source === "string" ? refMsg.source : Buffer.from(refMsg.source).toString("utf-8");
|
|
15479
|
-
const divider = '\n\n<div style="line-height:12px"><br></div>\n\n<div style="border-left:2px solid #ccc; padding-left:8px; margin-left:0; color:#666">\n---------- Forwarded message ---------<br>' + sourceStr + "\n</div>";
|
|
15480
|
-
if (mailOptions.html) {
|
|
15481
|
-
mailOptions.html += divider;
|
|
15482
|
-
} else if (mailOptions.text) {
|
|
15483
|
-
mailOptions.text += "\n\n---------- Forwarded message ---------\n" + sourceStr;
|
|
15484
|
-
}
|
|
15485
|
-
}
|
|
15486
|
-
} catch {
|
|
15487
|
-
}
|
|
15488
|
-
}
|
|
15489
|
-
}
|
|
15633
|
+
const mailOptions = await buildMailOptions(client, account, msg);
|
|
15490
15634
|
const info = await transporter.sendMail(mailOptions);
|
|
15491
15635
|
try {
|
|
15492
|
-
const rawMsg = await buildRawMessage(account, msg, info.messageId);
|
|
15493
|
-
await client.
|
|
15636
|
+
const rawMsg = await buildRawMessage(client, account, msg, info.messageId);
|
|
15637
|
+
await client.run(async (imap) => {
|
|
15494
15638
|
await imap.append("Sent", rawMsg, ["\\Seen"]);
|
|
15495
15639
|
});
|
|
15496
15640
|
} catch {
|
|
@@ -15499,48 +15643,55 @@ async function sendEmail(clients, account, msg) {
|
|
|
15499
15643
|
}
|
|
15500
15644
|
async function saveDraft(clients, account, msg) {
|
|
15501
15645
|
const client = clients.get(account);
|
|
15502
|
-
const
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15506
|
-
|
|
15646
|
+
const folder = "Drafts";
|
|
15647
|
+
const rawMsg = await buildRawMessage(client, account, msg);
|
|
15648
|
+
try {
|
|
15649
|
+
const result = await client.run((imap) => appendDraft(imap, folder, rawMsg));
|
|
15650
|
+
return { id: encodeId(folder, appendUid(result, folder)) };
|
|
15651
|
+
} catch (err) {
|
|
15652
|
+
throw imapOperationError(`failed to save IMAP draft to ${folder}`, err);
|
|
15653
|
+
}
|
|
15507
15654
|
}
|
|
15508
15655
|
async function updateDraft2(clients, account, id, update) {
|
|
15509
15656
|
const client = clients.get(account);
|
|
15510
15657
|
const { folder, uid } = decodeId(id);
|
|
15511
|
-
|
|
15512
|
-
|
|
15513
|
-
|
|
15514
|
-
|
|
15515
|
-
|
|
15516
|
-
|
|
15517
|
-
|
|
15518
|
-
|
|
15519
|
-
|
|
15520
|
-
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
|
|
15526
|
-
|
|
15527
|
-
if (update.
|
|
15528
|
-
|
|
15529
|
-
|
|
15530
|
-
|
|
15658
|
+
try {
|
|
15659
|
+
return await client.withMailbox(folder, async (imap) => {
|
|
15660
|
+
const existing = await imap.fetchOne(
|
|
15661
|
+
uid,
|
|
15662
|
+
{ source: true, envelope: true },
|
|
15663
|
+
{ uid: true }
|
|
15664
|
+
);
|
|
15665
|
+
if (!existing?.source) {
|
|
15666
|
+
throw new Error(`draft not found: ${id}`);
|
|
15667
|
+
}
|
|
15668
|
+
const origSubject = existing.envelope ? existing.envelope.subject ?? "" : "";
|
|
15669
|
+
const updatedMsg = {
|
|
15670
|
+
from: `${account.displayName ?? ""} <${account.email}>`,
|
|
15671
|
+
subject: update.subject ?? origSubject,
|
|
15672
|
+
attachDataUrls: true
|
|
15673
|
+
};
|
|
15674
|
+
if (update.body !== void 0) {
|
|
15675
|
+
if (update.isHtml) {
|
|
15676
|
+
updatedMsg.html = update.body;
|
|
15677
|
+
} else {
|
|
15678
|
+
updatedMsg.text = update.body;
|
|
15679
|
+
}
|
|
15531
15680
|
}
|
|
15532
|
-
|
|
15533
|
-
|
|
15534
|
-
|
|
15535
|
-
|
|
15536
|
-
|
|
15537
|
-
|
|
15681
|
+
const raw = await new Promise((resolve, reject) => {
|
|
15682
|
+
const mc = new MailComposer(updatedMsg);
|
|
15683
|
+
mc.compile().build((err, buf) => {
|
|
15684
|
+
if (err) reject(err);
|
|
15685
|
+
else resolve(buf);
|
|
15686
|
+
});
|
|
15538
15687
|
});
|
|
15688
|
+
const result = await appendDraft(imap, folder, raw.toString("utf-8"));
|
|
15689
|
+
await imap.messageDelete(uid, { uid: true });
|
|
15690
|
+
return { id: encodeId(folder, appendUid(result, folder)) };
|
|
15539
15691
|
});
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
});
|
|
15692
|
+
} catch (err) {
|
|
15693
|
+
throw imapOperationError(`failed to update IMAP draft ${id}`, err);
|
|
15694
|
+
}
|
|
15544
15695
|
}
|
|
15545
15696
|
async function moveEmail2(clients, account, id, destinationId) {
|
|
15546
15697
|
if (isTrashFolderAlias(destinationId)) {
|
|
@@ -15556,9 +15707,8 @@ async function moveEmail2(clients, account, id, destinationId) {
|
|
|
15556
15707
|
async function trashEmail(clients, account, id) {
|
|
15557
15708
|
const client = clients.get(account);
|
|
15558
15709
|
const { folder, uid } = decodeId(id);
|
|
15559
|
-
const
|
|
15560
|
-
|
|
15561
|
-
await imap.list()
|
|
15710
|
+
const dest = await client.run(
|
|
15711
|
+
async (imap) => resolveTrashMailbox(await imap.list())
|
|
15562
15712
|
);
|
|
15563
15713
|
return client.withMailbox(folder, async (lockedImap) => {
|
|
15564
15714
|
await lockedImap.messageMove(uid, dest, { uid: true });
|
|
@@ -15589,43 +15739,47 @@ async function sendDraft2(clients, account, id) {
|
|
|
15589
15739
|
async function addAttachmentToDraft2(clients, account, draftId, name, contentBytes, contentType) {
|
|
15590
15740
|
const client = clients.get(account);
|
|
15591
15741
|
const { folder, uid } = decodeId(draftId);
|
|
15592
|
-
|
|
15593
|
-
|
|
15594
|
-
|
|
15595
|
-
|
|
15596
|
-
|
|
15597
|
-
|
|
15598
|
-
|
|
15599
|
-
|
|
15600
|
-
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
const
|
|
15604
|
-
|
|
15605
|
-
|
|
15606
|
-
|
|
15607
|
-
|
|
15608
|
-
|
|
15609
|
-
|
|
15610
|
-
|
|
15611
|
-
|
|
15612
|
-
|
|
15613
|
-
|
|
15614
|
-
|
|
15615
|
-
|
|
15742
|
+
try {
|
|
15743
|
+
return await client.withMailbox(folder, async (imap) => {
|
|
15744
|
+
const existing = await imap.fetchOne(
|
|
15745
|
+
uid,
|
|
15746
|
+
{ source: true },
|
|
15747
|
+
{ uid: true }
|
|
15748
|
+
);
|
|
15749
|
+
if (!existing?.source) {
|
|
15750
|
+
throw new Error(`draft not found: ${draftId}`);
|
|
15751
|
+
}
|
|
15752
|
+
const sourceStr = typeof existing.source === "string" ? existing.source : Buffer.from(existing.source).toString("utf-8");
|
|
15753
|
+
const built = await new Promise((resolve, reject) => {
|
|
15754
|
+
const mc = new MailComposer({
|
|
15755
|
+
raw: sourceStr,
|
|
15756
|
+
attachments: [
|
|
15757
|
+
{
|
|
15758
|
+
filename: name,
|
|
15759
|
+
content: Buffer.from(contentBytes, "base64"),
|
|
15760
|
+
contentType: contentType ?? "application/octet-stream"
|
|
15761
|
+
}
|
|
15762
|
+
]
|
|
15763
|
+
});
|
|
15764
|
+
mc.compile().build((err, buf) => {
|
|
15765
|
+
if (err) reject(err);
|
|
15766
|
+
else resolve(buf);
|
|
15767
|
+
});
|
|
15616
15768
|
});
|
|
15769
|
+
const result = await appendDraft(imap, folder, built.toString("utf-8"));
|
|
15770
|
+
await imap.messageDelete(uid, { uid: true });
|
|
15771
|
+
return {
|
|
15772
|
+
id: encodeId(folder, appendUid(result, folder)),
|
|
15773
|
+
attachment: {
|
|
15774
|
+
id: randomUUID3(),
|
|
15775
|
+
name,
|
|
15776
|
+
contentType: contentType ?? "application/octet-stream"
|
|
15777
|
+
}
|
|
15778
|
+
};
|
|
15617
15779
|
});
|
|
15618
|
-
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
id: encodeId(folder, result.uid),
|
|
15622
|
-
attachment: {
|
|
15623
|
-
id: randomUUID3(),
|
|
15624
|
-
name,
|
|
15625
|
-
contentType: contentType ?? "application/octet-stream"
|
|
15626
|
-
}
|
|
15627
|
-
};
|
|
15628
|
-
});
|
|
15780
|
+
} catch (err) {
|
|
15781
|
+
throw imapOperationError(`failed to add attachment to IMAP draft ${draftId}`, err);
|
|
15782
|
+
}
|
|
15629
15783
|
}
|
|
15630
15784
|
async function removeAttachmentFromDraft2(clients, account, draftId, attachmentId) {
|
|
15631
15785
|
const client = clients.get(account);
|
|
@@ -15660,7 +15814,7 @@ async function markRead2(clients, account, id, isRead) {
|
|
|
15660
15814
|
}
|
|
15661
15815
|
});
|
|
15662
15816
|
}
|
|
15663
|
-
async function
|
|
15817
|
+
async function buildMailOptions(client, account, msg, messageId) {
|
|
15664
15818
|
const mailOptions = {
|
|
15665
15819
|
from: `${account.displayName ?? ""} <${account.email}>`,
|
|
15666
15820
|
to: msg.to.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", "),
|
|
@@ -15679,16 +15833,55 @@ async function buildRawMessage(account, msg, messageId) {
|
|
|
15679
15833
|
mailOptions.bcc = msg.bcc.map((a) => a.name ? `"${a.name}" <${a.address}>` : a.address).join(", ");
|
|
15680
15834
|
}
|
|
15681
15835
|
if (msg.attachments && msg.attachments.length > 0) {
|
|
15682
|
-
|
|
15836
|
+
mailOptions.attachments = msg.attachments.map((att) => ({
|
|
15683
15837
|
filename: att.name,
|
|
15684
15838
|
content: Buffer.from(att.contentBytes, "base64"),
|
|
15685
15839
|
contentType: att.contentType
|
|
15686
15840
|
}));
|
|
15687
|
-
mailOptions.attachments = fileAttachments;
|
|
15688
15841
|
}
|
|
15689
15842
|
if (messageId) {
|
|
15690
15843
|
mailOptions.messageId = messageId;
|
|
15691
15844
|
}
|
|
15845
|
+
await applyReferenceMessage(client, mailOptions, msg);
|
|
15846
|
+
return mailOptions;
|
|
15847
|
+
}
|
|
15848
|
+
async function applyReferenceMessage(client, mailOptions, msg) {
|
|
15849
|
+
if (!msg.inReplyTo && !msg.forwardMessageId) return;
|
|
15850
|
+
const refId = msg.inReplyTo || msg.forwardMessageId;
|
|
15851
|
+
if (!refId) return;
|
|
15852
|
+
try {
|
|
15853
|
+
const { folder: refFolder, uid: refUid } = decodeId(refId);
|
|
15854
|
+
const refMsg = await client.withMailbox(refFolder, async (imap) => {
|
|
15855
|
+
return imap.fetchOne(
|
|
15856
|
+
refUid,
|
|
15857
|
+
{ envelope: true, source: true },
|
|
15858
|
+
{ uid: true }
|
|
15859
|
+
);
|
|
15860
|
+
});
|
|
15861
|
+
if (refMsg?.envelope) {
|
|
15862
|
+
const env = refMsg.envelope;
|
|
15863
|
+
if (msg.inReplyTo && env.messageId && !msg.forwardMessageId) {
|
|
15864
|
+
mailOptions.inReplyTo = env.messageId;
|
|
15865
|
+
mailOptions.references = env.messageId;
|
|
15866
|
+
}
|
|
15867
|
+
}
|
|
15868
|
+
if (msg.forwardMessageId && refMsg?.source) {
|
|
15869
|
+
const sourceStr = typeof refMsg.source === "string" ? refMsg.source : Buffer.from(refMsg.source).toString("utf-8");
|
|
15870
|
+
const divider = '\n\n<div style="line-height:12px"><br></div>\n\n<div style="border-left:2px solid #ccc; padding-left:8px; margin-left:0; color:#666">\n---------- Forwarded message ---------<br>' + sourceStr + "\n</div>";
|
|
15871
|
+
if (mailOptions.html) {
|
|
15872
|
+
mailOptions.html = `${mailOptions.html}${divider}`;
|
|
15873
|
+
} else if (mailOptions.text) {
|
|
15874
|
+
mailOptions.text = `${mailOptions.text}
|
|
15875
|
+
|
|
15876
|
+
---------- Forwarded message ---------
|
|
15877
|
+
${sourceStr}`;
|
|
15878
|
+
}
|
|
15879
|
+
}
|
|
15880
|
+
} catch {
|
|
15881
|
+
}
|
|
15882
|
+
}
|
|
15883
|
+
async function buildRawMessage(client, account, msg, messageId) {
|
|
15884
|
+
const mailOptions = await buildMailOptions(client, account, msg, messageId);
|
|
15692
15885
|
return new Promise((resolve, reject) => {
|
|
15693
15886
|
const mc = new MailComposer(mailOptions);
|
|
15694
15887
|
mc.compile().build((err, buf) => {
|
|
@@ -15697,6 +15890,48 @@ async function buildRawMessage(account, msg, messageId) {
|
|
|
15697
15890
|
});
|
|
15698
15891
|
});
|
|
15699
15892
|
}
|
|
15893
|
+
async function appendDraft(imap, folder, rawMsg) {
|
|
15894
|
+
try {
|
|
15895
|
+
return await imap.append(folder, rawMsg, ["\\Draft"]);
|
|
15896
|
+
} catch (err) {
|
|
15897
|
+
if (!isImapCommandFailure(err)) throw err;
|
|
15898
|
+
return imap.append(folder, rawMsg);
|
|
15899
|
+
}
|
|
15900
|
+
}
|
|
15901
|
+
function appendUid(result, folder) {
|
|
15902
|
+
if (!result || typeof result !== "object") {
|
|
15903
|
+
throw new Error(`IMAP append to ${folder} did not return a UID`);
|
|
15904
|
+
}
|
|
15905
|
+
const uid = Number(result.uid);
|
|
15906
|
+
if (Number.isFinite(uid) && uid > 0) return uid;
|
|
15907
|
+
throw new Error(`IMAP append to ${folder} did not return a UID`);
|
|
15908
|
+
}
|
|
15909
|
+
function isImapCommandFailure(err) {
|
|
15910
|
+
const e = err;
|
|
15911
|
+
return typeof e.responseStatus === "string" || typeof e.message === "string" && e.message.includes("Command failed");
|
|
15912
|
+
}
|
|
15913
|
+
function imapOperationError(message, err) {
|
|
15914
|
+
const detail = formatImapError(err);
|
|
15915
|
+
return new Error(`${message}: ${detail}`, { cause: err });
|
|
15916
|
+
}
|
|
15917
|
+
function formatImapError(err) {
|
|
15918
|
+
const e = err;
|
|
15919
|
+
const parts = [];
|
|
15920
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15921
|
+
if (message) parts.push(message);
|
|
15922
|
+
for (const key of ["responseStatus", "responseText", "serverResponseCode", "response"]) {
|
|
15923
|
+
const value = e[key];
|
|
15924
|
+
if (value !== void 0 && value !== null) {
|
|
15925
|
+
parts.push(`${key}=${safeErrorValue(value)}`);
|
|
15926
|
+
}
|
|
15927
|
+
}
|
|
15928
|
+
return parts.join("; ");
|
|
15929
|
+
}
|
|
15930
|
+
function safeErrorValue(value) {
|
|
15931
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
15932
|
+
const text = raw ?? String(value);
|
|
15933
|
+
return text.length > 500 ? `${text.slice(0, 500)}\u2026` : text;
|
|
15934
|
+
}
|
|
15700
15935
|
|
|
15701
15936
|
// src/providers/imap/folders.ts
|
|
15702
15937
|
async function createFolder2(clients, account, input) {
|
|
@@ -17013,11 +17248,29 @@ var accountFullOutputSchema = z2.object({
|
|
|
17013
17248
|
email: z2.string(),
|
|
17014
17249
|
provider: providerIdEnum,
|
|
17015
17250
|
displayName: z2.string().optional(),
|
|
17016
|
-
tokens: z2.record(z2.string(), z2.unknown()),
|
|
17017
17251
|
addedAt: z2.string(),
|
|
17018
17252
|
signature: z2.string().optional(),
|
|
17019
17253
|
style: styleOutputSchema.optional()
|
|
17020
17254
|
});
|
|
17255
|
+
function publicAccount(account) {
|
|
17256
|
+
return {
|
|
17257
|
+
email: account.email,
|
|
17258
|
+
provider: account.provider,
|
|
17259
|
+
displayName: account.displayName,
|
|
17260
|
+
addedAt: account.addedAt,
|
|
17261
|
+
signature: account.signature,
|
|
17262
|
+
style: account.style
|
|
17263
|
+
};
|
|
17264
|
+
}
|
|
17265
|
+
function publicAccountResult(result) {
|
|
17266
|
+
if (typeof result !== "object" || result === null) {
|
|
17267
|
+
return { result };
|
|
17268
|
+
}
|
|
17269
|
+
const record2 = result;
|
|
17270
|
+
const account = record2.account;
|
|
17271
|
+
if (!account) return record2;
|
|
17272
|
+
return { ...record2, account: publicAccount(account) };
|
|
17273
|
+
}
|
|
17021
17274
|
var emailSummaryOutputSchema = z2.object({
|
|
17022
17275
|
id: z2.string(),
|
|
17023
17276
|
subject: z2.string(),
|
|
@@ -17043,8 +17296,17 @@ var folderInfoOutputSchema = z2.object({
|
|
|
17043
17296
|
totalItemCount: z2.number(),
|
|
17044
17297
|
unreadItemCount: z2.number()
|
|
17045
17298
|
});
|
|
17299
|
+
var meaningfulHtmlTagRe = /<\/?(?:p|div|br|blockquote|table|thead|tbody|tfoot|tr|td|th|ul|ol|li|a|span|font|b|strong|em|i|u|img|hr|pre|h[1-6])\b/i;
|
|
17300
|
+
function isSuspiciousPlainTextHtml(body) {
|
|
17301
|
+
return body.trim() !== "" && /\r\n|\r|\n/.test(body) && !meaningfulHtmlTagRe.test(body);
|
|
17302
|
+
}
|
|
17046
17303
|
function composeBody(input) {
|
|
17047
17304
|
const { body, format, signature, style, includeSignature } = input;
|
|
17305
|
+
if (format === "html" && isSuspiciousPlainTextHtml(body)) {
|
|
17306
|
+
throw new Error(
|
|
17307
|
+
'format: "html" requires valid HTML for multiline bodies. Use format: "markdown" for plain text with paragraphs, or add HTML tags such as <p> or <br>.'
|
|
17308
|
+
);
|
|
17309
|
+
}
|
|
17048
17310
|
const htmlBody = format === "markdown" ? markdownToHtml(body) : body;
|
|
17049
17311
|
const hasSignature = includeSignature && !!signature;
|
|
17050
17312
|
const hasStyle = !!(style && (style.fontFamily || style.fontSize || style.fontColor));
|
|
@@ -17155,7 +17417,8 @@ function registerAccountTools(server, ctx) {
|
|
|
17155
17417
|
email: args.email,
|
|
17156
17418
|
config: args.config
|
|
17157
17419
|
});
|
|
17158
|
-
|
|
17420
|
+
const data = publicAccountResult(res);
|
|
17421
|
+
return ok(data, data);
|
|
17159
17422
|
} catch (err) {
|
|
17160
17423
|
return fail(errMsg(err));
|
|
17161
17424
|
}
|
|
@@ -17200,7 +17463,8 @@ function registerAccountTools(server, ctx) {
|
|
|
17200
17463
|
code: args.code,
|
|
17201
17464
|
state: args.state
|
|
17202
17465
|
});
|
|
17203
|
-
|
|
17466
|
+
const data = publicAccountResult(res);
|
|
17467
|
+
return ok(data, data);
|
|
17204
17468
|
} catch (err) {
|
|
17205
17469
|
return fail(errMsg(err));
|
|
17206
17470
|
}
|
|
@@ -17347,7 +17611,7 @@ var BODY_LIMIT = 2e4;
|
|
|
17347
17611
|
var PAGE_SIZE = 100;
|
|
17348
17612
|
var MISSING_RECEIVED_AT = "1970-01-01T00:00:00.000Z";
|
|
17349
17613
|
function registerNewEmailTool(server, ctx) {
|
|
17350
|
-
const { store, registry, tools } = ctx;
|
|
17614
|
+
const { store, registry, tools, logger = noopLogger } = ctx;
|
|
17351
17615
|
if (!shouldRegister("get_new_emails", tools)) return;
|
|
17352
17616
|
const newEmailOutputSchema = z4.object({
|
|
17353
17617
|
account: z4.string(),
|
|
@@ -17389,20 +17653,46 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17389
17653
|
},
|
|
17390
17654
|
async (args) => {
|
|
17391
17655
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
17656
|
+
logger.debug("get-new-emails", "start", {
|
|
17657
|
+
account: args.account ?? null,
|
|
17658
|
+
limit
|
|
17659
|
+
});
|
|
17392
17660
|
if (args.account) {
|
|
17393
17661
|
try {
|
|
17394
17662
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
17395
|
-
const result = await collectCandidatesForAccount(store, provider, account);
|
|
17663
|
+
const result = await collectCandidatesForAccount(store, provider, account, logger);
|
|
17396
17664
|
const selected2 = oldestCandidatesFirst(result.candidates).slice(0, limit);
|
|
17397
|
-
|
|
17665
|
+
logger.debug("get-new-emails", "selected", {
|
|
17666
|
+
account: result.account.email,
|
|
17667
|
+
candidateCount: result.candidates.length,
|
|
17668
|
+
selectedCount: selected2.length,
|
|
17669
|
+
selectedIds: selected2.map((candidate) => candidate.summary.id),
|
|
17670
|
+
selectedReceivedAt: selected2.map((candidate) => candidate.timestamp),
|
|
17671
|
+
limit
|
|
17672
|
+
});
|
|
17673
|
+
const emails2 = limit === 0 ? [] : await hydrateAndAdvance(store, provider, result.account, selected2, logger);
|
|
17398
17674
|
const data2 = { count: emails2.length, emails: emails2, errors: [] };
|
|
17675
|
+
logger.debug("get-new-emails", "end", {
|
|
17676
|
+
account: result.account.email,
|
|
17677
|
+
returnedCount: emails2.length,
|
|
17678
|
+
errorCount: 0
|
|
17679
|
+
});
|
|
17399
17680
|
return ok(data2, data2);
|
|
17400
17681
|
} catch (err) {
|
|
17682
|
+
logger.debug("get-new-emails", "error", {
|
|
17683
|
+
account: args.account,
|
|
17684
|
+
message: errMsg(err)
|
|
17685
|
+
});
|
|
17401
17686
|
return fail(errMsg(err));
|
|
17402
17687
|
}
|
|
17403
17688
|
}
|
|
17404
17689
|
const accounts = store.listAccounts();
|
|
17405
17690
|
if (accounts.length === 0) {
|
|
17691
|
+
logger.debug("get-new-emails", "end", {
|
|
17692
|
+
accountCount: 0,
|
|
17693
|
+
returnedCount: 0,
|
|
17694
|
+
errorCount: 1
|
|
17695
|
+
});
|
|
17406
17696
|
return fail("no accounts registered. Call add_account first.");
|
|
17407
17697
|
}
|
|
17408
17698
|
const errors = [];
|
|
@@ -17412,15 +17702,27 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17412
17702
|
for (const stored of accounts) {
|
|
17413
17703
|
try {
|
|
17414
17704
|
const { provider, account } = registry.resolveByEmail(stored.email);
|
|
17415
|
-
const result = await collectCandidatesForAccount(store, provider, account);
|
|
17705
|
+
const result = await collectCandidatesForAccount(store, provider, account, logger);
|
|
17416
17706
|
providersByEmail.set(result.account.email, provider);
|
|
17417
17707
|
accountsByEmail.set(result.account.email, result.account);
|
|
17418
17708
|
collected.push(...result.candidates);
|
|
17419
17709
|
} catch (err) {
|
|
17710
|
+
logger.debug("get-new-emails", "accountError", {
|
|
17711
|
+
account: stored.email,
|
|
17712
|
+
message: errMsg(err)
|
|
17713
|
+
});
|
|
17420
17714
|
errors.push({ account: stored.email, message: errMsg(err) });
|
|
17421
17715
|
}
|
|
17422
17716
|
}
|
|
17423
17717
|
const selected = oldestCandidatesFirst(collected).slice(0, limit);
|
|
17718
|
+
logger.debug("get-new-emails", "selected", {
|
|
17719
|
+
accountCount: accounts.length,
|
|
17720
|
+
candidateCount: collected.length,
|
|
17721
|
+
selectedCount: selected.length,
|
|
17722
|
+
selectedIds: selected.map((candidate) => candidate.summary.id),
|
|
17723
|
+
selectedReceivedAt: selected.map((candidate) => candidate.timestamp),
|
|
17724
|
+
limit
|
|
17725
|
+
});
|
|
17424
17726
|
const emails = [];
|
|
17425
17727
|
if (limit > 0) {
|
|
17426
17728
|
const byAccount = /* @__PURE__ */ new Map();
|
|
@@ -17439,24 +17741,39 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17439
17741
|
store,
|
|
17440
17742
|
provider,
|
|
17441
17743
|
account,
|
|
17442
|
-
accountCandidates
|
|
17744
|
+
accountCandidates,
|
|
17745
|
+
logger
|
|
17443
17746
|
)
|
|
17444
17747
|
);
|
|
17445
17748
|
} catch (err) {
|
|
17749
|
+
logger.debug("get-new-emails", "accountError", {
|
|
17750
|
+
account: email,
|
|
17751
|
+
message: errMsg(err)
|
|
17752
|
+
});
|
|
17446
17753
|
errors.push({ account: email, message: errMsg(err) });
|
|
17447
17754
|
}
|
|
17448
17755
|
}
|
|
17449
17756
|
}
|
|
17450
17757
|
const orderedEmails = emails.sort(compareNewEmailOutputOldestFirst);
|
|
17451
17758
|
const data = { count: orderedEmails.length, emails: orderedEmails, errors };
|
|
17759
|
+
logger.debug("get-new-emails", "end", {
|
|
17760
|
+
accountCount: accounts.length,
|
|
17761
|
+
returnedCount: orderedEmails.length,
|
|
17762
|
+
errorCount: errors.length
|
|
17763
|
+
});
|
|
17452
17764
|
return ok(data, data);
|
|
17453
17765
|
}
|
|
17454
17766
|
);
|
|
17455
17767
|
}
|
|
17456
|
-
async function collectCandidatesForAccount(store, provider, account) {
|
|
17768
|
+
async function collectCandidatesForAccount(store, provider, account, logger) {
|
|
17457
17769
|
const checkpoint = normalizeCheckpoint2(account.newEmailCheckpoint);
|
|
17458
17770
|
if (!checkpoint) {
|
|
17459
|
-
await initializeCheckpoint(store, provider, account);
|
|
17771
|
+
await initializeCheckpoint(store, provider, account, logger);
|
|
17772
|
+
logger.debug("get-new-emails", "candidatesCollected", {
|
|
17773
|
+
account: account.email,
|
|
17774
|
+
initialized: true,
|
|
17775
|
+
candidateCount: 0
|
|
17776
|
+
});
|
|
17460
17777
|
return { account, candidates: [] };
|
|
17461
17778
|
}
|
|
17462
17779
|
const deliveredAtCheckpoint = new Set(checkpoint.deliveredIdsAtReceivedAt ?? []);
|
|
@@ -17486,9 +17803,18 @@ async function collectCandidatesForAccount(store, provider, account) {
|
|
|
17486
17803
|
if (sawOlderThanCheckpoint || !hasMore) break;
|
|
17487
17804
|
skip += items.length;
|
|
17488
17805
|
}
|
|
17806
|
+
logger.debug("get-new-emails", "candidatesCollected", {
|
|
17807
|
+
account: account.email,
|
|
17808
|
+
initialized: false,
|
|
17809
|
+
checkpointReceivedAt: checkpoint.receivedAt,
|
|
17810
|
+
deliveredIdCount: checkpoint.deliveredIdsAtReceivedAt?.length ?? 0,
|
|
17811
|
+
candidateCount: candidates.length,
|
|
17812
|
+
candidateIds: candidates.map((candidate) => candidate.summary.id),
|
|
17813
|
+
candidateReceivedAt: candidates.map((candidate) => candidate.timestamp)
|
|
17814
|
+
});
|
|
17489
17815
|
return { account, candidates };
|
|
17490
17816
|
}
|
|
17491
|
-
async function initializeCheckpoint(store, provider, account) {
|
|
17817
|
+
async function initializeCheckpoint(store, provider, account, logger) {
|
|
17492
17818
|
const { items } = await provider.listEmails(account, {
|
|
17493
17819
|
folder: "inbox",
|
|
17494
17820
|
limit: PAGE_SIZE
|
|
@@ -17500,9 +17826,22 @@ async function initializeCheckpoint(store, provider, account) {
|
|
|
17500
17826
|
receivedAt,
|
|
17501
17827
|
deliveredIdsAtReceivedAt
|
|
17502
17828
|
});
|
|
17829
|
+
logger.debug("get-new-emails", "checkpointInitialized", {
|
|
17830
|
+
account: account.email,
|
|
17831
|
+
receivedAt,
|
|
17832
|
+
deliveredIdCount: deliveredIdsAtReceivedAt.length
|
|
17833
|
+
});
|
|
17503
17834
|
}
|
|
17504
|
-
async function hydrateAndAdvance(store, provider, account, selected) {
|
|
17505
|
-
if (selected.length === 0)
|
|
17835
|
+
async function hydrateAndAdvance(store, provider, account, selected, logger) {
|
|
17836
|
+
if (selected.length === 0) {
|
|
17837
|
+
logger.debug("get-new-emails", "hydrated", {
|
|
17838
|
+
account: account.email,
|
|
17839
|
+
selectedCount: 0,
|
|
17840
|
+
hydratedCount: 0,
|
|
17841
|
+
claimedCount: 0
|
|
17842
|
+
});
|
|
17843
|
+
return [];
|
|
17844
|
+
}
|
|
17506
17845
|
const hydrated = [];
|
|
17507
17846
|
for (const candidate of selected) {
|
|
17508
17847
|
const full = await provider.readEmail(account, candidate.summary.id);
|
|
@@ -17517,7 +17856,19 @@ async function hydrateAndAdvance(store, provider, account, selected) {
|
|
|
17517
17856
|
receivedAt: candidate.timestamp,
|
|
17518
17857
|
ids: [candidate.summary.id, fullId]
|
|
17519
17858
|
}));
|
|
17859
|
+
logger.debug("get-new-emails", "hydrated", {
|
|
17860
|
+
account: account.email,
|
|
17861
|
+
selectedCount: selected.length,
|
|
17862
|
+
hydratedCount: hydrated.length
|
|
17863
|
+
});
|
|
17520
17864
|
const claimed = new Set(await store.claimNewEmails(account.email, claims));
|
|
17865
|
+
logger.debug("get-new-emails", "claimed", {
|
|
17866
|
+
account: account.email,
|
|
17867
|
+
claimCount: claims.length,
|
|
17868
|
+
claimedCount: claimed.size,
|
|
17869
|
+
claimIds: claims.map((claim) => claim.summaryId),
|
|
17870
|
+
claimedIds: [...claimed]
|
|
17871
|
+
});
|
|
17521
17872
|
return hydrated.filter(({ candidate }) => claimed.has(candidate.summary.id)).map(({ email }) => email);
|
|
17522
17873
|
}
|
|
17523
17874
|
function formatNewEmail(account, msg, summary) {
|
|
@@ -17583,7 +17934,7 @@ function compareTimestamp2(a, b) {
|
|
|
17583
17934
|
|
|
17584
17935
|
// src/tools/browse.ts
|
|
17585
17936
|
function registerBrowseTools(server, ctx) {
|
|
17586
|
-
const { store, registry, tools } = ctx;
|
|
17937
|
+
const { store, registry, tools, logger } = ctx;
|
|
17587
17938
|
const emailListOutputSchema = z5.object({
|
|
17588
17939
|
account: z5.string(),
|
|
17589
17940
|
count: z5.number(),
|
|
@@ -17633,7 +17984,7 @@ function registerBrowseTools(server, ctx) {
|
|
|
17633
17984
|
}
|
|
17634
17985
|
);
|
|
17635
17986
|
}
|
|
17636
|
-
registerNewEmailTool(server, { store, registry, tools });
|
|
17987
|
+
registerNewEmailTool(server, { store, registry, tools, logger });
|
|
17637
17988
|
if (shouldRegister("search_emails", tools)) {
|
|
17638
17989
|
server.registerTool(
|
|
17639
17990
|
"search_emails",
|
|
@@ -18041,7 +18392,7 @@ function registerOrganizeTools(server, ctx) {
|
|
|
18041
18392
|
}
|
|
18042
18393
|
|
|
18043
18394
|
// src/tools/compose.ts
|
|
18044
|
-
import { z as
|
|
18395
|
+
import { z as z9 } from "zod";
|
|
18045
18396
|
import { readFileSync } from "fs";
|
|
18046
18397
|
import { basename, extname } from "path";
|
|
18047
18398
|
|
|
@@ -18072,42 +18423,106 @@ var MIME_TYPES = {
|
|
|
18072
18423
|
".mp4": "video/mp4"
|
|
18073
18424
|
};
|
|
18074
18425
|
|
|
18426
|
+
// src/tools/edit-draft-verify.ts
|
|
18427
|
+
var EDIT_DRAFT_VERIFY_DELAYS_MS = [250, 1e3, 2e3];
|
|
18428
|
+
function normalizeDraftBody2(body) {
|
|
18429
|
+
return body.replace(/\r\n/g, "\n").trim();
|
|
18430
|
+
}
|
|
18431
|
+
function bodyEditPersisted(actualBody, expectation) {
|
|
18432
|
+
const actual = normalizeDraftBody2(actualBody);
|
|
18433
|
+
const expected = normalizeDraftBody2(expectation.expectedBody);
|
|
18434
|
+
if (actual === expected) return true;
|
|
18435
|
+
const oldText = normalizeDraftBody2(expectation.oldText);
|
|
18436
|
+
const replacementBody = normalizeDraftBody2(expectation.replacementBody);
|
|
18437
|
+
if (oldText === replacementBody) {
|
|
18438
|
+
return actual.includes(replacementBody);
|
|
18439
|
+
}
|
|
18440
|
+
return actual.includes(replacementBody) && !actual.includes(oldText);
|
|
18441
|
+
}
|
|
18442
|
+
function delay2(ms) {
|
|
18443
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18444
|
+
}
|
|
18445
|
+
async function readDraftWithVerifiedBody(provider, account, id, expectation) {
|
|
18446
|
+
let draft = await provider.readEmail(account, id);
|
|
18447
|
+
for (const delayMs of EDIT_DRAFT_VERIFY_DELAYS_MS) {
|
|
18448
|
+
const body2 = draft.bodyHtml ?? draft.bodyText ?? "";
|
|
18449
|
+
if (bodyEditPersisted(body2, expectation)) return draft;
|
|
18450
|
+
await delay2(delayMs);
|
|
18451
|
+
draft = await provider.readEmail(account, id);
|
|
18452
|
+
}
|
|
18453
|
+
const body = draft.bodyHtml ?? draft.bodyText ?? "";
|
|
18454
|
+
return bodyEditPersisted(body, expectation) ? draft : void 0;
|
|
18455
|
+
}
|
|
18456
|
+
|
|
18457
|
+
// src/tools/compose-schemas.ts
|
|
18458
|
+
import { z as z8 } from "zod";
|
|
18459
|
+
var sendEmailSchema = z8.object({
|
|
18460
|
+
account: z8.string().email(),
|
|
18461
|
+
to: z8.array(emailAddrSchema).min(1),
|
|
18462
|
+
cc: z8.array(emailAddrSchema).optional(),
|
|
18463
|
+
bcc: z8.array(emailAddrSchema).optional(),
|
|
18464
|
+
subject: z8.string(),
|
|
18465
|
+
body: z8.string(),
|
|
18466
|
+
format: z8.enum(["html", "markdown"]).describe(
|
|
18467
|
+
"Body format. 'html' sends the body as-is (must be valid HTML); multiline plain text with format='html' is rejected, so use 'markdown' for paragraphs or add tags like <p>/<br>. 'markdown' converts the body from Markdown to HTML for clean rendering on the recipient side."
|
|
18468
|
+
),
|
|
18469
|
+
include_signature: z8.boolean().describe(
|
|
18470
|
+
"Whether to append the account's saved HTML signature to the email. If true, don't include a signature in the body param to avoid double signature. Returns an error if true but no signature is configured for this account."
|
|
18471
|
+
),
|
|
18472
|
+
inReplyTo: z8.union([z8.string(), z8.literal(false)]).describe(
|
|
18473
|
+
"Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically. Set to `false` for a new email (not a reply)."
|
|
18474
|
+
),
|
|
18475
|
+
replyAll: z8.boolean().default(false).optional().describe(
|
|
18476
|
+
"When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
|
|
18477
|
+
),
|
|
18478
|
+
forwardMessageId: z8.string().optional().describe(
|
|
18479
|
+
"Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
|
|
18480
|
+
),
|
|
18481
|
+
attachments: z8.array(
|
|
18482
|
+
z8.object({
|
|
18483
|
+
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
18484
|
+
name: z8.string().optional().describe("Attachment filename. Defaults to the file's basename.")
|
|
18485
|
+
})
|
|
18486
|
+
).optional().describe(
|
|
18487
|
+
"File attachments to include. The server reads the files from disk and base64-encodes them automatically."
|
|
18488
|
+
)
|
|
18489
|
+
});
|
|
18490
|
+
var editDraftSchema = z8.object({
|
|
18491
|
+
account: z8.string().email(),
|
|
18492
|
+
id: z8.string().min(1).describe("Draft message ID to edit"),
|
|
18493
|
+
to: z8.array(emailAddrSchema).optional(),
|
|
18494
|
+
cc: z8.array(emailAddrSchema).optional(),
|
|
18495
|
+
bcc: z8.array(emailAddrSchema).optional(),
|
|
18496
|
+
subject: z8.string().optional(),
|
|
18497
|
+
old_text: z8.string().min(1).optional().describe(
|
|
18498
|
+
"Exact current HTML section to replace in the draft body. Copy this from `draftHtml` or from `read_email` with format='html'. Must match exactly once; unselected content is preserved."
|
|
18499
|
+
),
|
|
18500
|
+
new_text: z8.string().optional().describe(
|
|
18501
|
+
"Replacement content for `old_text`. The replacement is composed using `format` and `include_signature`, then inserted exactly where `old_text` matched."
|
|
18502
|
+
),
|
|
18503
|
+
body: z8.string().optional().describe(
|
|
18504
|
+
"Deprecated alias for `new_text`. Body-only full replacement is not supported; provide `old_text` with this field."
|
|
18505
|
+
),
|
|
18506
|
+
format: z8.enum(["html", "markdown"]).optional().describe(
|
|
18507
|
+
"Replacement format. Only meaningful when `new_text` or deprecated `body` is also provided. 'html' inserts the replacement as-is (must be valid HTML); multiline plain text with format='html' is rejected, so use 'markdown' for paragraphs or add tags like <p>/<br>. 'markdown' converts the replacement from Markdown to HTML."
|
|
18508
|
+
),
|
|
18509
|
+
include_signature: z8.boolean().optional().describe(
|
|
18510
|
+
"Whether to append the account's saved HTML signature to the replacement section. If true, don't include a signature in `new_text`/`body`. Only meaningful when replacement content is provided. Returns an error if true but no signature is configured for this account."
|
|
18511
|
+
),
|
|
18512
|
+
new_attachments: z8.array(
|
|
18513
|
+
z8.object({
|
|
18514
|
+
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
18515
|
+
name: z8.string().optional().describe("Attachment filename. Defaults to the file's basename.")
|
|
18516
|
+
})
|
|
18517
|
+
).optional().describe(
|
|
18518
|
+
"New file attachments to add to the draft. The server reads the files from disk and base64-encodes them automatically."
|
|
18519
|
+
),
|
|
18520
|
+
remove_attachments: z8.array(z8.string().min(1)).optional().describe("Attachment IDs to remove from the draft. Get attachment IDs from read_email.")
|
|
18521
|
+
});
|
|
18522
|
+
|
|
18075
18523
|
// src/tools/compose.ts
|
|
18076
18524
|
function registerComposeTools(server, ctx) {
|
|
18077
18525
|
const { store, registry, tools } = ctx;
|
|
18078
|
-
const sendEmailSchema = z8.object({
|
|
18079
|
-
account: z8.string().email(),
|
|
18080
|
-
to: z8.array(emailAddrSchema).min(1),
|
|
18081
|
-
cc: z8.array(emailAddrSchema).optional(),
|
|
18082
|
-
bcc: z8.array(emailAddrSchema).optional(),
|
|
18083
|
-
subject: z8.string(),
|
|
18084
|
-
body: z8.string(),
|
|
18085
|
-
format: z8.enum(["html", "markdown"]).describe(
|
|
18086
|
-
"Body format. 'html' sends the body as-is (must be valid HTML). 'markdown' converts the body from Markdown to HTML for clean rendering on the recipient side."
|
|
18087
|
-
),
|
|
18088
|
-
include_signature: z8.boolean().describe(
|
|
18089
|
-
"Whether to append the account's saved HTML signature to the email. If true, don't include a signature in the body param to avoid double signature. Returns an error if true but no signature is configured for this account."
|
|
18090
|
-
),
|
|
18091
|
-
inReplyTo: z8.union([z8.string(), z8.literal(false)]).describe(
|
|
18092
|
-
"Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically. Set to `false` for a new email (not a reply)."
|
|
18093
|
-
),
|
|
18094
|
-
replyAll: z8.boolean().default(false).optional().describe(
|
|
18095
|
-
"When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
|
|
18096
|
-
),
|
|
18097
|
-
forwardMessageId: z8.string().optional().describe(
|
|
18098
|
-
"Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
|
|
18099
|
-
),
|
|
18100
|
-
attachments: z8.array(
|
|
18101
|
-
z8.object({
|
|
18102
|
-
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
18103
|
-
name: z8.string().optional().describe(
|
|
18104
|
-
"Attachment filename. Defaults to the file's basename."
|
|
18105
|
-
)
|
|
18106
|
-
})
|
|
18107
|
-
).optional().describe(
|
|
18108
|
-
"File attachments to include. The server reads the files from disk and base64-encodes them automatically."
|
|
18109
|
-
)
|
|
18110
|
-
});
|
|
18111
18526
|
async function handleSendOrDraft(args, action, resultKey, toolName) {
|
|
18112
18527
|
try {
|
|
18113
18528
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
@@ -18163,8 +18578,8 @@ function registerComposeTools(server, ctx) {
|
|
|
18163
18578
|
}
|
|
18164
18579
|
}
|
|
18165
18580
|
const sendEmailOutputSchema = {
|
|
18166
|
-
sent:
|
|
18167
|
-
id:
|
|
18581
|
+
sent: z9.literal(true),
|
|
18582
|
+
id: z9.string()
|
|
18168
18583
|
};
|
|
18169
18584
|
if (shouldRegister("send_email", tools)) {
|
|
18170
18585
|
server.registerTool(
|
|
@@ -18183,9 +18598,9 @@ function registerComposeTools(server, ctx) {
|
|
|
18183
18598
|
);
|
|
18184
18599
|
}
|
|
18185
18600
|
const draftEmailOutputSchema = {
|
|
18186
|
-
draft:
|
|
18187
|
-
id:
|
|
18188
|
-
draftHtml:
|
|
18601
|
+
draft: z9.literal(true),
|
|
18602
|
+
id: z9.string(),
|
|
18603
|
+
draftHtml: z9.string().optional()
|
|
18189
18604
|
};
|
|
18190
18605
|
if (shouldRegister("draft_email", tools)) {
|
|
18191
18606
|
server.registerTool(
|
|
@@ -18203,46 +18618,10 @@ function registerComposeTools(server, ctx) {
|
|
|
18203
18618
|
)
|
|
18204
18619
|
);
|
|
18205
18620
|
}
|
|
18206
|
-
const editDraftSchema = z8.object({
|
|
18207
|
-
account: z8.string().email(),
|
|
18208
|
-
id: z8.string().min(1).describe("Draft message ID to edit"),
|
|
18209
|
-
to: z8.array(emailAddrSchema).optional(),
|
|
18210
|
-
cc: z8.array(emailAddrSchema).optional(),
|
|
18211
|
-
bcc: z8.array(emailAddrSchema).optional(),
|
|
18212
|
-
subject: z8.string().optional(),
|
|
18213
|
-
old_text: z8.string().min(1).optional().describe(
|
|
18214
|
-
"Exact current HTML section to replace in the draft body. Copy this from `draftHtml` or from `read_email` with format='html'. Must match exactly once; unselected content is preserved."
|
|
18215
|
-
),
|
|
18216
|
-
new_text: z8.string().optional().describe(
|
|
18217
|
-
"Replacement content for `old_text`. The replacement is composed using `format` and `include_signature`, then inserted exactly where `old_text` matched."
|
|
18218
|
-
),
|
|
18219
|
-
body: z8.string().optional().describe(
|
|
18220
|
-
"Deprecated alias for `new_text`. Body-only full replacement is not supported; provide `old_text` with this field."
|
|
18221
|
-
),
|
|
18222
|
-
format: z8.enum(["html", "markdown"]).optional().describe(
|
|
18223
|
-
"Replacement format. Only meaningful when `new_text` or deprecated `body` is also provided. 'html' inserts the replacement as-is (must be valid HTML). 'markdown' converts the replacement from Markdown to HTML."
|
|
18224
|
-
),
|
|
18225
|
-
include_signature: z8.boolean().optional().describe(
|
|
18226
|
-
"Whether to append the account's saved HTML signature to the replacement section. If true, don't include a signature in `new_text`/`body`. Only meaningful when replacement content is provided. Returns an error if true but no signature is configured for this account."
|
|
18227
|
-
),
|
|
18228
|
-
new_attachments: z8.array(
|
|
18229
|
-
z8.object({
|
|
18230
|
-
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
18231
|
-
name: z8.string().optional().describe(
|
|
18232
|
-
"Attachment filename. Defaults to the file's basename."
|
|
18233
|
-
)
|
|
18234
|
-
})
|
|
18235
|
-
).optional().describe(
|
|
18236
|
-
"New file attachments to add to the draft. The server reads the files from disk and base64-encodes them automatically."
|
|
18237
|
-
),
|
|
18238
|
-
remove_attachments: z8.array(z8.string().min(1)).optional().describe(
|
|
18239
|
-
"Attachment IDs to remove from the draft. Get attachment IDs from read_email."
|
|
18240
|
-
)
|
|
18241
|
-
});
|
|
18242
18621
|
const editDraftOutputSchema = {
|
|
18243
|
-
edited:
|
|
18244
|
-
id:
|
|
18245
|
-
draftHtml:
|
|
18622
|
+
edited: z9.literal(true),
|
|
18623
|
+
id: z9.string(),
|
|
18624
|
+
draftHtml: z9.string().optional()
|
|
18246
18625
|
};
|
|
18247
18626
|
if (shouldRegister("edit_draft", tools)) {
|
|
18248
18627
|
server.registerTool(
|
|
@@ -18281,6 +18660,7 @@ function registerComposeTools(server, ctx) {
|
|
|
18281
18660
|
}
|
|
18282
18661
|
let bodyPayload;
|
|
18283
18662
|
let isHtmlPayload;
|
|
18663
|
+
let bodyExpectation;
|
|
18284
18664
|
if (replacementText !== void 0) {
|
|
18285
18665
|
const existing = await provider.readEmail(account, a.id);
|
|
18286
18666
|
const existingBody = existing.bodyHtml ?? existing.bodyText ?? "";
|
|
@@ -18293,6 +18673,11 @@ function registerComposeTools(server, ctx) {
|
|
|
18293
18673
|
});
|
|
18294
18674
|
bodyPayload = applyExactTextEdit(existingBody, a.old_text ?? "", composed.body);
|
|
18295
18675
|
isHtmlPayload = composed.isHtml;
|
|
18676
|
+
bodyExpectation = {
|
|
18677
|
+
expectedBody: bodyPayload,
|
|
18678
|
+
oldText: a.old_text ?? "",
|
|
18679
|
+
replacementBody: composed.body
|
|
18680
|
+
};
|
|
18296
18681
|
}
|
|
18297
18682
|
const hasDraftUpdate = a.to !== void 0 || a.cc !== void 0 || a.bcc !== void 0 || a.subject !== void 0 || bodyPayload !== void 0;
|
|
18298
18683
|
let currentId = a.id;
|
|
@@ -18335,7 +18720,35 @@ function registerComposeTools(server, ctx) {
|
|
|
18335
18720
|
removedIds.push(attId);
|
|
18336
18721
|
}
|
|
18337
18722
|
}
|
|
18338
|
-
|
|
18723
|
+
let draft;
|
|
18724
|
+
if (bodyExpectation) {
|
|
18725
|
+
draft = await readDraftWithVerifiedBody(
|
|
18726
|
+
provider,
|
|
18727
|
+
account,
|
|
18728
|
+
currentId,
|
|
18729
|
+
bodyExpectation
|
|
18730
|
+
);
|
|
18731
|
+
if (!draft && provider.id === "outlook" && bodyPayload !== void 0) {
|
|
18732
|
+
const res = await provider.updateDraft(account, currentId, {
|
|
18733
|
+
body: bodyPayload,
|
|
18734
|
+
isHtml: isHtmlPayload
|
|
18735
|
+
});
|
|
18736
|
+
currentId = res.id;
|
|
18737
|
+
draft = await readDraftWithVerifiedBody(
|
|
18738
|
+
provider,
|
|
18739
|
+
account,
|
|
18740
|
+
currentId,
|
|
18741
|
+
bodyExpectation
|
|
18742
|
+
);
|
|
18743
|
+
}
|
|
18744
|
+
if (!draft) {
|
|
18745
|
+
return fail(
|
|
18746
|
+
"Draft body edit was not observable after saving. Retry edit_draft, or recreate the draft with draft_email before sending."
|
|
18747
|
+
);
|
|
18748
|
+
}
|
|
18749
|
+
} else {
|
|
18750
|
+
draft = await provider.readEmail(account, currentId);
|
|
18751
|
+
}
|
|
18339
18752
|
const result = {
|
|
18340
18753
|
edited: true,
|
|
18341
18754
|
id: currentId,
|
|
@@ -18349,8 +18762,8 @@ function registerComposeTools(server, ctx) {
|
|
|
18349
18762
|
);
|
|
18350
18763
|
}
|
|
18351
18764
|
const sendDraftOutputSchema = {
|
|
18352
|
-
sent:
|
|
18353
|
-
id:
|
|
18765
|
+
sent: z9.literal(true),
|
|
18766
|
+
id: z9.string()
|
|
18354
18767
|
};
|
|
18355
18768
|
if (shouldRegister("send_draft", tools)) {
|
|
18356
18769
|
server.registerTool(
|
|
@@ -18358,8 +18771,8 @@ function registerComposeTools(server, ctx) {
|
|
|
18358
18771
|
{
|
|
18359
18772
|
description: "Send an existing draft email by ID. Use this with draft IDs returned by `draft_email` or `edit_draft`. Disabled in --read-only mode.",
|
|
18360
18773
|
inputSchema: {
|
|
18361
|
-
account:
|
|
18362
|
-
id:
|
|
18774
|
+
account: z9.string().email(),
|
|
18775
|
+
id: z9.string().min(1).describe("Draft message ID to send")
|
|
18363
18776
|
},
|
|
18364
18777
|
outputSchema: sendDraftOutputSchema
|
|
18365
18778
|
},
|
|
@@ -18379,9 +18792,9 @@ function registerComposeTools(server, ctx) {
|
|
|
18379
18792
|
|
|
18380
18793
|
// src/tools/index.ts
|
|
18381
18794
|
function registerTools(server, opts) {
|
|
18382
|
-
const { store, registry, tools } = opts;
|
|
18795
|
+
const { store, registry, tools, logger } = opts;
|
|
18383
18796
|
registerAccountTools(server, { store, registry, tools });
|
|
18384
|
-
registerBrowseTools(server, { store, registry, tools });
|
|
18797
|
+
registerBrowseTools(server, { store, registry, tools, logger });
|
|
18385
18798
|
registerFolderTools(server, { registry, tools });
|
|
18386
18799
|
registerOrganizeTools(server, { registry, tools });
|
|
18387
18800
|
registerComposeTools(server, { store, registry, tools });
|
|
@@ -18390,7 +18803,7 @@ function registerTools(server, opts) {
|
|
|
18390
18803
|
// package.json
|
|
18391
18804
|
var package_default = {
|
|
18392
18805
|
name: "hypermail-mcp",
|
|
18393
|
-
version: "0.7.
|
|
18806
|
+
version: "0.7.15",
|
|
18394
18807
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
18395
18808
|
type: "module",
|
|
18396
18809
|
bin: {
|
|
@@ -18400,7 +18813,8 @@ var package_default = {
|
|
|
18400
18813
|
files: [
|
|
18401
18814
|
"dist",
|
|
18402
18815
|
"README.md",
|
|
18403
|
-
"LICENSE"
|
|
18816
|
+
"LICENSE",
|
|
18817
|
+
"examples"
|
|
18404
18818
|
],
|
|
18405
18819
|
scripts: {
|
|
18406
18820
|
build: "tsup",
|
|
@@ -18470,6 +18884,7 @@ var ENV_HTTP_PORT = "HYPERMAIL_HTTP_PORT";
|
|
|
18470
18884
|
var ENV_HTTP_HOST = "HYPERMAIL_HTTP_HOST";
|
|
18471
18885
|
var ENV_TOOLS_DISABLED = "HYPERMAIL_TOOLS_DISABLED";
|
|
18472
18886
|
var ENV_TOOLS_ENABLED = "HYPERMAIL_TOOLS_ENABLED";
|
|
18887
|
+
var ENV_DEBUG = "HYPERMAIL_DEBUG";
|
|
18473
18888
|
var ENV_OUTLOOK_CLIENT_ID = "HYPERMAIL_OUTLOOK_CLIENT_ID";
|
|
18474
18889
|
var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_OUTLOOK_TENANT_ID";
|
|
18475
18890
|
var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_GMAIL_CLIENT_ID";
|
|
@@ -18510,6 +18925,15 @@ function parseStringArray(value) {
|
|
|
18510
18925
|
if (trimmed === "") return [];
|
|
18511
18926
|
return trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
18512
18927
|
}
|
|
18928
|
+
function resolveDebugLogging(warnings) {
|
|
18929
|
+
const value = envRaw(ENV_DEBUG);
|
|
18930
|
+
if (value === void 0 || value.trim() === "") return false;
|
|
18931
|
+
const lower = value.trim().toLowerCase();
|
|
18932
|
+
if (["1", "true", "yes", "on", "debug"].includes(lower)) return true;
|
|
18933
|
+
if (["0", "false", "no", "off"].includes(lower)) return false;
|
|
18934
|
+
warnings.push(`Invalid ${ENV_DEBUG}; debug logging disabled.`);
|
|
18935
|
+
return false;
|
|
18936
|
+
}
|
|
18513
18937
|
function validateToolNames(toolNames, envName) {
|
|
18514
18938
|
if (!toolNames || toolNames.length === 0) return;
|
|
18515
18939
|
const known = new Set(KNOWN_TOOLS);
|
|
@@ -18591,6 +19015,7 @@ function loadConfig(cliOverrides = {}) {
|
|
|
18591
19015
|
const tools = resolveToolsConfig();
|
|
18592
19016
|
const providers = resolveProvidersConfig();
|
|
18593
19017
|
const dataDir = cliOverrides.dataDir ?? optionalEnvString(ENV_DATA_DIR);
|
|
19018
|
+
const debugLogging = resolveDebugLogging(warnings);
|
|
18594
19019
|
if (!optionalEnvString(ENV_KEY)) {
|
|
18595
19020
|
warnings.push(
|
|
18596
19021
|
`${ENV_KEY} is not set; a local generated key will be used. Set ${ENV_KEY} explicitly for portable hosted deployments.`
|
|
@@ -18602,7 +19027,8 @@ function loadConfig(cliOverrides = {}) {
|
|
|
18602
19027
|
transport,
|
|
18603
19028
|
http,
|
|
18604
19029
|
tools,
|
|
18605
|
-
providers
|
|
19030
|
+
providers,
|
|
19031
|
+
debugLogging
|
|
18606
19032
|
},
|
|
18607
19033
|
warnings
|
|
18608
19034
|
};
|
|
@@ -18648,7 +19074,16 @@ function resolveTools(config) {
|
|
|
18648
19074
|
// src/server.ts
|
|
18649
19075
|
async function startServer(opts) {
|
|
18650
19076
|
const { config } = opts;
|
|
18651
|
-
const
|
|
19077
|
+
const logger = createLogger({ enabled: config.debugLogging });
|
|
19078
|
+
const dataDir = resolveDataDir(config.dataDir);
|
|
19079
|
+
logger.debug("server", "startup", {
|
|
19080
|
+
version: VERSION,
|
|
19081
|
+
transport: config.transport,
|
|
19082
|
+
dataDir,
|
|
19083
|
+
toolsEnabled: config.tools?.enabled ?? null,
|
|
19084
|
+
toolsDisabled: config.tools?.disabled ?? null
|
|
19085
|
+
});
|
|
19086
|
+
const store = await AccountStore.open({ dataDir, logger });
|
|
18652
19087
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
18653
19088
|
const tools = resolveTools(config);
|
|
18654
19089
|
const createServer = () => {
|
|
@@ -18656,7 +19091,7 @@ async function startServer(opts) {
|
|
|
18656
19091
|
{ name: "hypermail-mcp", version: VERSION },
|
|
18657
19092
|
{ capabilities: { tools: {}, logging: {} } }
|
|
18658
19093
|
);
|
|
18659
|
-
registerTools(s, { store, registry, tools });
|
|
19094
|
+
registerTools(s, { store, registry, tools, logger });
|
|
18660
19095
|
return s;
|
|
18661
19096
|
};
|
|
18662
19097
|
if (config.transport === "http") {
|
|
@@ -18847,6 +19282,7 @@ Core environment variables:
|
|
|
18847
19282
|
HYPERMAIL_HTTP_HOST
|
|
18848
19283
|
HYPERMAIL_TOOLS_ENABLED
|
|
18849
19284
|
HYPERMAIL_TOOLS_DISABLED
|
|
19285
|
+
HYPERMAIL_DEBUG=1|true|yes|on|debug
|
|
18850
19286
|
|
|
18851
19287
|
Provider environment variables:
|
|
18852
19288
|
HYPERMAIL_OUTLOOK_CLIENT_ID
|