hypermail-mcp 0.7.12 → 0.7.13
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 +6 -0
- package/dist/cli.js +267 -66
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
A **Model Context Protocol** server that lets an agent operate any of the user's
|
|
4
4
|
inboxes through a single, unified tool surface.
|
|
5
5
|
|
|
6
|
+
> **v0.7.13** — Hardened `get_new_emails` against duplicate delivery when
|
|
7
|
+
> multiple MCP processes share the same encrypted account store. Store writes
|
|
8
|
+
> now reload and merge under a cross-process lock, checkpoints are monotonic,
|
|
9
|
+
> and new-email batches are atomically claimed before being returned. `trash_email`
|
|
10
|
+
> also now uses provider-native trash operations for Gmail and IMAP trash aliases.
|
|
11
|
+
>
|
|
6
12
|
> **v0.7.12** — Hardened Outlook reply/forward draft formatting when
|
|
7
13
|
> Microsoft Graph labels generated thread history as HTML but returns
|
|
8
14
|
> plain/unstructured text. Such histories are now defensively normalized with
|
package/dist/cli.js
CHANGED
|
@@ -13956,16 +13956,21 @@ async function tryKeytarSet(key) {
|
|
|
13956
13956
|
|
|
13957
13957
|
// src/store/account-store.ts
|
|
13958
13958
|
var FILE_NAME = "accounts.json.enc";
|
|
13959
|
+
var LOCK_STALE_MS = 3e4;
|
|
13960
|
+
var LOCK_TIMEOUT_MS = 1e4;
|
|
13961
|
+
var LOCK_RETRY_MS = 25;
|
|
13959
13962
|
var AccountStore = class _AccountStore {
|
|
13960
13963
|
constructor(filePath, key, data) {
|
|
13961
13964
|
this.filePath = filePath;
|
|
13962
13965
|
this.key = key;
|
|
13963
13966
|
this.data = data;
|
|
13967
|
+
this.lockPath = `${filePath}.lock`;
|
|
13964
13968
|
}
|
|
13965
13969
|
filePath;
|
|
13966
13970
|
key;
|
|
13967
13971
|
data;
|
|
13968
13972
|
writeLocks = /* @__PURE__ */ new Map();
|
|
13973
|
+
lockPath;
|
|
13969
13974
|
static async open(opts = {}) {
|
|
13970
13975
|
const dataDir = resolveDataDir(opts.dataDir);
|
|
13971
13976
|
await fs2.mkdir(dataDir, { recursive: true, mode: 448 });
|
|
@@ -13993,62 +13998,93 @@ var AccountStore = class _AccountStore {
|
|
|
13993
13998
|
return rec ? { ...rec } : void 0;
|
|
13994
13999
|
}
|
|
13995
14000
|
async upsertAccount(rec) {
|
|
13996
|
-
return this.runSerial(rec.email, async () => {
|
|
14001
|
+
return this.runSerial(rec.email, async () => this.updateLocked((data) => {
|
|
13997
14002
|
const norm = rec.email.trim().toLowerCase();
|
|
14003
|
+
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
14004
|
+
const current = idx >= 0 ? data.accounts[idx] : void 0;
|
|
13998
14005
|
const next = { ...rec, email: norm };
|
|
13999
|
-
const
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
|
|
14006
|
+
const mergedCheckpoint = mergeNewEmailCheckpoints(
|
|
14007
|
+
current?.newEmailCheckpoint,
|
|
14008
|
+
rec.newEmailCheckpoint
|
|
14009
|
+
);
|
|
14010
|
+
if (mergedCheckpoint) next.newEmailCheckpoint = mergedCheckpoint;
|
|
14011
|
+
else delete next.newEmailCheckpoint;
|
|
14012
|
+
if (idx >= 0) data.accounts[idx] = next;
|
|
14013
|
+
else data.accounts.push(next);
|
|
14014
|
+
return { result: { ...next }, changed: true };
|
|
14015
|
+
}));
|
|
14005
14016
|
}
|
|
14006
14017
|
async updateTokens(email, tokens) {
|
|
14007
|
-
return this.runSerial(email, async () => {
|
|
14018
|
+
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14008
14019
|
const norm = email.trim().toLowerCase();
|
|
14009
|
-
const idx =
|
|
14010
|
-
if (idx < 0) return void 0;
|
|
14011
|
-
const current =
|
|
14020
|
+
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
14021
|
+
if (idx < 0) return { result: void 0, changed: false };
|
|
14022
|
+
const current = data.accounts[idx];
|
|
14012
14023
|
const next = { ...current, tokens };
|
|
14013
|
-
|
|
14014
|
-
|
|
14015
|
-
|
|
14016
|
-
});
|
|
14024
|
+
data.accounts[idx] = next;
|
|
14025
|
+
return { result: { ...next }, changed: true };
|
|
14026
|
+
}));
|
|
14017
14027
|
}
|
|
14018
14028
|
async updateNewEmailCheckpoint(email, checkpoint) {
|
|
14019
|
-
return this.runSerial(email, async () => {
|
|
14029
|
+
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14020
14030
|
const norm = email.trim().toLowerCase();
|
|
14021
|
-
const idx =
|
|
14022
|
-
if (idx < 0) return void 0;
|
|
14023
|
-
const current =
|
|
14024
|
-
const
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
...currentCheckpoint.deliveredIdsAtReceivedAt ?? [],
|
|
14030
|
-
...checkpoint.deliveredIdsAtReceivedAt ?? []
|
|
14031
|
-
])
|
|
14032
|
-
]
|
|
14033
|
-
} : checkpoint;
|
|
14031
|
+
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
14032
|
+
if (idx < 0) return { result: void 0, changed: false };
|
|
14033
|
+
const current = data.accounts[idx];
|
|
14034
|
+
const mergedCheckpoint = mergeNewEmailCheckpoints(
|
|
14035
|
+
current.newEmailCheckpoint,
|
|
14036
|
+
checkpoint
|
|
14037
|
+
);
|
|
14038
|
+
if (!mergedCheckpoint) return { result: { ...current }, changed: false };
|
|
14034
14039
|
const next = {
|
|
14035
14040
|
...current,
|
|
14036
14041
|
newEmailCheckpoint: mergedCheckpoint
|
|
14037
14042
|
};
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
14041
|
-
|
|
14043
|
+
data.accounts[idx] = next;
|
|
14044
|
+
return { result: { ...next }, changed: true };
|
|
14045
|
+
}));
|
|
14046
|
+
}
|
|
14047
|
+
async claimNewEmails(email, candidates) {
|
|
14048
|
+
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14049
|
+
const norm = email.trim().toLowerCase();
|
|
14050
|
+
const idx = data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
14051
|
+
if (idx < 0 || candidates.length === 0) return { result: [], changed: false };
|
|
14052
|
+
const account = data.accounts[idx];
|
|
14053
|
+
let checkpoint = normalizeCheckpoint(account.newEmailCheckpoint);
|
|
14054
|
+
const claimed = [];
|
|
14055
|
+
const ordered = [...candidates].sort((a, b) => {
|
|
14056
|
+
const byTime = compareTimestamp(normalizeTimestamp(a.receivedAt) ?? a.receivedAt, normalizeTimestamp(b.receivedAt) ?? b.receivedAt);
|
|
14057
|
+
if (byTime !== 0) return byTime;
|
|
14058
|
+
return a.summaryId.localeCompare(b.summaryId);
|
|
14059
|
+
});
|
|
14060
|
+
for (const candidate of ordered) {
|
|
14061
|
+
const receivedAt = normalizeTimestamp(candidate.receivedAt);
|
|
14062
|
+
if (!receivedAt) continue;
|
|
14063
|
+
const ids = uniqueIds([candidate.summaryId, ...candidate.ids]);
|
|
14064
|
+
if (ids.length === 0) continue;
|
|
14065
|
+
if (isAlreadyDelivered(checkpoint, receivedAt, ids)) continue;
|
|
14066
|
+
claimed.push(candidate.summaryId);
|
|
14067
|
+
checkpoint = mergeNewEmailCheckpoints(checkpoint, {
|
|
14068
|
+
receivedAt,
|
|
14069
|
+
deliveredIdsAtReceivedAt: ids
|
|
14070
|
+
});
|
|
14071
|
+
}
|
|
14072
|
+
if (claimed.length === 0 || !checkpoint) return { result: claimed, changed: false };
|
|
14073
|
+
const next = {
|
|
14074
|
+
...account,
|
|
14075
|
+
newEmailCheckpoint: checkpoint
|
|
14076
|
+
};
|
|
14077
|
+
data.accounts[idx] = next;
|
|
14078
|
+
return { result: claimed, changed: true };
|
|
14079
|
+
}));
|
|
14042
14080
|
}
|
|
14043
14081
|
async removeAccount(email) {
|
|
14044
|
-
return this.runSerial(email, async () => {
|
|
14082
|
+
return this.runSerial(email, async () => this.updateLocked((data) => {
|
|
14045
14083
|
const norm = email.trim().toLowerCase();
|
|
14046
|
-
const before =
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
return true;
|
|
14051
|
-
});
|
|
14084
|
+
const before = data.accounts.length;
|
|
14085
|
+
data.accounts = data.accounts.filter((a) => a.email.toLowerCase() !== norm);
|
|
14086
|
+
return { result: data.accounts.length !== before, changed: data.accounts.length !== before };
|
|
14087
|
+
}));
|
|
14052
14088
|
}
|
|
14053
14089
|
async runSerial(email, task) {
|
|
14054
14090
|
const norm = email.trim().toLowerCase();
|
|
@@ -14067,11 +14103,120 @@ var AccountStore = class _AccountStore {
|
|
|
14067
14103
|
}
|
|
14068
14104
|
}
|
|
14069
14105
|
}
|
|
14106
|
+
async updateLocked(task) {
|
|
14107
|
+
return this.withFileLock(async () => {
|
|
14108
|
+
this.data = await this.readLatest();
|
|
14109
|
+
const { result, changed } = await task(this.data);
|
|
14110
|
+
if (changed) await this.flush();
|
|
14111
|
+
return result;
|
|
14112
|
+
});
|
|
14113
|
+
}
|
|
14114
|
+
async readLatest() {
|
|
14115
|
+
try {
|
|
14116
|
+
const buf = await fs2.readFile(this.filePath);
|
|
14117
|
+
return decrypt(buf, this.key);
|
|
14118
|
+
} catch (err) {
|
|
14119
|
+
if (err.code === "ENOENT") {
|
|
14120
|
+
return { version: 1, accounts: [] };
|
|
14121
|
+
}
|
|
14122
|
+
throw err;
|
|
14123
|
+
}
|
|
14124
|
+
}
|
|
14070
14125
|
async flush() {
|
|
14071
14126
|
const buf = encrypt(this.data, this.key);
|
|
14072
14127
|
await writeAtomic(this.filePath, buf);
|
|
14073
14128
|
}
|
|
14129
|
+
async withFileLock(task) {
|
|
14130
|
+
await this.acquireFileLock();
|
|
14131
|
+
try {
|
|
14132
|
+
return await task();
|
|
14133
|
+
} finally {
|
|
14134
|
+
await fs2.rm(this.lockPath, { recursive: true, force: true });
|
|
14135
|
+
}
|
|
14136
|
+
}
|
|
14137
|
+
async acquireFileLock() {
|
|
14138
|
+
const startedAt = Date.now();
|
|
14139
|
+
while (true) {
|
|
14140
|
+
try {
|
|
14141
|
+
await fs2.mkdir(this.lockPath, { mode: 448 });
|
|
14142
|
+
await fs2.writeFile(
|
|
14143
|
+
path2.join(this.lockPath, "owner"),
|
|
14144
|
+
`${process.pid}
|
|
14145
|
+
${(/* @__PURE__ */ new Date()).toISOString()}
|
|
14146
|
+
`,
|
|
14147
|
+
{ mode: 384 }
|
|
14148
|
+
);
|
|
14149
|
+
return;
|
|
14150
|
+
} catch (err) {
|
|
14151
|
+
if (err.code !== "EEXIST") throw err;
|
|
14152
|
+
await this.removeStaleLock();
|
|
14153
|
+
if (Date.now() - startedAt > LOCK_TIMEOUT_MS) {
|
|
14154
|
+
throw new Error(`timed out waiting for account store lock: ${this.lockPath}`);
|
|
14155
|
+
}
|
|
14156
|
+
await delay(LOCK_RETRY_MS);
|
|
14157
|
+
}
|
|
14158
|
+
}
|
|
14159
|
+
}
|
|
14160
|
+
async removeStaleLock() {
|
|
14161
|
+
try {
|
|
14162
|
+
const stat = await fs2.stat(this.lockPath);
|
|
14163
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
14164
|
+
await fs2.rm(this.lockPath, { recursive: true, force: true });
|
|
14165
|
+
}
|
|
14166
|
+
} catch (err) {
|
|
14167
|
+
if (err.code !== "ENOENT") throw err;
|
|
14168
|
+
}
|
|
14169
|
+
}
|
|
14074
14170
|
};
|
|
14171
|
+
function normalizeCheckpoint(checkpoint) {
|
|
14172
|
+
const receivedAt = normalizeTimestamp(checkpoint?.receivedAt);
|
|
14173
|
+
if (!receivedAt) return void 0;
|
|
14174
|
+
return {
|
|
14175
|
+
receivedAt,
|
|
14176
|
+
deliveredIdsAtReceivedAt: uniqueIds(checkpoint?.deliveredIdsAtReceivedAt ?? [])
|
|
14177
|
+
};
|
|
14178
|
+
}
|
|
14179
|
+
function mergeNewEmailCheckpoints(current, incoming) {
|
|
14180
|
+
const normalizedCurrent = normalizeCheckpoint(current);
|
|
14181
|
+
const normalizedIncoming = normalizeCheckpoint(incoming);
|
|
14182
|
+
if (!normalizedIncoming) return normalizedCurrent;
|
|
14183
|
+
if (!normalizedCurrent) return normalizedIncoming;
|
|
14184
|
+
const comparison = compareTimestamp(normalizedIncoming.receivedAt, normalizedCurrent.receivedAt);
|
|
14185
|
+
if (comparison > 0) return normalizedIncoming;
|
|
14186
|
+
if (comparison < 0) return normalizedCurrent;
|
|
14187
|
+
return {
|
|
14188
|
+
receivedAt: normalizedCurrent.receivedAt,
|
|
14189
|
+
deliveredIdsAtReceivedAt: uniqueIds([
|
|
14190
|
+
...normalizedCurrent.deliveredIdsAtReceivedAt ?? [],
|
|
14191
|
+
...normalizedIncoming.deliveredIdsAtReceivedAt ?? []
|
|
14192
|
+
])
|
|
14193
|
+
};
|
|
14194
|
+
}
|
|
14195
|
+
function isAlreadyDelivered(checkpoint, receivedAt, ids) {
|
|
14196
|
+
if (!checkpoint) return false;
|
|
14197
|
+
const comparison = compareTimestamp(receivedAt, checkpoint.receivedAt);
|
|
14198
|
+
if (comparison < 0) return true;
|
|
14199
|
+
if (comparison > 0) return false;
|
|
14200
|
+
const delivered = new Set(checkpoint.deliveredIdsAtReceivedAt ?? []);
|
|
14201
|
+
return ids.some((id) => delivered.has(id));
|
|
14202
|
+
}
|
|
14203
|
+
function normalizeTimestamp(value) {
|
|
14204
|
+
if (!value) return void 0;
|
|
14205
|
+
const ms = Date.parse(value);
|
|
14206
|
+
if (!Number.isFinite(ms)) return void 0;
|
|
14207
|
+
return new Date(ms).toISOString();
|
|
14208
|
+
}
|
|
14209
|
+
function compareTimestamp(a, b) {
|
|
14210
|
+
if (a < b) return -1;
|
|
14211
|
+
if (a > b) return 1;
|
|
14212
|
+
return 0;
|
|
14213
|
+
}
|
|
14214
|
+
function uniqueIds(ids) {
|
|
14215
|
+
return [...new Set(ids.filter((id) => id.length > 0))];
|
|
14216
|
+
}
|
|
14217
|
+
async function delay(ms) {
|
|
14218
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
14219
|
+
}
|
|
14075
14220
|
|
|
14076
14221
|
// src/providers/outlook/index.ts
|
|
14077
14222
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -14744,6 +14889,9 @@ var OutlookProvider = class {
|
|
|
14744
14889
|
async moveEmail(account, id, destinationId) {
|
|
14745
14890
|
return moveEmail(this.clients.get(account), account, id, destinationId);
|
|
14746
14891
|
}
|
|
14892
|
+
async trashEmail(account, id) {
|
|
14893
|
+
return moveEmail(this.clients.get(account), account, id, "deleteditems");
|
|
14894
|
+
}
|
|
14747
14895
|
async sendDraft(account, id) {
|
|
14748
14896
|
return sendDraft(this.clients.get(account), account, id);
|
|
14749
14897
|
}
|
|
@@ -14910,6 +15058,21 @@ var WELL_KNOWN_TO_IMAP = {
|
|
|
14910
15058
|
function resolveFolder(wellKnownOrPath) {
|
|
14911
15059
|
return WELL_KNOWN_TO_IMAP[wellKnownOrPath.toLowerCase()] ?? wellKnownOrPath;
|
|
14912
15060
|
}
|
|
15061
|
+
function isTrashFolderAlias(wellKnownOrPath) {
|
|
15062
|
+
const lower = wellKnownOrPath.toLowerCase();
|
|
15063
|
+
return lower === "deleteditems" || lower === "trash";
|
|
15064
|
+
}
|
|
15065
|
+
function resolveTrashMailbox(mailboxes) {
|
|
15066
|
+
for (const mailbox of mailboxes) {
|
|
15067
|
+
const specialUse = mailbox.specialUse?.toLowerCase();
|
|
15068
|
+
if (specialUse === "\\trash") return mailbox.path;
|
|
15069
|
+
const flags = mailbox.flags ? Array.from(mailbox.flags) : [];
|
|
15070
|
+
if (flags.some((flag) => flag.toLowerCase() === "\\trash")) {
|
|
15071
|
+
return mailbox.path;
|
|
15072
|
+
}
|
|
15073
|
+
}
|
|
15074
|
+
return "Trash";
|
|
15075
|
+
}
|
|
14913
15076
|
function clampLimit2(v, dflt, max) {
|
|
14914
15077
|
if (!v || v <= 0) return dflt;
|
|
14915
15078
|
return Math.min(v, max);
|
|
@@ -15380,6 +15543,9 @@ async function updateDraft2(clients, account, id, update) {
|
|
|
15380
15543
|
});
|
|
15381
15544
|
}
|
|
15382
15545
|
async function moveEmail2(clients, account, id, destinationId) {
|
|
15546
|
+
if (isTrashFolderAlias(destinationId)) {
|
|
15547
|
+
return trashEmail(clients, account, id);
|
|
15548
|
+
}
|
|
15383
15549
|
const client = clients.get(account);
|
|
15384
15550
|
const { folder, uid } = decodeId(id);
|
|
15385
15551
|
const dest = resolveFolder(destinationId);
|
|
@@ -15387,6 +15553,17 @@ async function moveEmail2(clients, account, id, destinationId) {
|
|
|
15387
15553
|
await imap.messageMove(uid, dest, { uid: true });
|
|
15388
15554
|
});
|
|
15389
15555
|
}
|
|
15556
|
+
async function trashEmail(clients, account, id) {
|
|
15557
|
+
const client = clients.get(account);
|
|
15558
|
+
const { folder, uid } = decodeId(id);
|
|
15559
|
+
const imap = await client.getImap();
|
|
15560
|
+
const dest = resolveTrashMailbox(
|
|
15561
|
+
await imap.list()
|
|
15562
|
+
);
|
|
15563
|
+
return client.withMailbox(folder, async (lockedImap) => {
|
|
15564
|
+
await lockedImap.messageMove(uid, dest, { uid: true });
|
|
15565
|
+
});
|
|
15566
|
+
}
|
|
15390
15567
|
async function sendDraft2(clients, account, id) {
|
|
15391
15568
|
const client = clients.get(account);
|
|
15392
15569
|
const { folder, uid } = decodeId(id);
|
|
@@ -15599,6 +15776,9 @@ var ImapProvider = class {
|
|
|
15599
15776
|
async moveEmail(account, id, destinationId) {
|
|
15600
15777
|
return moveEmail2(this.clients, account, id, destinationId);
|
|
15601
15778
|
}
|
|
15779
|
+
async trashEmail(account, id) {
|
|
15780
|
+
return trashEmail(this.clients, account, id);
|
|
15781
|
+
}
|
|
15602
15782
|
async sendDraft(account, id) {
|
|
15603
15783
|
return sendDraft2(this.clients, account, id);
|
|
15604
15784
|
}
|
|
@@ -16355,7 +16535,14 @@ async function updateDraft3(clients, account, id, update) {
|
|
|
16355
16535
|
});
|
|
16356
16536
|
return { id: updated.data.message?.id ?? updated.data.id ?? id };
|
|
16357
16537
|
}
|
|
16538
|
+
function isTrashDestination(destinationId) {
|
|
16539
|
+
const lower = destinationId.toLowerCase();
|
|
16540
|
+
return lower === "deleteditems" || lower === "trash";
|
|
16541
|
+
}
|
|
16358
16542
|
async function moveEmail3(clients, account, id, destinationId) {
|
|
16543
|
+
if (isTrashDestination(destinationId)) {
|
|
16544
|
+
return trashEmail2(clients, account, id);
|
|
16545
|
+
}
|
|
16359
16546
|
const { gmail } = clients.get(account);
|
|
16360
16547
|
const { addLabelIds, removeLabelIds } = resolveLabelsForMove(destinationId);
|
|
16361
16548
|
await gmail.users.messages.modify({
|
|
@@ -16364,6 +16551,13 @@ async function moveEmail3(clients, account, id, destinationId) {
|
|
|
16364
16551
|
requestBody: { addLabelIds, removeLabelIds }
|
|
16365
16552
|
});
|
|
16366
16553
|
}
|
|
16554
|
+
async function trashEmail2(clients, account, id) {
|
|
16555
|
+
const { gmail } = clients.get(account);
|
|
16556
|
+
await gmail.users.messages.trash({
|
|
16557
|
+
userId: "me",
|
|
16558
|
+
id
|
|
16559
|
+
});
|
|
16560
|
+
}
|
|
16367
16561
|
async function sendDraft3(clients, account, id) {
|
|
16368
16562
|
const { gmail } = clients.get(account);
|
|
16369
16563
|
const res = await gmail.users.drafts.send({
|
|
@@ -16685,6 +16879,9 @@ var GmailProvider = class {
|
|
|
16685
16879
|
async moveEmail(account, id, destinationId) {
|
|
16686
16880
|
return moveEmail3(this.clients, account, id, destinationId);
|
|
16687
16881
|
}
|
|
16882
|
+
async trashEmail(account, id) {
|
|
16883
|
+
return trashEmail2(this.clients, account, id);
|
|
16884
|
+
}
|
|
16688
16885
|
async sendDraft(account, id) {
|
|
16689
16886
|
return sendDraft3(this.clients, account, id);
|
|
16690
16887
|
}
|
|
@@ -17257,7 +17454,7 @@ function registerNewEmailTool(server, ctx) {
|
|
|
17257
17454
|
);
|
|
17258
17455
|
}
|
|
17259
17456
|
async function collectCandidatesForAccount(store, provider, account) {
|
|
17260
|
-
const checkpoint =
|
|
17457
|
+
const checkpoint = normalizeCheckpoint2(account.newEmailCheckpoint);
|
|
17261
17458
|
if (!checkpoint) {
|
|
17262
17459
|
await initializeCheckpoint(store, provider, account);
|
|
17263
17460
|
return { account, candidates: [] };
|
|
@@ -17275,7 +17472,7 @@ async function collectCandidatesForAccount(store, provider, account) {
|
|
|
17275
17472
|
let sawOlderThanCheckpoint = false;
|
|
17276
17473
|
for (const item of items) {
|
|
17277
17474
|
const timestamp = effectiveReceivedAt(item.receivedAt);
|
|
17278
|
-
const comparison =
|
|
17475
|
+
const comparison = compareTimestamp2(timestamp, checkpoint.receivedAt);
|
|
17279
17476
|
if (comparison > 0) {
|
|
17280
17477
|
candidates.push({ account, summary: item, timestamp });
|
|
17281
17478
|
} else if (comparison === 0) {
|
|
@@ -17306,13 +17503,22 @@ async function initializeCheckpoint(store, provider, account) {
|
|
|
17306
17503
|
}
|
|
17307
17504
|
async function hydrateAndAdvance(store, provider, account, selected) {
|
|
17308
17505
|
if (selected.length === 0) return [];
|
|
17309
|
-
const
|
|
17506
|
+
const hydrated = [];
|
|
17310
17507
|
for (const candidate of selected) {
|
|
17311
17508
|
const full = await provider.readEmail(account, candidate.summary.id);
|
|
17312
|
-
|
|
17509
|
+
hydrated.push({
|
|
17510
|
+
candidate,
|
|
17511
|
+
email: formatNewEmail(account.email, full, candidate.summary),
|
|
17512
|
+
fullId: full.id
|
|
17513
|
+
});
|
|
17313
17514
|
}
|
|
17314
|
-
|
|
17315
|
-
|
|
17515
|
+
const claims = hydrated.map(({ candidate, fullId }) => ({
|
|
17516
|
+
summaryId: candidate.summary.id,
|
|
17517
|
+
receivedAt: candidate.timestamp,
|
|
17518
|
+
ids: [candidate.summary.id, fullId]
|
|
17519
|
+
}));
|
|
17520
|
+
const claimed = new Set(await store.claimNewEmails(account.email, claims));
|
|
17521
|
+
return hydrated.filter(({ candidate }) => claimed.has(candidate.summary.id)).map(({ email }) => email);
|
|
17316
17522
|
}
|
|
17317
17523
|
function formatNewEmail(account, msg, summary) {
|
|
17318
17524
|
const body = selectBody(msg, "markdown");
|
|
@@ -17337,19 +17543,8 @@ function formatNewEmail(account, msg, summary) {
|
|
|
17337
17543
|
bodyOriginalLength: body.length
|
|
17338
17544
|
};
|
|
17339
17545
|
}
|
|
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);
|
|
17546
|
+
function normalizeCheckpoint2(checkpoint) {
|
|
17547
|
+
const receivedAt = normalizeTimestamp2(checkpoint?.receivedAt);
|
|
17353
17548
|
if (!receivedAt) return null;
|
|
17354
17549
|
return {
|
|
17355
17550
|
receivedAt,
|
|
@@ -17357,9 +17552,9 @@ function normalizeCheckpoint(checkpoint) {
|
|
|
17357
17552
|
};
|
|
17358
17553
|
}
|
|
17359
17554
|
function effectiveReceivedAt(receivedAt) {
|
|
17360
|
-
return
|
|
17555
|
+
return normalizeTimestamp2(receivedAt) ?? MISSING_RECEIVED_AT;
|
|
17361
17556
|
}
|
|
17362
|
-
function
|
|
17557
|
+
function normalizeTimestamp2(value) {
|
|
17363
17558
|
if (!value) return null;
|
|
17364
17559
|
const ms = Date.parse(value);
|
|
17365
17560
|
if (!Number.isFinite(ms)) return null;
|
|
@@ -17367,20 +17562,20 @@ function normalizeTimestamp(value) {
|
|
|
17367
17562
|
}
|
|
17368
17563
|
function oldestCandidatesFirst(items) {
|
|
17369
17564
|
return [...items].sort((a, b) => {
|
|
17370
|
-
const byTimestamp =
|
|
17565
|
+
const byTimestamp = compareTimestamp2(a.timestamp, b.timestamp);
|
|
17371
17566
|
if (byTimestamp !== 0) return byTimestamp;
|
|
17372
17567
|
return a.summary.id.localeCompare(b.summary.id);
|
|
17373
17568
|
});
|
|
17374
17569
|
}
|
|
17375
17570
|
function compareNewEmailOutputOldestFirst(a, b) {
|
|
17376
|
-
const byTimestamp =
|
|
17571
|
+
const byTimestamp = compareTimestamp2(
|
|
17377
17572
|
effectiveReceivedAt(a.receivedAt),
|
|
17378
17573
|
effectiveReceivedAt(b.receivedAt)
|
|
17379
17574
|
);
|
|
17380
17575
|
if (byTimestamp !== 0) return byTimestamp;
|
|
17381
17576
|
return a.id.localeCompare(b.id);
|
|
17382
17577
|
}
|
|
17383
|
-
function
|
|
17578
|
+
function compareTimestamp2(a, b) {
|
|
17384
17579
|
if (a < b) return -1;
|
|
17385
17580
|
if (a > b) return 1;
|
|
17386
17581
|
return 0;
|
|
@@ -17713,6 +17908,12 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17713
17908
|
const data = { marked: true, id: args.id, isRead };
|
|
17714
17909
|
return ok(data, data);
|
|
17715
17910
|
}
|
|
17911
|
+
async function trashMessage(args) {
|
|
17912
|
+
const { provider, account } = registry.resolveByEmail(args.account);
|
|
17913
|
+
await provider.trashEmail(account, args.id);
|
|
17914
|
+
const data = { trashed: true, id: args.id };
|
|
17915
|
+
return ok(data, data);
|
|
17916
|
+
}
|
|
17716
17917
|
const archiveMoveSchema = z7.object({
|
|
17717
17918
|
account: z7.string().email(),
|
|
17718
17919
|
id: z7.string().min(1).describe("Message ID to move")
|
|
@@ -17752,7 +17953,7 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17752
17953
|
},
|
|
17753
17954
|
async (args) => {
|
|
17754
17955
|
try {
|
|
17755
|
-
return await
|
|
17956
|
+
return await trashMessage(args);
|
|
17756
17957
|
} catch (err) {
|
|
17757
17958
|
return fail(errMsg(err));
|
|
17758
17959
|
}
|
|
@@ -18189,7 +18390,7 @@ function registerTools(server, opts) {
|
|
|
18189
18390
|
// package.json
|
|
18190
18391
|
var package_default = {
|
|
18191
18392
|
name: "hypermail-mcp",
|
|
18192
|
-
version: "0.7.
|
|
18393
|
+
version: "0.7.13",
|
|
18193
18394
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
18194
18395
|
type: "module",
|
|
18195
18396
|
bin: {
|