hypermail-mcp 0.7.9 → 0.7.11
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 -1
- package/dist/cli.js +189 -119
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.11** — Fixed Outlook reply/forward drafts whose Graph-generated
|
|
7
|
+
> quoted bodies are plain text by escaping them and patching the draft as HTML,
|
|
8
|
+
> so newly composed HTML replies no longer render as raw text.
|
|
9
|
+
>
|
|
10
|
+
> **v0.7.10** — Fixed an account-store race where Gmail/Outlook token
|
|
11
|
+
> refreshes could overwrite `get_new_emails` checkpoints. Checkpoint updates now
|
|
12
|
+
> preserve token data and merge delivered IDs at the same timestamp. `edit_draft`
|
|
13
|
+
> body edits now require exact selected-section replacement (`old_text` +
|
|
14
|
+
> `new_text`) so reply/forward history is preserved instead of overwritten.
|
|
15
|
+
>
|
|
6
16
|
> **v0.7.9** — Replaced server-side email watch/webhook/script delivery with
|
|
7
17
|
> the pull-based `get_new_emails` tool. Agents schedule their own repeated calls
|
|
8
18
|
> and fetch bounded batches of new inbox email.
|
|
@@ -245,7 +255,7 @@ account store.
|
|
|
245
255
|
| `move_email` | `account`, `id`, `destination` | Move to any folder by well-known name (`inbox`, `drafts`, etc.) or custom folder ID. |
|
|
246
256
|
| `send_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?`, `attachments?` | Send an email. `format` (`"html"` or `"markdown"`) controls body format — Markdown is converted to HTML via `marked`. Appends signature when `include_signature` is true. `inReplyTo` sends as threaded reply; `forwardMessageId` sends as forward. `inReplyTo` is required — set to `false` for new emails. `attachments` is an optional array of `{filePath, name?}` — files are read from disk and encoded automatically. |
|
|
247
257
|
| `draft_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?`, `attachments?` | Save as draft instead of sending. Same params as `send_email` including `attachments`. Returns the draft message ID and HTML body (`draftHtml`). `inReplyTo` is required — set to `false` for new emails. |
|
|
248
|
-
| `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `body?`, `format?`, `include_signature?`, `new_attachments?`, `remove_attachments?` | Edit an existing draft by ID.
|
|
258
|
+
| `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `old_text?`, `new_text?`, `body?`, `format?`, `include_signature?`, `new_attachments?`, `remove_attachments?` | Edit an existing draft by ID. Body edits require exact selected-section replacement: copy `old_text` from the current draft HTML (`draftHtml` or `read_email` with `format: "html"`) and provide `new_text`; the match must occur exactly once, and unselected content such as reply/forward history is preserved. Deprecated `body` is only an alias for `new_text` when `old_text` is also provided; body-only full replacement is rejected. `new_attachments` adds files (`{filePath, name?}[]`); `remove_attachments` removes by attachment ID (`string[]`). Returns the updated draft ID, HTML body (`draftHtml`), and attachment metadata. |
|
|
249
259
|
| `send_draft` | `account`, `id` | Send an existing draft email by ID. Use with draft IDs returned by `draft_email` or `edit_draft`. |
|
|
250
260
|
| `list_folders` | `account`, `parentFolderId?` | List available mail folders. Returns top-level folders by default, or children of `parentFolderId`. |
|
|
251
261
|
| `create_folder` | `account`, `displayName`, `parentFolderId?` | Create a new mail folder under root (default) or the given parent. |
|
package/dist/cli.js
CHANGED
|
@@ -13965,6 +13965,7 @@ var AccountStore = class _AccountStore {
|
|
|
13965
13965
|
filePath;
|
|
13966
13966
|
key;
|
|
13967
13967
|
data;
|
|
13968
|
+
writeLocks = /* @__PURE__ */ new Map();
|
|
13968
13969
|
static async open(opts = {}) {
|
|
13969
13970
|
const dataDir = resolveDataDir(opts.dataDir);
|
|
13970
13971
|
await fs2.mkdir(dataDir, { recursive: true, mode: 448 });
|
|
@@ -13992,21 +13993,79 @@ var AccountStore = class _AccountStore {
|
|
|
13992
13993
|
return rec ? { ...rec } : void 0;
|
|
13993
13994
|
}
|
|
13994
13995
|
async upsertAccount(rec) {
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
13996
|
+
return this.runSerial(rec.email, async () => {
|
|
13997
|
+
const norm = rec.email.trim().toLowerCase();
|
|
13998
|
+
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
|
+
});
|
|
14005
|
+
}
|
|
14006
|
+
async updateTokens(email, tokens) {
|
|
14007
|
+
return this.runSerial(email, async () => {
|
|
14008
|
+
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];
|
|
14012
|
+
const next = { ...current, tokens };
|
|
14013
|
+
this.data.accounts[idx] = next;
|
|
14014
|
+
await this.flush();
|
|
14015
|
+
return { ...next };
|
|
14016
|
+
});
|
|
14017
|
+
}
|
|
14018
|
+
async updateNewEmailCheckpoint(email, checkpoint) {
|
|
14019
|
+
return this.runSerial(email, async () => {
|
|
14020
|
+
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;
|
|
14034
|
+
const next = {
|
|
14035
|
+
...current,
|
|
14036
|
+
newEmailCheckpoint: mergedCheckpoint
|
|
14037
|
+
};
|
|
14038
|
+
this.data.accounts[idx] = next;
|
|
14039
|
+
await this.flush();
|
|
14040
|
+
return { ...next };
|
|
14041
|
+
});
|
|
14002
14042
|
}
|
|
14003
14043
|
async removeAccount(email) {
|
|
14044
|
+
return this.runSerial(email, async () => {
|
|
14045
|
+
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
|
+
});
|
|
14052
|
+
}
|
|
14053
|
+
async runSerial(email, task) {
|
|
14004
14054
|
const norm = email.trim().toLowerCase();
|
|
14005
|
-
const
|
|
14006
|
-
|
|
14007
|
-
|
|
14008
|
-
|
|
14009
|
-
|
|
14055
|
+
const previous = this.writeLocks.get(norm) ?? Promise.resolve();
|
|
14056
|
+
const run = previous.catch(() => void 0).then(task);
|
|
14057
|
+
const lock = run.then(
|
|
14058
|
+
() => void 0,
|
|
14059
|
+
() => void 0
|
|
14060
|
+
);
|
|
14061
|
+
this.writeLocks.set(norm, lock);
|
|
14062
|
+
try {
|
|
14063
|
+
return await run;
|
|
14064
|
+
} finally {
|
|
14065
|
+
if (this.writeLocks.get(norm) === lock) {
|
|
14066
|
+
this.writeLocks.delete(norm);
|
|
14067
|
+
}
|
|
14068
|
+
}
|
|
14010
14069
|
}
|
|
14011
14070
|
async flush() {
|
|
14012
14071
|
const buf = encrypt(this.data, this.key);
|
|
@@ -14203,10 +14262,10 @@ var OutlookClientFactory = class {
|
|
|
14203
14262
|
this.tenantId
|
|
14204
14263
|
);
|
|
14205
14264
|
if (nextTokens.msalCache !== tokens.msalCache) {
|
|
14206
|
-
store.
|
|
14207
|
-
|
|
14208
|
-
|
|
14209
|
-
|
|
14265
|
+
store.updateTokens(
|
|
14266
|
+
account.email,
|
|
14267
|
+
nextTokens
|
|
14268
|
+
).catch(() => {
|
|
14210
14269
|
});
|
|
14211
14270
|
}
|
|
14212
14271
|
return accessToken;
|
|
@@ -14300,16 +14359,22 @@ function clampLimit(v, dflt, max) {
|
|
|
14300
14359
|
|
|
14301
14360
|
// src/providers/outlook/write-ops.ts
|
|
14302
14361
|
var THREAD_MARKER = "<!-- hypermail-thread-boundary -->";
|
|
14362
|
+
function isTextBody(contentType) {
|
|
14363
|
+
return contentType?.toLowerCase() === "text";
|
|
14364
|
+
}
|
|
14365
|
+
function textToHtml(text) {
|
|
14366
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/\r\n|\r|\n/g, "<br>");
|
|
14367
|
+
}
|
|
14303
14368
|
async function buildDraftFromReference(client, createEndpoint, createPayload, converted) {
|
|
14304
14369
|
const draft = await client.api(createEndpoint).post(createPayload);
|
|
14305
14370
|
const draftMsg = await client.api(`/me/messages/${draft.id}`).select("body").get();
|
|
14306
|
-
const
|
|
14307
|
-
const
|
|
14371
|
+
const rawDraftBody = draftMsg.body?.content ?? "";
|
|
14372
|
+
const draftBody = isTextBody(draftMsg.body?.contentType) ? textToHtml(rawDraftBody) : rawDraftBody;
|
|
14308
14373
|
const spacer = '<div style="line-height:12px"><br></div>';
|
|
14309
14374
|
const prepend = converted.body + spacer + THREAD_MARKER;
|
|
14310
14375
|
const finalBody = draftBody.includes("<body") ? draftBody.replace(/(<body[^>]*>)/i, `$1${prepend}`) : prepend + draftBody;
|
|
14311
14376
|
await client.api(`/me/messages/${draft.id}`).patch({
|
|
14312
|
-
body: { contentType:
|
|
14377
|
+
body: { contentType: "HTML", content: finalBody }
|
|
14313
14378
|
});
|
|
14314
14379
|
for (const att of converted.attachments) {
|
|
14315
14380
|
await client.api(`/me/messages/${draft.id}/attachments`).post(att);
|
|
@@ -15826,10 +15891,10 @@ var GmailClientFactory = class {
|
|
|
15826
15891
|
expiryDate: updated.expiry_date ?? currentTokens.expiryDate,
|
|
15827
15892
|
scopes: updated.scope ? updated.scope.split(" ") : currentTokens.scopes
|
|
15828
15893
|
};
|
|
15829
|
-
await store.
|
|
15830
|
-
|
|
15831
|
-
|
|
15832
|
-
|
|
15894
|
+
await store.updateTokens(
|
|
15895
|
+
account.email,
|
|
15896
|
+
nextTokens
|
|
15897
|
+
).catch(() => {
|
|
15833
15898
|
});
|
|
15834
15899
|
});
|
|
15835
15900
|
persistLocks.set(key, chain);
|
|
@@ -16781,30 +16846,21 @@ function buildStyleAttr(style) {
|
|
|
16781
16846
|
if (style.fontColor) parts.push(`color: ${style.fontColor}`);
|
|
16782
16847
|
return parts.join("; ");
|
|
16783
16848
|
}
|
|
16784
|
-
function
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
const before = html.slice(0, markerIdx);
|
|
16788
|
-
let answerEnd = before.lastIndexOf(
|
|
16789
|
-
'<div style="line-height:12px"><br></div>'
|
|
16790
|
-
);
|
|
16791
|
-
if (answerEnd === -1) {
|
|
16792
|
-
const re = /<div\s+style=["'][^"']*line-height\s*:\s*12px[^"']*["']\s*>\s*<br\s*\/?>\s*<\/div>/gi;
|
|
16793
|
-
let m;
|
|
16794
|
-
while ((m = re.exec(before)) !== null) answerEnd = m.index;
|
|
16795
|
-
}
|
|
16796
|
-
return { threadStart: markerIdx, answerEnd: Math.max(answerEnd, 0) };
|
|
16849
|
+
function applyExactTextEdit(content, oldText, newText) {
|
|
16850
|
+
if (oldText.length === 0) {
|
|
16851
|
+
throw new Error("old_text must not be empty");
|
|
16797
16852
|
}
|
|
16798
|
-
const
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
return { threadStart: spacerMatch.index, answerEnd: spacerMatch.index };
|
|
16853
|
+
const first = content.indexOf(oldText);
|
|
16854
|
+
if (first === -1) {
|
|
16855
|
+
throw new Error("old_text was not found in the current draft body");
|
|
16802
16856
|
}
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
16857
|
+
const second = content.indexOf(oldText, first + oldText.length);
|
|
16858
|
+
if (second !== -1) {
|
|
16859
|
+
throw new Error(
|
|
16860
|
+
"old_text matched multiple sections in the current draft body; provide a more specific selection"
|
|
16861
|
+
);
|
|
16806
16862
|
}
|
|
16807
|
-
return
|
|
16863
|
+
return content.slice(0, first) + newText + content.slice(first + oldText.length);
|
|
16808
16864
|
}
|
|
16809
16865
|
function shouldRegister(name, tools) {
|
|
16810
16866
|
if (tools.enabledTools) {
|
|
@@ -17222,9 +17278,9 @@ async function initializeCheckpoint(store, provider, account) {
|
|
|
17222
17278
|
const first = items[0];
|
|
17223
17279
|
const receivedAt = first ? effectiveReceivedAt(first.receivedAt) : (/* @__PURE__ */ new Date()).toISOString();
|
|
17224
17280
|
const deliveredIdsAtReceivedAt = items.filter((item) => effectiveReceivedAt(item.receivedAt) === receivedAt).map((item) => item.id);
|
|
17225
|
-
await store.
|
|
17226
|
-
|
|
17227
|
-
|
|
17281
|
+
await store.updateNewEmailCheckpoint(account.email, {
|
|
17282
|
+
receivedAt,
|
|
17283
|
+
deliveredIdsAtReceivedAt
|
|
17228
17284
|
});
|
|
17229
17285
|
}
|
|
17230
17286
|
async function hydrateAndAdvance(store, provider, account, selected) {
|
|
@@ -17264,24 +17320,11 @@ async function advanceCheckpoint(store, account, selected) {
|
|
|
17264
17320
|
const ordered = oldestCandidatesFirst(selected);
|
|
17265
17321
|
const newest = ordered[ordered.length - 1];
|
|
17266
17322
|
if (!newest) return;
|
|
17267
|
-
const previous = normalizeCheckpoint(account.newEmailCheckpoint);
|
|
17268
17323
|
const newestTimestamp = newest.timestamp;
|
|
17269
17324
|
const idsAtNewest = ordered.filter((candidate) => candidate.timestamp === newestTimestamp).map((candidate) => candidate.summary.id);
|
|
17270
|
-
|
|
17271
|
-
|
|
17272
|
-
deliveredIdsAtReceivedAt
|
|
17273
|
-
.../* @__PURE__ */ new Set([
|
|
17274
|
-
...previous.deliveredIdsAtReceivedAt ?? [],
|
|
17275
|
-
...idsAtNewest
|
|
17276
|
-
])
|
|
17277
|
-
];
|
|
17278
|
-
}
|
|
17279
|
-
await store.upsertAccount({
|
|
17280
|
-
...account,
|
|
17281
|
-
newEmailCheckpoint: {
|
|
17282
|
-
receivedAt: newestTimestamp,
|
|
17283
|
-
deliveredIdsAtReceivedAt
|
|
17284
|
-
}
|
|
17325
|
+
await store.updateNewEmailCheckpoint(account.email, {
|
|
17326
|
+
receivedAt: newestTimestamp,
|
|
17327
|
+
deliveredIdsAtReceivedAt: idsAtNewest
|
|
17285
17328
|
});
|
|
17286
17329
|
}
|
|
17287
17330
|
function normalizeCheckpoint(checkpoint) {
|
|
@@ -17779,33 +17822,37 @@ function registerOrganizeTools(server, ctx) {
|
|
|
17779
17822
|
import { z as z8 } from "zod";
|
|
17780
17823
|
import { readFileSync } from "fs";
|
|
17781
17824
|
import { basename, extname } from "path";
|
|
17825
|
+
|
|
17826
|
+
// src/tools/mime-types.ts
|
|
17827
|
+
var MIME_TYPES = {
|
|
17828
|
+
".pdf": "application/pdf",
|
|
17829
|
+
".png": "image/png",
|
|
17830
|
+
".jpg": "image/jpeg",
|
|
17831
|
+
".jpeg": "image/jpeg",
|
|
17832
|
+
".gif": "image/gif",
|
|
17833
|
+
".svg": "image/svg+xml",
|
|
17834
|
+
".webp": "image/webp",
|
|
17835
|
+
".txt": "text/plain",
|
|
17836
|
+
".html": "text/html",
|
|
17837
|
+
".css": "text/css",
|
|
17838
|
+
".csv": "text/csv",
|
|
17839
|
+
".json": "application/json",
|
|
17840
|
+
".xml": "application/xml",
|
|
17841
|
+
".zip": "application/zip",
|
|
17842
|
+
".gz": "application/gzip",
|
|
17843
|
+
".tar": "application/x-tar",
|
|
17844
|
+
".doc": "application/msword",
|
|
17845
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
17846
|
+
".xls": "application/vnd.ms-excel",
|
|
17847
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
17848
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
17849
|
+
".mp3": "audio/mpeg",
|
|
17850
|
+
".mp4": "video/mp4"
|
|
17851
|
+
};
|
|
17852
|
+
|
|
17853
|
+
// src/tools/compose.ts
|
|
17782
17854
|
function registerComposeTools(server, ctx) {
|
|
17783
17855
|
const { store, registry, tools } = ctx;
|
|
17784
|
-
const MIME_TYPES = {
|
|
17785
|
-
".pdf": "application/pdf",
|
|
17786
|
-
".png": "image/png",
|
|
17787
|
-
".jpg": "image/jpeg",
|
|
17788
|
-
".jpeg": "image/jpeg",
|
|
17789
|
-
".gif": "image/gif",
|
|
17790
|
-
".svg": "image/svg+xml",
|
|
17791
|
-
".webp": "image/webp",
|
|
17792
|
-
".txt": "text/plain",
|
|
17793
|
-
".html": "text/html",
|
|
17794
|
-
".css": "text/css",
|
|
17795
|
-
".csv": "text/csv",
|
|
17796
|
-
".json": "application/json",
|
|
17797
|
-
".xml": "application/xml",
|
|
17798
|
-
".zip": "application/zip",
|
|
17799
|
-
".gz": "application/gzip",
|
|
17800
|
-
".tar": "application/x-tar",
|
|
17801
|
-
".doc": "application/msword",
|
|
17802
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
17803
|
-
".xls": "application/vnd.ms-excel",
|
|
17804
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
17805
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
17806
|
-
".mp3": "audio/mpeg",
|
|
17807
|
-
".mp4": "video/mp4"
|
|
17808
|
-
};
|
|
17809
17856
|
const sendEmailSchema = z8.object({
|
|
17810
17857
|
account: z8.string().email(),
|
|
17811
17858
|
to: z8.array(emailAddrSchema).min(1),
|
|
@@ -17941,12 +17988,20 @@ function registerComposeTools(server, ctx) {
|
|
|
17941
17988
|
cc: z8.array(emailAddrSchema).optional(),
|
|
17942
17989
|
bcc: z8.array(emailAddrSchema).optional(),
|
|
17943
17990
|
subject: z8.string().optional(),
|
|
17944
|
-
|
|
17991
|
+
old_text: z8.string().min(1).optional().describe(
|
|
17992
|
+
"Exact current HTML section to replace in the draft body. Copy this from `draftHtml` or from `read_email` with format='html'. Must match exactly once; unselected content is preserved."
|
|
17993
|
+
),
|
|
17994
|
+
new_text: z8.string().optional().describe(
|
|
17995
|
+
"Replacement content for `old_text`. The replacement is composed using `format` and `include_signature`, then inserted exactly where `old_text` matched."
|
|
17996
|
+
),
|
|
17997
|
+
body: z8.string().optional().describe(
|
|
17998
|
+
"Deprecated alias for `new_text`. Body-only full replacement is not supported; provide `old_text` with this field."
|
|
17999
|
+
),
|
|
17945
18000
|
format: z8.enum(["html", "markdown"]).optional().describe(
|
|
17946
|
-
"
|
|
18001
|
+
"Replacement format. Only meaningful when `new_text` or deprecated `body` is also provided. 'html' inserts the replacement as-is (must be valid HTML). 'markdown' converts the replacement from Markdown to HTML."
|
|
17947
18002
|
),
|
|
17948
18003
|
include_signature: z8.boolean().optional().describe(
|
|
17949
|
-
"Whether to
|
|
18004
|
+
"Whether to append the account's saved HTML signature to the replacement section. If true, don't include a signature in `new_text`/`body`. Only meaningful when replacement content is provided. Returns an error if true but no signature is configured for this account."
|
|
17950
18005
|
),
|
|
17951
18006
|
new_attachments: z8.array(
|
|
17952
18007
|
z8.object({
|
|
@@ -17971,7 +18026,7 @@ function registerComposeTools(server, ctx) {
|
|
|
17971
18026
|
server.registerTool(
|
|
17972
18027
|
"edit_draft",
|
|
17973
18028
|
{
|
|
17974
|
-
description: "Edit an existing draft email by ID. Only the fields you provide are updated \u2014 unmentioned fields stay unchanged.
|
|
18029
|
+
description: "Edit an existing draft email by ID. Only the fields you provide are updated \u2014 unmentioned fields stay unchanged. Body edits work like an exact text edit: provide `old_text` copied from the current draft HTML and `new_text` to replace that exact section. The match must occur exactly once, and all unselected content \u2014 including reply/forward history \u2014 is preserved. Deprecated `body` is accepted only as an alias for `new_text` when `old_text` is also provided. Returns the draft ID and the draft's updated HTML body content (`draftHtml`). Before sending, inspect `draftHtml` to verify the draft looks correct. Does not support changing `inReplyTo` or `forwardMessageId` \u2014 those are set at creation time via `draft_email`. Disabled in --read-only mode.",
|
|
17975
18030
|
inputSchema: editDraftSchema,
|
|
17976
18031
|
outputSchema: editDraftOutputSchema
|
|
17977
18032
|
},
|
|
@@ -17979,43 +18034,57 @@ function registerComposeTools(server, ctx) {
|
|
|
17979
18034
|
const a = args;
|
|
17980
18035
|
try {
|
|
17981
18036
|
const { provider, account } = registry.resolveByEmail(a.account);
|
|
17982
|
-
|
|
18037
|
+
const hasNewText = a.new_text !== void 0;
|
|
18038
|
+
const hasBodyAlias = a.body !== void 0;
|
|
18039
|
+
const hasOldText = a.old_text !== void 0;
|
|
18040
|
+
if (hasNewText && hasBodyAlias) {
|
|
18041
|
+
return fail("Provide only one of new_text or deprecated body, not both.");
|
|
18042
|
+
}
|
|
18043
|
+
if (hasBodyAlias && !hasOldText) {
|
|
18044
|
+
return fail(
|
|
18045
|
+
"Body-only full replacement is no longer supported. Provide old_text copied from the current draft HTML and use body as the replacement, or use new_text."
|
|
18046
|
+
);
|
|
18047
|
+
}
|
|
18048
|
+
if (hasNewText && !hasOldText) {
|
|
18049
|
+
return fail("new_text requires old_text so only the selected section is edited.");
|
|
18050
|
+
}
|
|
18051
|
+
if (hasOldText && !hasNewText && !hasBodyAlias) {
|
|
18052
|
+
return fail("old_text requires new_text with the replacement content.");
|
|
18053
|
+
}
|
|
18054
|
+
const replacementText = a.new_text ?? a.body;
|
|
18055
|
+
if (replacementText !== void 0 && a.include_signature && !account.signature) {
|
|
17983
18056
|
return fail(
|
|
17984
18057
|
"include_signature is true but no signature is configured for this account. Set up a signature first with set_account_settings."
|
|
17985
18058
|
);
|
|
17986
18059
|
}
|
|
17987
18060
|
let bodyPayload;
|
|
17988
18061
|
let isHtmlPayload;
|
|
17989
|
-
if (
|
|
18062
|
+
if (replacementText !== void 0) {
|
|
18063
|
+
const existing = await provider.readEmail(account, a.id);
|
|
18064
|
+
const existingBody = existing.bodyHtml ?? existing.bodyText ?? "";
|
|
17990
18065
|
const composed = composeBody({
|
|
17991
|
-
body:
|
|
18066
|
+
body: replacementText,
|
|
17992
18067
|
format: a.format ?? "html",
|
|
17993
18068
|
signature: account.signature,
|
|
17994
18069
|
style: account.style,
|
|
17995
18070
|
includeSignature: !!a.include_signature
|
|
17996
18071
|
});
|
|
17997
|
-
bodyPayload = composed.body;
|
|
18072
|
+
bodyPayload = applyExactTextEdit(existingBody, a.old_text ?? "", composed.body);
|
|
17998
18073
|
isHtmlPayload = composed.isHtml;
|
|
17999
18074
|
}
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18075
|
+
const hasDraftUpdate = a.to !== void 0 || a.cc !== void 0 || a.bcc !== void 0 || a.subject !== void 0 || bodyPayload !== void 0;
|
|
18076
|
+
let currentId = a.id;
|
|
18077
|
+
if (hasDraftUpdate) {
|
|
18078
|
+
const res = await provider.updateDraft(account, currentId, {
|
|
18079
|
+
to: a.to,
|
|
18080
|
+
cc: a.cc,
|
|
18081
|
+
bcc: a.bcc,
|
|
18082
|
+
subject: a.subject,
|
|
18083
|
+
body: bodyPayload,
|
|
18084
|
+
isHtml: isHtmlPayload
|
|
18085
|
+
});
|
|
18086
|
+
currentId = res.id;
|
|
18010
18087
|
}
|
|
18011
|
-
const res = await provider.updateDraft(account, a.id, {
|
|
18012
|
-
to: a.to,
|
|
18013
|
-
cc: a.cc,
|
|
18014
|
-
bcc: a.bcc,
|
|
18015
|
-
subject: a.subject,
|
|
18016
|
-
body: bodyPayload,
|
|
18017
|
-
isHtml: isHtmlPayload
|
|
18018
|
-
});
|
|
18019
18088
|
const newAttachmentIds = [];
|
|
18020
18089
|
if (a.new_attachments && a.new_attachments.length > 0) {
|
|
18021
18090
|
for (const att of a.new_attachments) {
|
|
@@ -18024,11 +18093,12 @@ function registerComposeTools(server, ctx) {
|
|
|
18024
18093
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
18025
18094
|
const attRes = await provider.addAttachmentToDraft(
|
|
18026
18095
|
account,
|
|
18027
|
-
|
|
18096
|
+
currentId,
|
|
18028
18097
|
att.name ?? basename(att.filePath),
|
|
18029
18098
|
fileData.toString("base64"),
|
|
18030
18099
|
contentType
|
|
18031
18100
|
);
|
|
18101
|
+
currentId = attRes.id;
|
|
18032
18102
|
newAttachmentIds.push(attRes.attachment.id);
|
|
18033
18103
|
}
|
|
18034
18104
|
}
|
|
@@ -18037,16 +18107,16 @@ function registerComposeTools(server, ctx) {
|
|
|
18037
18107
|
for (const attId of a.remove_attachments) {
|
|
18038
18108
|
await provider.removeAttachmentFromDraft(
|
|
18039
18109
|
account,
|
|
18040
|
-
|
|
18110
|
+
currentId,
|
|
18041
18111
|
attId
|
|
18042
18112
|
);
|
|
18043
18113
|
removedIds.push(attId);
|
|
18044
18114
|
}
|
|
18045
18115
|
}
|
|
18046
|
-
const draft = await provider.readEmail(account,
|
|
18116
|
+
const draft = await provider.readEmail(account, currentId);
|
|
18047
18117
|
const result = {
|
|
18048
18118
|
edited: true,
|
|
18049
|
-
id:
|
|
18119
|
+
id: currentId,
|
|
18050
18120
|
draftHtml: draft.bodyHtml ?? ""
|
|
18051
18121
|
};
|
|
18052
18122
|
return ok(result, result);
|
|
@@ -18098,7 +18168,7 @@ function registerTools(server, opts) {
|
|
|
18098
18168
|
// package.json
|
|
18099
18169
|
var package_default = {
|
|
18100
18170
|
name: "hypermail-mcp",
|
|
18101
|
-
version: "0.7.
|
|
18171
|
+
version: "0.7.11",
|
|
18102
18172
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
18103
18173
|
type: "module",
|
|
18104
18174
|
bin: {
|