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 +11 -0
- package/dist/cli.js +289 -67
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
|
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";
|
|
@@ -14365,11 +14510,32 @@ function isTextBody(contentType) {
|
|
|
14365
14510
|
function textToHtml(text) {
|
|
14366
14511
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
17506
|
+
const hydrated = [];
|
|
17289
17507
|
for (const candidate of selected) {
|
|
17290
17508
|
const full = await provider.readEmail(account, candidate.summary.id);
|
|
17291
|
-
|
|
17509
|
+
hydrated.push({
|
|
17510
|
+
candidate,
|
|
17511
|
+
email: formatNewEmail(account.email, full, candidate.summary),
|
|
17512
|
+
fullId: full.id
|
|
17513
|
+
});
|
|
17292
17514
|
}
|
|
17293
|
-
|
|
17294
|
-
|
|
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
|
-
|
|
17320
|
-
const
|
|
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
|
|
17555
|
+
return normalizeTimestamp2(receivedAt) ?? MISSING_RECEIVED_AT;
|
|
17340
17556
|
}
|
|
17341
|
-
function
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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: {
|