mailmate 1.4.0 → 1.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2e41c1bf66c9a41c75d683f9e7edcfa2d6ac5982f6a54c2d3cc4b2cbb5c36a8
4
- data.tar.gz: 378d1b75d3485de7cf5285e8b12f4fa0ebd7b144b9ebe4a5d4322f6a5d592726
3
+ metadata.gz: b395d407ecab6e32e9e9d94da136031b0edd5cfa0302809721940a0dcefa2ba7
4
+ data.tar.gz: 80d7dc99046e899093e57422153e1b413f7688b5e1d080aa9ae7898778b92136
5
5
  SHA512:
6
- metadata.gz: 710617dc65666c1eec32f4a6d1e7c4ac15c3aab6ce295548d59edaefacd86863ed2b716246bceb250dd9f7f67c4e3a54701366a3c4c65a6b69321b6df3024587
7
- data.tar.gz: a54ee5f4a6cf7ff02c35f6239ab63e805404db1610fa470358ac382bf4870b1fb8176a0990e5a01a24195275e6c603fe44b2d124b9c6829765f222872f874a10
6
+ metadata.gz: 55136934383ab2be45fec5c5787242594a33208b6d71bffedf0b0ee4a0e1176f5876b4b09611c9716afcf34e6d7132a687e6c907a27f586b9a89f5ef7c9e4da1
7
+ data.tar.gz: 481da51efb264b36587908af1ab321d5f63c2174a3f2502be5722bb45600243433c210e30ad869737e531a8d4bffa97ffd371835265346c27dfef177d74ff24f
data/README.md CHANGED
@@ -1,9 +1,147 @@
1
1
  # mailmate
2
2
 
3
- Ruby toolkit for [MailMate](https://freron.com) on macOS — a smart-mailbox filter engine, on-disk index readers, and CLI tools (`mmsearch`, `mmmessage`, `mmopen`, `mm-mailboxes`, `mmtags`, `mm-modify`, `mm-send`, `mm-draft`, `mmdiscover`) for searching, reading, modifying, and sending mail.
3
+ Ruby toolkit for [MailMate](https://freron.com) on macOS — a smart-mailbox filter engine, on-disk index readers, and CLI tools (`mmsearch`, `mmmessage`, `mmopen`, `mm-mailboxes`, `mmtags`, `mm-modify`, `mm-verify`, `mm-send`, `mm-draft`, `mmdiscover`) for searching, reading, modifying, and sending mail.
4
4
 
5
5
  **Requires macOS with MailMate installed.** The library code (filter parser, evaluator) works anywhere, but the integration with MailMate itself — AppleScript, on-disk index reads, the `emate` binary — is macOS-only by way of MailMate being macOS-only.
6
6
 
7
+ ## Privacy Policy
8
+
9
+ This gem — including its MCP server — contains **no networking code at all**.
10
+ It never opens a network connection of any kind, for any purpose: no HTTP, no
11
+ sockets, no telemetry endpoints. You can verify this from the source — there
12
+ isn't a single `net/*`, `open-uri`, or socket require anywhere in `lib/` or
13
+ `exe/`. The only network activity in the project's entire lifecycle happens at
14
+ install time: `gem install` fetches from rubygems.org, and `install.sh` may
15
+ additionally fetch a relocatable Ruby from GitHub. After that, nothing.
16
+
17
+ - **Data collection:** none. No telemetry, analytics, usage data, or crash
18
+ reports — and no code capable of transmitting them.
19
+ - **Usage and storage:** the gem reads MailMate's existing on-disk mail store
20
+ (`~/Library/Application Support/MailMate`) and drives the MailMate app via
21
+ AppleScript. It creates no data stores of its own beyond files you
22
+ explicitly ask it to write. The only optional configuration file is
23
+ `~/.config/mailmate/config.yml`, which you author yourself.
24
+ - **Third-party sharing:** none by the gem itself — it has no means to share
25
+ anything. Two things *adjacent* to it can move data off your machine, and
26
+ both are under your control: (1) `mm-send` / the `send` tool hands the
27
+ message to the MailMate app, and **MailMate** performs the delivery to the
28
+ recipients you named — the gem transmits nothing; (2) when the MCP server
29
+ is used from an AI client (Claude Desktop, Claude Code, etc.), message
30
+ content returned by its tools enters that client's conversation and is
31
+ transmitted to that AI provider under *its* privacy policy — choose which
32
+ messages you surface accordingly.
33
+ - **Data retention:** none. The gem retains nothing between invocations;
34
+ your mail stays wherever MailMate keeps it.
35
+ - **Contact:** brian@murphydye.com, or open an issue at
36
+ <https://github.com/brianmd/mailmate/issues>.
37
+
38
+ ## Install
39
+
40
+ Pick the path that matches how you'll use it:
41
+
42
+ - **Claude Code plugin** — MCP tools for Claude, zero manual setup (below)
43
+ - **One-line installer** — the MCP server for any MCP client, fully isolated under `~/.mailmate-mcp`
44
+ - **`gem install mailmate`** — the CLI tools (and MCP server) on your own Ruby
45
+
46
+ ### Requirements
47
+
48
+ - **macOS** with **MailMate** installed (and running, for any command that drives the UI or sends mail).
49
+ - **Ruby ≥ 3.0** — for the plugin and one-line installer this is optional: if no suitable Ruby is found, they download a private relocatable Ruby into `~/.mailmate-mcp/ruby` and never touch your system.
50
+ - No third-party CLI tools — the gem only shells out to macOS-bundled `plutil`, `osascript`, and `open`, plus MailMate's bundled `emate`.
51
+
52
+ ### Claude Code plugin
53
+
54
+ This repo doubles as a Claude Code plugin marketplace. Inside Claude Code:
55
+
56
+ ```
57
+ /plugin marketplace add brianmd/mailmate
58
+ /plugin install mailmate@brianmd
59
+ ```
60
+
61
+ The plugin's MCP server self-provisions on first launch — Ruby (if needed) and gem dependencies go into `~/.mailmate-mcp`; nothing touches your system Ruby, Homebrew, or shell profile. It runs the plugin's bundled source, so plugin updates take effect without waiting for a gem release. Uninstall: `/plugin uninstall mailmate`, then `rm -rf ~/.mailmate-mcp`.
62
+
63
+ ### One-line installer (any MCP client)
64
+
65
+ ```bash
66
+ curl -fsSL https://raw.githubusercontent.com/brianmd/mailmate/main/install.sh | bash
67
+ ```
68
+
69
+ Installs the released gem into an isolated `GEM_HOME` under `~/.mailmate-mcp` (provisioning a private Ruby only if none ≥ 3.0 is found), writes a launcher shim, and registers it with Claude Code when the `claude` CLI is present (pass `--no-register` to skip). For Claude Desktop, add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "mailmate": { "command": "/Users/<you>/.mailmate-mcp/bin/mailmate-mcp" }
75
+ }
76
+ }
77
+ ```
78
+
79
+ `~/.mailmate-mcp` is the entire footprint. Uninstall: `bash install.sh --uninstall` (or just delete the directory), plus `claude mcp remove mailmate`.
80
+
81
+ ### As a Ruby gem (CLI tools)
82
+
83
+ ```bash
84
+ gem install mailmate
85
+ ```
86
+
87
+ Then optionally bootstrap your config (will happen automatically on first invocation of any command from an interactive shell if it hasn't been run before):
88
+
89
+ ```bash
90
+ mmdiscover
91
+ ```
92
+
93
+ `mmdiscover` reads MailMate's `Sources.plist` and `Identities.plist`, shows you the accounts and addresses it found, and offers to write `~/.config/mailmate/config.yml` from them. It also writes `~/.config/mailmate/bundle_loader.rb` for MailMate bundles. Running it explicitly is only needed in non-TTY contexts (cron jobs, MCP servers) — there, the gem falls back to built-in defaults and warns once.
94
+
95
+ ### Optional: `mmmessage --markdown`
96
+
97
+ **On the vast majority of Ruby setups (stock `arm64-darwin` or `x86_64-darwin` Ruby) this step is a no-op — nokogiri ships a precompiled binary, you can skip the rest of this section and move on.** Keep reading only if your `gem install` actually fails.
98
+
99
+ `mmmessage --markdown` renders HTML-only message bodies as readable markdown. It needs the `reverse_markdown` gem, which has `nokogiri` as a transitive dependency:
100
+
101
+ ```bash
102
+ gem install reverse_markdown
103
+ ```
104
+
105
+ That single command pulls `nokogiri` in automatically — no separate `gem install nokogiri` step. This is kept out of the base install because nokogiri ships a native extension. On Ruby/platform combinations without a precompiled match nokogiri falls back to compiling from source — it vendors its own libxml2/libxslt, but it does need a C compiler, which on macOS means Xcode Command Line Tools (`xcode-select --install`). If `gem install reverse_markdown` fails, that's almost certainly the cause.
106
+
107
+ If you never use `--markdown`, you never pay any of this. If you do invoke `--markdown` without the gem installed, `mmmessage` warns with a clear install hint and falls back to the raw HTML body (it does not abort — so the in-process MCP server survives a missing optional dependency). The plugin launcher and one-line installer attempt this gem automatically and degrade the same way if it fails to build.
108
+
109
+ ### From source (development)
110
+
111
+ If you're hacking on the gem itself, skip `gem install` and put the repo's `exe/` on your `PATH`. Clone wherever you keep source repos, then prepend its `exe/` to `PATH` from your shell's rc file (`~/.zshrc`, `~/.bashrc`, etc.):
112
+
113
+ ```bash
114
+ git clone https://github.com/brianmd/mailmate.git
115
+ cd mailmate
116
+
117
+ # In your shell rc file, add (adjust the path to wherever you cloned):
118
+ # export PATH="/absolute/path/to/mailmate/exe:$PATH"
119
+ # Then reload the shell (open a new tab, or `source` the rc file).
120
+ ```
121
+
122
+ Then `mmdiscover` as above.
123
+
124
+ ### MCP server (manual setup)
125
+
126
+ The gem ships an MCP server (`exe/mailmate-mcp`) that exposes the same surface to AI assistants as JSON-RPC tools: `search`, `message`, `modify`, `verify`, `send`, `draft`, `open`, `list_mailboxes`, `list_tags`, `resolve_id`. Every tool carries MCP annotations (`readOnlyHint`/`destructiveHint`) so clients can apply sensible permission behavior. If you installed via the plugin or one-line installer above, this is already wired up; after a plain `gem install mailmate`, register it yourself:
127
+
128
+ ```bash
129
+ claude mcp add --scope user mailmate "$(which mailmate-mcp)"
130
+ ```
131
+
132
+ Or add manually to `~/.claude.json` under `"mcpServers"`:
133
+
134
+ ```json
135
+ "mailmate": {
136
+ "type": "stdio",
137
+ "command": "/absolute/path/to/mailmate-mcp",
138
+ "args": [],
139
+ "env": {}
140
+ }
141
+ ```
142
+
143
+ For Claude Desktop, use the same command path in `claude_desktop_config.json` as shown under the one-line installer. Restart Claude Desktop after any change to server code or config.
144
+
7
145
  ## Example usage
8
146
 
9
147
  ### `mmsearch` — find messages
@@ -12,11 +150,11 @@ Ruby toolkit for [MailMate](https://freron.com) on macOS — a smart-mailbox fil
12
150
  # Default: today's mail, all mailboxes
13
151
  mmsearch
14
152
 
15
- # From "Medium" in the last 7 days
16
- mmsearch 'f medium d 7d'
153
+ # From "Substack" in the last 7 days
154
+ mmsearch 'f substack d 7d'
17
155
 
18
- # Subject contains "rent due", not the word "draft"
19
- mmsearch 's "rent due" !draft'
156
+ # Subject contains "invoice due", not the word "draft"
157
+ mmsearch 's "invoice due" !draft'
20
158
 
21
159
  # Received in May 2026
22
160
  mmsearch 'd 2026-05'
@@ -41,7 +179,7 @@ mmsearch 'f acme' 'id flags subject from' --limit 20 --no-align
41
179
  | `T <tag>` | Tags / IMAP keywords (`K` is a synonym). |
42
180
  | `!<value>` | Negate, e.g. `f !smith` = From does NOT contain smith. |
43
181
 
44
- The `--mailbox` argument accepts an account, an `account/path`, a bare mailbox name matched across accounts, or a **smart-mailbox name** (e.g. `Medium`, `Whisper`, `Personal Inbox`) whose filter is ANDed into the search.
182
+ The `--mailbox` argument accepts an account, an `account/path`, a bare mailbox name matched across accounts, or a **smart-mailbox name** (e.g. `Newsletters`, `Receipts`, `Priority`) whose filter is ANDed into the search.
45
183
 
46
184
  **Output fields.** Default columns are `flags date time direction party subject`. Prefix a field list with `+` to add to the defaults; a bare list replaces them (`id` is always the first column).
47
185
 
@@ -149,17 +287,47 @@ mm-modify 183715 tag processed read move Archive
149
287
  # Dry-run first
150
288
  mm-modify 183715 archive --dry-run
151
289
 
152
- # Verify the new flags after acting
290
+ # Print the current flags after acting (raw probe)
153
291
  mm-modify 183715 read --verify
154
292
 
155
293
  # As of 1.2.0, `--verify` works in `--dry-run` mode too — pair them as a
156
294
  # post-hoc state probe (run the action first, then re-run with --dry-run
157
295
  # --verify to confirm the change took effect).
158
296
  mm-modify 183715 read --dry-run --verify
297
+
298
+ # Effect verification (--check): confirm the action actually landed on THIS
299
+ # eml-id by re-reading its #flags index. This is the only way to catch a
300
+ # Message-ID that resolved to a different duplicate copy — AppleScript can't
301
+ # report which message it acted on, but the index can show whether OURS
302
+ # changed. A mismatch exits 3. Opt-in because MailMate flushes #flags ~5s
303
+ # after acting, so --check polls up to --check-timeout (default 8s).
304
+ mm-modify 183715 tag urgent --check
159
305
  ```
160
306
 
161
307
  **Tip — batch your actions.** Doing all related changes in **one** `mm-modify` invocation is still worthwhile: one open/close cycle instead of two, and the chain runs deterministically without you having to think about ordering. Splitting is safe — `path_for` falls back to a filesystem glob if MailMate's `#source` index is briefly stale after a fast-move — just slower than batching.
162
308
 
309
+ ### `mm-verify` — confirm a batch of modifies with ONE flush-wait
310
+
311
+ Inline `--check` pays MailMate's ~5 s `#flags`-flush latency *per message* — verifying 50 modifies that way would cost ~250 s. But `#flags` is a single global file flushed once, so a whole batch can be confirmed by waiting for that flush **once**. `mm-modify --emit-check` performs the action and prints a JSON *check-ticket* instead of waiting; collect the tickets, then hand them to `mm-verify`.
312
+
313
+ ```bash
314
+ # Action phase: act on N messages, no per-message wait. Each --emit-check
315
+ # prints a one-line ticket {eml_id, message_id, expectations} to stdout.
316
+ for id in 183715 183720 183733; do
317
+ mm-modify "$id" tag triaged --emit-check >> tickets.jsonl
318
+ done
319
+
320
+ # Verify phase: confirm the whole batch in one index-flush wait.
321
+ mm-verify --file tickets.jsonl
322
+ # → JSON {checked, passed, failed, waited_seconds, results:[{eml_id, ok, flags, unmet}]}
323
+ # exit 0 if all confirmed, 3 if any failed.
324
+
325
+ # Tickets can also be piped, or passed as a JSON-array argument:
326
+ mm-modify 183715 flag --emit-check | mm-verify
327
+ ```
328
+
329
+ A failed ticket means that action didn't land on that eml-id — it registered on a different duplicate copy, or not at all. Chains containing `move`/`archive`/`delete` aren't flag-verifiable and carry empty expectations (auto-pass).
330
+
163
331
  ### `mm-send` — send mail
164
332
 
165
333
  `mm-send` is a thin wrapper around MailMate's bundled `emate mailto`, with `--markup markdown` enforced. The body is read from stdin.
@@ -246,98 +414,16 @@ A few rough edges to be aware of:
246
414
 
247
415
  ## Status
248
416
 
417
+ 1.6.0 — Distribution release. The repo is now a Claude Code plugin marketplace (`/plugin marketplace add brianmd/mailmate`), and a one-line `install.sh` provisions the MCP server into an isolated `~/.mailmate-mcp` — including a private relocatable Ruby when no Ruby ≥ 3.0 is present — without touching system Ruby, Homebrew, or shell profiles. Every MCP tool now carries a `title` plus `readOnlyHint`/`destructiveHint` annotations (Claude clients use these for permission behavior; Anthropic's directory review requires them), and the README gains a formal Privacy Policy section. No changes to CLI or library behavior.
418
+
419
+ 1.5.0 — Reliability and batch-verification for `mm-modify`, plus search/read speedups. `mm-modify` gains a no-window retry guard (a `mid:` open that spawns no viewer would otherwise act on the wrong message) and opt-in effect verification: `--check` confirms a flag/tag/read action landed on the target eml-id by re-reading `#flags` (the only way to catch a duplicate-Message-ID misland). Because MailMate flushes `#flags` to disk ~5 s after acting, a new **`mm-verify`** command plus `mm-modify --emit-check` decouple acting from confirming — collect JSON check-tickets across a batch and verify them all in one flush-wait instead of paying the latency per message. `mmsearch` is substantially faster (compiled date ranges, cheapest-spec-first ordering, bulk-unpack index reader, inverted body search) with bit-identical output; the persistent MCP server now invalidates index caches on disk change. `mmmessage` shows user tags and lazy-loads the `mail` gem (`--raw`/`--mailmate` skip it). MCP: `message` gains `markdown`, `modify` gains a `check` mode (`none|inline|defer`), and a new `verify` tool batch-confirms deferred tickets.
420
+
249
421
  1.2.0 — `mm-modify` no longer brings MailMate to the foreground and is roughly 8× faster on single-action invocations. Internally: the open call uses `open -g -a MailMate <url>` to keep MailMate in the background, and the fixed `--settle` sleeps are replaced by active waits (polling for the spawned viewer window to appear). `mmopen` gains a `--background` / `-g` flag for ad-hoc use. `mm-modify --verify` now works in `--dry-run` mode as a post-hoc state probe. `--settle` is preserved for backward compat; it now caps the active-wait timeout rather than fixing sleep duration.
250
422
 
251
423
  1.1.0 — `reverse_markdown` (and its transitive `nokogiri` dep) is now opt-in rather than auto-installed. Run `gem install reverse_markdown` if you want `mmmessage --markdown`; everything else is unchanged.
252
424
 
253
425
  1.0.0 — initial public release; API stable from this point. Breaking changes bump the major version going forward.
254
426
 
255
- ## Install
256
-
257
- ### Requirements
258
-
259
- - **macOS** with **MailMate** installed (and running, for any command that drives the UI or sends mail).
260
- - **Ruby ≥ 3.0**.
261
- - No third-party CLI tools — the gem only shells out to macOS-bundled `plutil`, `osascript`, and `open`, plus MailMate's bundled `emate`.
262
-
263
- ```bash
264
- gem install mailmate
265
- ```
266
-
267
- Then optionally bootstrap your config (will happen automatically on first invocation of any command from an interactive shell if it hasn't been run before):
268
-
269
- ```bash
270
- mmdiscover
271
- ```
272
-
273
- `mmdiscover` reads MailMate's `Sources.plist` and `Identities.plist`, shows you the accounts and addresses it found, and offers to write `~/.config/mailmate/config.yml` from them. It also writes `~/.config/mailmate/bundle_loader.rb` for MailMate bundles. Running it explicitly is only needed in non-TTY contexts (cron jobs, MCP servers) — there, the gem falls back to built-in defaults and warns once.
274
-
275
- ### Optional: `mmmessage --markdown`
276
-
277
- **On the vast majority of Ruby setups (stock `arm64-darwin` or `x86_64-darwin` Ruby) this step is a no-op — nokogiri ships a precompiled binary, you can skip the rest of this section and move on.** Keep reading only if your `gem install` actually fails.
278
-
279
- `mmmessage --markdown` renders HTML-only message bodies as readable markdown. It needs the `reverse_markdown` gem, which has `nokogiri` as a transitive dependency:
280
-
281
- ```bash
282
- gem install reverse_markdown
283
- ```
284
-
285
- That single command pulls `nokogiri` in automatically — no separate `gem install nokogiri` step. This is kept out of the base install because nokogiri ships a native extension. On Ruby/platform combinations without a precompiled match nokogiri falls back to compiling from source — it vendors its own libxml2/libxslt, but it does need a C compiler, which on macOS means Xcode Command Line Tools (`xcode-select --install`). If `gem install reverse_markdown` fails, that's almost certainly the cause.
286
-
287
- If you never use `--markdown`, you never pay any of this. If you do invoke `--markdown` without the gem already being installed (default on most Ruby versions), `mmmessage` exits with a clear install hint.
288
-
289
- ### From source (development)
290
-
291
- If you're hacking on the gem itself, skip `gem install` and put the repo's `exe/` on your `PATH`. Clone wherever you keep source repos, then prepend its `exe/` to `PATH` from your shell's rc file (`~/.zshrc`, `~/.bashrc`, etc.):
292
-
293
- ```bash
294
- git clone https://github.com/brianmd/mailmate.git
295
- cd mailmate
296
-
297
- # In your shell rc file, add (adjust the path to wherever you cloned):
298
- # export PATH="/absolute/path/to/mailmate/exe:$PATH"
299
- # Then reload the shell (open a new tab, or `source` the rc file).
300
- ```
301
-
302
- Then `mmdiscover` as above.
303
-
304
- ### MCP server
305
-
306
- The gem also ships an MCP server (`exe/mailmate-mcp`) that exposes the same surface to AI assistants as JSON-RPC tools: `search`, `message`, `modify`, `send`, `open`, `list_mailboxes`, `list_tags`, `resolve_id`. After `gem install mailmate`, `mailmate-mcp` is on your `PATH`.
307
-
308
- #### Claude Code (global, all projects)
309
-
310
- ```bash
311
- claude mcp add --scope user mailmate "$(which mailmate-mcp)"
312
- ```
313
-
314
- Or add manually to `~/.claude.json` under `"mcpServers"`:
315
-
316
- ```json
317
- "mailmate": {
318
- "type": "stdio",
319
- "command": "/absolute/path/to/mailmate-mcp",
320
- "args": [],
321
- "env": {}
322
- }
323
- ```
324
-
325
- #### Claude Desktop
326
-
327
- Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
328
-
329
- ```json
330
- {
331
- "mcpServers": {
332
- "mailmate": {
333
- "command": "/absolute/path/to/mailmate-mcp"
334
- }
335
- }
336
- }
337
- ```
338
-
339
- Restart Claude Desktop after any change to server code or config.
340
-
341
427
  ## Commands
342
428
 
343
429
  | Command | What it does |
@@ -347,7 +433,8 @@ Restart Claude Desktop after any change to server code or config.
347
433
  | `mmopen` | Open one message in MailMate's UI (via `open mid:…`). `--print` returns the URL. |
348
434
  | `mm-mailboxes` | List accounts, IMAP mailboxes (with optional counts), and smart-mailbox names. |
349
435
  | `mmtags` | List user tags applied to messages (with counts) or defined in Preferences. |
350
- | `mm-modify` | Mark read/flag/tag/archive a message via AppleScript; same-account `move` uses a fast `.eml`-rename path with no UI takeover. |
436
+ | `mm-modify` | Mark read/flag/tag/archive a message via AppleScript; same-account `move` uses a fast `.eml`-rename path with no UI takeover. `--check` confirms the change landed; `--emit-check` defers confirmation to `mm-verify`. |
437
+ | `mm-verify` | Batch-confirm `mm-modify --emit-check` tickets against the `#flags` index in one flush-wait. JSON in, JSON summary out. |
351
438
  | `mm-send` | Send mail through `emate` with a markdown body on stdin. |
352
439
  | `mm-draft` | Like `mm-send`, but only ever opens a draft — refuses `--send-now` (exit 2). |
353
440
  | `mmdiscover` | First-run bootstrap; (re-)writes the user config from MailMate's plists. |
@@ -364,7 +451,7 @@ The CLI tools take an `eml-id` — the integer filename of MailMate's `.eml` sto
364
451
  require "mailmate"
365
452
 
366
453
  # Parse and evaluate a MailMate smart-mailbox filter
367
- ast = Mailmate.compile_filter("from.name = 'Medium' and #date-received > '1 days ago'")
454
+ ast = Mailmate.compile_filter("from.name = 'Substack' and #date-received > '1 days ago'")
368
455
  # ... feed `ast` to Mailmate::Evaluator ...
369
456
 
370
457
  # Read the binary `#flags` index
data/exe/mm-verify ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+ require "mailmate"
6
+ require "mailmate/cli/verify"
7
+
8
+ exit Mailmate::CLI::Verify.run(ARGV)
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "optparse"
4
- require "mail"
4
+ # `mail` is required lazily inside run() — the --raw and --mailmate paths
5
+ # never parse the message, so they shouldn't pay ~50 ms loading the gem.
5
6
 
6
7
  module Mailmate
7
8
  module CLI
@@ -47,6 +48,7 @@ module Mailmate
47
48
  return 0
48
49
  end
49
50
 
51
+ require "mail"
50
52
  mail = Mail.read(path)
51
53
  print_headers(mail, eml_id, path) unless opts[:text_only]
52
54
  $stdout.puts text_body(mail, markdown: opts[:markdown])
@@ -91,6 +93,8 @@ module Mailmate
91
93
  $stdout.puts "message-id: #{mail.message_id}"
92
94
  thread_id = Mailmate::Attributes.thread_id_for(mail)
93
95
  $stdout.puts "thread-id: #{thread_id}" if thread_id
96
+ tags = user_tags(eml_id)
97
+ $stdout.puts "tags: #{tags.join(", ")}" unless tags.empty?
94
98
  if mail.attachments.any?
95
99
  $stdout.puts "attachments:"
96
100
  mail.attachments.each do |a|
@@ -105,6 +109,18 @@ module Mailmate
105
109
  $stdout.puts
106
110
  end
107
111
 
112
+ # User tags applied to this message, from MailMate's `#flags` index —
113
+ # tags live there as IMAP keywords, not in the .eml, so the parsed Mail
114
+ # can't see them. Drops `\…` (RFC) and `$…` (Apple/Thunderbird) system
115
+ # flags so only user-facing tags show. Same derivation as mmsearch's
116
+ # `tags` column. Returns [] when the index is missing or the message
117
+ # has none.
118
+ def user_tags(eml_id)
119
+ return [] if eml_id.nil?
120
+ flags = (Mailmate::IndexReader.for("#flags").flags_for(eml_id.to_i) rescue [])
121
+ flags.reject { |f| f.start_with?("\\", "$") }
122
+ end
123
+
108
124
  def text_body(mail, markdown: false)
109
125
  if mail.text_part
110
126
  # text/plain is already plain — markdown flag is a no-op here.
@@ -145,8 +161,11 @@ module Mailmate
145
161
  rescue LoadError => e
146
162
  warn "mmmessage --markdown needs the reverse_markdown gem (which pulls nokogiri)."
147
163
  warn "Install it with: gem install reverse_markdown"
148
- warn "(underlying: #{e.message})"
149
- exit 3
164
+ warn "(underlying: #{e.message}) — falling back to raw HTML."
165
+ # Degrade to the raw HTML rather than `exit`: a library method must
166
+ # not kill its host, and the in-process MCP server would otherwise
167
+ # die on the SystemExit (its dispatch rescues StandardError only).
168
+ return html
150
169
  end
151
170
  doc = Nokogiri::HTML(html)
152
171
  doc.css("style, script").remove