hypermail-mcp 0.7.4 → 0.7.6

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,6 +3,18 @@
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.6** — Strict provider env var enforcement. Legacy env vars
7
+ > (`MS_CLIENT_ID`, `MS_TENANT_ID`, `GOOGLE_CLIENT_ID`,
8
+ > `GOOGLE_CLIENT_SECRET`) are unconditionally ignored; only the dedicated
9
+ > `HYPERMAIL_PROVIDERS_*` names are resolved.
10
+ >
11
+ > **v0.7.5** — Attachments via file path on `send_email`/`draft_email`
12
+ > (`attachments` param). `edit_draft` gains `new_attachments` and
13
+ > `remove_attachments` — `add_attachment_to_draft` is removed (23 tools now).
14
+ > Draft editing uses multi-strategy thread boundary detection for more reliable
15
+ > quoted-thread preservation. Watcher now supports script emission (`runScript`)
16
+ > alongside webhook delivery.
17
+ >
6
18
  > **v0.7.4** — `inReplyTo` is now a required parameter on `send_email` and
7
19
  > `draft_email` (was optional). Set it to `false` for a new email, or pass a
8
20
  > message ID to thread a reply. This forces the agent to make an explicit choice
@@ -13,13 +25,8 @@ inboxes through a single, unified tool surface.
13
25
  > the entire content — including the quoted thread. Now only the answer part
14
26
  > (above the spacer delimiter) is replaced.
15
27
  >
16
- > **v0.7.2** — `add_attachment_to_draft` now accepts `filePath` (absolute path to
17
- a local file) as an alternative to `contentBytes`. The file is read from disk and
18
- base64-encoded automatically, the MIME type is inferred from the extension, and
19
- `name` defaults to the file's basename.
20
- >> **v0.7.1** — Every config field is now settable via a dedicated
21
- > `HYPERMAIL_*` env var. Legacy env vars (`MS_CLIENT_ID`, `MS_TENANT_ID`,
22
- > `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) still work as fallbacks. See
28
+ > **v0.7.1** — Every config field is now settable via a dedicated
29
+ > `HYPERMAIL_*` env var. Legacy provider env vars are no longer accepted. See
23
30
  > [Environment Variables](#environment-variables) for the full reference.
24
31
  >
25
32
  > **v0.7.0** — Email watch mode: background poll loop detects new inbox
@@ -117,8 +124,8 @@ docker run -d \
117
124
  --name hypermail-mcp \
118
125
  -p 3000:3000 \
119
126
  -e HYPERMAIL_MCP_KEY=<32-byte-key> \
120
- -e MS_CLIENT_ID=<your-client-id> \
121
- -e MS_TENANT_ID=<your-tenant-id> \
127
+ -e HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID=<your-client-id> \
128
+ -e HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID=<your-tenant-id> \
122
129
  -v hypermail-data:/data \
123
130
  hypermail-mcp
124
131
  ```
@@ -148,8 +155,8 @@ The server listens on `http://127.0.0.1:3000/mcp`. Pi connects via the
148
155
  | --- | --- | --- |
149
156
  | `HYPERMAIL_MCP_DATA_DIR` | Where to keep the encrypted accounts blob | `~/.hypermail-mcp` |
150
157
  | `HYPERMAIL_MCP_KEY` | 32-byte AES-256-GCM key (hex, base64, or any passphrase — derived via SHA-256). Required for hosted deployments. Auto-generated for stdio. | auto-generated, stored via OS keychain (`keytar`) or a local `master.key` file |
151
- | `MS_CLIENT_ID` | Azure Entra public client (application) id used for device-code login | placeholder — **set your own for production** |
152
- | `MS_TENANT_ID` | Tenant for the authority URL | `common` |
158
+ | `HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID` | Azure Entra public client (application) id used for device-code login | placeholder — **set your own for production** |
159
+ | `HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID` | Tenant for the authority URL | `common` |
153
160
 
154
161
  CLI flags: `--http`, `--port`, `--host`, `--data-dir`, `--read-only`, `--help`.
155
162
 
@@ -193,9 +200,10 @@ and read emails.
193
200
 
194
201
  Every config field can be set via a dedicated `HYPERMAIL_*` env var, following
195
202
  a dotted-path naming convention (`HYPERMAIL_HTTP_PORT`,
196
- `HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID`, etc.). Legacy env vars
197
- (`MS_CLIENT_ID`, `MS_TENANT_ID`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`)
198
- still work as fallbacks for backward compatibility.
203
+ `HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID`, etc.). Legacy provider env vars such
204
+ as `MS_CLIENT_ID`, `MS_TENANT_ID`, `GOOGLE_CLIENT_ID`, and
205
+ `GOOGLE_CLIENT_SECRET` are ignored; use the `HYPERMAIL_PROVIDERS_*` names
206
+ below.
199
207
 
200
208
  | Env var | Config path | Type |
201
209
  | --- | --- | --- |
@@ -213,6 +221,10 @@ still work as fallbacks for backward compatibility.
213
221
  | `HYPERMAIL_WATCH_WEBHOOK_URL` | `watch.webhook.url` | `string` |
214
222
  | `HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS` | `watch.webhook.retry.maxAttempts` | `int` |
215
223
  | `HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS` | `watch.webhook.retry.baseDelayMs` | `int` |
224
+ | `HYPERMAIL_WATCH_SCRIPT_COMMAND` | `watch.script.command` | `string` |
225
+ | `HYPERMAIL_WATCH_SCRIPT_TIMEOUT_MS` | `watch.script.timeoutMs` | `int` |
226
+ | `HYPERMAIL_WATCH_SCRIPT_RETRY_MAX_ATTEMPTS` | `watch.script.retry.maxAttempts` | `int` |
227
+ | `HYPERMAIL_WATCH_SCRIPT_RETRY_BASE_DELAY_MS` | `watch.script.retry.baseDelayMs` | `int` |
216
228
 
217
229
  **Priority order:** CLI flags > config file > `HYPERMAIL_*` env var > hardcoded default.
218
230
 
@@ -237,11 +249,10 @@ account store.
237
249
  | `archive_email` | `account`, `id` | Move a message to the Archive folder. Disabled under `--read-only`. |
238
250
  | `trash_email` | `account`, `id` | Move a message to Deleted Items (trash). Disabled under `--read-only`. |
239
251
  | `move_email` | `account`, `id`, `destination` | Move to any folder by well-known name (`inbox`, `drafts`, etc.) or custom folder ID. Disabled under `--read-only`. |
240
- | `send_email` | `account`, `to[]`, `cc?`, `bcc?`, `subject`, `body`, `format`, `include_signature`, `inReplyTo`, `replyAll?`, `forwardMessageId?` | 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. Disabled under `--read-only`. |
241
- | `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`). `inReplyTo` is required — set to `false` for new emails. Disabled under `--read-only`. |
242
- | `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`. |
252
+ | `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. Disabled under `--read-only`. |
253
+ | `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. Disabled under `--read-only`. |
254
+ | `edit_draft` | `account`, `id`, `to?`, `cc?`, `bcc?`, `subject?`, `body?`, `format?`, `include_signature?`, `new_attachments?`, `remove_attachments?` | Edit an existing draft by ID. Only provided fields are updated. `new_attachments` adds files (`{filePath, name?}[]`); `remove_attachments` removes by attachment ID (`string[]`). Returns the updated draft ID, HTML body (`draftHtml`), and attachment metadata. Disabled under `--read-only`. |
243
255
  | `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`. |
244
- | `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`. |
245
256
  | `list_folders` | `account`, `parentFolderId?` | List available mail folders. Returns top-level folders by default, or children of `parentFolderId`. |
246
257
  | `create_folder` | `account`, `displayName`, `parentFolderId?` | Create a new mail folder under root (default) or the given parent. Disabled under `--read-only`. |
247
258
  | `delete_folder` | `account`, `folderId` | Delete a mail folder by ID. Disabled under `--read-only`. |
@@ -252,9 +263,9 @@ account store.
252
263
  ## Email Watch
253
264
 
254
265
  When enabled, hypermail-mcp runs a background poll loop that scans inboxes for
255
- new messages and POSTs each one to a configurable webhook URL. Intended for
256
- push-based email triage — downstream agents (e.g. Mastra) receive full email
257
- content without polling.
266
+ new messages and delivers each one via webhook POST and/or script execution.
267
+ Intended for push-based email triage — downstream agents (e.g. Mastra) receive
268
+ full email content without polling.
258
269
 
259
270
  ```jsonc
260
271
  {
@@ -264,6 +275,11 @@ content without polling.
264
275
  "webhook": {
265
276
  "url": "http://localhost:3000/api/email-webhook",
266
277
  "retry": { "maxAttempts": 5, "baseDelayMs": 1000 }
278
+ },
279
+ "script": {
280
+ "command": "node /path/to/email-handler.js",
281
+ "timeoutMs": 30000,
282
+ "retry": { "maxAttempts": 3, "baseDelayMs": 1000 }
267
283
  }
268
284
  }
269
285
  }
@@ -276,16 +292,25 @@ content without polling.
276
292
  | `watch.webhook.url` | — | Endpoint that receives `POST` with `EmailFull` JSON |
277
293
  | `watch.webhook.retry.maxAttempts` | `5` | Max delivery attempts (1–10) |
278
294
  | `watch.webhook.retry.baseDelayMs` | `1000` | Base backoff delay (× 2^attempt) |
295
+ | `watch.script.command` | — | Shell command spawned with email JSON on stdin |
296
+ | `watch.script.timeoutMs` | `30000` | Max script runtime before SIGKILL |
297
+ | `watch.script.retry.maxAttempts` | `3` | Max script delivery attempts |
298
+ | `watch.script.retry.baseDelayMs` | `1000` | Base backoff delay (× 2^attempt) |
279
299
 
280
300
  **Behavior:**
281
301
  - Polls **all accounts** in the store, **inbox only**.
282
302
  - Detects new emails via `lastSeenIds` (capped at 200) stored in the encrypted
283
303
  account file — no duplicate emits across restarts.
284
- - One `POST` per email (full body: subject, sender, text, HTML, attachments
285
- metadata, thread ID via `EmailFull`).
286
- - Delivery uses exponential backoff (`baseDelay × 2^attempt`). Retries on
287
- non-2xx responses and connection errors. Logs and moves on after
288
- `maxAttempts` exhausted never blocks the poll loop.
304
+ - Two delivery modes (can be used together):
305
+ - **Webhook:** One `POST` per email (full body as `EmailFull` JSON).
306
+ - **Script:** Spawns a shell command with the `EmailFull` JSON piped to
307
+ stdin. The script receives `conversationId`, `subject`, `from`, `toRecipients`,
308
+ `body`, `isRead`, `sentDateTime`, `attachments`, plus an `account` field.
309
+ - Both modes use exponential backoff (`baseDelay × 2^attempt`). Retries on
310
+ failures (non-2xx for webhook, non-zero exit for script). Logs and moves on
311
+ after `maxAttempts` exhausted — never blocks the poll loop.
312
+ - Each delivery mode is fire-and-forget — the poll loop continues while
313
+ delivery runs in the background.
289
314
  - Works in both **stdio** and **HTTP** transport modes — the poll interval
290
315
  fires normally alongside MCP message handling.
291
316