proton-mail-bridge-client 2.16.3 → 2.17.0

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.
Files changed (231) hide show
  1. package/README.md +149 -12
  2. package/dist/cli.d.ts +16 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +178 -1
  5. package/dist/cli.js.map +1 -1
  6. package/dist/config-env.d.ts +2 -1
  7. package/dist/config-env.d.ts.map +1 -1
  8. package/dist/config-env.js +24 -0
  9. package/dist/config-env.js.map +1 -1
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +172 -8
  13. package/dist/index.js.map +1 -1
  14. package/dist/services/account-manager.d.ts +7 -0
  15. package/dist/services/account-manager.d.ts.map +1 -1
  16. package/dist/services/account-manager.js +59 -0
  17. package/dist/services/account-manager.js.map +1 -1
  18. package/dist/services/audit-service.d.ts +20 -0
  19. package/dist/services/audit-service.d.ts.map +1 -1
  20. package/dist/services/audit-service.js +266 -9
  21. package/dist/services/audit-service.js.map +1 -1
  22. package/dist/services/autoresponder-service.d.ts +35 -6
  23. package/dist/services/autoresponder-service.d.ts.map +1 -1
  24. package/dist/services/autoresponder-service.js +223 -108
  25. package/dist/services/autoresponder-service.js.map +1 -1
  26. package/dist/services/brief-service.d.ts.map +1 -1
  27. package/dist/services/brief-service.js +3 -2
  28. package/dist/services/brief-service.js.map +1 -1
  29. package/dist/services/category-service.d.ts +91 -0
  30. package/dist/services/category-service.d.ts.map +1 -0
  31. package/dist/services/category-service.js +320 -0
  32. package/dist/services/category-service.js.map +1 -0
  33. package/dist/services/commitment-service.d.ts.map +1 -1
  34. package/dist/services/commitment-service.js +3 -2
  35. package/dist/services/commitment-service.js.map +1 -1
  36. package/dist/services/contact-service.d.ts +60 -0
  37. package/dist/services/contact-service.d.ts.map +1 -0
  38. package/dist/services/contact-service.js +248 -0
  39. package/dist/services/contact-service.js.map +1 -0
  40. package/dist/services/delivery-queue-service.d.ts +4 -0
  41. package/dist/services/delivery-queue-service.d.ts.map +1 -1
  42. package/dist/services/delivery-queue-service.js +12 -2
  43. package/dist/services/delivery-queue-service.js.map +1 -1
  44. package/dist/services/draft-store-service.d.ts.map +1 -1
  45. package/dist/services/draft-store-service.js +3 -2
  46. package/dist/services/draft-store-service.js.map +1 -1
  47. package/dist/services/embedding-service.d.ts +68 -0
  48. package/dist/services/embedding-service.d.ts.map +1 -0
  49. package/dist/services/embedding-service.js +465 -0
  50. package/dist/services/embedding-service.js.map +1 -0
  51. package/dist/services/event-log-service.d.ts +67 -0
  52. package/dist/services/event-log-service.d.ts.map +1 -0
  53. package/dist/services/event-log-service.js +280 -0
  54. package/dist/services/event-log-service.js.map +1 -0
  55. package/dist/services/grant-service.d.ts +84 -0
  56. package/dist/services/grant-service.d.ts.map +1 -0
  57. package/dist/services/grant-service.js +214 -0
  58. package/dist/services/grant-service.js.map +1 -0
  59. package/dist/services/local-index-service.d.ts +18 -1
  60. package/dist/services/local-index-service.d.ts.map +1 -1
  61. package/dist/services/local-index-service.js +54 -4
  62. package/dist/services/local-index-service.js.map +1 -1
  63. package/dist/services/reply-reminder-service.d.ts +4 -0
  64. package/dist/services/reply-reminder-service.d.ts.map +1 -1
  65. package/dist/services/reply-reminder-service.js +22 -2
  66. package/dist/services/reply-reminder-service.js.map +1 -1
  67. package/dist/services/rules-service.d.ts +9 -1
  68. package/dist/services/rules-service.d.ts.map +1 -1
  69. package/dist/services/rules-service.js +8 -3
  70. package/dist/services/rules-service.js.map +1 -1
  71. package/dist/services/sieve-export.d.ts +19 -0
  72. package/dist/services/sieve-export.d.ts.map +1 -0
  73. package/dist/services/sieve-export.js +192 -0
  74. package/dist/services/sieve-export.js.map +1 -0
  75. package/dist/services/simple-imap-service.d.ts +13 -0
  76. package/dist/services/simple-imap-service.d.ts.map +1 -1
  77. package/dist/services/simple-imap-service.js +140 -17
  78. package/dist/services/simple-imap-service.js.map +1 -1
  79. package/dist/services/snooze-service.d.ts +4 -0
  80. package/dist/services/snooze-service.d.ts.map +1 -1
  81. package/dist/services/snooze-service.js +12 -3
  82. package/dist/services/snooze-service.js.map +1 -1
  83. package/dist/services/template-service.d.ts.map +1 -1
  84. package/dist/services/template-service.js +3 -2
  85. package/dist/services/template-service.js.map +1 -1
  86. package/dist/services/triage-plan-service.d.ts +2 -1
  87. package/dist/services/triage-plan-service.d.ts.map +1 -1
  88. package/dist/services/triage-plan-service.js +3 -2
  89. package/dist/services/triage-plan-service.js.map +1 -1
  90. package/dist/tools/attachments.js +1 -1
  91. package/dist/tools/attachments.js.map +1 -1
  92. package/dist/tools/categories.d.ts +4 -0
  93. package/dist/tools/categories.d.ts.map +1 -0
  94. package/dist/tools/categories.js +269 -0
  95. package/dist/tools/categories.js.map +1 -0
  96. package/dist/tools/contacts.d.ts +6 -0
  97. package/dist/tools/contacts.d.ts.map +1 -0
  98. package/dist/tools/contacts.js +295 -0
  99. package/dist/tools/contacts.js.map +1 -0
  100. package/dist/tools/context.d.ts +4 -0
  101. package/dist/tools/context.d.ts.map +1 -1
  102. package/dist/tools/context.js +3 -0
  103. package/dist/tools/context.js.map +1 -1
  104. package/dist/tools/definitions.d.ts +333 -5
  105. package/dist/tools/definitions.d.ts.map +1 -1
  106. package/dist/tools/definitions.js +169 -5
  107. package/dist/tools/definitions.js.map +1 -1
  108. package/dist/tools/delegation.d.ts +44 -0
  109. package/dist/tools/delegation.d.ts.map +1 -0
  110. package/dist/tools/delegation.js +180 -0
  111. package/dist/tools/delegation.js.map +1 -0
  112. package/dist/tools/diagnostics.d.ts.map +1 -1
  113. package/dist/tools/diagnostics.js +6 -0
  114. package/dist/tools/diagnostics.js.map +1 -1
  115. package/dist/tools/drafts-support.d.ts +2 -1
  116. package/dist/tools/drafts-support.d.ts.map +1 -1
  117. package/dist/tools/drafts-support.js +10 -2
  118. package/dist/tools/drafts-support.js.map +1 -1
  119. package/dist/tools/drafts.d.ts.map +1 -1
  120. package/dist/tools/drafts.js +8 -0
  121. package/dist/tools/drafts.js.map +1 -1
  122. package/dist/tools/events.d.ts +47 -0
  123. package/dist/tools/events.d.ts.map +1 -0
  124. package/dist/tools/events.js +240 -0
  125. package/dist/tools/events.js.map +1 -0
  126. package/dist/tools/reading.d.ts.map +1 -1
  127. package/dist/tools/reading.js +123 -3
  128. package/dist/tools/reading.js.map +1 -1
  129. package/dist/tools/resources.d.ts +4 -0
  130. package/dist/tools/resources.d.ts.map +1 -1
  131. package/dist/tools/resources.js +9 -0
  132. package/dist/tools/resources.js.map +1 -1
  133. package/dist/tools/rules-runner.d.ts.map +1 -1
  134. package/dist/tools/rules-runner.js +1 -0
  135. package/dist/tools/rules-runner.js.map +1 -1
  136. package/dist/tools/rules.d.ts +9 -0
  137. package/dist/tools/rules.d.ts.map +1 -1
  138. package/dist/tools/rules.js +50 -19
  139. package/dist/tools/rules.js.map +1 -1
  140. package/dist/tools/sending.d.ts.map +1 -1
  141. package/dist/tools/sending.js +4 -0
  142. package/dist/tools/sending.js.map +1 -1
  143. package/dist/tools/style.d.ts +2 -0
  144. package/dist/tools/style.d.ts.map +1 -1
  145. package/dist/tools/style.js +1 -1
  146. package/dist/tools/style.js.map +1 -1
  147. package/dist/tools/templates-and-reminders.d.ts +3 -0
  148. package/dist/tools/templates-and-reminders.d.ts.map +1 -1
  149. package/dist/tools/templates-and-reminders.js +31 -17
  150. package/dist/tools/templates-and-reminders.js.map +1 -1
  151. package/dist/tools/triage.d.ts.map +1 -1
  152. package/dist/tools/triage.js +6 -2
  153. package/dist/tools/triage.js.map +1 -1
  154. package/dist/types/index.d.ts +23 -0
  155. package/dist/types/index.d.ts.map +1 -1
  156. package/dist/utils/account-identity.d.ts.map +1 -1
  157. package/dist/utils/account-identity.js +3 -2
  158. package/dist/utils/account-identity.js.map +1 -1
  159. package/dist/utils/approval.d.ts +133 -0
  160. package/dist/utils/approval.d.ts.map +1 -0
  161. package/dist/utils/approval.js +507 -0
  162. package/dist/utils/approval.js.map +1 -0
  163. package/dist/utils/atomic-write.d.ts +1 -1
  164. package/dist/utils/atomic-write.d.ts.map +1 -1
  165. package/dist/utils/atomic-write.js +10 -3
  166. package/dist/utils/atomic-write.js.map +1 -1
  167. package/dist/utils/contact-text.d.ts +8 -0
  168. package/dist/utils/contact-text.d.ts.map +1 -0
  169. package/dist/utils/contact-text.js +24 -0
  170. package/dist/utils/contact-text.js.map +1 -0
  171. package/dist/utils/delegation.d.ts +17 -0
  172. package/dist/utils/delegation.d.ts.map +1 -0
  173. package/dist/utils/delegation.js +72 -0
  174. package/dist/utils/delegation.js.map +1 -0
  175. package/dist/utils/file-lock.d.ts.map +1 -1
  176. package/dist/utils/file-lock.js +5 -4
  177. package/dist/utils/file-lock.js.map +1 -1
  178. package/dist/utils/mail-security.d.ts +4 -2
  179. package/dist/utils/mail-security.d.ts.map +1 -1
  180. package/dist/utils/mail-security.js +357 -51
  181. package/dist/utils/mail-security.js.map +1 -1
  182. package/dist/utils/ocr.d.ts +24 -0
  183. package/dist/utils/ocr.d.ts.map +1 -0
  184. package/dist/utils/ocr.js +109 -0
  185. package/dist/utils/ocr.js.map +1 -0
  186. package/dist/utils/office-text-worker.d.ts +2 -0
  187. package/dist/utils/office-text-worker.d.ts.map +1 -0
  188. package/dist/utils/office-text-worker.js +17 -0
  189. package/dist/utils/office-text-worker.js.map +1 -0
  190. package/dist/utils/office-text.d.ts +32 -0
  191. package/dist/utils/office-text.d.ts.map +1 -0
  192. package/dist/utils/office-text.js +540 -0
  193. package/dist/utils/office-text.js.map +1 -0
  194. package/dist/utils/pdf-text-worker.js +2 -2
  195. package/dist/utils/pdf-text-worker.js.map +1 -1
  196. package/dist/utils/pdf-text.d.ts +10 -0
  197. package/dist/utils/pdf-text.d.ts.map +1 -1
  198. package/dist/utils/pdf-text.js +49 -9
  199. package/dist/utils/pdf-text.js.map +1 -1
  200. package/dist/utils/png.d.ts +2 -0
  201. package/dist/utils/png.d.ts.map +1 -0
  202. package/dist/utils/png.js +26 -0
  203. package/dist/utils/png.js.map +1 -0
  204. package/dist/utils/regular-file.d.ts +9 -0
  205. package/dist/utils/regular-file.d.ts.map +1 -0
  206. package/dist/utils/regular-file.js +34 -0
  207. package/dist/utils/regular-file.js.map +1 -0
  208. package/dist/utils/runtime-policy.d.ts.map +1 -1
  209. package/dist/utils/runtime-policy.js +2 -0
  210. package/dist/utils/runtime-policy.js.map +1 -1
  211. package/dist/utils/safe-write.d.ts +7 -1
  212. package/dist/utils/safe-write.d.ts.map +1 -1
  213. package/dist/utils/safe-write.js +27 -7
  214. package/dist/utils/safe-write.js.map +1 -1
  215. package/dist/utils/server-state-paths.d.ts +12 -0
  216. package/dist/utils/server-state-paths.d.ts.map +1 -0
  217. package/dist/utils/server-state-paths.js +69 -0
  218. package/dist/utils/server-state-paths.js.map +1 -0
  219. package/dist/utils/style-profile.d.ts +16 -0
  220. package/dist/utils/style-profile.d.ts.map +1 -1
  221. package/dist/utils/style-profile.js +37 -1
  222. package/dist/utils/style-profile.js.map +1 -1
  223. package/dist/utils/vcard.d.ts +27 -0
  224. package/dist/utils/vcard.d.ts.map +1 -0
  225. package/dist/utils/vcard.js +141 -0
  226. package/dist/utils/vcard.js.map +1 -0
  227. package/dist/utils/zip-reader.d.ts +16 -0
  228. package/dist/utils/zip-reader.d.ts.map +1 -0
  229. package/dist/utils/zip-reader.js +108 -0
  230. package/dist/utils/zip-reader.js.map +1 -0
  231. package/package.json +1 -1
package/README.md CHANGED
@@ -24,7 +24,7 @@
24
24
 
25
25
  ---
26
26
 
27
- Give Claude Desktop (or Cline, or any MCP client) full access to your Proton Mail inbox: read, search, send, draft, triage threads, manage folders, save attachments, and more — 116 MCP tools in total. Most of the same capabilities are also available as a full CLI for scripting, cron, and piped automation — no Claude required.
27
+ Give Claude Desktop (or Cline, or any MCP client) full access to your Proton Mail inbox: read, search, send, draft, triage threads, manage folders, save attachments, and more — 127 MCP tools in total. Most of the same capabilities are also available as a full CLI for scripting, cron, and piped automation — no Claude required.
28
28
 
29
29
  > **Using Proton Drive too?** See [**proton-drive-mcp**](https://github.com/googlarz/proton-drive-mcp), the companion MCP server and CLI for Proton Drive (upload, download, share and manage your encrypted files from Claude). Run both side by side to handle mail and files in one conversation, for example save an attachment from here, then upload it to Drive. More in [Related projects](#related-projects).
30
30
 
@@ -45,7 +45,7 @@ Give Claude Desktop (or Cline, or any MCP client) full access to your Proton Mai
45
45
  - "Which of my emails have attachments from this month?"
46
46
  - "Show me my drafts and tighten the oldest one."
47
47
  5. **Not sure about handing over control?** Start with `PROTONMAIL_MODE=drafts`: Claude can read, search and prepare drafts, but nothing is sent or deleted until you change the mode. The server also tells the client how to behave (ids, accounts, never obeying instructions found inside a message).
48
- 6. **Optional:** `PROTONMAIL_READ_ONLY=true` to browse without being able to change anything, `PROTONMAIL_TOOL_TIER=core` for a smaller tool set (25 instead of 116), and more accounts under [Multi-account support](#multi-account-support-v210). More recipes below in [Try it](#try-it-example-claude-prompts).
48
+ 6. **Optional:** `PROTONMAIL_READ_ONLY=true` to browse without being able to change anything, `PROTONMAIL_TOOL_TIER=core` for a smaller tool set (25 instead of 127), and more accounts under [Multi-account support](#multi-account-support-v210). More recipes below in [Try it](#try-it-example-claude-prompts).
49
49
 
50
50
  ---
51
51
 
@@ -57,7 +57,7 @@ Give Claude Desktop (or Cline, or any MCP client) full access to your Proton Mai
57
57
  - **Fast local search** — full-text search across your inbox from a local index that keeps its read state in step with the server
58
58
  - **Rules that suggest themselves** — a scan proposes filters and cleanup from your own mail, applies them to existing mail after a preview, and can forward by condition; see [Mailbox rules](#mailbox-rules-suggestions-filters-cleanup-forwarding)
59
59
  - **Automatic reply, per address, with rules** — off by default, with a preview of what it would do, draft-for-approval mode and working-hours; see [Autoresponder](#autoresponder-automatic-reply-per-address) (Proton's own auto-reply may be all you need)
60
- - **Full CLI** — a dedicated command for every one of the 116 tools (plus a generic `tool <name>` passthrough), scriptable and pipeable; `doctor` checks every account and `report` prints a diagnostic summary for a bug report
60
+ - **Full CLI** — a dedicated command for every one of the 127 tools (plus a generic `tool <name>` passthrough), scriptable and pipeable; `doctor` checks every account and `report` prints a diagnostic summary for a bug report
61
61
  - **Runs where you do** — Claude Desktop bundle, npm, Homebrew, or a [Docker image](#running-in-docker-headless-machines) for a headless machine
62
62
  - **Privacy-native** — no third-party email service involved; your mail stays on your machine
63
63
 
@@ -298,7 +298,7 @@ Reload the Cline extension after saving. Proton Mail tools will appear in Cline'
298
298
  For a server with no desktop, next to a headless Bridge. The image installs the published npm package and is published for each release to `ghcr.io/googlarz/proton-mail-bridge-client` (`:<version>` and `:latest`, linux/amd64 and linux/arm64). To build it yourself from this repo:
299
299
 
300
300
  ```bash
301
- docker build -f docker/Dockerfile --build-arg VERSION=2.16.3 -t proton-mail-bridge-client .
301
+ docker build -f docker/Dockerfile --build-arg VERSION=2.17.0 -t proton-mail-bridge-client .
302
302
  docker run -i --rm --network host \
303
303
  -e PROTONMAIL_USERNAME=you@proton.me -e PROTONMAIL_PASSWORD_FILE=/run/secrets/bridge \
304
304
  -v /path/to/bridge-password:/run/secrets/bridge:ro -v bridge-client-data:/data \
@@ -317,7 +317,10 @@ An automatic reply for one address, plus a note that address appends to what it
317
317
  - **When.** After a background sync has indexed new mail, so only while this server runs with `PROTONMAIL_AUTO_SYNC` on; with several clients open, the instance holding the sync lock does it. `outsideHours` (with a `timezone`) limits it to when you are not at work.
318
318
  - **What it does.** `mode: "send"` sends; `mode: "draft"` leaves a draft in your drafts for you to review and send. If the server may not send (`PROTONMAIL_MODE=drafts`, `ALLOW_SEND` off, or outbound mail confined to yourself), a reply becomes a draft by itself; if it may not write drafts either (read-only), it is refused.
319
319
  - **Rules**, first match wins: by sender (`boss@example.com`, `*@client.example`), words in the subject, and whether you have written to the sender before (`known: "yes"/"no"`). Each gives `reply`, `draft` or `skip`, optionally with its own text. Mail that matches none gets the default reply.
320
- - **Text.** Placeholders `{senderFirstName}`, `{senderName}`, `{senderAddress}`, `{subject}`, `{startsAt}`, `{endsAt}`.
320
+ - **Text.** Placeholders `{senderFirstName}`, `{senderName}`, `{senderAddress}`, `{subject}`, `{startsAt}`, `{endsAt}`, `{returnDate}`, `{backupContact}`. `returnDate` and `backupContact` are plain text you set once (`returnDate` may be a date such as `2027-01-04`, written out in the reply's language); nobody is written to at the backup contact.
321
+ - **Language.** `bodies` and `subjects` map a language code (`en`, `pl`, `de`) to a text. If the message answered is detected to be in one of those languages, that text is used and dates are written in it; anything else, or a language with no text, gets `body` / `subject`. Detection is a word-list guess on the subject and the start of the message, so a short or mixed-language message may be taken for the wrong language or none.
322
+ - **Draft in your style.** In draft mode, if a style profile exists (`get_style_profile`), the draft opens and closes with your usual greeting and closing for that language instead of a fixed text. Sent replies use the text as written.
323
+ - **VIP rules.** A rule's `from` accepts `tag:vip` (or any tag you set with `set_contact`), so a rule can treat people you tagged differently. The tag is looked up by the sender address in the message header.
321
324
  - **Safety headers.** `Auto-Submitted: auto-replied`, `Precedence: bulk`, `X-Auto-Response-Suppress: All`; the reply threads under the message it answers.
322
325
  - **Control.** Switching it on needs `confirmed: true` when `PROTONMAIL_CONFIRM_DESTRUCTIVE` is on and the server can send. Switching it off is always allowed, in every mode.
323
326
 
@@ -332,6 +335,8 @@ set_autoresponder account=work@example.com enabled=true confirmed=true
332
335
  get_autoresponder account=work@example.com # state, counts, who wrote, why mail was skipped, the log
333
336
  ```
334
337
 
338
+ For a reply in the sender's language add `bodies={"pl":"Dzień dobry {senderFirstName}, wracam {returnDate}.","de":"Guten Tag {senderFirstName}, ich bin ab {returnDate} zurück."}` and `returnDate=2027-01-04`. `preview_autoresponder` shows, for each message, the language it detected, where the text came from (`textFrom`: rule, language or default), whether a draft would be `styled`, and the text itself.
339
+
335
340
  `outgoingNote` is appended, after the signature, to every mail sent from that address while the rule is active (a send with `appendSignature: false` opts out). On the command line: `set-autoresponder`, `autoresponder` and `preview-autoresponder`. The rule, the log and the memory of who was answered live in `autoresponder.json` in the account's data directory; every account (every address Bridge exposes) has its own.
336
341
 
337
342
  ## Mail firewall (prompt-injection guard)
@@ -340,6 +345,75 @@ Mail is untrusted text that a model reads. `get_email_by_id` and `get_emails_by_
340
345
 
341
346
  After a **high** message has been read, the tools that reach outside (`send_email`, replies, `forward_email`, `send_draft`, `schedule_draft`, `delete_email`, `delete_thread`, `bulk_delete`, `empty_folder`, `unsubscribe_sender`, `respond_to_invite`, and switching on `set_rule` or `set_autoresponder`) ask for `confirmed: true` and quote the reason, even when `PROTONMAIL_CONFIRM_DESTRUCTIVE` is off. Previews (`dryRun`) and switching things off pass. Rules and the autoresponder never act by themselves on a high-risk message. This narrows what a persuaded model can do; it cannot read intent (see the [threat model](docs/threat-model.md)).
342
347
 
348
+ **What is scanned.** The scan walks a whole result and reads every string in it: deeply nested ones, any field (not only body and subject), nodes without an `id`, and text split across adjacent fields. Hidden characters (control, zero-width, direction-changing, compatibility forms) are normalised before the patterns are matched, at every size, so a word split by one is still found. A result part that is not JSON is read as plain text, and `structuredContent` and resource parts are scanned too. An object key longer than 24 characters or containing whitespace is scanned as text. Short machine fields (`id`, `date`, `mimeType`, `hash`, `size` and the like) are skipped only while the value looks like what the key claims. A big result that is clean is scanned without tainting the session. `get_events` results are scanned too (sender, subject and `detail` of each event), and a high rating there taints the session like a message does.
349
+
350
+ **The scan has a budget.** At most 24,000,000 characters and about 1 second of work per result. Only when that runs out is whatever was not looked at reported as **high risk** with the reason `oversized-unscanned`, which taints the session exactly like a message that reads like an instruction, so that padding cannot buy a bypass. The cost: a very slow machine can run out of time on a large result that is entirely harmless and taint the session early; the tools that reach outside then ask for `confirmed: true` as described above (or for your approval, with `PROTONMAIL_APPROVAL`).
351
+
352
+ **One large mail text.** When a single mail text is longer than 512 KB (512,000 characters), it is not matched whole: its two ends and a few windows between them are assessed, and the assessment reports `oversized-unscanned ... (partially assessed)` as **high risk**, so such a message taints the session like one that reads like an instruction. In short: a single mail text above 512 KB counts as high risk. Ordinary mail below that size is assessed whole.
353
+
354
+ **Known gaps.** Short object keys (up to 24 characters, no whitespace) are not scanned; a space-free, filename-like value under a key such as `id` is skipped as a machine field and not scanned; a result that is not JSON and is longer than 2,000,000 characters has only its first and last 100,000 characters scanned (and is reported as high risk). Text you own can taint a session as well: logs, templates or rules that contain instruction-like phrases read as such. The patterns remain pattern-based: the scan narrows common tricks and does not read intent.
355
+
356
+ ## Human approval (MCP elicitation)
357
+
358
+ `confirmed: true` is an argument, so a model that was persuaded (or a mail that says "pass confirmed") can set it itself; the gate above is a speed bump, not a lock. With `PROTONMAIL_APPROVAL` set, the **server asks you**, through the client, and only your answer counts:
359
+
360
+ | `PROTONMAIL_APPROVAL` | What happens |
361
+ |---|---|
362
+ | `off` (default) | Nothing changes: `confirmed: true` works as before. |
363
+ | `elicit` | For a call that needs approval, the server sends an MCP elicitation request and runs the call only if you accept. If the client does not support elicitation, nothing changes (the model's `confirmed: true` still works). |
364
+ | `require` | As `elicit`, but a call that needs approval from a client without elicitation support is refused: it fails closed instead of falling back to `confirmed: true`. |
365
+
366
+ In `elicit` (with a client that supports it) and in `require`, a `confirmed` the model passes is **ignored**: it is removed from the arguments before anything looks at it, so the model's word never counts, only yours. Which calls ask: the ones the confirmation gate covers (sends, replies, forwards, `send_draft`, `schedule_draft`, deletes, `empty_folder`, `unsubscribe_sender`, `respond_to_invite`, `apply_rules` with `dryRun: false`, bulk and thread deletes and trashes, switching on `set_autoresponder`, `set_rule` with forwarding, adding the `\Deleted` flag), plus `approve_plan`, `send_test_email`, `delete_draft`, `delete_template` and `delete_label`. A preview (`dryRun`) does not ask.
367
+
368
+ **What you are asked.** A question with the tool, the account and the details that decide the effect: the recipients that will really be used (for a stored draft, those in the draft), the subject, how many messages, or what the plan would change. For the calls that change standing behaviour or touch many messages the question carries the effect too: `set_autoresponder` (on or off, mode, whether it answers people outside your contacts, subject and text, window, limits, rules), `set_rule` (the rule as it would be stored, from the proposal or stored rule it names, and for forwarding the cap of 50 messages a day), `apply_rules` (the matches of a real run, counted per rule), `bulk_delete` with a filter, `empty_folder` and `delete_folder` (a message count from the local index, which may be out of date, shown as "count unknown" when the index has nothing for the folder), `unsubscribe_sender` (by mail or by a link the server does not open), `respond_to_invite` (organiser and event) and `send_test_email`. The summary is **built by the server** from what it knows (its own stores and the call's arguments), never from text the model wrote about the action. Pieces that come from mail (subjects, addresses, ids) are shown as quoted single-line values, stripped of control, bidirectional and zero-width characters and cut short, after a fixed label; the whole text is cut at 1,500 characters (2,500 for `set_rule`, `set_autoresponder` and `apply_rules`) and only the first three messages are listed. A lookup that takes more than 5 seconds or fails leaves that line out; the question is still asked.
369
+
370
+ For a reply or forward the recipients shown are the ones the reply tool will really use, **Reply-To included**, marked when it differs from the sender. `set_contact` and `import_contacts` show the person, tags, merge target and, for an import, the file with its card count.
371
+
372
+ **What you approved is what runs.** For `send_draft`, `schedule_draft`, `delete_draft`, `approve_plan`, `import_contacts`, `set_rule` and `set_autoresponder`, the server keeps a digest of the draft, plan, vCard file, rule or automatic reply it showed you, on the call itself under a symbol that JSON from a client cannot carry, so the model can neither set nor forge it. If the content is different when the call runs (a draft edited in between, a file replaced, a rule or automatic reply changed), the call is refused and you are not asked to approve something else by accident. `schedule_draft` sends the snapshot it took when it was scheduled. The confirmation gate and the handlers read `dryRun`, `enabled`, `active` and `action` the same way; a malformed value gives the same error in both, instead of one skipping the gate.
373
+
374
+ If you decline, dismiss it, do not answer within `PROTONMAIL_APPROVAL_TIMEOUT_SECONDS` (default 120, 1 to 3600), or the question cannot be delivered, the call is **not run** and the model is told not to retry unless you ask.
375
+
376
+ Limits: **`elicit` on a client that cannot ask falls back to the model's own `confirmed: true`**, so it is not a lock there; only `require` fails closed. This also depends on the client. A client that shows the question without its details, or that answers it automatically, takes the protection away; which clients support elicitation, and how they present it, has not been surveyed here. The question is only as good as the facts the server has, and it cannot judge whether a send is wise. Approval does not replace read-only mode, `PROTONMAIL_ALLOWED_ACTIONS`, the outbound allow-list or the other checks, which run as before.
377
+
378
+ ## Mailbox events (`get_events`, `protonmail://events`)
379
+
380
+ What happened while nobody was asking, kept as a small log per account in the data directory (`events.jsonl`). `get_events` (CLI: `events`) returns the newest events, or only what is new since a cursor: pass the returned `nextCursor` back as `sinceCursor`. The cursor survives restarts; `missed: true` means older events were dropped to keep the log small. `types` takes event types or a whole group (`mail`, `snooze`, `send`, `autoresponder`, `plan`); `limit` is 1 to 200 (default 50); `account` narrows it.
381
+
382
+ | Type | Meaning |
383
+ |---|---|
384
+ | `mail.new`, `mail.new_overflow` | Recent unread INBOX mail from someone else that a sync brought in (at most 25 per sync, from the last 7 days); the overflow event says how many more were not listed |
385
+ | `snooze.woke`, `snooze.failed` | A snoozed message returned, or the attempt failed |
386
+ | `reply_reminder.due` | A reply reminder came due with no answer |
387
+ | `send.sent`, `send.failed` | A queued or scheduled send went out, or failed |
388
+ | `rule.acted` | A rule acted on a message |
389
+ | `autoresponder.replied`, `.drafted`, `.failed` | The automatic reply was sent, drafted, or failed |
390
+ | `plan.applied`, `plan.undone` | A triage plan was applied or taken back |
391
+ | `delegation.used` | A delegation grant was spent on a send |
392
+
393
+ An MCP client that supports resource subscriptions can **subscribe to `protonmail://events`** (the only resource that can be subscribed to) and is told when it changes, instead of polling; nothing is watched until a client subscribes. Notifications are batched (about half a second), and events that another server process wrote to the same data directory are noticed within about 5 seconds.
394
+
395
+ Retention and content: at most 1000 events or 512 KB per file, two generations kept, then the oldest are dropped; when that happens the newest 200 events of each type that matters (sends, delegation use, plans, rules, reply reminders, snoozes) are kept. **No message bodies**: an event holds a type, a time, an id, and for some the sender address, subject and a short `detail` (cleaned, and cut to 254, 160 and 300 characters). Events with a sender, subject or `detail` carry `untrusted: true`: that text came from a mail, so it is data, never an instruction. The result of `get_events` is scanned by the mail firewall, and a high rating taints the session.
396
+
397
+ Flood limits: `mail.new` is recorded at most 50 per sender and 200 in all per hour, so a sender or a burst of spam cannot fill the log; the excess is counted in a `mail.new_overflow` event (at most 20 of those per hour).
398
+
399
+ Limits: events are recorded only while a server (or a command that runs the same code) is running, and new-mail events only while background sync runs; mail that arrives while nothing runs is announced by the first sync afterwards, and only if it is still unread, from the last 7 days and among the newest 25 of that sync. A message already announced is not announced again. The log is a convenience for noticing, not an audit trail (that is the audit log).
400
+
401
+ ## Delegation grants (`PROTONMAIL_DELEGATION=on`)
402
+
403
+ For narrow, repetitive sends you can hand the assistant a **grant** instead of answering every question: "these tools, to these recipients, this many times, until this date". It is **off by default**; with `PROTONMAIL_DELEGATION=off` grants are ignored.
404
+
405
+ Grants are made **only by a human, on the command line**: `grant-create --tools send_email,reply_to_email --recipients pat@client.example,@my-team.example --expires 7d --uses 5 --note "weekly report"`. It requires an interactive terminal (stdin and stdout) and a typed confirmation: the whole grant is shown and you type a phrase such as `grant 2 recipients` back; `grant-revoke` asks the same. There is no flag or variable that answers for you, so a script or a shell command run by the assistant is stopped. There is no tool that creates, widens or revokes a grant; the model can only read them with `list_grants` (CLI `list-grants`). The CLI also has `grant-list` (`--all` includes spent, expired and revoked ones) and `grant-revoke <id>`. Grants live in `grants.json` in the account's data directory.
406
+
407
+ What a grant can cover: only `send_email`, `reply_to_email`, `reply_all_email`, `forward_email`, `send_draft` and `schedule_draft`; recipients as exact addresses or `@domain` (exactly that domain: no wildcards, no subdomains; at most 50); at most 50 uses and at most 30 days; a note of up to 200 characters. **Every** To, Cc and Bcc address that the send will really use must match, so a reply is matched on the recipients worked out from the message and a draft on the ones in the draft. An address that cannot be read exactly (non-ASCII look-alikes, odd formatting) is never covered. What a grant cannot cover: deletes, `empty_folder`, rules, the autoresponder, plans, flags or anything else.
408
+
409
+ It stands in for the answer only where one would be asked (your approval, or `confirmed: true` under `PROTONMAIL_CONFIRM_DESTRUCTIVE` or a mode that confirms). It **never bypasses** the taint gate (after a message that reads like an instruction to an AI, grants do not apply and you are asked), read-only mode, `PROTONMAIL_ALLOW_SEND`, `PROTONMAIL_ALLOWED_ACTIONS` or the outbound allow-lists; those still run. A use is spent **before** the mail is sent ("claim before send"), in one locked step shared by every server process on the data directory, so two processes cannot both take the last use. The use is written to the audit log and as a `delegation.used` event. It is at most once: **a send that then fails does not give the use back** (and a crash can never create an extra one). A call that a grant answers for carries **no draft digest** (nobody was asked), so a draft edited in the short window between the grant check and the send is not caught by a digest; that gap is known and open. A grant that no longer covers the call at that point (used up, expired, revoked, a draft edited in between) stops the send. An unreadable `grants.json` honours no grant. A stored grant whose `createdAt` is missing, or whose `expiresAt` is more than 30 days after `createdAt`, is not honoured either (the cap is checked on read, not only when the grant is made). A forward grant covers only messages of the grant's own account, and so does a reply: it must be sent from the account of the original message. A mail scheduled with `schedule_draft` goes out as the snapshot taken when it was scheduled.
410
+
411
+ **The boundary, plainly.** Grants protect against a model that has **only MCP tools**. They do **not** protect against a model that can write files or run a shell: `grants.json` is a plain file in the data directory and is not signed, so such a model can write a grant itself. The terminal and typed phrase stop a shell command, but not a model that drives a pseudo-terminal and types the phrase. If your client gives the model file or shell access, do not rely on grants. A key held in the system keychain, so that a file alone is not enough, is a possible future step; it is not built.
412
+
413
+ **A forward grant is the whole mailbox.** A grant on `forward_email` lets the model forward **any** message of the grant's own account to the granted recipients, not one thread. Grant forwarding only to addresses you would trust with the entire mailbox.
414
+
415
+ Limits: a grant is a standing permission for whoever can reach the assistant while it lasts, so keep recipients narrow and expiries short. It does not judge content: within its recipients, a persuaded model can send what it likes until the uses run out.
416
+
343
417
  ## Brief (what changed since you last looked)
344
418
 
345
419
  `get_brief` (CLI: `brief`) reports from the local index how much mail came since the last brief, split into people and automatic mail, what waits for your reply, replies you are waiting for that are overdue, and suspicious mail (see the mail firewall above). `markSeen` moves the "since" mark, so a daily run reports only the day. To get it without opening anything, run it from your system's scheduler:
@@ -364,7 +438,31 @@ A local ledger of what you promised and what was promised to you. `find_commitme
364
438
 
365
439
  ## Search by meaning (several wordings)
366
440
 
367
- `search_indexed_emails` takes `queries`: several wordings of one question, up to 8. Claude supplies the variants (synonyms, other languages: `["deposit", "kaucja", "Kaution", "rental agreement"]`), the server searches each in the local index and fuses the results, so a message found by several wordings ranks first and every hit says which wordings found it (`matchedBy`). There is no model and no download: the meaning comes from the model you already use. What it cannot find is mail that uses none of the words tried.
441
+ `search_indexed_emails` takes `queries`: several wordings of one question, up to 8. Claude supplies the variants (synonyms, other languages: `["deposit", "kaucja", "Kaution", "rental agreement"]`), the server searches each in the local index and fuses the results, so a message found by several wordings ranks first and every hit says which wordings found it (`matchedBy`). By default there is no model and no download: the meaning comes from the model you already use. What it cannot find is mail that uses none of the words tried.
442
+
443
+ **Optional: search by meaning with vectors.** Set `PROTONMAIL_EMBEDDINGS_URL` to an embedding endpoint **you run**, and `search_indexed_emails` gains `mode`: `lexical` (words), `semantic` (vectors) or `hybrid` (both, fused). Unset, which is the default, none of this runs and nothing is built. No model is shipped or downloaded.
444
+
445
+ - **Endpoint.** Ollama (`http://127.0.0.1:11434/api/embed`) or anything OpenAI-compatible (a URL ending `/v1/embeddings`). `PROTONMAIL_EMBEDDINGS_MODEL` names the model (default `nomic-embed-text`).
446
+ - **Privacy.** The text of each indexed message that gets a vector (subject, sender and the first 1000 characters of its preview), and each semantic query, is sent to that endpoint. Only a host on this machine (loopback) is accepted. A remote host is refused unless you set `PROTONMAIL_EMBEDDINGS_ALLOW_REMOTE=true`, which logs a warning; your mail text then goes to that host. Redirects are not followed.
447
+ - **What is built.** Vectors for the newest 10000 indexed messages, built in the background and stored in `mail-embeddings.bin` next to the index. It is derived from your mail, so treat it like the index. `get_index_status` has an `embeddings` block: configured, model, vectors built, pending.
448
+ - **Fallback.** The default `mode` is `hybrid` once the endpoint is set up and vectors exist, otherwise `lexical`. If semantic ranking cannot run (endpoint down, no vectors yet) you get the lexical result, with `mode` and `semanticNotes` saying why. Semantic hits carry a cosine `similarity`.
449
+ - **Quality is your model's.** Results are only as good as the embedding model you pick. A small local model can miss what a person would call obvious or rank a loose match high; treat `similarity` as a hint. This project has not compared models.
450
+
451
+ ## Contacts (people, not addresses)
452
+
453
+ `find_contact` (CLI: `find-contact`) finds a person by name, part of a name or address and returns them as one identity: their addresses (merged ones included), organisation, tags, your notes, first and last contact, message counts both ways, the last few subjects, and whether you have ever written to them. Counts, dates and names come from the local index each time; only what you say is stored. `set_contact` sets a display name, notes and tags (such as `vip`), merges another address into the same person, or splits one out again. `import_contacts` reads a local `.vcf` file (vCard 2.1, 3.0, 4.0) and takes name, e-mail addresses, organisation and note; it fills only what is not set yet, **never reads `CATEGORIES` and never sets a tag**, so a file cannot mark anyone as a VIP. An import also **never attaches an address to a person who already exists**: a card that names a known person (or whose address belongs to another person) adds nothing for those addresses and is reported under `conflicts` (the first 20, with `conflictCount` for the total). Merging two addresses into one person is something only `set_contact` with `mergeWith` does, so a file cannot give its own address someone's tags. A tag can be used in an autoresponder rule as `tag:vip` in `from`. Because tags change what the autoresponder does, `set_contact` and `import_contacts` are gated like the tools that reach outside: after a suspicious message was read in the session they ask for `confirmed: true` (or for your approval, with `PROTONMAIL_APPROVAL`).
454
+
455
+ Limits: at most 5 MB and 5000 cards per import (the result says if cards were left); nothing is fetched over the network and nothing changes in the mailbox. This is a store of your own (in the account's data directory, one per account), not Proton's address book, which Bridge does not expose: export your contacts from Proton as vCard if you want them here. A name taken from mail headers is whatever the sender wrote; it is cleaned of control and invisible characters and shown, never followed.
456
+
457
+ ## Own categories (separate from Proton's)
458
+
459
+ Proton Mail sorts mail into its own categories. According to public sources they are server-side and not exposed over IMAP, so this server cannot read what Proton decided, change it or add to it. These categories are a different thing: yours, kept on this machine, per account. `define_category` gives one a name, a description in your words (shown, never used for sorting), and the signals that put mail in it: example senders (`a@b.example`, or `@b.example` for a domain), subject words, list/bulk mail or not, mail from people you have written to, and optionally a label to add. The name, description, label and keywords are checked for text that reads like an instruction to an AI and are refused if it does; that check folds compatibility forms, drops invisible characters and reads Cyrillic and Greek look-alike letters as Latin, but the folding is **partial**: a heuristic, not a complete list of disguises. `starter: true` adds a starter set (Receipts, Newsletters, Social, Updates, People) when you ask for it. A name that matches one of Proton's is reported as a clash.
460
+
461
+ `categorize_inbox` sorts the newest INBOX messages (default 30, up to 100) and says why for each one. It is deterministic: no model decides, and who the sender is counts only when the message passed authentication. By default it is a **preview**. With `dryRun: false` it makes a plan that adds each category's label, which goes through `approve_plan` and can be taken back with `undo_plan` like any other plan. Mail that already has the label, categories without a label and labels that do not exist yet are left out and listed. `correct_category` tells it where a message really belongs: the sender's weight towards that category goes up (stored locally), the sender's next mail follows, and the answer lists which other recent messages would change. Corrections are not copied between accounts.
462
+
463
+ `Reply-To` is no longer used for sorting; the sender, subject words, list headers, whether you wrote to the sender and your corrections are. `define_category` and `correct_category` change a local store, so `PROTONMAIL_READ_ONLY` blocks them, and they are written to the audit log.
464
+
465
+ What it does not do: see or touch Proton's categories, run by itself (it sorts what is in the INBOX when you ask), or place a message that fits no category.
368
466
 
369
467
  ## Mailbox rules (suggestions, filters, cleanup, forwarding)
370
468
 
@@ -388,8 +486,31 @@ apply_rules dryRun=false limit=100
388
486
  get_rules # rules, waiting proposals, what the rules did lately
389
487
  ```
390
488
 
489
+ **Export to Proton's own filters (`export_sieve`).** Proton's filters run on Proton's servers, also while this server and your computer are off, and accept [Sieve](https://proton.me/support/sieve-advanced-custom-filters). `export_sieve` (CLI: `export-sieve`) compiles the enabled rules, in order, to one script you paste into a new Sieve filter in Proton's settings. It changes nothing and sends nothing.
490
+
491
+ - **Checked against** Proton's Sieve page and the public source of Proton's web client (read 2026-10-29): the extensions, the `address` / `header` / `exists` tests and the `fileinto`, `addflag` and `stop` actions the script uses, and the names `archive`, folders and labels are given by.
492
+ - **Assumed, not verified:** that a custom folder is named by its path without `Folders/`, a label by its name without `Labels/`, and that `exists "X-Attached"` means an attachment (taken from Proton's web client). **The script has not been run on a real Proton account.** Paste it into the filter editor, which checks the syntax, and try it on a test message before you rely on it. The folders and labels it names must already exist.
493
+ - **Not exported, each listed in `unsupported` with its reason:** rules on a folder other than INBOX or on age (`olderThanDays`), since Proton filters run once, as mail arrives; anything that trashes or deletes (here those run only on request, after a preview); `forwardTo` (Proton's Sieve documents no redirect, and a forward on Proton's side would skip this server's send checks); switched-off rules; rules whose only condition is `unread`. A condition that cannot be expressed keeps the whole rule out, because dropping it would widen what the rule matches; an action that cannot be expressed is left out of an otherwise exported rule.
494
+ - **Known limit:** locally the first matching rule that archives or moves a message decides where it goes, and the script keeps a later move from overriding an earlier one by guarding it with the earlier rule's condition. That guard can only name an earlier rule whose conditions could be exported. If an earlier rule's conditions cannot be expressed in Sieve, that rule is left out and a **later rule's move is not guarded by it**: on Proton a message may be moved by the later rule where locally the earlier rule would have decided. Read the `unsupported` list for rules left out before you paste the script.
495
+ - **Behaviour differs:** the script acts on every new message, which on Proton may include mail it puts in Spam (the local rules never touch it), and your existing Proton filters run as well. Locally the first matching rule that moves a message decides where it goes; the script guards later moves the same way. A rule name, folder or label containing a control character or `${` is kept out, since Proton's Sieve can read `${` as a variable.
496
+
391
497
  The scan and `apply_rules` work from the local index (by default INBOX and Sent; run `sync_emails` for other folders), so check that it is fresh. Rules, proposals and the log live in `rules.json` in the account's data directory; each account has its own.
392
498
 
499
+ ## Audit log (tamper-evident)
500
+
501
+ Tool calls that change something are recorded in `audit.log` in the data directory, with credentials and message bodies redacted. Each entry carries a sequence number and the SHA-256 hash of the entry before it, so `verify_audit_log` (CLI: `verify-audit-log`) can tell that an entry was **edited, deleted from the middle or reordered**. It returns `ok`, how many entries it checked, the first broken `seq` with the reason, and the head hash. Entries written before the chain existed have no hash and are counted as `legacyEntries`, not checked.
502
+
503
+ What it does not catch:
504
+
505
+ - **Someone who can write both files.** Next to `audit.log` the server keeps `audit.head`, the sequence number and hash of the newest entry (written after each append). Someone who can write only `audit.log` cannot hide anymore: deleting or truncating the newest entries, emptying or removing the file, or stripping the chain fields makes verification fail (reported as `deleted` or `edited`), and a new entry carries on from the head `audit.head` recorded. But whoever can write **both** files can rewrite the chain, recompute every hash and write a matching `audit.head`, and that is not detected. Only a **head hash kept somewhere else** catches it: note the one `verify_audit_log` returns and compare it later; this server cannot store one out of its own reach. So this is evidence against a careless or partial edit, not a defence against someone with the same access as the server. `audit.head` is in the same directory as the log, so anyone (or any model with file access) who can write one can write the other; for real evidence, store the head hash elsewhere, outside the machine or at least outside the data directory.
506
+ - **A repaired head.** If `audit.head` is damaged, or is unreadable and then replaced, the next entry the server records writes a new `audit.head` from the log's own newest entry. Verification reports the problem until then, but the signal can disappear after the next append, so check `verify_audit_log` (and keep the head hash) rather than relying on seeing an old failure later. An `audit.head` that cannot be read at all makes verification return `ok: false` with the reason `head-unreadable`; the server keeps appending to the log regardless.
507
+ - **Planted files.** Every file the server reads from its own data directory, `audit.log` and `audit.head` included, is read through a regular-file guard: a FIFO, device or dangling symlink in its place is refused (`ENOTREG`) instead of hanging the server, and an oversized file is refused (`EFBIG`). An `audit.head` that cannot be read gives `head-unreadable`.
508
+ - **Rotation.** The log rotates at 5 MB into `audit.log.1` and `audit.log.2`, and the oldest is dropped. Verification reads all three files and reports `rotatedBeforeSeq` when older entries have been rotated away; that is not treated as tampering, but entries before it can no longer be checked, and a head hash saved before the rotation drops out of the log cannot be found in it.
509
+
510
+ - **A write cut short.** A last line left half-written by a crash is skipped and counted (`detail` says how many), the next entry starts on a fresh line, and the entries around it are checked as usual. An unreadable line in the middle of the chain is still reported as an edit.
511
+
512
+ `verify_audit_log` only reads.
513
+
393
514
  ## Multi-account support (v2.1.0+)
394
515
 
395
516
  Use more than one Proton mailbox from the same server: either several addresses of one account (Bridge's **Split Addresses** feature) or separate Proton accounts signed in to the same Bridge. Each one gets its own isolated service stack, local index, and drafts storage.
@@ -529,7 +650,7 @@ All commands support `--json` for machine-readable output, and any MCP tool is d
529
650
 
530
651
  `<command> --help` prints that command's usage without running it. Flags accept `--flag value` or `--flag=value` (a value starting with `--` needs the `=` form, or must come after a bare `--`); unknown flags are an error. Exit codes: `0` success, `1` failure (including a failed `doctor` or `connection-status` check, or a failed item in a `batch`/`bulk-*` run), `2` usage error.
531
652
 
532
- **Full command reference: [docs/cli.md](docs/cli.md)** (a named command for every one of the 116 tools, across read, triage, compose, mailbox actions, folders, drafts, templates, analytics, and diagnostics).
653
+ **Full command reference: [docs/cli.md](docs/cli.md)** (a named command for every one of the 127 tools, across read, triage, compose, mailbox actions, folders, drafts, templates, analytics, and diagnostics).
533
654
 
534
655
  ---
535
656
 
@@ -551,7 +672,7 @@ A mode is a floor: the individual flags below can make things stricter (`PROTONM
551
672
  All flags work in both the MCP server and CLI:
552
673
 
553
674
  ```bash
554
- PROTONMAIL_TOOL_TIER=core # expose 25 core tools instead of all 116 — saves context window
675
+ PROTONMAIL_TOOL_TIER=core # expose 25 core tools instead of all 127 — saves context window
555
676
  PROTONMAIL_READ_ONLY=true # disable all write operations
556
677
  PROTONMAIL_ALLOW_SEND=false # disable SMTP sends only (other writes still work)
557
678
  PROTONMAIL_CONFIRM_DESTRUCTIVE=true # require confirmed:true on send, reply, forward, delete
@@ -595,7 +716,7 @@ PROTONMAIL_PASSWORD_COMMAND='pass proton/password'
595
716
  PROTONMAIL_DATA_DIR="$HOME/.proton-mail-bridge-client"
596
717
 
597
718
  # Tools
598
- PROTONMAIL_TOOL_TIER='full' # 'core' exposes 25 essential tools, incl. draft review/edit and list_accounts (saves context window); 'full' exposes all 100
719
+ PROTONMAIL_TOOL_TIER='full' # 'core' exposes 25 essential tools, incl. draft review/edit and list_accounts (saves context window); 'full' exposes all 127
599
720
 
600
721
  # Safety
601
722
  PROTONMAIL_READ_ONLY='false'
@@ -603,6 +724,9 @@ PROTONMAIL_ALLOW_SEND='true'
603
724
  PROTONMAIL_ALLOW_REMOTE_DRAFT_SYNC='true'
604
725
  PROTONMAIL_ALLOWED_ACTIONS='mark_read,mark_unread,star,unstar,archive,trash,restore,move,delete'
605
726
  PROTONMAIL_CONFIRM_DESTRUCTIVE='false'
727
+ PROTONMAIL_APPROVAL='off' # off | elicit | require: ask the person through MCP elicitation instead of trusting the model's confirmed:true (see Human approval)
728
+ PROTONMAIL_APPROVAL_TIMEOUT_SECONDS='120' # 1-3600: how long the question waits; no answer means the call is not run
729
+ PROTONMAIL_DELEGATION='off' # off | on: honour delegation grants made with grant-create (see Delegation grants)
606
730
  PROTONMAIL_RESTRICT_OUTBOUND_TO_SELF='false' # true: sends may only go to your own Bridge login address (To, CC and BCC; +tag aliases count as yours); anything else is refused, and queued/scheduled sends are re-checked when they fire. Covers send_email, reply_to_email, reply_all_email, forward_email, send_draft, schedule_draft, send_test_email, unsubscribe_sender. The safest way to try the server.
607
731
  PROTONMAIL_ALLOW_EMPTY_FOLDER='false' # true: enables empty_folder, which permanently deletes ALL messages in a folder (still needs confirmed:true; without it only a preview is returned). Off by default; use bulk_delete for a subset.
608
732
  PROTONMAIL_ALLOW_UNSAFE_HTML='false' # HTML in outgoing mail is always sanitized (scripts, event handlers and remote images/beacons removed). A send that passes sanitizeHtml:false is IGNORED — still sanitized, with a warning logged — unless this is exactly 'true'. Leave it off unless you deliberately send raw HTML.
@@ -632,6 +756,13 @@ PROTONMAIL_SHORTEN_LINKS='false' # true = long tracking links in Markdown bo
632
756
  PROTONMAIL_MAX_INLINE_KB='40' # in KB (older name PROTONMAIL_MAX_INLINE_BYTES still works, also in KB): largest attachment get_attachment_content returns inline as base64
633
757
  PROTONMAIL_PDF_MAX_MB='10' # largest PDF get_attachment_text extracts text from (1-100 MB); bigger ones are refused with a message
634
758
  PROTONMAIL_PDF_MAX_PAGES='50' # pages get_attachment_text reads from a PDF (1-2000); a longer PDF is cut off with a note saying how many pages are missing
759
+ PROTONMAIL_OCR_COMMAND='' # optional: a program run as '<command> <image file> stdout' (tesseract's convention) so get_attachment_text can read images and PDF pages without a text layer. Started without a shell, 60 s limit; unset, such images and scans fail
760
+ PROTONMAIL_INDEX_ATTACHMENTS='false' # true: text of PDF, DOCX, XLSX and PPTX attachments is added to the local index so search finds it (bounded, no OCR); more disk, slower sync
761
+
762
+ # Semantic search (optional; nothing runs unless the URL is set)
763
+ PROTONMAIL_EMBEDDINGS_URL='' # http://127.0.0.1:11434/api/embed (Ollama) or an OpenAI-compatible .../v1/embeddings; mail text is sent there
764
+ PROTONMAIL_EMBEDDINGS_MODEL='nomic-embed-text'
765
+ PROTONMAIL_EMBEDDINGS_ALLOW_REMOTE='false' # true: accept an endpoint on another machine (your mail text goes to that host)
635
766
 
636
767
  # Tuning and diagnostics
637
768
  PROTONMAIL_OP_DELAY_MS='0' # minimum gap in ms between IMAP mailbox operations (0-5000); raise it to be gentler on a slow Bridge. 0 = no throttling
@@ -685,7 +816,7 @@ With several accounts or a signature, the server sends from whichever `from` the
685
816
  `get_email_stats` · `get_email_analytics` · `get_contacts` · `get_volume_trends` · `folder_stats` · `top_senders`
686
817
 
687
818
  ### Diagnostics
688
- `get_connection_status` · `get_runtime_status` · `run_doctor` · `get_audit_logs` · `run_background_sync` · `wait_for_mailbox_changes` · `sync_emails` · `get_index_status` · `clear_cache` · `clear_index` · `get_logs`
819
+ `get_connection_status` · `get_runtime_status` · `run_doctor` · `get_audit_logs` · `verify_audit_log` · `get_events` · `list_grants` · `run_background_sync` · `wait_for_mailbox_changes` · `sync_emails` · `get_index_status` · `clear_cache` · `clear_index` · `get_logs`
689
820
 
690
821
  ### Unsubscribe & trust
691
822
  `get_unsubscribe_info` · `unsubscribe_sender` — `get_email_by_id` also returns a `security` block (DKIM/SPF/DMARC, encryption, spam score)
@@ -696,11 +827,16 @@ With several accounts or a signature, the server sends from whichever `from` the
696
827
  ### Templates
697
828
  `create_template` · `list_templates` · `get_template` · `delete_template` · `render_template` — `{{variable}}` substitution, render then pass the result to `send_email`
698
829
 
830
+ ### Contacts & categories
831
+ `find_contact` · `set_contact` · `import_contacts` · `define_category` · `list_categories` · `categorize_inbox` · `correct_category` — see [Contacts](#contacts-people-not-addresses) and [Own categories](#own-categories-separate-from-protons)
832
+
699
833
  ### Import/export & attachments
700
834
  `export_email` · `import_email` · `get_attachment_text` · `get_emails_by_ids`
701
835
 
702
836
  `get_attachment_text` reads text-like attachments and PDFs. A PDF comes back page by page, each page headed `--- Page N of M ---`, and the result has a `pdf` field with `totalPages`, `extractedPages`, `omittedPages` and `truncated`. It is limited by `PROTONMAIL_PDF_MAX_MB` (default 10) and `PROTONMAIL_PDF_MAX_PAGES` (default 50); past the page limit the text ends with a line saying how many pages were left out. A password-protected PDF, a damaged file and a scan without a text layer each fail with a short explanation. The PDF is read in memory with [unpdf](https://github.com/unjs/unpdf) (pure JavaScript, no native module): nothing is written to disk, nothing is downloaded, and a document that takes longer than 30 seconds is stopped.
703
837
 
838
+ It also reads **Word, Excel and PowerPoint files** (`.docx`, `.xlsx`, `.pptx`): a DOCX as its text, an XLSX as each sheet's name followed by its cells row by row, a PPTX slide by slide. They are opened in memory by a small built-in reader (no Office, no macros run, nothing written to disk). Limits: files up to 20 MB, the text cut at 500000 characters, and a ZIP with too many or too large parts is refused. Not supported: the old binary `.doc`, `.xls` and `.ppt`, password-protected files, and files with a DTD declaration; they are not read. Text inside images or embedded objects is not read either. Office files are read in a worker thread with a 10 second limit, so a heavy file cannot stall the server; control characters in the text are dropped. With `PROTONMAIL_OCR_COMMAND` set (for example `tesseract`, or with arguments such as `tesseract --psm 6`; words are split on spaces, single and double quotes group, and there is no shell, no variable or glob expansion), an image, and up to 10 pages of a PDF that has no text layer, are read by that program instead of failing; it is run without a shell as `<program> <arguments> <image file> stdout`, with a 60 second limit (the process is killed with SIGKILL at the limit), on images up to 20 MB. OCR is off unless you set it, and its output is only as good as the program's. With `PROTONMAIL_INDEX_ATTACHMENTS=true` the text of PDF and Office attachments (up to 8000 characters per attachment and 32000 per message, the first 20 pages of a PDF, no OCR) is added to the local index when a message is synced, so `search_indexed_emails` finds words that appear only in an attachment; an attachment that cannot be read is skipped.
839
+
704
840
  ---
705
841
 
706
842
  ## Using it as a library
@@ -773,8 +909,9 @@ Measured in October 2026 (v2.7.0) on a copy of the same ~57,000-message index. "
773
909
  - List and search results are compact by default: attachments are metadata only, and empty or derivable fields are left out. Pass `fields: ["subject","from","date"]` to trim further — 10 results is roughly 800 tokens that way, versus about 11,000 for 50 full results.
774
910
  - Defaults are bounded: `search_emails`/`search_indexed_emails` return 50 (pass `limit` for more), `list_drafts` 20, `list_scheduled_sends` 50 newest first. Each reports `hasMore`; page with `offset`.
775
911
  - Draft and queued-message bodies are shortened to a preview in list and update responses (`bodyTruncated`, `bodyLength`); use `get_draft` for one full draft. Attachment content is never inlined in a listing.
776
- - Large attachments: set `PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR` and pass `saveTo`, rather than raising `PROTONMAIL_MAX_INLINE_KB`.
777
- - **`PROTONMAIL_TOOL_TIER=core`** exposes 25 tools instead of 116: about 7k tokens of tool definitions instead of about 21k (roughly 67% less; the earlier measurement with a reference tokenizer, 7k against 20k for 96 tools, scaled by the 3.7% the four newest tool definitions add), paid on every conversation that loads the server.
912
+ - Large attachments: set `PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR` and pass `saveTo`, rather than raising `PROTONMAIL_MAX_INLINE_KB`. **Do not point `PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR` at a broad directory such as `$HOME`:** `save_attachment` could then overwrite shell startup files, LaunchAgents or `~/.claude/settings.json`. The server refuses destinations inside any account's data directory (its `attachments` folder excepted) and the trusted CA file (`save_attachment(s)`, `export_email`), even when the download directory contains them, but it protects nothing else. This was already true before 2.17.
913
+ - Office files are read in a small pool: at most 2 documents at a time, up to 16 waiting (further ones fail with a `busy` error), each worker memory-limited and each job stopped after 10 seconds from its start.
914
+ - **`PROTONMAIL_TOOL_TIER=core`** exposes 25 tools instead of 127: about 7k tokens of tool definitions instead of about 21k (measured with 116 tools, before the eleven tools of 2.17 were added and not re-measured; the full set now costs somewhat more) (roughly 67% less; the earlier measurement with a reference tokenizer, 7k against 20k for 96 tools, scaled by the 3.7% the four newest tool definitions add), paid on every conversation that loads the server.
778
915
  - Repeating an `update_draft` that changes nothing on a draft whose remote copy is already in sync does no store write and no IMAP upload. (A draft whose last sync failed, or that was edited with `syncToRemote:false`, still syncs.)
779
916
 
780
917
  **Working with drafts**
package/dist/cli.d.ts CHANGED
@@ -38,6 +38,22 @@ export declare function watchHealthNotice(previous: string | undefined, error: u
38
38
  body: string;
39
39
  };
40
40
  };
41
+ export declare function parseGrantExpiry(value: string, now?: Date): Date;
42
+ export declare const isInteractiveTerminal: (input?: {
43
+ isTTY?: boolean;
44
+ } | undefined, output?: {
45
+ isTTY?: boolean;
46
+ } | undefined) => boolean;
47
+ export declare const grantCreatePhrase: (recipientCount: number) => string;
48
+ export declare const grantRevokePhrase: (id: string) => string;
49
+ export interface GrantTerminal {
50
+ isInteractive(): boolean;
51
+ /** Shows the text and returns the line the person typed ("" when the input ended). */
52
+ ask(text: string): Promise<string>;
53
+ write(text: string): void;
54
+ }
55
+ export declare function runGrantCreate(parsed: ParsedCliArgs, terminal?: GrantTerminal): Promise<void>;
56
+ export declare function runGrantRevoke(parsed: ParsedCliArgs, terminal?: GrantTerminal): Promise<void>;
41
57
  interface ToolOnlyCommand {
42
58
  command: string;
43
59
  tool: string;
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAaA,OAAO,KAAK,EAAgB,gBAAgB,EAAqB,MAAM,kBAAkB,CAAC;AAqB1F,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,QAAQ,CAAC;CACjB;AAiBD,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAM3C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAuD1D;AA2ID,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAOD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAsErD,CAAC;AAoBF,eAAO,MAAM,iBAAiB,YAAI,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAGlE,wBAAgB,kBAAkB,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAQlG;AAID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA+DtD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcnE;AAyKD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAY5F;AAiND,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAoB5G;AAgGD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGnD;AAGD,wBAAgB,aAAa,CAAC,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,EAAE,IAAI,SAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAS7G;AA+2BD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,OAAO,GACb;IAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAYzE;AA2SD,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IAIb,SAAS,CAAC,EAAE,MAAM,CAAC;IAMnB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,eAAO,MAAM,kBAAkB,EAAE,eAAe,EAmF/C,CAAC;AA+EF,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAuP1C;AA+ED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAcA,OAAO,KAAK,EAA+B,gBAAgB,EAAqB,MAAM,kBAAkB,CAAC;AAuBzG,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,QAAQ,CAAC;CACjB;AAiBD,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAM3C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAuD1D;AA8ID,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAOD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAyErD,CAAC;AAoBF,eAAO,MAAM,iBAAiB,YAAI,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAGlE,wBAAgB,kBAAkB,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAQlG;AAID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA+DtD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcnE;AAyKD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAY5F;AAiND,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAoB5G;AAgGD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGnD;AAGD,wBAAgB,aAAa,CAAC,OAAO,EAAE,gBAAgB,CAAC,SAAS,CAAC,EAAE,IAAI,SAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAS7G;AA+2BD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,OAAO,GACb;IAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAYzE;AA8QD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAa,GAAG,IAAI,CAStE;AAiBD,eAAO,MAAM,qBAAqB,WAAW;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,WAA0B;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,KAAoB,OACjH,CAAC;AACzC,eAAO,MAAM,iBAAiB,mBAAoB,MAAM,KAAG,MAA+E,CAAC;AAC3I,eAAO,MAAM,iBAAiB,OAAQ,MAAM,KAAG,MAAwB,CAAC;AAExE,MAAM,WAAW,aAAa;IAC5B,aAAa,IAAI,OAAO,CAAC;IACzB,sFAAsF;IACtF,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAyBD,wBAAsB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,GAAE,aAA6B,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDlH;AAgBD,wBAAsB,cAAc,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,GAAE,aAA6B,GAAG,OAAO,CAAC,IAAI,CAAC,CAalH;AA6CD,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IAIb,SAAS,CAAC,EAAE,MAAM,CAAC;IAMnB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,eAAO,MAAM,kBAAkB,EAAE,eAAe,EA8F/C,CAAC;AA+EF,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAgQ1C;AA+ED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzD"}