hypermail-mcp 0.7.15 → 0.7.16
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 +9 -2
- package/dist/cli.js +94 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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.16** — Hardened `draft_email` after successful draft creation:
|
|
7
|
+
> if post-save readback fails, the tool now returns the created draft ID with
|
|
8
|
+
> `warning` and `draftReadbackError` instead of losing the draft behind an
|
|
9
|
+
> error. Compose operations also emit sanitized `HYPERMAIL_DEBUG` stage logs,
|
|
10
|
+
> IMAP drafts now use the server-advertised `\\Drafts` mailbox when present,
|
|
11
|
+
> and the CLI supports `--version`.
|
|
12
|
+
>
|
|
6
13
|
> **v0.7.15** — Hardened IMAP draft creation: drafts now append directly to
|
|
7
14
|
> the Drafts mailbox, retry without the `\\Draft` flag when an IMAP server
|
|
8
15
|
> rejects flagged APPEND, and preserve IMAP reply/forward context in drafts the
|
|
@@ -205,7 +212,7 @@ Hypermail uses flat `HYPERMAIL_*` environment variables as the source of truth.
|
|
|
205
212
|
There is no runtime config file. CLI flags only override transport, host, port,
|
|
206
213
|
and data directory for a single invocation.
|
|
207
214
|
|
|
208
|
-
CLI flags: `--http`, `--port`, `--host`, `--data-dir`, `--help`.
|
|
215
|
+
CLI flags: `--http`, `--port`, `--host`, `--data-dir`, `--version`, `--help`.
|
|
209
216
|
|
|
210
217
|
Subcommands: `hypermail-mcp generate-key` — generate a base64 32-byte key for
|
|
211
218
|
`HYPERMAIL_KEY`.
|
|
@@ -284,7 +291,7 @@ account store.
|
|
|
284
291
|
| `trash_email` | `account`, `id` | Move a message to Deleted Items (trash). |
|
|
285
292
|
| `move_email` | `account`, `id`, `destination` | Move to any folder by well-known name (`inbox`, `drafts`, etc.) or custom folder ID. |
|
|
286
293
|
| `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`; multiline plain text with `format: "html"` is rejected, so use `"markdown"` for paragraphs or add tags like `<p>`/`<br>`. 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. |
|
|
287
|
-
| `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. |
|
|
294
|
+
| `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, when readback succeeds, HTML body (`draftHtml`). If the draft is created but readback fails, returns the draft ID with `warning` and `draftReadbackError` so callers can retry `read_email` or continue with `send_draft`. `inReplyTo` is required — set to `false` for new emails. |
|
|
288
295
|
| `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. Multiline plain-text replacements with `format: "html"` are rejected; use `"markdown"` for paragraphs or add tags like `<p>`/`<br>`. Body edits are re-read after saving; if the updated body is not observable after retries, the tool returns an error instead of reporting success. `new_attachments` adds files (`{filePath, name?}[]`); `remove_attachments` removes by attachment ID (`string[]`). Returns the updated draft ID, HTML body (`draftHtml`), and attachment metadata. |
|
|
289
296
|
| `send_draft` | `account`, `id` | Send an existing draft email by ID. Use with draft IDs returned by `draft_email` or `edit_draft`. |
|
|
290
297
|
| `list_folders` | `account`, `parentFolderId?` | List available mail folders. Returns top-level folders by default, or children of `parentFolderId`. |
|
package/dist/cli.js
CHANGED
|
@@ -15264,6 +15264,17 @@ function resolveTrashMailbox(mailboxes) {
|
|
|
15264
15264
|
}
|
|
15265
15265
|
return "Trash";
|
|
15266
15266
|
}
|
|
15267
|
+
function resolveDraftMailbox(mailboxes) {
|
|
15268
|
+
for (const mailbox of mailboxes) {
|
|
15269
|
+
const specialUse = mailbox.specialUse?.toLowerCase();
|
|
15270
|
+
if (specialUse === "\\drafts") return mailbox.path;
|
|
15271
|
+
const flags = mailbox.flags ? Array.from(mailbox.flags) : [];
|
|
15272
|
+
if (flags.some((flag) => flag.toLowerCase() === "\\drafts")) {
|
|
15273
|
+
return mailbox.path;
|
|
15274
|
+
}
|
|
15275
|
+
}
|
|
15276
|
+
return "Drafts";
|
|
15277
|
+
}
|
|
15267
15278
|
function clampLimit2(v, dflt, max) {
|
|
15268
15279
|
if (!v || v <= 0) return dflt;
|
|
15269
15280
|
return Math.min(v, max);
|
|
@@ -15643,10 +15654,13 @@ async function sendEmail(clients, account, msg) {
|
|
|
15643
15654
|
}
|
|
15644
15655
|
async function saveDraft(clients, account, msg) {
|
|
15645
15656
|
const client = clients.get(account);
|
|
15646
|
-
const folder = "Drafts";
|
|
15647
15657
|
const rawMsg = await buildRawMessage(client, account, msg);
|
|
15658
|
+
let folder = "Drafts";
|
|
15648
15659
|
try {
|
|
15649
|
-
const result = await client.run((imap) =>
|
|
15660
|
+
const result = await client.run(async (imap) => {
|
|
15661
|
+
folder = resolveDraftMailbox(await imap.list());
|
|
15662
|
+
return appendDraft(imap, folder, rawMsg);
|
|
15663
|
+
});
|
|
15650
15664
|
return { id: encodeId(folder, appendUid(result, folder)) };
|
|
15651
15665
|
} catch (err) {
|
|
15652
15666
|
throw imapOperationError(`failed to save IMAP draft to ${folder}`, err);
|
|
@@ -18522,10 +18536,21 @@ var editDraftSchema = z8.object({
|
|
|
18522
18536
|
|
|
18523
18537
|
// src/tools/compose.ts
|
|
18524
18538
|
function registerComposeTools(server, ctx) {
|
|
18525
|
-
const { store, registry, tools } = ctx;
|
|
18539
|
+
const { store, registry, tools, logger } = ctx;
|
|
18526
18540
|
async function handleSendOrDraft(args, action, resultKey, toolName) {
|
|
18527
18541
|
try {
|
|
18528
18542
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
18543
|
+
logger?.debug("compose", "start", {
|
|
18544
|
+
tool: toolName,
|
|
18545
|
+
account: account.email,
|
|
18546
|
+
provider: provider.id,
|
|
18547
|
+
toCount: args.to.length,
|
|
18548
|
+
ccCount: args.cc?.length ?? 0,
|
|
18549
|
+
bccCount: args.bcc?.length ?? 0,
|
|
18550
|
+
hasReply: !!args.inReplyTo,
|
|
18551
|
+
hasForward: !!args.forwardMessageId,
|
|
18552
|
+
attachmentCount: args.attachments?.length ?? 0
|
|
18553
|
+
});
|
|
18529
18554
|
if (args.include_signature && !account.signature) {
|
|
18530
18555
|
return fail(
|
|
18531
18556
|
"include_signature is true but no signature is configured for this account. Set up a signature first with set_account_settings."
|
|
@@ -18538,6 +18563,14 @@ function registerComposeTools(server, ctx) {
|
|
|
18538
18563
|
style: account.style,
|
|
18539
18564
|
includeSignature: args.include_signature
|
|
18540
18565
|
});
|
|
18566
|
+
logger?.debug("compose", "composed", {
|
|
18567
|
+
tool: toolName,
|
|
18568
|
+
account: account.email,
|
|
18569
|
+
provider: provider.id,
|
|
18570
|
+
format: args.format,
|
|
18571
|
+
isHtml: composed.isHtml,
|
|
18572
|
+
includeSignature: args.include_signature
|
|
18573
|
+
});
|
|
18541
18574
|
if (args.inReplyTo && args.forwardMessageId) {
|
|
18542
18575
|
return fail(
|
|
18543
18576
|
"inReplyTo and forwardMessageId are mutually exclusive \u2014 use one or the other"
|
|
@@ -18555,6 +18588,12 @@ function registerComposeTools(server, ctx) {
|
|
|
18555
18588
|
};
|
|
18556
18589
|
});
|
|
18557
18590
|
}
|
|
18591
|
+
logger?.debug("compose", "attachmentsProcessed", {
|
|
18592
|
+
tool: toolName,
|
|
18593
|
+
account: account.email,
|
|
18594
|
+
provider: provider.id,
|
|
18595
|
+
attachmentCount: processedAttachments?.length ?? 0
|
|
18596
|
+
});
|
|
18558
18597
|
const res = await action(provider, account, {
|
|
18559
18598
|
to: args.to,
|
|
18560
18599
|
cc: args.cc,
|
|
@@ -18567,13 +18606,42 @@ function registerComposeTools(server, ctx) {
|
|
|
18567
18606
|
forwardMessageId: args.forwardMessageId,
|
|
18568
18607
|
attachments: processedAttachments
|
|
18569
18608
|
});
|
|
18609
|
+
logger?.debug("compose", "providerActionSuccess", {
|
|
18610
|
+
tool: toolName,
|
|
18611
|
+
account: account.email,
|
|
18612
|
+
provider: provider.id,
|
|
18613
|
+
hasId: !!res.id
|
|
18614
|
+
});
|
|
18570
18615
|
const result = { [resultKey]: true, ...res };
|
|
18571
18616
|
if (toolName === "draft_email" && res.id) {
|
|
18572
|
-
|
|
18573
|
-
|
|
18617
|
+
try {
|
|
18618
|
+
const draft = await provider.readEmail(account, res.id);
|
|
18619
|
+
result.draftHtml = draft.bodyHtml;
|
|
18620
|
+
logger?.debug("compose", "draftReadbackSuccess", {
|
|
18621
|
+
tool: toolName,
|
|
18622
|
+
account: account.email,
|
|
18623
|
+
provider: provider.id,
|
|
18624
|
+
hasDraftHtml: draft.bodyHtml !== void 0
|
|
18625
|
+
});
|
|
18626
|
+
} catch (readErr) {
|
|
18627
|
+
const message = errMsg(readErr);
|
|
18628
|
+
result.warning = "Draft was created, but reading it back for draftHtml failed. Use read_email with the returned id to inspect it, or continue with send_draft if appropriate.";
|
|
18629
|
+
result.draftReadbackError = message;
|
|
18630
|
+
logger?.debug("compose", "draftReadbackError", {
|
|
18631
|
+
tool: toolName,
|
|
18632
|
+
account: account.email,
|
|
18633
|
+
provider: provider.id,
|
|
18634
|
+
message
|
|
18635
|
+
});
|
|
18636
|
+
}
|
|
18574
18637
|
}
|
|
18575
18638
|
return ok(result, result);
|
|
18576
18639
|
} catch (err) {
|
|
18640
|
+
logger?.debug("compose", "error", {
|
|
18641
|
+
tool: toolName,
|
|
18642
|
+
account: args.account,
|
|
18643
|
+
message: errMsg(err)
|
|
18644
|
+
});
|
|
18577
18645
|
return fail(errMsg(err));
|
|
18578
18646
|
}
|
|
18579
18647
|
}
|
|
@@ -18600,7 +18668,9 @@ function registerComposeTools(server, ctx) {
|
|
|
18600
18668
|
const draftEmailOutputSchema = {
|
|
18601
18669
|
draft: z9.literal(true),
|
|
18602
18670
|
id: z9.string(),
|
|
18603
|
-
draftHtml: z9.string().optional()
|
|
18671
|
+
draftHtml: z9.string().optional(),
|
|
18672
|
+
warning: z9.string().optional(),
|
|
18673
|
+
draftReadbackError: z9.string().optional()
|
|
18604
18674
|
};
|
|
18605
18675
|
if (shouldRegister("draft_email", tools)) {
|
|
18606
18676
|
server.registerTool(
|
|
@@ -18797,13 +18867,13 @@ function registerTools(server, opts) {
|
|
|
18797
18867
|
registerBrowseTools(server, { store, registry, tools, logger });
|
|
18798
18868
|
registerFolderTools(server, { registry, tools });
|
|
18799
18869
|
registerOrganizeTools(server, { registry, tools });
|
|
18800
|
-
registerComposeTools(server, { store, registry, tools });
|
|
18870
|
+
registerComposeTools(server, { store, registry, tools, logger });
|
|
18801
18871
|
}
|
|
18802
18872
|
|
|
18803
18873
|
// package.json
|
|
18804
18874
|
var package_default = {
|
|
18805
18875
|
name: "hypermail-mcp",
|
|
18806
|
-
version: "0.7.
|
|
18876
|
+
version: "0.7.16",
|
|
18807
18877
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
18808
18878
|
type: "module",
|
|
18809
18879
|
bin: {
|
|
@@ -19210,10 +19280,16 @@ function parseArgs(argv) {
|
|
|
19210
19280
|
if (argv.length > 1) {
|
|
19211
19281
|
throw new Error(`Unknown argument for generate-key: ${argv[1]}`);
|
|
19212
19282
|
}
|
|
19213
|
-
return {
|
|
19283
|
+
return {
|
|
19284
|
+
command: "generate-key",
|
|
19285
|
+
help: false,
|
|
19286
|
+
version: false,
|
|
19287
|
+
overrides: {}
|
|
19288
|
+
};
|
|
19214
19289
|
}
|
|
19215
19290
|
const out = {
|
|
19216
19291
|
help: false,
|
|
19292
|
+
version: false,
|
|
19217
19293
|
overrides: {}
|
|
19218
19294
|
};
|
|
19219
19295
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -19244,6 +19320,9 @@ function parseArgs(argv) {
|
|
|
19244
19320
|
case "--help":
|
|
19245
19321
|
out.help = true;
|
|
19246
19322
|
break;
|
|
19323
|
+
case "--version":
|
|
19324
|
+
out.version = true;
|
|
19325
|
+
break;
|
|
19247
19326
|
default:
|
|
19248
19327
|
if (arg.startsWith("-")) {
|
|
19249
19328
|
throw new Error(`Unknown option: ${arg}`);
|
|
@@ -19268,6 +19347,7 @@ Options:
|
|
|
19268
19347
|
--port <n> HTTP port (default: 3000)
|
|
19269
19348
|
--host <addr> HTTP bind address (default: 127.0.0.1)
|
|
19270
19349
|
--data-dir <path> Where to store the encrypted accounts file
|
|
19350
|
+
--version Show package version
|
|
19271
19351
|
-h, --help Show this help
|
|
19272
19352
|
|
|
19273
19353
|
Configuration:
|
|
@@ -19311,6 +19391,11 @@ async function main() {
|
|
|
19311
19391
|
process.stdout.write(helpText());
|
|
19312
19392
|
return;
|
|
19313
19393
|
}
|
|
19394
|
+
if (opts.version) {
|
|
19395
|
+
process.stdout.write(`${VERSION}
|
|
19396
|
+
`);
|
|
19397
|
+
return;
|
|
19398
|
+
}
|
|
19314
19399
|
const { config, warnings } = loadConfig(opts.overrides);
|
|
19315
19400
|
for (const warning of warnings) {
|
|
19316
19401
|
process.stderr.write(`[hypermail-mcp] warning: ${warning}
|