hypermail-mcp 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +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.1** — Every config field is now settable via a dedicated
6
+ > **v0.7.3** — `edit_draft` now preserves the quoted thread history when editing
7
+ > Outlook reply/forward drafts. Previously, editing a draft body would overwrite
8
+ > the entire content — including the quoted thread. Now only the answer part
9
+ > (above the spacer delimiter) is replaced.
10
+ >
11
+ > **v0.7.2** — `add_attachment_to_draft` now accepts `filePath` (absolute path to
12
+ a local file) as an alternative to `contentBytes`. The file is read from disk and
13
+ base64-encoded automatically, the MIME type is inferred from the extension, and
14
+ `name` defaults to the file's basename.
15
+ >> **v0.7.1** — Every config field is now settable via a dedicated
7
16
  > `HYPERMAIL_*` env var. Legacy env vars (`MS_CLIENT_ID`, `MS_TENANT_ID`,
8
17
  > `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) still work as fallbacks. See
9
18
  > [Environment Variables](#environment-variables) for the full reference.
@@ -227,7 +236,7 @@ account store.
227
236
  | `draft_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature?`, `inReplyTo?`, `replyAll?`, `forwardMessageId?` | Save as draft instead of sending. `format` (`"html"` or `"markdown"`) controls body format — Markdown is converted to HTML via `marked`. Returns the draft message ID and HTML body (`draftHtml`). Disabled under `--read-only`. |
228
237
  | `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `body?`, `format?`, `include_signature?` | Edit an existing draft by ID. Only provided fields are updated. `format` only meaningful when `body` is provided. Returns the updated draft ID and HTML body (`draftHtml`). Disabled under `--read-only`. |
229
238
  | `send_draft` | `account`, `id` | Send an existing draft email by ID. Use with draft IDs returned by `draft_email` or `edit_draft`. Disabled under `--read-only`. |
230
- | `add_attachment_to_draft` | `account`, `id`, `name`, `contentBytes`, `contentType?` | Attach a base64-encoded file to an existing draft email by ID. Disabled under `--read-only`. |
239
+ | `add_attachment_to_draft` | `account`, `id`, `name?`, `contentBytes?`, `filePath?`, `contentType?` | Attach a file to an existing draft by ID. Provide `contentBytes` (base64) or `filePath` (absolute path — auto-encodes). Disabled under `--read-only`. |
231
240
  | `list_folders` | `account`, `parentFolderId?` | List available mail folders. Returns top-level folders by default, or children of `parentFolderId`. |
232
241
  | `create_folder` | `account`, `displayName`, `parentFolderId?` | Create a new mail folder under root (default) or the given parent. Disabled under `--read-only`. |
233
242
  | `delete_folder` | `account`, `folderId` | Delete a mail folder by ID. Disabled under `--read-only`. |
package/dist/cli.js CHANGED
@@ -3301,6 +3301,8 @@ function registerOrganizeTools(server, ctx) {
3301
3301
 
3302
3302
  // src/tools/compose.ts
3303
3303
  import { z as z6 } from "zod";
3304
+ import { readFileSync } from "fs";
3305
+ import { basename, extname } from "path";
3304
3306
  function registerComposeTools(server, ctx) {
3305
3307
  const { store, registry, tools } = ctx;
3306
3308
  const sendEmailSchema = z6.object({
@@ -3458,6 +3460,18 @@ function registerComposeTools(server, ctx) {
3458
3460
  bodyPayload = composed.body;
3459
3461
  isHtmlPayload = composed.isHtml;
3460
3462
  }
3463
+ if (bodyPayload !== void 0) {
3464
+ const spacer = '<div style="line-height:12px"><br></div>';
3465
+ try {
3466
+ const existing = await provider.readEmail(account, a.id);
3467
+ const existingHtml = existing.bodyHtml ?? "";
3468
+ const spacerIdx = existingHtml.indexOf(spacer);
3469
+ if (spacerIdx !== -1) {
3470
+ bodyPayload = bodyPayload + existingHtml.slice(spacerIdx);
3471
+ }
3472
+ } catch {
3473
+ }
3474
+ }
3461
3475
  const res = await provider.updateDraft(account, a.id, {
3462
3476
  to: a.to,
3463
3477
  cc: a.cc,
@@ -3506,6 +3520,31 @@ function registerComposeTools(server, ctx) {
3506
3520
  }
3507
3521
  );
3508
3522
  }
3523
+ const MIME_TYPES = {
3524
+ ".pdf": "application/pdf",
3525
+ ".png": "image/png",
3526
+ ".jpg": "image/jpeg",
3527
+ ".jpeg": "image/jpeg",
3528
+ ".gif": "image/gif",
3529
+ ".svg": "image/svg+xml",
3530
+ ".webp": "image/webp",
3531
+ ".txt": "text/plain",
3532
+ ".html": "text/html",
3533
+ ".css": "text/css",
3534
+ ".csv": "text/csv",
3535
+ ".json": "application/json",
3536
+ ".xml": "application/xml",
3537
+ ".zip": "application/zip",
3538
+ ".gz": "application/gzip",
3539
+ ".tar": "application/x-tar",
3540
+ ".doc": "application/msword",
3541
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
3542
+ ".xls": "application/vnd.ms-excel",
3543
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
3544
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
3545
+ ".mp3": "audio/mpeg",
3546
+ ".mp4": "video/mp4"
3547
+ };
3509
3548
  const addAttachmentOutputSchema = {
3510
3549
  attached: z6.literal(true),
3511
3550
  id: z6.string(),
@@ -3519,25 +3558,48 @@ function registerComposeTools(server, ctx) {
3519
3558
  server.registerTool(
3520
3559
  "add_attachment_to_draft",
3521
3560
  {
3522
- description: "Add a file attachment to an existing draft email by ID. `contentBytes` must be base64-encoded file content. `contentType` is the MIME type (e.g. 'application/pdf'); defaults to 'application/octet-stream' if omitted. Disabled in --read-only mode.",
3523
- inputSchema: {
3561
+ description: "Add a file attachment to an existing draft email by ID. Provide exactly one of `contentBytes` (base64-encoded content) or `filePath` (absolute path to a local file). When using `filePath`, the file is read from disk and base64-encoded automatically, and `name` defaults to the file's basename if not provided. `contentType` is the MIME type (e.g. 'application/pdf'); when using `filePath` it is inferred from the extension if omitted. Disabled in --read-only mode.",
3562
+ inputSchema: z6.object({
3524
3563
  account: z6.string().email(),
3525
3564
  id: z6.string().min(1).describe("Draft message ID"),
3526
- name: z6.string().min(1).describe("Attachment filename (e.g. 'report.pdf')"),
3527
- contentBytes: z6.string().min(1).describe("Base64-encoded file content"),
3528
- contentType: z6.string().optional().describe("MIME type (e.g. 'application/pdf')")
3529
- },
3565
+ name: z6.string().min(1).optional().describe(
3566
+ "Attachment filename (e.g. 'report.pdf'). Defaults to the file's basename when `filePath` is used."
3567
+ ),
3568
+ contentBytes: z6.string().min(1).optional().describe("Base64-encoded file content. Mutually exclusive with `filePath`."),
3569
+ filePath: z6.string().min(1).optional().describe(
3570
+ "Absolute path to a local file. The file is read and base64-encoded automatically. Mutually exclusive with `contentBytes`."
3571
+ ),
3572
+ contentType: z6.string().optional().describe("MIME type (e.g. 'application/pdf'). Inferred from `filePath` extension if omitted.")
3573
+ }).refine(
3574
+ (v) => Boolean(v.contentBytes) !== Boolean(v.filePath),
3575
+ { message: "Provide exactly one of `contentBytes` or `filePath`." }
3576
+ ),
3530
3577
  outputSchema: addAttachmentOutputSchema
3531
3578
  },
3532
3579
  async (args) => {
3533
3580
  try {
3534
3581
  const { provider, account } = registry.resolveByEmail(args.account);
3582
+ let contentBytes;
3583
+ let name;
3584
+ let contentType = args.contentType;
3585
+ if (args.filePath) {
3586
+ const fileData = readFileSync(args.filePath);
3587
+ contentBytes = fileData.toString("base64");
3588
+ name = args.name ?? basename(args.filePath);
3589
+ if (!contentType) {
3590
+ const ext = extname(args.filePath).toLowerCase();
3591
+ contentType = MIME_TYPES[ext] ?? "application/octet-stream";
3592
+ }
3593
+ } else {
3594
+ contentBytes = args.contentBytes;
3595
+ name = args.name;
3596
+ }
3535
3597
  const res = await provider.addAttachmentToDraft(
3536
3598
  account,
3537
3599
  args.id,
3538
- args.name,
3539
- args.contentBytes,
3540
- args.contentType
3600
+ name,
3601
+ contentBytes,
3602
+ contentType
3541
3603
  );
3542
3604
  const data = {
3543
3605
  attached: true,
@@ -3566,7 +3628,7 @@ function registerTools(server, opts) {
3566
3628
  // package.json
3567
3629
  var package_default = {
3568
3630
  name: "hypermail-mcp",
3569
- version: "0.7.1",
3631
+ version: "0.7.3",
3570
3632
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
3571
3633
  type: "module",
3572
3634
  bin: {
@@ -3747,7 +3809,7 @@ var WatcherManager = class {
3747
3809
  };
3748
3810
 
3749
3811
  // src/config.ts
3750
- import { readFileSync } from "fs";
3812
+ import { readFileSync as readFileSync2 } from "fs";
3751
3813
  import { z as z7 } from "zod";
3752
3814
  var httpConfigSchema = z7.object({
3753
3815
  enabled: z7.boolean().default(false),
@@ -3886,7 +3948,7 @@ function loadConfig(configPath, cliOverrides = {}) {
3886
3948
  let raw = {};
3887
3949
  if (configPath) {
3888
3950
  try {
3889
- raw = JSON.parse(readFileSync(configPath, "utf-8"));
3951
+ raw = JSON.parse(readFileSync2(configPath, "utf-8"));
3890
3952
  } catch (err) {
3891
3953
  const detail = err instanceof SyntaxError ? "Invalid JSON" : err instanceof Error ? err.message : String(err);
3892
3954
  throw new Error(`Failed to read config file "${configPath}": ${detail}`);