hypermail-mcp 0.7.13 → 0.7.14
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 +10 -1
- package/dist/cli.js +459 -111
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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({
|
|
@@ -17347,7 +17524,7 @@ var BODY_LIMIT = 2e4;
|
|
|
17347
17524
|
var PAGE_SIZE = 100;
|
|
17348
17525
|
var MISSING_RECEIVED_AT = "1970-01-01T00:00:00.000Z";
|
|
17349
17526
|
function registerNewEmailTool(server, ctx) {
|
|
17350
|
-
const { store, registry, tools } = ctx;
|
|
17527
|
+
const { store, registry, tools, logger = noopLogger } = ctx;
|
|
17351
17528
|
if (!shouldRegister("get_new_emails", tools)) return;
|
|
17352
17529
|
const newEmailOutputSchema = z4.object({
|
|
17353
17530
|
account: z4.string(),
|
|
@@ -17389,20 +17566,46 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17389
17566
|
},
|
|
17390
17567
|
async (args) => {
|
|
17391
17568
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
17569
|
+
logger.debug("get-new-emails", "start", {
|
|
17570
|
+
account: args.account ?? null,
|
|
17571
|
+
limit
|
|
17572
|
+
});
|
|
17392
17573
|
if (args.account) {
|
|
17393
17574
|
try {
|
|
17394
17575
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
17395
|
-
const result = await collectCandidatesForAccount(store, provider, account);
|
|
17576
|
+
const result = await collectCandidatesForAccount(store, provider, account, logger);
|
|
17396
17577
|
const selected2 = oldestCandidatesFirst(result.candidates).slice(0, limit);
|
|
17397
|
-
|
|
17578
|
+
logger.debug("get-new-emails", "selected", {
|
|
17579
|
+
account: result.account.email,
|
|
17580
|
+
candidateCount: result.candidates.length,
|
|
17581
|
+
selectedCount: selected2.length,
|
|
17582
|
+
selectedIds: selected2.map((candidate) => candidate.summary.id),
|
|
17583
|
+
selectedReceivedAt: selected2.map((candidate) => candidate.timestamp),
|
|
17584
|
+
limit
|
|
17585
|
+
});
|
|
17586
|
+
const emails2 = limit === 0 ? [] : await hydrateAndAdvance(store, provider, result.account, selected2, logger);
|
|
17398
17587
|
const data2 = { count: emails2.length, emails: emails2, errors: [] };
|
|
17588
|
+
logger.debug("get-new-emails", "end", {
|
|
17589
|
+
account: result.account.email,
|
|
17590
|
+
returnedCount: emails2.length,
|
|
17591
|
+
errorCount: 0
|
|
17592
|
+
});
|
|
17399
17593
|
return ok(data2, data2);
|
|
17400
17594
|
} catch (err) {
|
|
17595
|
+
logger.debug("get-new-emails", "error", {
|
|
17596
|
+
account: args.account,
|
|
17597
|
+
message: errMsg(err)
|
|
17598
|
+
});
|
|
17401
17599
|
return fail(errMsg(err));
|
|
17402
17600
|
}
|
|
17403
17601
|
}
|
|
17404
17602
|
const accounts = store.listAccounts();
|
|
17405
17603
|
if (accounts.length === 0) {
|
|
17604
|
+
logger.debug("get-new-emails", "end", {
|
|
17605
|
+
accountCount: 0,
|
|
17606
|
+
returnedCount: 0,
|
|
17607
|
+
errorCount: 1
|
|
17608
|
+
});
|
|
17406
17609
|
return fail("no accounts registered. Call add_account first.");
|
|
17407
17610
|
}
|
|
17408
17611
|
const errors = [];
|
|
@@ -17412,15 +17615,27 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17412
17615
|
for (const stored of accounts) {
|
|
17413
17616
|
try {
|
|
17414
17617
|
const { provider, account } = registry.resolveByEmail(stored.email);
|
|
17415
|
-
const result = await collectCandidatesForAccount(store, provider, account);
|
|
17618
|
+
const result = await collectCandidatesForAccount(store, provider, account, logger);
|
|
17416
17619
|
providersByEmail.set(result.account.email, provider);
|
|
17417
17620
|
accountsByEmail.set(result.account.email, result.account);
|
|
17418
17621
|
collected.push(...result.candidates);
|
|
17419
17622
|
} catch (err) {
|
|
17623
|
+
logger.debug("get-new-emails", "accountError", {
|
|
17624
|
+
account: stored.email,
|
|
17625
|
+
message: errMsg(err)
|
|
17626
|
+
});
|
|
17420
17627
|
errors.push({ account: stored.email, message: errMsg(err) });
|
|
17421
17628
|
}
|
|
17422
17629
|
}
|
|
17423
17630
|
const selected = oldestCandidatesFirst(collected).slice(0, limit);
|
|
17631
|
+
logger.debug("get-new-emails", "selected", {
|
|
17632
|
+
accountCount: accounts.length,
|
|
17633
|
+
candidateCount: collected.length,
|
|
17634
|
+
selectedCount: selected.length,
|
|
17635
|
+
selectedIds: selected.map((candidate) => candidate.summary.id),
|
|
17636
|
+
selectedReceivedAt: selected.map((candidate) => candidate.timestamp),
|
|
17637
|
+
limit
|
|
17638
|
+
});
|
|
17424
17639
|
const emails = [];
|
|
17425
17640
|
if (limit > 0) {
|
|
17426
17641
|
const byAccount = /* @__PURE__ */ new Map();
|
|
@@ -17439,24 +17654,39 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17439
17654
|
store,
|
|
17440
17655
|
provider,
|
|
17441
17656
|
account,
|
|
17442
|
-
accountCandidates
|
|
17657
|
+
accountCandidates,
|
|
17658
|
+
logger
|
|
17443
17659
|
)
|
|
17444
17660
|
);
|
|
17445
17661
|
} catch (err) {
|
|
17662
|
+
logger.debug("get-new-emails", "accountError", {
|
|
17663
|
+
account: email,
|
|
17664
|
+
message: errMsg(err)
|
|
17665
|
+
});
|
|
17446
17666
|
errors.push({ account: email, message: errMsg(err) });
|
|
17447
17667
|
}
|
|
17448
17668
|
}
|
|
17449
17669
|
}
|
|
17450
17670
|
const orderedEmails = emails.sort(compareNewEmailOutputOldestFirst);
|
|
17451
17671
|
const data = { count: orderedEmails.length, emails: orderedEmails, errors };
|
|
17672
|
+
logger.debug("get-new-emails", "end", {
|
|
17673
|
+
accountCount: accounts.length,
|
|
17674
|
+
returnedCount: orderedEmails.length,
|
|
17675
|
+
errorCount: errors.length
|
|
17676
|
+
});
|
|
17452
17677
|
return ok(data, data);
|
|
17453
17678
|
}
|
|
17454
17679
|
);
|
|
17455
17680
|
}
|
|
17456
|
-
async function collectCandidatesForAccount(store, provider, account) {
|
|
17681
|
+
async function collectCandidatesForAccount(store, provider, account, logger) {
|
|
17457
17682
|
const checkpoint = normalizeCheckpoint2(account.newEmailCheckpoint);
|
|
17458
17683
|
if (!checkpoint) {
|
|
17459
|
-
await initializeCheckpoint(store, provider, account);
|
|
17684
|
+
await initializeCheckpoint(store, provider, account, logger);
|
|
17685
|
+
logger.debug("get-new-emails", "candidatesCollected", {
|
|
17686
|
+
account: account.email,
|
|
17687
|
+
initialized: true,
|
|
17688
|
+
candidateCount: 0
|
|
17689
|
+
});
|
|
17460
17690
|
return { account, candidates: [] };
|
|
17461
17691
|
}
|
|
17462
17692
|
const deliveredAtCheckpoint = new Set(checkpoint.deliveredIdsAtReceivedAt ?? []);
|
|
@@ -17486,9 +17716,18 @@ async function collectCandidatesForAccount(store, provider, account) {
|
|
|
17486
17716
|
if (sawOlderThanCheckpoint || !hasMore) break;
|
|
17487
17717
|
skip += items.length;
|
|
17488
17718
|
}
|
|
17719
|
+
logger.debug("get-new-emails", "candidatesCollected", {
|
|
17720
|
+
account: account.email,
|
|
17721
|
+
initialized: false,
|
|
17722
|
+
checkpointReceivedAt: checkpoint.receivedAt,
|
|
17723
|
+
deliveredIdCount: checkpoint.deliveredIdsAtReceivedAt?.length ?? 0,
|
|
17724
|
+
candidateCount: candidates.length,
|
|
17725
|
+
candidateIds: candidates.map((candidate) => candidate.summary.id),
|
|
17726
|
+
candidateReceivedAt: candidates.map((candidate) => candidate.timestamp)
|
|
17727
|
+
});
|
|
17489
17728
|
return { account, candidates };
|
|
17490
17729
|
}
|
|
17491
|
-
async function initializeCheckpoint(store, provider, account) {
|
|
17730
|
+
async function initializeCheckpoint(store, provider, account, logger) {
|
|
17492
17731
|
const { items } = await provider.listEmails(account, {
|
|
17493
17732
|
folder: "inbox",
|
|
17494
17733
|
limit: PAGE_SIZE
|
|
@@ -17500,9 +17739,22 @@ async function initializeCheckpoint(store, provider, account) {
|
|
|
17500
17739
|
receivedAt,
|
|
17501
17740
|
deliveredIdsAtReceivedAt
|
|
17502
17741
|
});
|
|
17742
|
+
logger.debug("get-new-emails", "checkpointInitialized", {
|
|
17743
|
+
account: account.email,
|
|
17744
|
+
receivedAt,
|
|
17745
|
+
deliveredIdCount: deliveredIdsAtReceivedAt.length
|
|
17746
|
+
});
|
|
17503
17747
|
}
|
|
17504
|
-
async function hydrateAndAdvance(store, provider, account, selected) {
|
|
17505
|
-
if (selected.length === 0)
|
|
17748
|
+
async function hydrateAndAdvance(store, provider, account, selected, logger) {
|
|
17749
|
+
if (selected.length === 0) {
|
|
17750
|
+
logger.debug("get-new-emails", "hydrated", {
|
|
17751
|
+
account: account.email,
|
|
17752
|
+
selectedCount: 0,
|
|
17753
|
+
hydratedCount: 0,
|
|
17754
|
+
claimedCount: 0
|
|
17755
|
+
});
|
|
17756
|
+
return [];
|
|
17757
|
+
}
|
|
17506
17758
|
const hydrated = [];
|
|
17507
17759
|
for (const candidate of selected) {
|
|
17508
17760
|
const full = await provider.readEmail(account, candidate.summary.id);
|
|
@@ -17517,7 +17769,19 @@ async function hydrateAndAdvance(store, provider, account, selected) {
|
|
|
17517
17769
|
receivedAt: candidate.timestamp,
|
|
17518
17770
|
ids: [candidate.summary.id, fullId]
|
|
17519
17771
|
}));
|
|
17772
|
+
logger.debug("get-new-emails", "hydrated", {
|
|
17773
|
+
account: account.email,
|
|
17774
|
+
selectedCount: selected.length,
|
|
17775
|
+
hydratedCount: hydrated.length
|
|
17776
|
+
});
|
|
17520
17777
|
const claimed = new Set(await store.claimNewEmails(account.email, claims));
|
|
17778
|
+
logger.debug("get-new-emails", "claimed", {
|
|
17779
|
+
account: account.email,
|
|
17780
|
+
claimCount: claims.length,
|
|
17781
|
+
claimedCount: claimed.size,
|
|
17782
|
+
claimIds: claims.map((claim) => claim.summaryId),
|
|
17783
|
+
claimedIds: [...claimed]
|
|
17784
|
+
});
|
|
17521
17785
|
return hydrated.filter(({ candidate }) => claimed.has(candidate.summary.id)).map(({ email }) => email);
|
|
17522
17786
|
}
|
|
17523
17787
|
function formatNewEmail(account, msg, summary) {
|
|
@@ -17583,7 +17847,7 @@ function compareTimestamp2(a, b) {
|
|
|
17583
17847
|
|
|
17584
17848
|
// src/tools/browse.ts
|
|
17585
17849
|
function registerBrowseTools(server, ctx) {
|
|
17586
|
-
const { store, registry, tools } = ctx;
|
|
17850
|
+
const { store, registry, tools, logger } = ctx;
|
|
17587
17851
|
const emailListOutputSchema = z5.object({
|
|
17588
17852
|
account: z5.string(),
|
|
17589
17853
|
count: z5.number(),
|
|
@@ -17633,7 +17897,7 @@ function registerBrowseTools(server, ctx) {
|
|
|
17633
17897
|
}
|
|
17634
17898
|
);
|
|
17635
17899
|
}
|
|
17636
|
-
registerNewEmailTool(server, { store, registry, tools });
|
|
17900
|
+
registerNewEmailTool(server, { store, registry, tools, logger });
|
|
17637
17901
|
if (shouldRegister("search_emails", tools)) {
|
|
17638
17902
|
server.registerTool(
|
|
17639
17903
|
"search_emails",
|
|
@@ -18041,7 +18305,7 @@ function registerOrganizeTools(server, ctx) {
|
|
|
18041
18305
|
}
|
|
18042
18306
|
|
|
18043
18307
|
// src/tools/compose.ts
|
|
18044
|
-
import { z as
|
|
18308
|
+
import { z as z9 } from "zod";
|
|
18045
18309
|
import { readFileSync } from "fs";
|
|
18046
18310
|
import { basename, extname } from "path";
|
|
18047
18311
|
|
|
@@ -18072,42 +18336,106 @@ var MIME_TYPES = {
|
|
|
18072
18336
|
".mp4": "video/mp4"
|
|
18073
18337
|
};
|
|
18074
18338
|
|
|
18339
|
+
// src/tools/edit-draft-verify.ts
|
|
18340
|
+
var EDIT_DRAFT_VERIFY_DELAYS_MS = [250, 1e3, 2e3];
|
|
18341
|
+
function normalizeDraftBody2(body) {
|
|
18342
|
+
return body.replace(/\r\n/g, "\n").trim();
|
|
18343
|
+
}
|
|
18344
|
+
function bodyEditPersisted(actualBody, expectation) {
|
|
18345
|
+
const actual = normalizeDraftBody2(actualBody);
|
|
18346
|
+
const expected = normalizeDraftBody2(expectation.expectedBody);
|
|
18347
|
+
if (actual === expected) return true;
|
|
18348
|
+
const oldText = normalizeDraftBody2(expectation.oldText);
|
|
18349
|
+
const replacementBody = normalizeDraftBody2(expectation.replacementBody);
|
|
18350
|
+
if (oldText === replacementBody) {
|
|
18351
|
+
return actual.includes(replacementBody);
|
|
18352
|
+
}
|
|
18353
|
+
return actual.includes(replacementBody) && !actual.includes(oldText);
|
|
18354
|
+
}
|
|
18355
|
+
function delay2(ms) {
|
|
18356
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18357
|
+
}
|
|
18358
|
+
async function readDraftWithVerifiedBody(provider, account, id, expectation) {
|
|
18359
|
+
let draft = await provider.readEmail(account, id);
|
|
18360
|
+
for (const delayMs of EDIT_DRAFT_VERIFY_DELAYS_MS) {
|
|
18361
|
+
const body2 = draft.bodyHtml ?? draft.bodyText ?? "";
|
|
18362
|
+
if (bodyEditPersisted(body2, expectation)) return draft;
|
|
18363
|
+
await delay2(delayMs);
|
|
18364
|
+
draft = await provider.readEmail(account, id);
|
|
18365
|
+
}
|
|
18366
|
+
const body = draft.bodyHtml ?? draft.bodyText ?? "";
|
|
18367
|
+
return bodyEditPersisted(body, expectation) ? draft : void 0;
|
|
18368
|
+
}
|
|
18369
|
+
|
|
18370
|
+
// src/tools/compose-schemas.ts
|
|
18371
|
+
import { z as z8 } from "zod";
|
|
18372
|
+
var sendEmailSchema = z8.object({
|
|
18373
|
+
account: z8.string().email(),
|
|
18374
|
+
to: z8.array(emailAddrSchema).min(1),
|
|
18375
|
+
cc: z8.array(emailAddrSchema).optional(),
|
|
18376
|
+
bcc: z8.array(emailAddrSchema).optional(),
|
|
18377
|
+
subject: z8.string(),
|
|
18378
|
+
body: z8.string(),
|
|
18379
|
+
format: z8.enum(["html", "markdown"]).describe(
|
|
18380
|
+
"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."
|
|
18381
|
+
),
|
|
18382
|
+
include_signature: z8.boolean().describe(
|
|
18383
|
+
"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."
|
|
18384
|
+
),
|
|
18385
|
+
inReplyTo: z8.union([z8.string(), z8.literal(false)]).describe(
|
|
18386
|
+
"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)."
|
|
18387
|
+
),
|
|
18388
|
+
replyAll: z8.boolean().default(false).optional().describe(
|
|
18389
|
+
"When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
|
|
18390
|
+
),
|
|
18391
|
+
forwardMessageId: z8.string().optional().describe(
|
|
18392
|
+
"Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
|
|
18393
|
+
),
|
|
18394
|
+
attachments: z8.array(
|
|
18395
|
+
z8.object({
|
|
18396
|
+
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
18397
|
+
name: z8.string().optional().describe("Attachment filename. Defaults to the file's basename.")
|
|
18398
|
+
})
|
|
18399
|
+
).optional().describe(
|
|
18400
|
+
"File attachments to include. The server reads the files from disk and base64-encodes them automatically."
|
|
18401
|
+
)
|
|
18402
|
+
});
|
|
18403
|
+
var editDraftSchema = z8.object({
|
|
18404
|
+
account: z8.string().email(),
|
|
18405
|
+
id: z8.string().min(1).describe("Draft message ID to edit"),
|
|
18406
|
+
to: z8.array(emailAddrSchema).optional(),
|
|
18407
|
+
cc: z8.array(emailAddrSchema).optional(),
|
|
18408
|
+
bcc: z8.array(emailAddrSchema).optional(),
|
|
18409
|
+
subject: z8.string().optional(),
|
|
18410
|
+
old_text: z8.string().min(1).optional().describe(
|
|
18411
|
+
"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."
|
|
18412
|
+
),
|
|
18413
|
+
new_text: z8.string().optional().describe(
|
|
18414
|
+
"Replacement content for `old_text`. The replacement is composed using `format` and `include_signature`, then inserted exactly where `old_text` matched."
|
|
18415
|
+
),
|
|
18416
|
+
body: z8.string().optional().describe(
|
|
18417
|
+
"Deprecated alias for `new_text`. Body-only full replacement is not supported; provide `old_text` with this field."
|
|
18418
|
+
),
|
|
18419
|
+
format: z8.enum(["html", "markdown"]).optional().describe(
|
|
18420
|
+
"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."
|
|
18421
|
+
),
|
|
18422
|
+
include_signature: z8.boolean().optional().describe(
|
|
18423
|
+
"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."
|
|
18424
|
+
),
|
|
18425
|
+
new_attachments: z8.array(
|
|
18426
|
+
z8.object({
|
|
18427
|
+
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
18428
|
+
name: z8.string().optional().describe("Attachment filename. Defaults to the file's basename.")
|
|
18429
|
+
})
|
|
18430
|
+
).optional().describe(
|
|
18431
|
+
"New file attachments to add to the draft. The server reads the files from disk and base64-encodes them automatically."
|
|
18432
|
+
),
|
|
18433
|
+
remove_attachments: z8.array(z8.string().min(1)).optional().describe("Attachment IDs to remove from the draft. Get attachment IDs from read_email.")
|
|
18434
|
+
});
|
|
18435
|
+
|
|
18075
18436
|
// src/tools/compose.ts
|
|
18076
18437
|
function registerComposeTools(server, ctx) {
|
|
18077
18438
|
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
18439
|
async function handleSendOrDraft(args, action, resultKey, toolName) {
|
|
18112
18440
|
try {
|
|
18113
18441
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
@@ -18163,8 +18491,8 @@ function registerComposeTools(server, ctx) {
|
|
|
18163
18491
|
}
|
|
18164
18492
|
}
|
|
18165
18493
|
const sendEmailOutputSchema = {
|
|
18166
|
-
sent:
|
|
18167
|
-
id:
|
|
18494
|
+
sent: z9.literal(true),
|
|
18495
|
+
id: z9.string()
|
|
18168
18496
|
};
|
|
18169
18497
|
if (shouldRegister("send_email", tools)) {
|
|
18170
18498
|
server.registerTool(
|
|
@@ -18183,9 +18511,9 @@ function registerComposeTools(server, ctx) {
|
|
|
18183
18511
|
);
|
|
18184
18512
|
}
|
|
18185
18513
|
const draftEmailOutputSchema = {
|
|
18186
|
-
draft:
|
|
18187
|
-
id:
|
|
18188
|
-
draftHtml:
|
|
18514
|
+
draft: z9.literal(true),
|
|
18515
|
+
id: z9.string(),
|
|
18516
|
+
draftHtml: z9.string().optional()
|
|
18189
18517
|
};
|
|
18190
18518
|
if (shouldRegister("draft_email", tools)) {
|
|
18191
18519
|
server.registerTool(
|
|
@@ -18203,46 +18531,10 @@ function registerComposeTools(server, ctx) {
|
|
|
18203
18531
|
)
|
|
18204
18532
|
);
|
|
18205
18533
|
}
|
|
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
18534
|
const editDraftOutputSchema = {
|
|
18243
|
-
edited:
|
|
18244
|
-
id:
|
|
18245
|
-
draftHtml:
|
|
18535
|
+
edited: z9.literal(true),
|
|
18536
|
+
id: z9.string(),
|
|
18537
|
+
draftHtml: z9.string().optional()
|
|
18246
18538
|
};
|
|
18247
18539
|
if (shouldRegister("edit_draft", tools)) {
|
|
18248
18540
|
server.registerTool(
|
|
@@ -18281,6 +18573,7 @@ function registerComposeTools(server, ctx) {
|
|
|
18281
18573
|
}
|
|
18282
18574
|
let bodyPayload;
|
|
18283
18575
|
let isHtmlPayload;
|
|
18576
|
+
let bodyExpectation;
|
|
18284
18577
|
if (replacementText !== void 0) {
|
|
18285
18578
|
const existing = await provider.readEmail(account, a.id);
|
|
18286
18579
|
const existingBody = existing.bodyHtml ?? existing.bodyText ?? "";
|
|
@@ -18293,6 +18586,11 @@ function registerComposeTools(server, ctx) {
|
|
|
18293
18586
|
});
|
|
18294
18587
|
bodyPayload = applyExactTextEdit(existingBody, a.old_text ?? "", composed.body);
|
|
18295
18588
|
isHtmlPayload = composed.isHtml;
|
|
18589
|
+
bodyExpectation = {
|
|
18590
|
+
expectedBody: bodyPayload,
|
|
18591
|
+
oldText: a.old_text ?? "",
|
|
18592
|
+
replacementBody: composed.body
|
|
18593
|
+
};
|
|
18296
18594
|
}
|
|
18297
18595
|
const hasDraftUpdate = a.to !== void 0 || a.cc !== void 0 || a.bcc !== void 0 || a.subject !== void 0 || bodyPayload !== void 0;
|
|
18298
18596
|
let currentId = a.id;
|
|
@@ -18335,7 +18633,35 @@ function registerComposeTools(server, ctx) {
|
|
|
18335
18633
|
removedIds.push(attId);
|
|
18336
18634
|
}
|
|
18337
18635
|
}
|
|
18338
|
-
|
|
18636
|
+
let draft;
|
|
18637
|
+
if (bodyExpectation) {
|
|
18638
|
+
draft = await readDraftWithVerifiedBody(
|
|
18639
|
+
provider,
|
|
18640
|
+
account,
|
|
18641
|
+
currentId,
|
|
18642
|
+
bodyExpectation
|
|
18643
|
+
);
|
|
18644
|
+
if (!draft && provider.id === "outlook" && bodyPayload !== void 0) {
|
|
18645
|
+
const res = await provider.updateDraft(account, currentId, {
|
|
18646
|
+
body: bodyPayload,
|
|
18647
|
+
isHtml: isHtmlPayload
|
|
18648
|
+
});
|
|
18649
|
+
currentId = res.id;
|
|
18650
|
+
draft = await readDraftWithVerifiedBody(
|
|
18651
|
+
provider,
|
|
18652
|
+
account,
|
|
18653
|
+
currentId,
|
|
18654
|
+
bodyExpectation
|
|
18655
|
+
);
|
|
18656
|
+
}
|
|
18657
|
+
if (!draft) {
|
|
18658
|
+
return fail(
|
|
18659
|
+
"Draft body edit was not observable after saving. Retry edit_draft, or recreate the draft with draft_email before sending."
|
|
18660
|
+
);
|
|
18661
|
+
}
|
|
18662
|
+
} else {
|
|
18663
|
+
draft = await provider.readEmail(account, currentId);
|
|
18664
|
+
}
|
|
18339
18665
|
const result = {
|
|
18340
18666
|
edited: true,
|
|
18341
18667
|
id: currentId,
|
|
@@ -18349,8 +18675,8 @@ function registerComposeTools(server, ctx) {
|
|
|
18349
18675
|
);
|
|
18350
18676
|
}
|
|
18351
18677
|
const sendDraftOutputSchema = {
|
|
18352
|
-
sent:
|
|
18353
|
-
id:
|
|
18678
|
+
sent: z9.literal(true),
|
|
18679
|
+
id: z9.string()
|
|
18354
18680
|
};
|
|
18355
18681
|
if (shouldRegister("send_draft", tools)) {
|
|
18356
18682
|
server.registerTool(
|
|
@@ -18358,8 +18684,8 @@ function registerComposeTools(server, ctx) {
|
|
|
18358
18684
|
{
|
|
18359
18685
|
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
18686
|
inputSchema: {
|
|
18361
|
-
account:
|
|
18362
|
-
id:
|
|
18687
|
+
account: z9.string().email(),
|
|
18688
|
+
id: z9.string().min(1).describe("Draft message ID to send")
|
|
18363
18689
|
},
|
|
18364
18690
|
outputSchema: sendDraftOutputSchema
|
|
18365
18691
|
},
|
|
@@ -18379,9 +18705,9 @@ function registerComposeTools(server, ctx) {
|
|
|
18379
18705
|
|
|
18380
18706
|
// src/tools/index.ts
|
|
18381
18707
|
function registerTools(server, opts) {
|
|
18382
|
-
const { store, registry, tools } = opts;
|
|
18708
|
+
const { store, registry, tools, logger } = opts;
|
|
18383
18709
|
registerAccountTools(server, { store, registry, tools });
|
|
18384
|
-
registerBrowseTools(server, { store, registry, tools });
|
|
18710
|
+
registerBrowseTools(server, { store, registry, tools, logger });
|
|
18385
18711
|
registerFolderTools(server, { registry, tools });
|
|
18386
18712
|
registerOrganizeTools(server, { registry, tools });
|
|
18387
18713
|
registerComposeTools(server, { store, registry, tools });
|
|
@@ -18390,7 +18716,7 @@ function registerTools(server, opts) {
|
|
|
18390
18716
|
// package.json
|
|
18391
18717
|
var package_default = {
|
|
18392
18718
|
name: "hypermail-mcp",
|
|
18393
|
-
version: "0.7.
|
|
18719
|
+
version: "0.7.14",
|
|
18394
18720
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
18395
18721
|
type: "module",
|
|
18396
18722
|
bin: {
|
|
@@ -18470,6 +18796,7 @@ var ENV_HTTP_PORT = "HYPERMAIL_HTTP_PORT";
|
|
|
18470
18796
|
var ENV_HTTP_HOST = "HYPERMAIL_HTTP_HOST";
|
|
18471
18797
|
var ENV_TOOLS_DISABLED = "HYPERMAIL_TOOLS_DISABLED";
|
|
18472
18798
|
var ENV_TOOLS_ENABLED = "HYPERMAIL_TOOLS_ENABLED";
|
|
18799
|
+
var ENV_DEBUG = "HYPERMAIL_DEBUG";
|
|
18473
18800
|
var ENV_OUTLOOK_CLIENT_ID = "HYPERMAIL_OUTLOOK_CLIENT_ID";
|
|
18474
18801
|
var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_OUTLOOK_TENANT_ID";
|
|
18475
18802
|
var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_GMAIL_CLIENT_ID";
|
|
@@ -18510,6 +18837,15 @@ function parseStringArray(value) {
|
|
|
18510
18837
|
if (trimmed === "") return [];
|
|
18511
18838
|
return trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
18512
18839
|
}
|
|
18840
|
+
function resolveDebugLogging(warnings) {
|
|
18841
|
+
const value = envRaw(ENV_DEBUG);
|
|
18842
|
+
if (value === void 0 || value.trim() === "") return false;
|
|
18843
|
+
const lower = value.trim().toLowerCase();
|
|
18844
|
+
if (["1", "true", "yes", "on", "debug"].includes(lower)) return true;
|
|
18845
|
+
if (["0", "false", "no", "off"].includes(lower)) return false;
|
|
18846
|
+
warnings.push(`Invalid ${ENV_DEBUG}; debug logging disabled.`);
|
|
18847
|
+
return false;
|
|
18848
|
+
}
|
|
18513
18849
|
function validateToolNames(toolNames, envName) {
|
|
18514
18850
|
if (!toolNames || toolNames.length === 0) return;
|
|
18515
18851
|
const known = new Set(KNOWN_TOOLS);
|
|
@@ -18591,6 +18927,7 @@ function loadConfig(cliOverrides = {}) {
|
|
|
18591
18927
|
const tools = resolveToolsConfig();
|
|
18592
18928
|
const providers = resolveProvidersConfig();
|
|
18593
18929
|
const dataDir = cliOverrides.dataDir ?? optionalEnvString(ENV_DATA_DIR);
|
|
18930
|
+
const debugLogging = resolveDebugLogging(warnings);
|
|
18594
18931
|
if (!optionalEnvString(ENV_KEY)) {
|
|
18595
18932
|
warnings.push(
|
|
18596
18933
|
`${ENV_KEY} is not set; a local generated key will be used. Set ${ENV_KEY} explicitly for portable hosted deployments.`
|
|
@@ -18602,7 +18939,8 @@ function loadConfig(cliOverrides = {}) {
|
|
|
18602
18939
|
transport,
|
|
18603
18940
|
http,
|
|
18604
18941
|
tools,
|
|
18605
|
-
providers
|
|
18942
|
+
providers,
|
|
18943
|
+
debugLogging
|
|
18606
18944
|
},
|
|
18607
18945
|
warnings
|
|
18608
18946
|
};
|
|
@@ -18648,7 +18986,16 @@ function resolveTools(config) {
|
|
|
18648
18986
|
// src/server.ts
|
|
18649
18987
|
async function startServer(opts) {
|
|
18650
18988
|
const { config } = opts;
|
|
18651
|
-
const
|
|
18989
|
+
const logger = createLogger({ enabled: config.debugLogging });
|
|
18990
|
+
const dataDir = resolveDataDir(config.dataDir);
|
|
18991
|
+
logger.debug("server", "startup", {
|
|
18992
|
+
version: VERSION,
|
|
18993
|
+
transport: config.transport,
|
|
18994
|
+
dataDir,
|
|
18995
|
+
toolsEnabled: config.tools?.enabled ?? null,
|
|
18996
|
+
toolsDisabled: config.tools?.disabled ?? null
|
|
18997
|
+
});
|
|
18998
|
+
const store = await AccountStore.open({ dataDir, logger });
|
|
18652
18999
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
18653
19000
|
const tools = resolveTools(config);
|
|
18654
19001
|
const createServer = () => {
|
|
@@ -18656,7 +19003,7 @@ async function startServer(opts) {
|
|
|
18656
19003
|
{ name: "hypermail-mcp", version: VERSION },
|
|
18657
19004
|
{ capabilities: { tools: {}, logging: {} } }
|
|
18658
19005
|
);
|
|
18659
|
-
registerTools(s, { store, registry, tools });
|
|
19006
|
+
registerTools(s, { store, registry, tools, logger });
|
|
18660
19007
|
return s;
|
|
18661
19008
|
};
|
|
18662
19009
|
if (config.transport === "http") {
|
|
@@ -18847,6 +19194,7 @@ Core environment variables:
|
|
|
18847
19194
|
HYPERMAIL_HTTP_HOST
|
|
18848
19195
|
HYPERMAIL_TOOLS_ENABLED
|
|
18849
19196
|
HYPERMAIL_TOOLS_DISABLED
|
|
19197
|
+
HYPERMAIL_DEBUG=1|true|yes|on|debug
|
|
18850
19198
|
|
|
18851
19199
|
Provider environment variables:
|
|
18852
19200
|
HYPERMAIL_OUTLOOK_CLIENT_ID
|