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 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 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";
@@ -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 = normalizeCheckpoint(account.newEmailCheckpoint);
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 = compareTimestamp(timestamp, checkpoint.receivedAt);
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 emails = [];
17506
+ const hydrated = [];
17310
17507
  for (const candidate of selected) {
17311
17508
  const full = await provider.readEmail(account, candidate.summary.id);
17312
- 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
+ });
17313
17514
  }
17314
- await advanceCheckpoint(store, account, selected);
17315
- 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);
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
- async function advanceCheckpoint(store, account, selected) {
17341
- const ordered = oldestCandidatesFirst(selected);
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 normalizeTimestamp(receivedAt) ?? MISSING_RECEIVED_AT;
17555
+ return normalizeTimestamp2(receivedAt) ?? MISSING_RECEIVED_AT;
17361
17556
  }
17362
- function normalizeTimestamp(value) {
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 = compareTimestamp(a.timestamp, b.timestamp);
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 = compareTimestamp(
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 compareTimestamp(a, b) {
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 moveToWellKnown(args, "deleteditems", "trashed");
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.12",
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: {