hypermail-mcp 0.7.12 → 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 +16 -1
- package/dist/cli.js +719 -170
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13954,18 +13954,81 @@ 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";
|
|
14015
|
+
var LOCK_STALE_MS = 3e4;
|
|
14016
|
+
var LOCK_TIMEOUT_MS = 1e4;
|
|
14017
|
+
var LOCK_RETRY_MS = 25;
|
|
13959
14018
|
var AccountStore = class _AccountStore {
|
|
13960
|
-
constructor(filePath, key, data) {
|
|
14019
|
+
constructor(filePath, key, data, logger) {
|
|
13961
14020
|
this.filePath = filePath;
|
|
13962
14021
|
this.key = key;
|
|
13963
14022
|
this.data = data;
|
|
14023
|
+
this.logger = logger;
|
|
14024
|
+
this.lockPath = `${filePath}.lock`;
|
|
13964
14025
|
}
|
|
13965
14026
|
filePath;
|
|
13966
14027
|
key;
|
|
13967
14028
|
data;
|
|
14029
|
+
logger;
|
|
13968
14030
|
writeLocks = /* @__PURE__ */ new Map();
|
|
14031
|
+
lockPath;
|
|
13969
14032
|
static async open(opts = {}) {
|
|
13970
14033
|
const dataDir = resolveDataDir(opts.dataDir);
|
|
13971
14034
|
await fs2.mkdir(dataDir, { recursive: true, mode: 448 });
|
|
@@ -13982,7 +14045,12 @@ var AccountStore = class _AccountStore {
|
|
|
13982
14045
|
throw err;
|
|
13983
14046
|
}
|
|
13984
14047
|
}
|
|
13985
|
-
|
|
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);
|
|
13986
14054
|
}
|
|
13987
14055
|
listAccounts() {
|
|
13988
14056
|
return this.data.accounts.map((a) => ({ ...a }));
|
|
@@ -13993,62 +14061,181 @@ var AccountStore = class _AccountStore {
|
|
|
13993
14061
|
return rec ? { ...rec } : void 0;
|
|
13994
14062
|
}
|
|
13995
14063
|
async upsertAccount(rec) {
|
|
13996
|
-
return this.runSerial(rec.email, async () => {
|
|
14064
|
+
return this.runSerial(rec.email, async () => this.updateLocked((data) => {
|
|
13997
14065
|
const norm = rec.email.trim().toLowerCase();
|
|
14066
|
+
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
14067
|
+
const current = idx >= 0 ? data.accounts[idx] : void 0;
|
|
13998
14068
|
const next = { ...rec, email: norm };
|
|
13999
|
-
const
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14069
|
+
const mergedCheckpoint = mergeNewEmailCheckpoints(
|
|
14070
|
+
current?.newEmailCheckpoint,
|
|
14071
|
+
rec.newEmailCheckpoint
|
|
14072
|
+
);
|
|
14073
|
+
if (mergedCheckpoint) next.newEmailCheckpoint = mergedCheckpoint;
|
|
14074
|
+
else delete next.newEmailCheckpoint;
|
|
14075
|
+
if (idx >= 0) data.accounts[idx] = next;
|
|
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
|
+
});
|
|
14084
|
+
return { result: { ...next }, changed: true };
|
|
14085
|
+
}));
|
|
14005
14086
|
}
|
|
14006
14087
|
async updateTokens(email, tokens) {
|
|
14007
|
-
return this.runSerial(email, async () => {
|
|
14088
|
+
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14008
14089
|
const norm = email.trim().toLowerCase();
|
|
14009
|
-
const idx =
|
|
14010
|
-
if (idx < 0)
|
|
14011
|
-
|
|
14090
|
+
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
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
|
+
}
|
|
14099
|
+
const current = data.accounts[idx];
|
|
14012
14100
|
const next = { ...current, tokens };
|
|
14013
|
-
|
|
14014
|
-
|
|
14015
|
-
|
|
14016
|
-
|
|
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
|
+
});
|
|
14110
|
+
return { result: { ...next }, changed: true };
|
|
14111
|
+
}));
|
|
14017
14112
|
}
|
|
14018
14113
|
async updateNewEmailCheckpoint(email, checkpoint) {
|
|
14019
|
-
return this.runSerial(email, async () => {
|
|
14114
|
+
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14020
14115
|
const norm = email.trim().toLowerCase();
|
|
14021
|
-
const idx =
|
|
14022
|
-
if (idx < 0)
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14116
|
+
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
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
|
+
}
|
|
14127
|
+
const current = data.accounts[idx];
|
|
14128
|
+
const mergedCheckpoint = mergeNewEmailCheckpoints(
|
|
14129
|
+
current.newEmailCheckpoint,
|
|
14130
|
+
checkpoint
|
|
14131
|
+
);
|
|
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
|
+
}
|
|
14034
14142
|
const next = {
|
|
14035
14143
|
...current,
|
|
14036
14144
|
newEmailCheckpoint: mergedCheckpoint
|
|
14037
14145
|
};
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
14041
|
-
|
|
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
|
+
});
|
|
14156
|
+
return { result: { ...next }, changed: true };
|
|
14157
|
+
}));
|
|
14158
|
+
}
|
|
14159
|
+
async claimNewEmails(email, candidates) {
|
|
14160
|
+
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14161
|
+
const norm = email.trim().toLowerCase();
|
|
14162
|
+
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
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
|
+
}
|
|
14174
|
+
const account = data.accounts[idx];
|
|
14175
|
+
let checkpoint = normalizeCheckpoint(account.newEmailCheckpoint);
|
|
14176
|
+
const claimed = [];
|
|
14177
|
+
const ordered = [...candidates].sort((a, b) => {
|
|
14178
|
+
const byTime = compareTimestamp(normalizeTimestamp(a.receivedAt) ?? a.receivedAt, normalizeTimestamp(b.receivedAt) ?? b.receivedAt);
|
|
14179
|
+
if (byTime !== 0) return byTime;
|
|
14180
|
+
return a.summaryId.localeCompare(b.summaryId);
|
|
14181
|
+
});
|
|
14182
|
+
for (const candidate of ordered) {
|
|
14183
|
+
const receivedAt = normalizeTimestamp(candidate.receivedAt);
|
|
14184
|
+
if (!receivedAt) continue;
|
|
14185
|
+
const ids = uniqueIds([candidate.summaryId, ...candidate.ids]);
|
|
14186
|
+
if (ids.length === 0) continue;
|
|
14187
|
+
if (isAlreadyDelivered(checkpoint, receivedAt, ids)) continue;
|
|
14188
|
+
claimed.push(candidate.summaryId);
|
|
14189
|
+
checkpoint = mergeNewEmailCheckpoints(checkpoint, {
|
|
14190
|
+
receivedAt,
|
|
14191
|
+
deliveredIdsAtReceivedAt: ids
|
|
14192
|
+
});
|
|
14193
|
+
}
|
|
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
|
+
}
|
|
14208
|
+
const next = {
|
|
14209
|
+
...account,
|
|
14210
|
+
newEmailCheckpoint: checkpoint
|
|
14211
|
+
};
|
|
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
|
+
});
|
|
14224
|
+
return { result: claimed, changed: true };
|
|
14225
|
+
}));
|
|
14042
14226
|
}
|
|
14043
14227
|
async removeAccount(email) {
|
|
14044
|
-
return this.runSerial(email, async () => {
|
|
14228
|
+
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14045
14229
|
const norm = email.trim().toLowerCase();
|
|
14046
|
-
const before =
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
|
|
14051
|
-
|
|
14230
|
+
const before = data.accounts.length;
|
|
14231
|
+
data.accounts = data.accounts.filter((a) => a.email.toLowerCase() !== norm);
|
|
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 };
|
|
14238
|
+
}));
|
|
14052
14239
|
}
|
|
14053
14240
|
async runSerial(email, task) {
|
|
14054
14241
|
const norm = email.trim().toLowerCase();
|
|
@@ -14067,11 +14254,146 @@ var AccountStore = class _AccountStore {
|
|
|
14067
14254
|
}
|
|
14068
14255
|
}
|
|
14069
14256
|
}
|
|
14257
|
+
async updateLocked(task) {
|
|
14258
|
+
return this.withFileLock(async () => {
|
|
14259
|
+
this.data = await this.readLatest();
|
|
14260
|
+
const { result, changed } = await task(this.data);
|
|
14261
|
+
if (changed) await this.flush();
|
|
14262
|
+
return result;
|
|
14263
|
+
});
|
|
14264
|
+
}
|
|
14265
|
+
async readLatest() {
|
|
14266
|
+
try {
|
|
14267
|
+
const buf = await fs2.readFile(this.filePath);
|
|
14268
|
+
return decrypt(buf, this.key);
|
|
14269
|
+
} catch (err) {
|
|
14270
|
+
if (err.code === "ENOENT") {
|
|
14271
|
+
return { version: 1, accounts: [] };
|
|
14272
|
+
}
|
|
14273
|
+
throw err;
|
|
14274
|
+
}
|
|
14275
|
+
}
|
|
14070
14276
|
async flush() {
|
|
14071
14277
|
const buf = encrypt(this.data, this.key);
|
|
14072
14278
|
await writeAtomic(this.filePath, buf);
|
|
14279
|
+
this.logger.debug("account-store", "flush", {
|
|
14280
|
+
filePath: this.filePath,
|
|
14281
|
+
accountCount: this.data.accounts.length
|
|
14282
|
+
});
|
|
14283
|
+
}
|
|
14284
|
+
async withFileLock(task) {
|
|
14285
|
+
await this.acquireFileLock();
|
|
14286
|
+
try {
|
|
14287
|
+
return await task();
|
|
14288
|
+
} finally {
|
|
14289
|
+
await fs2.rm(this.lockPath, { recursive: true, force: true });
|
|
14290
|
+
this.logger.debug("account-store", "lockReleased", {
|
|
14291
|
+
lockPath: this.lockPath
|
|
14292
|
+
});
|
|
14293
|
+
}
|
|
14294
|
+
}
|
|
14295
|
+
async acquireFileLock() {
|
|
14296
|
+
const startedAt = Date.now();
|
|
14297
|
+
let loggedWait = false;
|
|
14298
|
+
while (true) {
|
|
14299
|
+
try {
|
|
14300
|
+
await fs2.mkdir(this.lockPath, { mode: 448 });
|
|
14301
|
+
await fs2.writeFile(
|
|
14302
|
+
path2.join(this.lockPath, "owner"),
|
|
14303
|
+
`${process.pid}
|
|
14304
|
+
${(/* @__PURE__ */ new Date()).toISOString()}
|
|
14305
|
+
`,
|
|
14306
|
+
{ mode: 384 }
|
|
14307
|
+
);
|
|
14308
|
+
this.logger.debug("account-store", "lockAcquired", {
|
|
14309
|
+
lockPath: this.lockPath,
|
|
14310
|
+
waitedMs: Date.now() - startedAt
|
|
14311
|
+
});
|
|
14312
|
+
return;
|
|
14313
|
+
} catch (err) {
|
|
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
|
+
}
|
|
14321
|
+
await this.removeStaleLock();
|
|
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
|
+
});
|
|
14327
|
+
throw new Error(`timed out waiting for account store lock: ${this.lockPath}`);
|
|
14328
|
+
}
|
|
14329
|
+
await delay(LOCK_RETRY_MS);
|
|
14330
|
+
}
|
|
14331
|
+
}
|
|
14332
|
+
}
|
|
14333
|
+
async removeStaleLock() {
|
|
14334
|
+
try {
|
|
14335
|
+
const stat = await fs2.stat(this.lockPath);
|
|
14336
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
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
|
+
});
|
|
14342
|
+
}
|
|
14343
|
+
} catch (err) {
|
|
14344
|
+
if (err.code !== "ENOENT") throw err;
|
|
14345
|
+
}
|
|
14073
14346
|
}
|
|
14074
14347
|
};
|
|
14348
|
+
function normalizeCheckpoint(checkpoint) {
|
|
14349
|
+
const receivedAt = normalizeTimestamp(checkpoint?.receivedAt);
|
|
14350
|
+
if (!receivedAt) return void 0;
|
|
14351
|
+
return {
|
|
14352
|
+
receivedAt,
|
|
14353
|
+
deliveredIdsAtReceivedAt: uniqueIds(checkpoint?.deliveredIdsAtReceivedAt ?? [])
|
|
14354
|
+
};
|
|
14355
|
+
}
|
|
14356
|
+
function mergeNewEmailCheckpoints(current, incoming) {
|
|
14357
|
+
const normalizedCurrent = normalizeCheckpoint(current);
|
|
14358
|
+
const normalizedIncoming = normalizeCheckpoint(incoming);
|
|
14359
|
+
if (!normalizedIncoming) return normalizedCurrent;
|
|
14360
|
+
if (!normalizedCurrent) return normalizedIncoming;
|
|
14361
|
+
const comparison = compareTimestamp(normalizedIncoming.receivedAt, normalizedCurrent.receivedAt);
|
|
14362
|
+
if (comparison > 0) return normalizedIncoming;
|
|
14363
|
+
if (comparison < 0) return normalizedCurrent;
|
|
14364
|
+
return {
|
|
14365
|
+
receivedAt: normalizedCurrent.receivedAt,
|
|
14366
|
+
deliveredIdsAtReceivedAt: uniqueIds([
|
|
14367
|
+
...normalizedCurrent.deliveredIdsAtReceivedAt ?? [],
|
|
14368
|
+
...normalizedIncoming.deliveredIdsAtReceivedAt ?? []
|
|
14369
|
+
])
|
|
14370
|
+
};
|
|
14371
|
+
}
|
|
14372
|
+
function isAlreadyDelivered(checkpoint, receivedAt, ids) {
|
|
14373
|
+
if (!checkpoint) return false;
|
|
14374
|
+
const comparison = compareTimestamp(receivedAt, checkpoint.receivedAt);
|
|
14375
|
+
if (comparison < 0) return true;
|
|
14376
|
+
if (comparison > 0) return false;
|
|
14377
|
+
const delivered = new Set(checkpoint.deliveredIdsAtReceivedAt ?? []);
|
|
14378
|
+
return ids.some((id) => delivered.has(id));
|
|
14379
|
+
}
|
|
14380
|
+
function normalizeTimestamp(value) {
|
|
14381
|
+
if (!value) return void 0;
|
|
14382
|
+
const ms = Date.parse(value);
|
|
14383
|
+
if (!Number.isFinite(ms)) return void 0;
|
|
14384
|
+
return new Date(ms).toISOString();
|
|
14385
|
+
}
|
|
14386
|
+
function compareTimestamp(a, b) {
|
|
14387
|
+
if (a < b) return -1;
|
|
14388
|
+
if (a > b) return 1;
|
|
14389
|
+
return 0;
|
|
14390
|
+
}
|
|
14391
|
+
function uniqueIds(ids) {
|
|
14392
|
+
return [...new Set(ids.filter((id) => id.length > 0))];
|
|
14393
|
+
}
|
|
14394
|
+
async function delay(ms) {
|
|
14395
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
14396
|
+
}
|
|
14075
14397
|
|
|
14076
14398
|
// src/providers/outlook/index.ts
|
|
14077
14399
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -14490,8 +14812,8 @@ async function updateDraft(client, account, id, update) {
|
|
|
14490
14812
|
payload.attachments = converted.attachments;
|
|
14491
14813
|
}
|
|
14492
14814
|
}
|
|
14493
|
-
await client.api(`/me/messages/${encodeURIComponent(id)}`).patch(payload);
|
|
14494
|
-
return { id };
|
|
14815
|
+
const updated = await client.api(`/me/messages/${encodeURIComponent(id)}`).header("Prefer", "return=representation").patch(payload);
|
|
14816
|
+
return { id: updated?.id ?? id };
|
|
14495
14817
|
}
|
|
14496
14818
|
async function addAttachmentToDraft(client, account, draftId, name, contentBytes, contentType) {
|
|
14497
14819
|
const att = await client.api(`/me/messages/${encodeURIComponent(draftId)}/attachments`).post({
|
|
@@ -14744,6 +15066,9 @@ var OutlookProvider = class {
|
|
|
14744
15066
|
async moveEmail(account, id, destinationId) {
|
|
14745
15067
|
return moveEmail(this.clients.get(account), account, id, destinationId);
|
|
14746
15068
|
}
|
|
15069
|
+
async trashEmail(account, id) {
|
|
15070
|
+
return moveEmail(this.clients.get(account), account, id, "deleteditems");
|
|
15071
|
+
}
|
|
14747
15072
|
async sendDraft(account, id) {
|
|
14748
15073
|
return sendDraft(this.clients.get(account), account, id);
|
|
14749
15074
|
}
|
|
@@ -14910,6 +15235,21 @@ var WELL_KNOWN_TO_IMAP = {
|
|
|
14910
15235
|
function resolveFolder(wellKnownOrPath) {
|
|
14911
15236
|
return WELL_KNOWN_TO_IMAP[wellKnownOrPath.toLowerCase()] ?? wellKnownOrPath;
|
|
14912
15237
|
}
|
|
15238
|
+
function isTrashFolderAlias(wellKnownOrPath) {
|
|
15239
|
+
const lower = wellKnownOrPath.toLowerCase();
|
|
15240
|
+
return lower === "deleteditems" || lower === "trash";
|
|
15241
|
+
}
|
|
15242
|
+
function resolveTrashMailbox(mailboxes) {
|
|
15243
|
+
for (const mailbox of mailboxes) {
|
|
15244
|
+
const specialUse = mailbox.specialUse?.toLowerCase();
|
|
15245
|
+
if (specialUse === "\\trash") return mailbox.path;
|
|
15246
|
+
const flags = mailbox.flags ? Array.from(mailbox.flags) : [];
|
|
15247
|
+
if (flags.some((flag) => flag.toLowerCase() === "\\trash")) {
|
|
15248
|
+
return mailbox.path;
|
|
15249
|
+
}
|
|
15250
|
+
}
|
|
15251
|
+
return "Trash";
|
|
15252
|
+
}
|
|
14913
15253
|
function clampLimit2(v, dflt, max) {
|
|
14914
15254
|
if (!v || v <= 0) return dflt;
|
|
14915
15255
|
return Math.min(v, max);
|
|
@@ -15380,6 +15720,9 @@ async function updateDraft2(clients, account, id, update) {
|
|
|
15380
15720
|
});
|
|
15381
15721
|
}
|
|
15382
15722
|
async function moveEmail2(clients, account, id, destinationId) {
|
|
15723
|
+
if (isTrashFolderAlias(destinationId)) {
|
|
15724
|
+
return trashEmail(clients, account, id);
|
|
15725
|
+
}
|
|
15383
15726
|
const client = clients.get(account);
|
|
15384
15727
|
const { folder, uid } = decodeId(id);
|
|
15385
15728
|
const dest = resolveFolder(destinationId);
|
|
@@ -15387,6 +15730,17 @@ async function moveEmail2(clients, account, id, destinationId) {
|
|
|
15387
15730
|
await imap.messageMove(uid, dest, { uid: true });
|
|
15388
15731
|
});
|
|
15389
15732
|
}
|
|
15733
|
+
async function trashEmail(clients, account, id) {
|
|
15734
|
+
const client = clients.get(account);
|
|
15735
|
+
const { folder, uid } = decodeId(id);
|
|
15736
|
+
const imap = await client.getImap();
|
|
15737
|
+
const dest = resolveTrashMailbox(
|
|
15738
|
+
await imap.list()
|
|
15739
|
+
);
|
|
15740
|
+
return client.withMailbox(folder, async (lockedImap) => {
|
|
15741
|
+
await lockedImap.messageMove(uid, dest, { uid: true });
|
|
15742
|
+
});
|
|
15743
|
+
}
|
|
15390
15744
|
async function sendDraft2(clients, account, id) {
|
|
15391
15745
|
const client = clients.get(account);
|
|
15392
15746
|
const { folder, uid } = decodeId(id);
|
|
@@ -15599,6 +15953,9 @@ var ImapProvider = class {
|
|
|
15599
15953
|
async moveEmail(account, id, destinationId) {
|
|
15600
15954
|
return moveEmail2(this.clients, account, id, destinationId);
|
|
15601
15955
|
}
|
|
15956
|
+
async trashEmail(account, id) {
|
|
15957
|
+
return trashEmail(this.clients, account, id);
|
|
15958
|
+
}
|
|
15602
15959
|
async sendDraft(account, id) {
|
|
15603
15960
|
return sendDraft2(this.clients, account, id);
|
|
15604
15961
|
}
|
|
@@ -16355,7 +16712,14 @@ async function updateDraft3(clients, account, id, update) {
|
|
|
16355
16712
|
});
|
|
16356
16713
|
return { id: updated.data.message?.id ?? updated.data.id ?? id };
|
|
16357
16714
|
}
|
|
16715
|
+
function isTrashDestination(destinationId) {
|
|
16716
|
+
const lower = destinationId.toLowerCase();
|
|
16717
|
+
return lower === "deleteditems" || lower === "trash";
|
|
16718
|
+
}
|
|
16358
16719
|
async function moveEmail3(clients, account, id, destinationId) {
|
|
16720
|
+
if (isTrashDestination(destinationId)) {
|
|
16721
|
+
return trashEmail2(clients, account, id);
|
|
16722
|
+
}
|
|
16359
16723
|
const { gmail } = clients.get(account);
|
|
16360
16724
|
const { addLabelIds, removeLabelIds } = resolveLabelsForMove(destinationId);
|
|
16361
16725
|
await gmail.users.messages.modify({
|
|
@@ -16364,6 +16728,13 @@ async function moveEmail3(clients, account, id, destinationId) {
|
|
|
16364
16728
|
requestBody: { addLabelIds, removeLabelIds }
|
|
16365
16729
|
});
|
|
16366
16730
|
}
|
|
16731
|
+
async function trashEmail2(clients, account, id) {
|
|
16732
|
+
const { gmail } = clients.get(account);
|
|
16733
|
+
await gmail.users.messages.trash({
|
|
16734
|
+
userId: "me",
|
|
16735
|
+
id
|
|
16736
|
+
});
|
|
16737
|
+
}
|
|
16367
16738
|
async function sendDraft3(clients, account, id) {
|
|
16368
16739
|
const { gmail } = clients.get(account);
|
|
16369
16740
|
const res = await gmail.users.drafts.send({
|
|
@@ -16685,6 +17056,9 @@ var GmailProvider = class {
|
|
|
16685
17056
|
async moveEmail(account, id, destinationId) {
|
|
16686
17057
|
return moveEmail3(this.clients, account, id, destinationId);
|
|
16687
17058
|
}
|
|
17059
|
+
async trashEmail(account, id) {
|
|
17060
|
+
return trashEmail2(this.clients, account, id);
|
|
17061
|
+
}
|
|
16688
17062
|
async sendDraft(account, id) {
|
|
16689
17063
|
return sendDraft3(this.clients, account, id);
|
|
16690
17064
|
}
|
|
@@ -17150,7 +17524,7 @@ var BODY_LIMIT = 2e4;
|
|
|
17150
17524
|
var PAGE_SIZE = 100;
|
|
17151
17525
|
var MISSING_RECEIVED_AT = "1970-01-01T00:00:00.000Z";
|
|
17152
17526
|
function registerNewEmailTool(server, ctx) {
|
|
17153
|
-
const { store, registry, tools } = ctx;
|
|
17527
|
+
const { store, registry, tools, logger = noopLogger } = ctx;
|
|
17154
17528
|
if (!shouldRegister("get_new_emails", tools)) return;
|
|
17155
17529
|
const newEmailOutputSchema = z4.object({
|
|
17156
17530
|
account: z4.string(),
|
|
@@ -17192,20 +17566,46 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17192
17566
|
},
|
|
17193
17567
|
async (args) => {
|
|
17194
17568
|
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
17569
|
+
logger.debug("get-new-emails", "start", {
|
|
17570
|
+
account: args.account ?? null,
|
|
17571
|
+
limit
|
|
17572
|
+
});
|
|
17195
17573
|
if (args.account) {
|
|
17196
17574
|
try {
|
|
17197
17575
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
17198
|
-
const result = await collectCandidatesForAccount(store, provider, account);
|
|
17576
|
+
const result = await collectCandidatesForAccount(store, provider, account, logger);
|
|
17199
17577
|
const selected2 = oldestCandidatesFirst(result.candidates).slice(0, limit);
|
|
17200
|
-
|
|
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);
|
|
17201
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
|
+
});
|
|
17202
17593
|
return ok(data2, data2);
|
|
17203
17594
|
} catch (err) {
|
|
17595
|
+
logger.debug("get-new-emails", "error", {
|
|
17596
|
+
account: args.account,
|
|
17597
|
+
message: errMsg(err)
|
|
17598
|
+
});
|
|
17204
17599
|
return fail(errMsg(err));
|
|
17205
17600
|
}
|
|
17206
17601
|
}
|
|
17207
17602
|
const accounts = store.listAccounts();
|
|
17208
17603
|
if (accounts.length === 0) {
|
|
17604
|
+
logger.debug("get-new-emails", "end", {
|
|
17605
|
+
accountCount: 0,
|
|
17606
|
+
returnedCount: 0,
|
|
17607
|
+
errorCount: 1
|
|
17608
|
+
});
|
|
17209
17609
|
return fail("no accounts registered. Call add_account first.");
|
|
17210
17610
|
}
|
|
17211
17611
|
const errors = [];
|
|
@@ -17215,15 +17615,27 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17215
17615
|
for (const stored of accounts) {
|
|
17216
17616
|
try {
|
|
17217
17617
|
const { provider, account } = registry.resolveByEmail(stored.email);
|
|
17218
|
-
const result = await collectCandidatesForAccount(store, provider, account);
|
|
17618
|
+
const result = await collectCandidatesForAccount(store, provider, account, logger);
|
|
17219
17619
|
providersByEmail.set(result.account.email, provider);
|
|
17220
17620
|
accountsByEmail.set(result.account.email, result.account);
|
|
17221
17621
|
collected.push(...result.candidates);
|
|
17222
17622
|
} catch (err) {
|
|
17623
|
+
logger.debug("get-new-emails", "accountError", {
|
|
17624
|
+
account: stored.email,
|
|
17625
|
+
message: errMsg(err)
|
|
17626
|
+
});
|
|
17223
17627
|
errors.push({ account: stored.email, message: errMsg(err) });
|
|
17224
17628
|
}
|
|
17225
17629
|
}
|
|
17226
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
|
+
});
|
|
17227
17639
|
const emails = [];
|
|
17228
17640
|
if (limit > 0) {
|
|
17229
17641
|
const byAccount = /* @__PURE__ */ new Map();
|
|
@@ -17242,24 +17654,39 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17242
17654
|
store,
|
|
17243
17655
|
provider,
|
|
17244
17656
|
account,
|
|
17245
|
-
accountCandidates
|
|
17657
|
+
accountCandidates,
|
|
17658
|
+
logger
|
|
17246
17659
|
)
|
|
17247
17660
|
);
|
|
17248
17661
|
} catch (err) {
|
|
17662
|
+
logger.debug("get-new-emails", "accountError", {
|
|
17663
|
+
account: email,
|
|
17664
|
+
message: errMsg(err)
|
|
17665
|
+
});
|
|
17249
17666
|
errors.push({ account: email, message: errMsg(err) });
|
|
17250
17667
|
}
|
|
17251
17668
|
}
|
|
17252
17669
|
}
|
|
17253
17670
|
const orderedEmails = emails.sort(compareNewEmailOutputOldestFirst);
|
|
17254
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
|
+
});
|
|
17255
17677
|
return ok(data, data);
|
|
17256
17678
|
}
|
|
17257
17679
|
);
|
|
17258
17680
|
}
|
|
17259
|
-
async function collectCandidatesForAccount(store, provider, account) {
|
|
17260
|
-
const checkpoint =
|
|
17681
|
+
async function collectCandidatesForAccount(store, provider, account, logger) {
|
|
17682
|
+
const checkpoint = normalizeCheckpoint2(account.newEmailCheckpoint);
|
|
17261
17683
|
if (!checkpoint) {
|
|
17262
|
-
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
|
+
});
|
|
17263
17690
|
return { account, candidates: [] };
|
|
17264
17691
|
}
|
|
17265
17692
|
const deliveredAtCheckpoint = new Set(checkpoint.deliveredIdsAtReceivedAt ?? []);
|
|
@@ -17275,7 +17702,7 @@ async function collectCandidatesForAccount(store, provider, account) {
|
|
|
17275
17702
|
let sawOlderThanCheckpoint = false;
|
|
17276
17703
|
for (const item of items) {
|
|
17277
17704
|
const timestamp = effectiveReceivedAt(item.receivedAt);
|
|
17278
|
-
const comparison =
|
|
17705
|
+
const comparison = compareTimestamp2(timestamp, checkpoint.receivedAt);
|
|
17279
17706
|
if (comparison > 0) {
|
|
17280
17707
|
candidates.push({ account, summary: item, timestamp });
|
|
17281
17708
|
} else if (comparison === 0) {
|
|
@@ -17289,9 +17716,18 @@ async function collectCandidatesForAccount(store, provider, account) {
|
|
|
17289
17716
|
if (sawOlderThanCheckpoint || !hasMore) break;
|
|
17290
17717
|
skip += items.length;
|
|
17291
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
|
+
});
|
|
17292
17728
|
return { account, candidates };
|
|
17293
17729
|
}
|
|
17294
|
-
async function initializeCheckpoint(store, provider, account) {
|
|
17730
|
+
async function initializeCheckpoint(store, provider, account, logger) {
|
|
17295
17731
|
const { items } = await provider.listEmails(account, {
|
|
17296
17732
|
folder: "inbox",
|
|
17297
17733
|
limit: PAGE_SIZE
|
|
@@ -17303,16 +17739,50 @@ async function initializeCheckpoint(store, provider, account) {
|
|
|
17303
17739
|
receivedAt,
|
|
17304
17740
|
deliveredIdsAtReceivedAt
|
|
17305
17741
|
});
|
|
17742
|
+
logger.debug("get-new-emails", "checkpointInitialized", {
|
|
17743
|
+
account: account.email,
|
|
17744
|
+
receivedAt,
|
|
17745
|
+
deliveredIdCount: deliveredIdsAtReceivedAt.length
|
|
17746
|
+
});
|
|
17306
17747
|
}
|
|
17307
|
-
async function hydrateAndAdvance(store, provider, account, selected) {
|
|
17308
|
-
if (selected.length === 0)
|
|
17309
|
-
|
|
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
|
+
}
|
|
17758
|
+
const hydrated = [];
|
|
17310
17759
|
for (const candidate of selected) {
|
|
17311
17760
|
const full = await provider.readEmail(account, candidate.summary.id);
|
|
17312
|
-
|
|
17761
|
+
hydrated.push({
|
|
17762
|
+
candidate,
|
|
17763
|
+
email: formatNewEmail(account.email, full, candidate.summary),
|
|
17764
|
+
fullId: full.id
|
|
17765
|
+
});
|
|
17313
17766
|
}
|
|
17314
|
-
|
|
17315
|
-
|
|
17767
|
+
const claims = hydrated.map(({ candidate, fullId }) => ({
|
|
17768
|
+
summaryId: candidate.summary.id,
|
|
17769
|
+
receivedAt: candidate.timestamp,
|
|
17770
|
+
ids: [candidate.summary.id, fullId]
|
|
17771
|
+
}));
|
|
17772
|
+
logger.debug("get-new-emails", "hydrated", {
|
|
17773
|
+
account: account.email,
|
|
17774
|
+
selectedCount: selected.length,
|
|
17775
|
+
hydratedCount: hydrated.length
|
|
17776
|
+
});
|
|
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
|
+
});
|
|
17785
|
+
return hydrated.filter(({ candidate }) => claimed.has(candidate.summary.id)).map(({ email }) => email);
|
|
17316
17786
|
}
|
|
17317
17787
|
function formatNewEmail(account, msg, summary) {
|
|
17318
17788
|
const body = selectBody(msg, "markdown");
|
|
@@ -17337,19 +17807,8 @@ function formatNewEmail(account, msg, summary) {
|
|
|
17337
17807
|
bodyOriginalLength: body.length
|
|
17338
17808
|
};
|
|
17339
17809
|
}
|
|
17340
|
-
|
|
17341
|
-
const
|
|
17342
|
-
const newest = ordered[ordered.length - 1];
|
|
17343
|
-
if (!newest) return;
|
|
17344
|
-
const newestTimestamp = newest.timestamp;
|
|
17345
|
-
const idsAtNewest = ordered.filter((candidate) => candidate.timestamp === newestTimestamp).map((candidate) => candidate.summary.id);
|
|
17346
|
-
await store.updateNewEmailCheckpoint(account.email, {
|
|
17347
|
-
receivedAt: newestTimestamp,
|
|
17348
|
-
deliveredIdsAtReceivedAt: idsAtNewest
|
|
17349
|
-
});
|
|
17350
|
-
}
|
|
17351
|
-
function normalizeCheckpoint(checkpoint) {
|
|
17352
|
-
const receivedAt = normalizeTimestamp(checkpoint?.receivedAt);
|
|
17810
|
+
function normalizeCheckpoint2(checkpoint) {
|
|
17811
|
+
const receivedAt = normalizeTimestamp2(checkpoint?.receivedAt);
|
|
17353
17812
|
if (!receivedAt) return null;
|
|
17354
17813
|
return {
|
|
17355
17814
|
receivedAt,
|
|
@@ -17357,9 +17816,9 @@ function normalizeCheckpoint(checkpoint) {
|
|
|
17357
17816
|
};
|
|
17358
17817
|
}
|
|
17359
17818
|
function effectiveReceivedAt(receivedAt) {
|
|
17360
|
-
return
|
|
17819
|
+
return normalizeTimestamp2(receivedAt) ?? MISSING_RECEIVED_AT;
|
|
17361
17820
|
}
|
|
17362
|
-
function
|
|
17821
|
+
function normalizeTimestamp2(value) {
|
|
17363
17822
|
if (!value) return null;
|
|
17364
17823
|
const ms = Date.parse(value);
|
|
17365
17824
|
if (!Number.isFinite(ms)) return null;
|
|
@@ -17367,20 +17826,20 @@ function normalizeTimestamp(value) {
|
|
|
17367
17826
|
}
|
|
17368
17827
|
function oldestCandidatesFirst(items) {
|
|
17369
17828
|
return [...items].sort((a, b) => {
|
|
17370
|
-
const byTimestamp =
|
|
17829
|
+
const byTimestamp = compareTimestamp2(a.timestamp, b.timestamp);
|
|
17371
17830
|
if (byTimestamp !== 0) return byTimestamp;
|
|
17372
17831
|
return a.summary.id.localeCompare(b.summary.id);
|
|
17373
17832
|
});
|
|
17374
17833
|
}
|
|
17375
17834
|
function compareNewEmailOutputOldestFirst(a, b) {
|
|
17376
|
-
const byTimestamp =
|
|
17835
|
+
const byTimestamp = compareTimestamp2(
|
|
17377
17836
|
effectiveReceivedAt(a.receivedAt),
|
|
17378
17837
|
effectiveReceivedAt(b.receivedAt)
|
|
17379
17838
|
);
|
|
17380
17839
|
if (byTimestamp !== 0) return byTimestamp;
|
|
17381
17840
|
return a.id.localeCompare(b.id);
|
|
17382
17841
|
}
|
|
17383
|
-
function
|
|
17842
|
+
function compareTimestamp2(a, b) {
|
|
17384
17843
|
if (a < b) return -1;
|
|
17385
17844
|
if (a > b) return 1;
|
|
17386
17845
|
return 0;
|
|
@@ -17388,7 +17847,7 @@ function compareTimestamp(a, b) {
|
|
|
17388
17847
|
|
|
17389
17848
|
// src/tools/browse.ts
|
|
17390
17849
|
function registerBrowseTools(server, ctx) {
|
|
17391
|
-
const { store, registry, tools } = ctx;
|
|
17850
|
+
const { store, registry, tools, logger } = ctx;
|
|
17392
17851
|
const emailListOutputSchema = z5.object({
|
|
17393
17852
|
account: z5.string(),
|
|
17394
17853
|
count: z5.number(),
|
|
@@ -17438,7 +17897,7 @@ function registerBrowseTools(server, ctx) {
|
|
|
17438
17897
|
}
|
|
17439
17898
|
);
|
|
17440
17899
|
}
|
|
17441
|
-
registerNewEmailTool(server, { store, registry, tools });
|
|
17900
|
+
registerNewEmailTool(server, { store, registry, tools, logger });
|
|
17442
17901
|
if (shouldRegister("search_emails", tools)) {
|
|
17443
17902
|
server.registerTool(
|
|
17444
17903
|
"search_emails",
|
|
@@ -17713,6 +18172,12 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17713
18172
|
const data = { marked: true, id: args.id, isRead };
|
|
17714
18173
|
return ok(data, data);
|
|
17715
18174
|
}
|
|
18175
|
+
async function trashMessage(args) {
|
|
18176
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
18177
|
+
await provider.trashEmail(account, args.id);
|
|
18178
|
+
const data = { trashed: true, id: args.id };
|
|
18179
|
+
return ok(data, data);
|
|
18180
|
+
}
|
|
17716
18181
|
const archiveMoveSchema = z7.object({
|
|
17717
18182
|
account: z7.string().email(),
|
|
17718
18183
|
id: z7.string().min(1).describe("Message ID to move")
|
|
@@ -17752,7 +18217,7 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17752
18217
|
},
|
|
17753
18218
|
async (args) => {
|
|
17754
18219
|
try {
|
|
17755
|
-
return await
|
|
18220
|
+
return await trashMessage(args);
|
|
17756
18221
|
} catch (err) {
|
|
17757
18222
|
return fail(errMsg(err));
|
|
17758
18223
|
}
|
|
@@ -17840,7 +18305,7 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17840
18305
|
}
|
|
17841
18306
|
|
|
17842
18307
|
// src/tools/compose.ts
|
|
17843
|
-
import { z as
|
|
18308
|
+
import { z as z9 } from "zod";
|
|
17844
18309
|
import { readFileSync } from "fs";
|
|
17845
18310
|
import { basename, extname } from "path";
|
|
17846
18311
|
|
|
@@ -17871,42 +18336,106 @@ var MIME_TYPES = {
|
|
|
17871
18336
|
".mp4": "video/mp4"
|
|
17872
18337
|
};
|
|
17873
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
|
+
|
|
17874
18436
|
// src/tools/compose.ts
|
|
17875
18437
|
function registerComposeTools(server, ctx) {
|
|
17876
18438
|
const { store, registry, tools } = ctx;
|
|
17877
|
-
const sendEmailSchema = z8.object({
|
|
17878
|
-
account: z8.string().email(),
|
|
17879
|
-
to: z8.array(emailAddrSchema).min(1),
|
|
17880
|
-
cc: z8.array(emailAddrSchema).optional(),
|
|
17881
|
-
bcc: z8.array(emailAddrSchema).optional(),
|
|
17882
|
-
subject: z8.string(),
|
|
17883
|
-
body: z8.string(),
|
|
17884
|
-
format: z8.enum(["html", "markdown"]).describe(
|
|
17885
|
-
"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."
|
|
17886
|
-
),
|
|
17887
|
-
include_signature: z8.boolean().describe(
|
|
17888
|
-
"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."
|
|
17889
|
-
),
|
|
17890
|
-
inReplyTo: z8.union([z8.string(), z8.literal(false)]).describe(
|
|
17891
|
-
"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)."
|
|
17892
|
-
),
|
|
17893
|
-
replyAll: z8.boolean().default(false).optional().describe(
|
|
17894
|
-
"When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
|
|
17895
|
-
),
|
|
17896
|
-
forwardMessageId: z8.string().optional().describe(
|
|
17897
|
-
"Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
|
|
17898
|
-
),
|
|
17899
|
-
attachments: z8.array(
|
|
17900
|
-
z8.object({
|
|
17901
|
-
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
17902
|
-
name: z8.string().optional().describe(
|
|
17903
|
-
"Attachment filename. Defaults to the file's basename."
|
|
17904
|
-
)
|
|
17905
|
-
})
|
|
17906
|
-
).optional().describe(
|
|
17907
|
-
"File attachments to include. The server reads the files from disk and base64-encodes them automatically."
|
|
17908
|
-
)
|
|
17909
|
-
});
|
|
17910
18439
|
async function handleSendOrDraft(args, action, resultKey, toolName) {
|
|
17911
18440
|
try {
|
|
17912
18441
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
@@ -17962,8 +18491,8 @@ function registerComposeTools(server, ctx) {
|
|
|
17962
18491
|
}
|
|
17963
18492
|
}
|
|
17964
18493
|
const sendEmailOutputSchema = {
|
|
17965
|
-
sent:
|
|
17966
|
-
id:
|
|
18494
|
+
sent: z9.literal(true),
|
|
18495
|
+
id: z9.string()
|
|
17967
18496
|
};
|
|
17968
18497
|
if (shouldRegister("send_email", tools)) {
|
|
17969
18498
|
server.registerTool(
|
|
@@ -17982,9 +18511,9 @@ function registerComposeTools(server, ctx) {
|
|
|
17982
18511
|
);
|
|
17983
18512
|
}
|
|
17984
18513
|
const draftEmailOutputSchema = {
|
|
17985
|
-
draft:
|
|
17986
|
-
id:
|
|
17987
|
-
draftHtml:
|
|
18514
|
+
draft: z9.literal(true),
|
|
18515
|
+
id: z9.string(),
|
|
18516
|
+
draftHtml: z9.string().optional()
|
|
17988
18517
|
};
|
|
17989
18518
|
if (shouldRegister("draft_email", tools)) {
|
|
17990
18519
|
server.registerTool(
|
|
@@ -18002,46 +18531,10 @@ function registerComposeTools(server, ctx) {
|
|
|
18002
18531
|
)
|
|
18003
18532
|
);
|
|
18004
18533
|
}
|
|
18005
|
-
const editDraftSchema = z8.object({
|
|
18006
|
-
account: z8.string().email(),
|
|
18007
|
-
id: z8.string().min(1).describe("Draft message ID to edit"),
|
|
18008
|
-
to: z8.array(emailAddrSchema).optional(),
|
|
18009
|
-
cc: z8.array(emailAddrSchema).optional(),
|
|
18010
|
-
bcc: z8.array(emailAddrSchema).optional(),
|
|
18011
|
-
subject: z8.string().optional(),
|
|
18012
|
-
old_text: z8.string().min(1).optional().describe(
|
|
18013
|
-
"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."
|
|
18014
|
-
),
|
|
18015
|
-
new_text: z8.string().optional().describe(
|
|
18016
|
-
"Replacement content for `old_text`. The replacement is composed using `format` and `include_signature`, then inserted exactly where `old_text` matched."
|
|
18017
|
-
),
|
|
18018
|
-
body: z8.string().optional().describe(
|
|
18019
|
-
"Deprecated alias for `new_text`. Body-only full replacement is not supported; provide `old_text` with this field."
|
|
18020
|
-
),
|
|
18021
|
-
format: z8.enum(["html", "markdown"]).optional().describe(
|
|
18022
|
-
"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."
|
|
18023
|
-
),
|
|
18024
|
-
include_signature: z8.boolean().optional().describe(
|
|
18025
|
-
"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."
|
|
18026
|
-
),
|
|
18027
|
-
new_attachments: z8.array(
|
|
18028
|
-
z8.object({
|
|
18029
|
-
filePath: z8.string().min(1).describe("Absolute path to a local file"),
|
|
18030
|
-
name: z8.string().optional().describe(
|
|
18031
|
-
"Attachment filename. Defaults to the file's basename."
|
|
18032
|
-
)
|
|
18033
|
-
})
|
|
18034
|
-
).optional().describe(
|
|
18035
|
-
"New file attachments to add to the draft. The server reads the files from disk and base64-encodes them automatically."
|
|
18036
|
-
),
|
|
18037
|
-
remove_attachments: z8.array(z8.string().min(1)).optional().describe(
|
|
18038
|
-
"Attachment IDs to remove from the draft. Get attachment IDs from read_email."
|
|
18039
|
-
)
|
|
18040
|
-
});
|
|
18041
18534
|
const editDraftOutputSchema = {
|
|
18042
|
-
edited:
|
|
18043
|
-
id:
|
|
18044
|
-
draftHtml:
|
|
18535
|
+
edited: z9.literal(true),
|
|
18536
|
+
id: z9.string(),
|
|
18537
|
+
draftHtml: z9.string().optional()
|
|
18045
18538
|
};
|
|
18046
18539
|
if (shouldRegister("edit_draft", tools)) {
|
|
18047
18540
|
server.registerTool(
|
|
@@ -18080,6 +18573,7 @@ function registerComposeTools(server, ctx) {
|
|
|
18080
18573
|
}
|
|
18081
18574
|
let bodyPayload;
|
|
18082
18575
|
let isHtmlPayload;
|
|
18576
|
+
let bodyExpectation;
|
|
18083
18577
|
if (replacementText !== void 0) {
|
|
18084
18578
|
const existing = await provider.readEmail(account, a.id);
|
|
18085
18579
|
const existingBody = existing.bodyHtml ?? existing.bodyText ?? "";
|
|
@@ -18092,6 +18586,11 @@ function registerComposeTools(server, ctx) {
|
|
|
18092
18586
|
});
|
|
18093
18587
|
bodyPayload = applyExactTextEdit(existingBody, a.old_text ?? "", composed.body);
|
|
18094
18588
|
isHtmlPayload = composed.isHtml;
|
|
18589
|
+
bodyExpectation = {
|
|
18590
|
+
expectedBody: bodyPayload,
|
|
18591
|
+
oldText: a.old_text ?? "",
|
|
18592
|
+
replacementBody: composed.body
|
|
18593
|
+
};
|
|
18095
18594
|
}
|
|
18096
18595
|
const hasDraftUpdate = a.to !== void 0 || a.cc !== void 0 || a.bcc !== void 0 || a.subject !== void 0 || bodyPayload !== void 0;
|
|
18097
18596
|
let currentId = a.id;
|
|
@@ -18134,7 +18633,35 @@ function registerComposeTools(server, ctx) {
|
|
|
18134
18633
|
removedIds.push(attId);
|
|
18135
18634
|
}
|
|
18136
18635
|
}
|
|
18137
|
-
|
|
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
|
+
}
|
|
18138
18665
|
const result = {
|
|
18139
18666
|
edited: true,
|
|
18140
18667
|
id: currentId,
|
|
@@ -18148,8 +18675,8 @@ function registerComposeTools(server, ctx) {
|
|
|
18148
18675
|
);
|
|
18149
18676
|
}
|
|
18150
18677
|
const sendDraftOutputSchema = {
|
|
18151
|
-
sent:
|
|
18152
|
-
id:
|
|
18678
|
+
sent: z9.literal(true),
|
|
18679
|
+
id: z9.string()
|
|
18153
18680
|
};
|
|
18154
18681
|
if (shouldRegister("send_draft", tools)) {
|
|
18155
18682
|
server.registerTool(
|
|
@@ -18157,8 +18684,8 @@ function registerComposeTools(server, ctx) {
|
|
|
18157
18684
|
{
|
|
18158
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.",
|
|
18159
18686
|
inputSchema: {
|
|
18160
|
-
account:
|
|
18161
|
-
id:
|
|
18687
|
+
account: z9.string().email(),
|
|
18688
|
+
id: z9.string().min(1).describe("Draft message ID to send")
|
|
18162
18689
|
},
|
|
18163
18690
|
outputSchema: sendDraftOutputSchema
|
|
18164
18691
|
},
|
|
@@ -18178,9 +18705,9 @@ function registerComposeTools(server, ctx) {
|
|
|
18178
18705
|
|
|
18179
18706
|
// src/tools/index.ts
|
|
18180
18707
|
function registerTools(server, opts) {
|
|
18181
|
-
const { store, registry, tools } = opts;
|
|
18708
|
+
const { store, registry, tools, logger } = opts;
|
|
18182
18709
|
registerAccountTools(server, { store, registry, tools });
|
|
18183
|
-
registerBrowseTools(server, { store, registry, tools });
|
|
18710
|
+
registerBrowseTools(server, { store, registry, tools, logger });
|
|
18184
18711
|
registerFolderTools(server, { registry, tools });
|
|
18185
18712
|
registerOrganizeTools(server, { registry, tools });
|
|
18186
18713
|
registerComposeTools(server, { store, registry, tools });
|
|
@@ -18189,7 +18716,7 @@ function registerTools(server, opts) {
|
|
|
18189
18716
|
// package.json
|
|
18190
18717
|
var package_default = {
|
|
18191
18718
|
name: "hypermail-mcp",
|
|
18192
|
-
version: "0.7.
|
|
18719
|
+
version: "0.7.14",
|
|
18193
18720
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
18194
18721
|
type: "module",
|
|
18195
18722
|
bin: {
|
|
@@ -18269,6 +18796,7 @@ var ENV_HTTP_PORT = "HYPERMAIL_HTTP_PORT";
|
|
|
18269
18796
|
var ENV_HTTP_HOST = "HYPERMAIL_HTTP_HOST";
|
|
18270
18797
|
var ENV_TOOLS_DISABLED = "HYPERMAIL_TOOLS_DISABLED";
|
|
18271
18798
|
var ENV_TOOLS_ENABLED = "HYPERMAIL_TOOLS_ENABLED";
|
|
18799
|
+
var ENV_DEBUG = "HYPERMAIL_DEBUG";
|
|
18272
18800
|
var ENV_OUTLOOK_CLIENT_ID = "HYPERMAIL_OUTLOOK_CLIENT_ID";
|
|
18273
18801
|
var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_OUTLOOK_TENANT_ID";
|
|
18274
18802
|
var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_GMAIL_CLIENT_ID";
|
|
@@ -18309,6 +18837,15 @@ function parseStringArray(value) {
|
|
|
18309
18837
|
if (trimmed === "") return [];
|
|
18310
18838
|
return trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
18311
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
|
+
}
|
|
18312
18849
|
function validateToolNames(toolNames, envName) {
|
|
18313
18850
|
if (!toolNames || toolNames.length === 0) return;
|
|
18314
18851
|
const known = new Set(KNOWN_TOOLS);
|
|
@@ -18390,6 +18927,7 @@ function loadConfig(cliOverrides = {}) {
|
|
|
18390
18927
|
const tools = resolveToolsConfig();
|
|
18391
18928
|
const providers = resolveProvidersConfig();
|
|
18392
18929
|
const dataDir = cliOverrides.dataDir ?? optionalEnvString(ENV_DATA_DIR);
|
|
18930
|
+
const debugLogging = resolveDebugLogging(warnings);
|
|
18393
18931
|
if (!optionalEnvString(ENV_KEY)) {
|
|
18394
18932
|
warnings.push(
|
|
18395
18933
|
`${ENV_KEY} is not set; a local generated key will be used. Set ${ENV_KEY} explicitly for portable hosted deployments.`
|
|
@@ -18401,7 +18939,8 @@ function loadConfig(cliOverrides = {}) {
|
|
|
18401
18939
|
transport,
|
|
18402
18940
|
http,
|
|
18403
18941
|
tools,
|
|
18404
|
-
providers
|
|
18942
|
+
providers,
|
|
18943
|
+
debugLogging
|
|
18405
18944
|
},
|
|
18406
18945
|
warnings
|
|
18407
18946
|
};
|
|
@@ -18447,7 +18986,16 @@ function resolveTools(config) {
|
|
|
18447
18986
|
// src/server.ts
|
|
18448
18987
|
async function startServer(opts) {
|
|
18449
18988
|
const { config } = opts;
|
|
18450
|
-
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 });
|
|
18451
18999
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
18452
19000
|
const tools = resolveTools(config);
|
|
18453
19001
|
const createServer = () => {
|
|
@@ -18455,7 +19003,7 @@ async function startServer(opts) {
|
|
|
18455
19003
|
{ name: "hypermail-mcp", version: VERSION },
|
|
18456
19004
|
{ capabilities: { tools: {}, logging: {} } }
|
|
18457
19005
|
);
|
|
18458
|
-
registerTools(s, { store, registry, tools });
|
|
19006
|
+
registerTools(s, { store, registry, tools, logger });
|
|
18459
19007
|
return s;
|
|
18460
19008
|
};
|
|
18461
19009
|
if (config.transport === "http") {
|
|
@@ -18646,6 +19194,7 @@ Core environment variables:
|
|
|
18646
19194
|
HYPERMAIL_HTTP_HOST
|
|
18647
19195
|
HYPERMAIL_TOOLS_ENABLED
|
|
18648
19196
|
HYPERMAIL_TOOLS_DISABLED
|
|
19197
|
+
HYPERMAIL_DEBUG=1|true|yes|on|debug
|
|
18649
19198
|
|
|
18650
19199
|
Provider environment variables:
|
|
18651
19200
|
HYPERMAIL_OUTLOOK_CLIENT_ID
|