hypermail-mcp 0.7.11 → 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 CHANGED
@@ -3,6 +3,17 @@
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
+ >
12
+ > **v0.7.12** — Hardened Outlook reply/forward draft formatting when
13
+ > Microsoft Graph labels generated thread history as HTML but returns
14
+ > plain/unstructured text. Such histories are now defensively normalized with
15
+ > escaped content and line breaks so quoted messages remain readable.
16
+ >
6
17
  > **v0.7.11** — Fixed Outlook reply/forward drafts whose Graph-generated
7
18
  > quoted bodies are plain text by escaping them and patching the draft as HTML,
8
19
  > so newly composed HTML replies no longer render as raw text.
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 idx = this.data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
14000
- if (idx >= 0) this.data.accounts[idx] = next;
14001
- else this.data.accounts.push(next);
14002
- await this.flush();
14003
- return { ...next };
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 = this.data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
14010
- if (idx < 0) return void 0;
14011
- const current = this.data.accounts[idx];
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
- this.data.accounts[idx] = next;
14014
- await this.flush();
14015
- return { ...next };
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 = this.data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
14022
- if (idx < 0) return void 0;
14023
- const current = this.data.accounts[idx];
14024
- const currentCheckpoint = current.newEmailCheckpoint;
14025
- const mergedCheckpoint = currentCheckpoint?.receivedAt === checkpoint.receivedAt ? {
14026
- receivedAt: checkpoint.receivedAt,
14027
- deliveredIdsAtReceivedAt: [
14028
- .../* @__PURE__ */ new Set([
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
- this.data.accounts[idx] = next;
14039
- await this.flush();
14040
- return { ...next };
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 = this.data.accounts.length;
14047
- this.data.accounts = this.data.accounts.filter((a) => a.email.toLowerCase() !== norm);
14048
- if (this.data.accounts.length === before) return false;
14049
- await this.flush();
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";
@@ -14365,11 +14510,32 @@ function isTextBody(contentType) {
14365
14510
  function textToHtml(text) {
14366
14511
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/\r\n|\r|\n/g, "<br>");
14367
14512
  }
14513
+ function hasMeaningfulHtmlTag(html) {
14514
+ return /<\/?(?:p|div|br|blockquote|table|thead|tbody|tfoot|tr|td|th|ul|ol|li|a|span|font|b|strong|em|i|u|img|hr|pre)\b/i.test(html);
14515
+ }
14516
+ function wrapQuotedHistory(content) {
14517
+ return `<blockquote style="margin:0 0 0 .8ex;border-left:1px solid #ccc;padding-left:1ex">${content}</blockquote>`;
14518
+ }
14519
+ function normalizeDraftBody(content, contentType) {
14520
+ if (isTextBody(contentType)) return textToHtml(content);
14521
+ if (contentType?.toLowerCase() !== "html") return content;
14522
+ const bodyMatch = /(<body\b[^>]*>)([\s\S]*?)(<\/body>)/i.exec(content);
14523
+ const inner = bodyMatch ? bodyMatch[2] ?? "" : content;
14524
+ if (inner.trim() === "" || hasMeaningfulHtmlTag(inner)) return content;
14525
+ const normalized = wrapQuotedHistory(textToHtml(inner));
14526
+ if (!bodyMatch) return normalized;
14527
+ const bodyOpen = bodyMatch[1] ?? "";
14528
+ const bodyClose = bodyMatch[3] ?? "";
14529
+ return content.slice(0, bodyMatch.index) + bodyOpen + normalized + bodyClose + content.slice(bodyMatch.index + bodyMatch[0].length);
14530
+ }
14368
14531
  async function buildDraftFromReference(client, createEndpoint, createPayload, converted) {
14369
14532
  const draft = await client.api(createEndpoint).post(createPayload);
14370
14533
  const draftMsg = await client.api(`/me/messages/${draft.id}`).select("body").get();
14371
14534
  const rawDraftBody = draftMsg.body?.content ?? "";
14372
- const draftBody = isTextBody(draftMsg.body?.contentType) ? textToHtml(rawDraftBody) : rawDraftBody;
14535
+ const draftBody = normalizeDraftBody(
14536
+ rawDraftBody,
14537
+ draftMsg.body?.contentType
14538
+ );
14373
14539
  const spacer = '<div style="line-height:12px"><br></div>';
14374
14540
  const prepend = converted.body + spacer + THREAD_MARKER;
14375
14541
  const finalBody = draftBody.includes("<body") ? draftBody.replace(/(<body[^>]*>)/i, `$1${prepend}`) : prepend + draftBody;
@@ -14723,6 +14889,9 @@ var OutlookProvider = class {
14723
14889
  async moveEmail(account, id, destinationId) {
14724
14890
  return moveEmail(this.clients.get(account), account, id, destinationId);
14725
14891
  }
14892
+ async trashEmail(account, id) {
14893
+ return moveEmail(this.clients.get(account), account, id, "deleteditems");
14894
+ }
14726
14895
  async sendDraft(account, id) {
14727
14896
  return sendDraft(this.clients.get(account), account, id);
14728
14897
  }
@@ -14889,6 +15058,21 @@ var WELL_KNOWN_TO_IMAP = {
14889
15058
  function resolveFolder(wellKnownOrPath) {
14890
15059
  return WELL_KNOWN_TO_IMAP[wellKnownOrPath.toLowerCase()] ?? wellKnownOrPath;
14891
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
+ }
14892
15076
  function clampLimit2(v, dflt, max) {
14893
15077
  if (!v || v <= 0) return dflt;
14894
15078
  return Math.min(v, max);
@@ -15359,6 +15543,9 @@ async function updateDraft2(clients, account, id, update) {
15359
15543
  });
15360
15544
  }
15361
15545
  async function moveEmail2(clients, account, id, destinationId) {
15546
+ if (isTrashFolderAlias(destinationId)) {
15547
+ return trashEmail(clients, account, id);
15548
+ }
15362
15549
  const client = clients.get(account);
15363
15550
  const { folder, uid } = decodeId(id);
15364
15551
  const dest = resolveFolder(destinationId);
@@ -15366,6 +15553,17 @@ async function moveEmail2(clients, account, id, destinationId) {
15366
15553
  await imap.messageMove(uid, dest, { uid: true });
15367
15554
  });
15368
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
+ }
15369
15567
  async function sendDraft2(clients, account, id) {
15370
15568
  const client = clients.get(account);
15371
15569
  const { folder, uid } = decodeId(id);
@@ -15578,6 +15776,9 @@ var ImapProvider = class {
15578
15776
  async moveEmail(account, id, destinationId) {
15579
15777
  return moveEmail2(this.clients, account, id, destinationId);
15580
15778
  }
15779
+ async trashEmail(account, id) {
15780
+ return trashEmail(this.clients, account, id);
15781
+ }
15581
15782
  async sendDraft(account, id) {
15582
15783
  return sendDraft2(this.clients, account, id);
15583
15784
  }
@@ -16334,7 +16535,14 @@ async function updateDraft3(clients, account, id, update) {
16334
16535
  });
16335
16536
  return { id: updated.data.message?.id ?? updated.data.id ?? id };
16336
16537
  }
16538
+ function isTrashDestination(destinationId) {
16539
+ const lower = destinationId.toLowerCase();
16540
+ return lower === "deleteditems" || lower === "trash";
16541
+ }
16337
16542
  async function moveEmail3(clients, account, id, destinationId) {
16543
+ if (isTrashDestination(destinationId)) {
16544
+ return trashEmail2(clients, account, id);
16545
+ }
16338
16546
  const { gmail } = clients.get(account);
16339
16547
  const { addLabelIds, removeLabelIds } = resolveLabelsForMove(destinationId);
16340
16548
  await gmail.users.messages.modify({
@@ -16343,6 +16551,13 @@ async function moveEmail3(clients, account, id, destinationId) {
16343
16551
  requestBody: { addLabelIds, removeLabelIds }
16344
16552
  });
16345
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
+ }
16346
16561
  async function sendDraft3(clients, account, id) {
16347
16562
  const { gmail } = clients.get(account);
16348
16563
  const res = await gmail.users.drafts.send({
@@ -16664,6 +16879,9 @@ var GmailProvider = class {
16664
16879
  async moveEmail(account, id, destinationId) {
16665
16880
  return moveEmail3(this.clients, account, id, destinationId);
16666
16881
  }
16882
+ async trashEmail(account, id) {
16883
+ return trashEmail2(this.clients, account, id);
16884
+ }
16667
16885
  async sendDraft(account, id) {
16668
16886
  return sendDraft3(this.clients, account, id);
16669
16887
  }
@@ -17236,7 +17454,7 @@ function registerNewEmailTool(server, ctx) {
17236
17454
  );
17237
17455
  }
17238
17456
  async function collectCandidatesForAccount(store, provider, account) {
17239
- const checkpoint = normalizeCheckpoint(account.newEmailCheckpoint);
17457
+ const checkpoint = normalizeCheckpoint2(account.newEmailCheckpoint);
17240
17458
  if (!checkpoint) {
17241
17459
  await initializeCheckpoint(store, provider, account);
17242
17460
  return { account, candidates: [] };
@@ -17254,7 +17472,7 @@ async function collectCandidatesForAccount(store, provider, account) {
17254
17472
  let sawOlderThanCheckpoint = false;
17255
17473
  for (const item of items) {
17256
17474
  const timestamp = effectiveReceivedAt(item.receivedAt);
17257
- const comparison = compareTimestamp(timestamp, checkpoint.receivedAt);
17475
+ const comparison = compareTimestamp2(timestamp, checkpoint.receivedAt);
17258
17476
  if (comparison > 0) {
17259
17477
  candidates.push({ account, summary: item, timestamp });
17260
17478
  } else if (comparison === 0) {
@@ -17285,13 +17503,22 @@ async function initializeCheckpoint(store, provider, account) {
17285
17503
  }
17286
17504
  async function hydrateAndAdvance(store, provider, account, selected) {
17287
17505
  if (selected.length === 0) return [];
17288
- const emails = [];
17506
+ const hydrated = [];
17289
17507
  for (const candidate of selected) {
17290
17508
  const full = await provider.readEmail(account, candidate.summary.id);
17291
- emails.push(formatNewEmail(account.email, full, candidate.summary));
17509
+ hydrated.push({
17510
+ candidate,
17511
+ email: formatNewEmail(account.email, full, candidate.summary),
17512
+ fullId: full.id
17513
+ });
17292
17514
  }
17293
- await advanceCheckpoint(store, account, selected);
17294
- return emails;
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);
17295
17522
  }
17296
17523
  function formatNewEmail(account, msg, summary) {
17297
17524
  const body = selectBody(msg, "markdown");
@@ -17316,19 +17543,8 @@ function formatNewEmail(account, msg, summary) {
17316
17543
  bodyOriginalLength: body.length
17317
17544
  };
17318
17545
  }
17319
- async function advanceCheckpoint(store, account, selected) {
17320
- const ordered = oldestCandidatesFirst(selected);
17321
- const newest = ordered[ordered.length - 1];
17322
- if (!newest) return;
17323
- const newestTimestamp = newest.timestamp;
17324
- const idsAtNewest = ordered.filter((candidate) => candidate.timestamp === newestTimestamp).map((candidate) => candidate.summary.id);
17325
- await store.updateNewEmailCheckpoint(account.email, {
17326
- receivedAt: newestTimestamp,
17327
- deliveredIdsAtReceivedAt: idsAtNewest
17328
- });
17329
- }
17330
- function normalizeCheckpoint(checkpoint) {
17331
- const receivedAt = normalizeTimestamp(checkpoint?.receivedAt);
17546
+ function normalizeCheckpoint2(checkpoint) {
17547
+ const receivedAt = normalizeTimestamp2(checkpoint?.receivedAt);
17332
17548
  if (!receivedAt) return null;
17333
17549
  return {
17334
17550
  receivedAt,
@@ -17336,9 +17552,9 @@ function normalizeCheckpoint(checkpoint) {
17336
17552
  };
17337
17553
  }
17338
17554
  function effectiveReceivedAt(receivedAt) {
17339
- return normalizeTimestamp(receivedAt) ?? MISSING_RECEIVED_AT;
17555
+ return normalizeTimestamp2(receivedAt) ?? MISSING_RECEIVED_AT;
17340
17556
  }
17341
- function normalizeTimestamp(value) {
17557
+ function normalizeTimestamp2(value) {
17342
17558
  if (!value) return null;
17343
17559
  const ms = Date.parse(value);
17344
17560
  if (!Number.isFinite(ms)) return null;
@@ -17346,20 +17562,20 @@ function normalizeTimestamp(value) {
17346
17562
  }
17347
17563
  function oldestCandidatesFirst(items) {
17348
17564
  return [...items].sort((a, b) => {
17349
- const byTimestamp = compareTimestamp(a.timestamp, b.timestamp);
17565
+ const byTimestamp = compareTimestamp2(a.timestamp, b.timestamp);
17350
17566
  if (byTimestamp !== 0) return byTimestamp;
17351
17567
  return a.summary.id.localeCompare(b.summary.id);
17352
17568
  });
17353
17569
  }
17354
17570
  function compareNewEmailOutputOldestFirst(a, b) {
17355
- const byTimestamp = compareTimestamp(
17571
+ const byTimestamp = compareTimestamp2(
17356
17572
  effectiveReceivedAt(a.receivedAt),
17357
17573
  effectiveReceivedAt(b.receivedAt)
17358
17574
  );
17359
17575
  if (byTimestamp !== 0) return byTimestamp;
17360
17576
  return a.id.localeCompare(b.id);
17361
17577
  }
17362
- function compareTimestamp(a, b) {
17578
+ function compareTimestamp2(a, b) {
17363
17579
  if (a < b) return -1;
17364
17580
  if (a > b) return 1;
17365
17581
  return 0;
@@ -17692,6 +17908,12 @@ function registerOrganizeTools(server, ctx) {
17692
17908
  const data = { marked: true, id: args.id, isRead };
17693
17909
  return ok(data, data);
17694
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
+ }
17695
17917
  const archiveMoveSchema = z7.object({
17696
17918
  account: z7.string().email(),
17697
17919
  id: z7.string().min(1).describe("Message ID to move")
@@ -17731,7 +17953,7 @@ function registerOrganizeTools(server, ctx) {
17731
17953
  },
17732
17954
  async (args) => {
17733
17955
  try {
17734
- return await moveToWellKnown(args, "deleteditems", "trashed");
17956
+ return await trashMessage(args);
17735
17957
  } catch (err) {
17736
17958
  return fail(errMsg(err));
17737
17959
  }
@@ -18168,7 +18390,7 @@ function registerTools(server, opts) {
18168
18390
  // package.json
18169
18391
  var package_default = {
18170
18392
  name: "hypermail-mcp",
18171
- version: "0.7.11",
18393
+ version: "0.7.13",
18172
18394
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
18173
18395
  type: "module",
18174
18396
  bin: {