hypermail-mcp 0.7.15 → 0.7.17
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 +16 -2
- package/dist/cli.js +264 -53
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.17** — Hardened Outlook message IDs returned by `list_emails` and
|
|
7
|
+
> `search_emails`: Outlook reads now request Microsoft Graph immutable IDs,
|
|
8
|
+
> `search_emails` probes results and marks stale/not-found messages with
|
|
9
|
+
> `stale: true`, and `read_email` / `read_attachment` keep a legacy mutable-ID
|
|
10
|
+
> fallback for older IDs. Graph errors now include structured diagnostic details
|
|
11
|
+
> when available.
|
|
12
|
+
>
|
|
13
|
+
> **v0.7.16** — Hardened `draft_email` after successful draft creation:
|
|
14
|
+
> if post-save readback fails, the tool now returns the created draft ID with
|
|
15
|
+
> `warning` and `draftReadbackError` instead of losing the draft behind an
|
|
16
|
+
> error. Compose operations also emit sanitized `HYPERMAIL_DEBUG` stage logs,
|
|
17
|
+
> IMAP drafts now use the server-advertised `\\Drafts` mailbox when present,
|
|
18
|
+
> and the CLI supports `--version`.
|
|
19
|
+
>
|
|
6
20
|
> **v0.7.15** — Hardened IMAP draft creation: drafts now append directly to
|
|
7
21
|
> the Drafts mailbox, retry without the `\\Draft` flag when an IMAP server
|
|
8
22
|
> rejects flagged APPEND, and preserve IMAP reply/forward context in drafts the
|
|
@@ -205,7 +219,7 @@ Hypermail uses flat `HYPERMAIL_*` environment variables as the source of truth.
|
|
|
205
219
|
There is no runtime config file. CLI flags only override transport, host, port,
|
|
206
220
|
and data directory for a single invocation.
|
|
207
221
|
|
|
208
|
-
CLI flags: `--http`, `--port`, `--host`, `--data-dir`, `--help`.
|
|
222
|
+
CLI flags: `--http`, `--port`, `--host`, `--data-dir`, `--version`, `--help`.
|
|
209
223
|
|
|
210
224
|
Subcommands: `hypermail-mcp generate-key` — generate a base64 32-byte key for
|
|
211
225
|
`HYPERMAIL_KEY`.
|
|
@@ -284,7 +298,7 @@ account store.
|
|
|
284
298
|
| `trash_email` | `account`, `id` | Move a message to Deleted Items (trash). |
|
|
285
299
|
| `move_email` | `account`, `id`, `destination` | Move to any folder by well-known name (`inbox`, `drafts`, etc.) or custom folder ID. |
|
|
286
300
|
| `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. |
|
|
301
|
+
| `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
302
|
| `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
303
|
| `send_draft` | `account`, `id` | Send an existing draft email by ID. Use with draft IDs returned by `draft_email` or `edit_draft`. |
|
|
290
304
|
| `list_folders` | `account`, `parentFolderId?` | List available mail folders. Returns top-level folders by default, or children of `parentFolderId`. |
|
package/dist/cli.js
CHANGED
|
@@ -14846,21 +14846,122 @@ import { ResponseType } from "@microsoft/microsoft-graph-client";
|
|
|
14846
14846
|
import { writeFileSync } from "fs";
|
|
14847
14847
|
import { tmpdir } from "os";
|
|
14848
14848
|
import { join as pathJoin } from "path";
|
|
14849
|
+
var OUTLOOK_IMMUTABLE_ID_PREFER = 'IdType="ImmutableId"';
|
|
14850
|
+
var MESSAGE_SELECT = [
|
|
14851
|
+
"id",
|
|
14852
|
+
"subject",
|
|
14853
|
+
"from",
|
|
14854
|
+
"toRecipients",
|
|
14855
|
+
"receivedDateTime",
|
|
14856
|
+
"bodyPreview",
|
|
14857
|
+
"isRead",
|
|
14858
|
+
"hasAttachments"
|
|
14859
|
+
].join(",");
|
|
14860
|
+
var FULL_MESSAGE_SELECT = [
|
|
14861
|
+
"id",
|
|
14862
|
+
"subject",
|
|
14863
|
+
"from",
|
|
14864
|
+
"toRecipients",
|
|
14865
|
+
"ccRecipients",
|
|
14866
|
+
"bccRecipients",
|
|
14867
|
+
"receivedDateTime",
|
|
14868
|
+
"bodyPreview",
|
|
14869
|
+
"isRead",
|
|
14870
|
+
"hasAttachments",
|
|
14871
|
+
"body"
|
|
14872
|
+
].join(",");
|
|
14873
|
+
function graphErrorFields(err) {
|
|
14874
|
+
if (typeof err !== "object" || err === null) {
|
|
14875
|
+
return [String(err)];
|
|
14876
|
+
}
|
|
14877
|
+
const record2 = err;
|
|
14878
|
+
const fields = [];
|
|
14879
|
+
for (const key of ["code", "statusCode", "message"]) {
|
|
14880
|
+
const value = record2[key];
|
|
14881
|
+
if (typeof value === "string" || typeof value === "number") fields.push(value);
|
|
14882
|
+
}
|
|
14883
|
+
if (typeof record2.body === "string") {
|
|
14884
|
+
try {
|
|
14885
|
+
const parsed = JSON.parse(record2.body);
|
|
14886
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
14887
|
+
const graphError = parsed.error;
|
|
14888
|
+
if (typeof graphError === "object" && graphError !== null) {
|
|
14889
|
+
const graphRecord = graphError;
|
|
14890
|
+
for (const key of ["code", "message"]) {
|
|
14891
|
+
const value = graphRecord[key];
|
|
14892
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
14893
|
+
fields.push(value);
|
|
14894
|
+
}
|
|
14895
|
+
}
|
|
14896
|
+
}
|
|
14897
|
+
}
|
|
14898
|
+
} catch {
|
|
14899
|
+
fields.push(record2.body);
|
|
14900
|
+
}
|
|
14901
|
+
}
|
|
14902
|
+
return fields;
|
|
14903
|
+
}
|
|
14904
|
+
function isStaleMessageIdError(err) {
|
|
14905
|
+
const fields = graphErrorFields(err);
|
|
14906
|
+
if (fields.some((value) => value === 404)) return true;
|
|
14907
|
+
return fields.some((value) => {
|
|
14908
|
+
if (typeof value !== "string") return false;
|
|
14909
|
+
const normalized = value.toLowerCase();
|
|
14910
|
+
return normalized.includes("erroritemnotfound") || normalized.includes("invalidid") || normalized.includes("object was not found in the store") || normalized.includes("not found in the store");
|
|
14911
|
+
});
|
|
14912
|
+
}
|
|
14913
|
+
async function probeMessage(client, id, useImmutableIds) {
|
|
14914
|
+
let req = client.api(`/me/messages/${encodeURIComponent(id)}`).select("id");
|
|
14915
|
+
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
14916
|
+
await req.get();
|
|
14917
|
+
}
|
|
14918
|
+
async function probeSearchResult(client, id) {
|
|
14919
|
+
try {
|
|
14920
|
+
await probeMessage(client, id, true);
|
|
14921
|
+
return true;
|
|
14922
|
+
} catch (err) {
|
|
14923
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14924
|
+
}
|
|
14925
|
+
try {
|
|
14926
|
+
await probeMessage(client, id, false);
|
|
14927
|
+
return true;
|
|
14928
|
+
} catch (err) {
|
|
14929
|
+
if (isStaleMessageIdError(err)) return false;
|
|
14930
|
+
throw err;
|
|
14931
|
+
}
|
|
14932
|
+
}
|
|
14933
|
+
async function getMessage(client, id) {
|
|
14934
|
+
try {
|
|
14935
|
+
const message2 = await client.api(`/me/messages/${encodeURIComponent(id)}`).header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).select(FULL_MESSAGE_SELECT).get();
|
|
14936
|
+
return { message: message2, useImmutableIds: true };
|
|
14937
|
+
} catch (err) {
|
|
14938
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14939
|
+
}
|
|
14940
|
+
const message = await client.api(`/me/messages/${encodeURIComponent(id)}`).select(FULL_MESSAGE_SELECT).get();
|
|
14941
|
+
return { message, useImmutableIds: false };
|
|
14942
|
+
}
|
|
14943
|
+
async function listAttachments(client, messageId, useImmutableIds) {
|
|
14944
|
+
let req = client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments`).select("id,name,contentType,size");
|
|
14945
|
+
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
14946
|
+
const attRes = await req.get();
|
|
14947
|
+
return attRes.value;
|
|
14948
|
+
}
|
|
14949
|
+
async function getAttachmentMetadata(client, messageId, attachmentId, useImmutableIds) {
|
|
14950
|
+
let req = client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`).select("name,contentType");
|
|
14951
|
+
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
14952
|
+
return await req.get();
|
|
14953
|
+
}
|
|
14954
|
+
async function getAttachmentValue(client, messageId, attachmentId, useImmutableIds) {
|
|
14955
|
+
let req = client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}/$value`).responseType(ResponseType.ARRAYBUFFER);
|
|
14956
|
+
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
14957
|
+
return await req.get();
|
|
14958
|
+
}
|
|
14849
14959
|
async function listEmails(client, account, opts) {
|
|
14850
14960
|
const limit = clampLimit(opts.limit, 25, 100);
|
|
14851
14961
|
const folder = opts.folder ?? "inbox";
|
|
14852
14962
|
const filterParts = [];
|
|
14853
14963
|
if (opts.unreadOnly) filterParts.push("isRead eq false");
|
|
14854
|
-
let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).top(limit).skip(opts.skip ?? 0).select(
|
|
14855
|
-
"id",
|
|
14856
|
-
"subject",
|
|
14857
|
-
"from",
|
|
14858
|
-
"toRecipients",
|
|
14859
|
-
"receivedDateTime",
|
|
14860
|
-
"bodyPreview",
|
|
14861
|
-
"isRead",
|
|
14862
|
-
"hasAttachments"
|
|
14863
|
-
].join(",")).orderby("receivedDateTime DESC");
|
|
14964
|
+
let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).top(limit).skip(opts.skip ?? 0).select(MESSAGE_SELECT).orderby("receivedDateTime DESC");
|
|
14864
14965
|
if (filterParts.length > 0) req = req.filter(filterParts.join(" and "));
|
|
14865
14966
|
const res = await req.get();
|
|
14866
14967
|
return {
|
|
@@ -14870,41 +14971,22 @@ async function listEmails(client, account, opts) {
|
|
|
14870
14971
|
}
|
|
14871
14972
|
async function searchEmails(client, account, query, opts) {
|
|
14872
14973
|
const limit = clampLimit(opts.limit, 25, 100);
|
|
14873
|
-
const res = await client.api("/me/messages").header("ConsistencyLevel", "eventual").top(limit).search(`"${query.replace(/"/g, '\\"')}"`).select(
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
"isRead",
|
|
14882
|
-
"hasAttachments"
|
|
14883
|
-
].join(",")
|
|
14884
|
-
).get();
|
|
14885
|
-
return res.value.map((m) => mapSummary(m));
|
|
14974
|
+
const res = await client.api("/me/messages").header("ConsistencyLevel", "eventual").header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).top(limit).search(`"${query.replace(/"/g, '\\"')}"`).select(MESSAGE_SELECT).get();
|
|
14975
|
+
const summaries = res.value.map((m) => mapSummary(m));
|
|
14976
|
+
return Promise.all(
|
|
14977
|
+
summaries.map(async (summary) => {
|
|
14978
|
+
const readable = await probeSearchResult(client, summary.id);
|
|
14979
|
+
return readable ? summary : { ...summary, stale: true };
|
|
14980
|
+
})
|
|
14981
|
+
);
|
|
14886
14982
|
}
|
|
14887
14983
|
async function readEmail(client, account, id) {
|
|
14888
|
-
const m = await client
|
|
14889
|
-
[
|
|
14890
|
-
"id",
|
|
14891
|
-
"subject",
|
|
14892
|
-
"from",
|
|
14893
|
-
"toRecipients",
|
|
14894
|
-
"ccRecipients",
|
|
14895
|
-
"bccRecipients",
|
|
14896
|
-
"receivedDateTime",
|
|
14897
|
-
"bodyPreview",
|
|
14898
|
-
"isRead",
|
|
14899
|
-
"hasAttachments",
|
|
14900
|
-
"body"
|
|
14901
|
-
].join(",")
|
|
14902
|
-
).get();
|
|
14984
|
+
const { message: m, useImmutableIds } = await getMessage(client, id);
|
|
14903
14985
|
let attachments = void 0;
|
|
14904
14986
|
if (m.hasAttachments) {
|
|
14905
14987
|
try {
|
|
14906
|
-
const attRes = await client.
|
|
14907
|
-
attachments = attRes.
|
|
14988
|
+
const attRes = await listAttachments(client, m.id, useImmutableIds);
|
|
14989
|
+
attachments = attRes.map((a) => ({
|
|
14908
14990
|
id: a.id,
|
|
14909
14991
|
name: a.name,
|
|
14910
14992
|
contentType: a.contentType,
|
|
@@ -14925,8 +15007,22 @@ async function readEmail(client, account, id) {
|
|
|
14925
15007
|
};
|
|
14926
15008
|
}
|
|
14927
15009
|
async function readAttachment(client, account, messageId, attachmentId) {
|
|
14928
|
-
|
|
14929
|
-
|
|
15010
|
+
let useImmutableIds = true;
|
|
15011
|
+
let att;
|
|
15012
|
+
try {
|
|
15013
|
+
att = await getAttachmentMetadata(client, messageId, attachmentId, true);
|
|
15014
|
+
} catch (err) {
|
|
15015
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
15016
|
+
useImmutableIds = false;
|
|
15017
|
+
att = await getAttachmentMetadata(client, messageId, attachmentId, false);
|
|
15018
|
+
}
|
|
15019
|
+
let data;
|
|
15020
|
+
try {
|
|
15021
|
+
data = await getAttachmentValue(client, messageId, attachmentId, useImmutableIds);
|
|
15022
|
+
} catch (err) {
|
|
15023
|
+
if (!useImmutableIds || !isStaleMessageIdError(err)) throw err;
|
|
15024
|
+
data = await getAttachmentValue(client, messageId, attachmentId, false);
|
|
15025
|
+
}
|
|
14930
15026
|
const outPath = pathJoin(tmpdir(), att.name);
|
|
14931
15027
|
writeFileSync(outPath, Buffer.from(data));
|
|
14932
15028
|
return {
|
|
@@ -15264,6 +15360,17 @@ function resolveTrashMailbox(mailboxes) {
|
|
|
15264
15360
|
}
|
|
15265
15361
|
return "Trash";
|
|
15266
15362
|
}
|
|
15363
|
+
function resolveDraftMailbox(mailboxes) {
|
|
15364
|
+
for (const mailbox of mailboxes) {
|
|
15365
|
+
const specialUse = mailbox.specialUse?.toLowerCase();
|
|
15366
|
+
if (specialUse === "\\drafts") return mailbox.path;
|
|
15367
|
+
const flags = mailbox.flags ? Array.from(mailbox.flags) : [];
|
|
15368
|
+
if (flags.some((flag) => flag.toLowerCase() === "\\drafts")) {
|
|
15369
|
+
return mailbox.path;
|
|
15370
|
+
}
|
|
15371
|
+
}
|
|
15372
|
+
return "Drafts";
|
|
15373
|
+
}
|
|
15267
15374
|
function clampLimit2(v, dflt, max) {
|
|
15268
15375
|
if (!v || v <= 0) return dflt;
|
|
15269
15376
|
return Math.min(v, max);
|
|
@@ -15643,10 +15750,13 @@ async function sendEmail(clients, account, msg) {
|
|
|
15643
15750
|
}
|
|
15644
15751
|
async function saveDraft(clients, account, msg) {
|
|
15645
15752
|
const client = clients.get(account);
|
|
15646
|
-
const folder = "Drafts";
|
|
15647
15753
|
const rawMsg = await buildRawMessage(client, account, msg);
|
|
15754
|
+
let folder = "Drafts";
|
|
15648
15755
|
try {
|
|
15649
|
-
const result = await client.run((imap) =>
|
|
15756
|
+
const result = await client.run(async (imap) => {
|
|
15757
|
+
folder = resolveDraftMailbox(await imap.list());
|
|
15758
|
+
return appendDraft(imap, folder, rawMsg);
|
|
15759
|
+
});
|
|
15650
15760
|
return { id: encodeId(folder, appendUid(result, folder)) };
|
|
15651
15761
|
} catch (err) {
|
|
15652
15762
|
throw imapOperationError(`failed to save IMAP draft to ${folder}`, err);
|
|
@@ -17215,12 +17325,41 @@ function fail(message) {
|
|
|
17215
17325
|
content: [{ type: "text", text: message }]
|
|
17216
17326
|
};
|
|
17217
17327
|
}
|
|
17328
|
+
function graphErrorDetails(err) {
|
|
17329
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
17330
|
+
const record2 = err;
|
|
17331
|
+
const details = {};
|
|
17332
|
+
for (const key of ["code", "statusCode", "requestId", "date", "innerError"]) {
|
|
17333
|
+
if (record2[key] !== void 0) details[key] = record2[key];
|
|
17334
|
+
}
|
|
17335
|
+
if (typeof record2.body === "string") {
|
|
17336
|
+
try {
|
|
17337
|
+
const parsed = JSON.parse(record2.body);
|
|
17338
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
17339
|
+
const graphError = parsed.error;
|
|
17340
|
+
if (typeof graphError === "object" && graphError !== null) {
|
|
17341
|
+
const graphRecord = graphError;
|
|
17342
|
+
for (const key of ["code", "message", "innerError"]) {
|
|
17343
|
+
if (graphRecord[key] !== void 0 && details[key] === void 0) {
|
|
17344
|
+
details[key] = graphRecord[key];
|
|
17345
|
+
}
|
|
17346
|
+
}
|
|
17347
|
+
}
|
|
17348
|
+
}
|
|
17349
|
+
} catch {
|
|
17350
|
+
}
|
|
17351
|
+
}
|
|
17352
|
+
return Object.keys(details).length > 0 ? details : void 0;
|
|
17353
|
+
}
|
|
17218
17354
|
function errMsg(err) {
|
|
17219
17355
|
const message = err instanceof Error ? err.message : String(err);
|
|
17220
17356
|
if (message.includes("HYPERMAIL_GMAIL_CLIENT_ID")) {
|
|
17221
17357
|
return "Missing Gmail OAuth configuration: set HYPERMAIL_GMAIL_CLIENT_ID before adding a Gmail account.";
|
|
17222
17358
|
}
|
|
17223
|
-
|
|
17359
|
+
const details = graphErrorDetails(err);
|
|
17360
|
+
if (!details) return message;
|
|
17361
|
+
return `${message}
|
|
17362
|
+
${JSON.stringify({ error: details }, null, 2)}`;
|
|
17224
17363
|
}
|
|
17225
17364
|
var providerIdEnum = z2.enum(["outlook", "imap", "gmail"]);
|
|
17226
17365
|
var emailAddrSchema = z2.object({
|
|
@@ -17280,7 +17419,8 @@ var emailSummaryOutputSchema = z2.object({
|
|
|
17280
17419
|
preview: z2.string().optional(),
|
|
17281
17420
|
isRead: z2.boolean().optional(),
|
|
17282
17421
|
hasAttachments: z2.boolean().optional(),
|
|
17283
|
-
folder: z2.string().optional()
|
|
17422
|
+
folder: z2.string().optional(),
|
|
17423
|
+
stale: z2.boolean().optional()
|
|
17284
17424
|
});
|
|
17285
17425
|
var attachmentMetaOutputSchema = z2.object({
|
|
17286
17426
|
id: z2.string(),
|
|
@@ -18522,10 +18662,21 @@ var editDraftSchema = z8.object({
|
|
|
18522
18662
|
|
|
18523
18663
|
// src/tools/compose.ts
|
|
18524
18664
|
function registerComposeTools(server, ctx) {
|
|
18525
|
-
const { store, registry, tools } = ctx;
|
|
18665
|
+
const { store, registry, tools, logger } = ctx;
|
|
18526
18666
|
async function handleSendOrDraft(args, action, resultKey, toolName) {
|
|
18527
18667
|
try {
|
|
18528
18668
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
18669
|
+
logger?.debug("compose", "start", {
|
|
18670
|
+
tool: toolName,
|
|
18671
|
+
account: account.email,
|
|
18672
|
+
provider: provider.id,
|
|
18673
|
+
toCount: args.to.length,
|
|
18674
|
+
ccCount: args.cc?.length ?? 0,
|
|
18675
|
+
bccCount: args.bcc?.length ?? 0,
|
|
18676
|
+
hasReply: !!args.inReplyTo,
|
|
18677
|
+
hasForward: !!args.forwardMessageId,
|
|
18678
|
+
attachmentCount: args.attachments?.length ?? 0
|
|
18679
|
+
});
|
|
18529
18680
|
if (args.include_signature && !account.signature) {
|
|
18530
18681
|
return fail(
|
|
18531
18682
|
"include_signature is true but no signature is configured for this account. Set up a signature first with set_account_settings."
|
|
@@ -18538,6 +18689,14 @@ function registerComposeTools(server, ctx) {
|
|
|
18538
18689
|
style: account.style,
|
|
18539
18690
|
includeSignature: args.include_signature
|
|
18540
18691
|
});
|
|
18692
|
+
logger?.debug("compose", "composed", {
|
|
18693
|
+
tool: toolName,
|
|
18694
|
+
account: account.email,
|
|
18695
|
+
provider: provider.id,
|
|
18696
|
+
format: args.format,
|
|
18697
|
+
isHtml: composed.isHtml,
|
|
18698
|
+
includeSignature: args.include_signature
|
|
18699
|
+
});
|
|
18541
18700
|
if (args.inReplyTo && args.forwardMessageId) {
|
|
18542
18701
|
return fail(
|
|
18543
18702
|
"inReplyTo and forwardMessageId are mutually exclusive \u2014 use one or the other"
|
|
@@ -18555,6 +18714,12 @@ function registerComposeTools(server, ctx) {
|
|
|
18555
18714
|
};
|
|
18556
18715
|
});
|
|
18557
18716
|
}
|
|
18717
|
+
logger?.debug("compose", "attachmentsProcessed", {
|
|
18718
|
+
tool: toolName,
|
|
18719
|
+
account: account.email,
|
|
18720
|
+
provider: provider.id,
|
|
18721
|
+
attachmentCount: processedAttachments?.length ?? 0
|
|
18722
|
+
});
|
|
18558
18723
|
const res = await action(provider, account, {
|
|
18559
18724
|
to: args.to,
|
|
18560
18725
|
cc: args.cc,
|
|
@@ -18567,13 +18732,42 @@ function registerComposeTools(server, ctx) {
|
|
|
18567
18732
|
forwardMessageId: args.forwardMessageId,
|
|
18568
18733
|
attachments: processedAttachments
|
|
18569
18734
|
});
|
|
18735
|
+
logger?.debug("compose", "providerActionSuccess", {
|
|
18736
|
+
tool: toolName,
|
|
18737
|
+
account: account.email,
|
|
18738
|
+
provider: provider.id,
|
|
18739
|
+
hasId: !!res.id
|
|
18740
|
+
});
|
|
18570
18741
|
const result = { [resultKey]: true, ...res };
|
|
18571
18742
|
if (toolName === "draft_email" && res.id) {
|
|
18572
|
-
|
|
18573
|
-
|
|
18743
|
+
try {
|
|
18744
|
+
const draft = await provider.readEmail(account, res.id);
|
|
18745
|
+
result.draftHtml = draft.bodyHtml;
|
|
18746
|
+
logger?.debug("compose", "draftReadbackSuccess", {
|
|
18747
|
+
tool: toolName,
|
|
18748
|
+
account: account.email,
|
|
18749
|
+
provider: provider.id,
|
|
18750
|
+
hasDraftHtml: draft.bodyHtml !== void 0
|
|
18751
|
+
});
|
|
18752
|
+
} catch (readErr) {
|
|
18753
|
+
const message = errMsg(readErr);
|
|
18754
|
+
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.";
|
|
18755
|
+
result.draftReadbackError = message;
|
|
18756
|
+
logger?.debug("compose", "draftReadbackError", {
|
|
18757
|
+
tool: toolName,
|
|
18758
|
+
account: account.email,
|
|
18759
|
+
provider: provider.id,
|
|
18760
|
+
message
|
|
18761
|
+
});
|
|
18762
|
+
}
|
|
18574
18763
|
}
|
|
18575
18764
|
return ok(result, result);
|
|
18576
18765
|
} catch (err) {
|
|
18766
|
+
logger?.debug("compose", "error", {
|
|
18767
|
+
tool: toolName,
|
|
18768
|
+
account: args.account,
|
|
18769
|
+
message: errMsg(err)
|
|
18770
|
+
});
|
|
18577
18771
|
return fail(errMsg(err));
|
|
18578
18772
|
}
|
|
18579
18773
|
}
|
|
@@ -18600,7 +18794,9 @@ function registerComposeTools(server, ctx) {
|
|
|
18600
18794
|
const draftEmailOutputSchema = {
|
|
18601
18795
|
draft: z9.literal(true),
|
|
18602
18796
|
id: z9.string(),
|
|
18603
|
-
draftHtml: z9.string().optional()
|
|
18797
|
+
draftHtml: z9.string().optional(),
|
|
18798
|
+
warning: z9.string().optional(),
|
|
18799
|
+
draftReadbackError: z9.string().optional()
|
|
18604
18800
|
};
|
|
18605
18801
|
if (shouldRegister("draft_email", tools)) {
|
|
18606
18802
|
server.registerTool(
|
|
@@ -18797,13 +18993,13 @@ function registerTools(server, opts) {
|
|
|
18797
18993
|
registerBrowseTools(server, { store, registry, tools, logger });
|
|
18798
18994
|
registerFolderTools(server, { registry, tools });
|
|
18799
18995
|
registerOrganizeTools(server, { registry, tools });
|
|
18800
|
-
registerComposeTools(server, { store, registry, tools });
|
|
18996
|
+
registerComposeTools(server, { store, registry, tools, logger });
|
|
18801
18997
|
}
|
|
18802
18998
|
|
|
18803
18999
|
// package.json
|
|
18804
19000
|
var package_default = {
|
|
18805
19001
|
name: "hypermail-mcp",
|
|
18806
|
-
version: "0.7.
|
|
19002
|
+
version: "0.7.17",
|
|
18807
19003
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
18808
19004
|
type: "module",
|
|
18809
19005
|
bin: {
|
|
@@ -19210,10 +19406,16 @@ function parseArgs(argv) {
|
|
|
19210
19406
|
if (argv.length > 1) {
|
|
19211
19407
|
throw new Error(`Unknown argument for generate-key: ${argv[1]}`);
|
|
19212
19408
|
}
|
|
19213
|
-
return {
|
|
19409
|
+
return {
|
|
19410
|
+
command: "generate-key",
|
|
19411
|
+
help: false,
|
|
19412
|
+
version: false,
|
|
19413
|
+
overrides: {}
|
|
19414
|
+
};
|
|
19214
19415
|
}
|
|
19215
19416
|
const out = {
|
|
19216
19417
|
help: false,
|
|
19418
|
+
version: false,
|
|
19217
19419
|
overrides: {}
|
|
19218
19420
|
};
|
|
19219
19421
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -19244,6 +19446,9 @@ function parseArgs(argv) {
|
|
|
19244
19446
|
case "--help":
|
|
19245
19447
|
out.help = true;
|
|
19246
19448
|
break;
|
|
19449
|
+
case "--version":
|
|
19450
|
+
out.version = true;
|
|
19451
|
+
break;
|
|
19247
19452
|
default:
|
|
19248
19453
|
if (arg.startsWith("-")) {
|
|
19249
19454
|
throw new Error(`Unknown option: ${arg}`);
|
|
@@ -19268,6 +19473,7 @@ Options:
|
|
|
19268
19473
|
--port <n> HTTP port (default: 3000)
|
|
19269
19474
|
--host <addr> HTTP bind address (default: 127.0.0.1)
|
|
19270
19475
|
--data-dir <path> Where to store the encrypted accounts file
|
|
19476
|
+
--version Show package version
|
|
19271
19477
|
-h, --help Show this help
|
|
19272
19478
|
|
|
19273
19479
|
Configuration:
|
|
@@ -19311,6 +19517,11 @@ async function main() {
|
|
|
19311
19517
|
process.stdout.write(helpText());
|
|
19312
19518
|
return;
|
|
19313
19519
|
}
|
|
19520
|
+
if (opts.version) {
|
|
19521
|
+
process.stdout.write(`${VERSION}
|
|
19522
|
+
`);
|
|
19523
|
+
return;
|
|
19524
|
+
}
|
|
19314
19525
|
const { config, warnings } = loadConfig(opts.overrides);
|
|
19315
19526
|
for (const warning of warnings) {
|
|
19316
19527
|
process.stderr.write(`[hypermail-mcp] warning: ${warning}
|