mailmate 1.2.0 → 1.4.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 +4 -4
- data/README.md +74 -2
- data/exe/mm-draft +8 -0
- data/lib/mailmate/cli/draft.rb +37 -0
- data/lib/mailmate/cli/message.rb +2 -0
- data/lib/mailmate/cli/modify.rb +27 -0
- data/lib/mailmate/cli/send.rb +30 -0
- data/lib/mailmate/mcp.rb +118 -10
- data/lib/mailmate/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2e41c1bf66c9a41c75d683f9e7edcfa2d6ac5982f6a54c2d3cc4b2cbb5c36a8
|
|
4
|
+
data.tar.gz: 378d1b75d3485de7cf5285e8b12f4fa0ebd7b144b9ebe4a5d4322f6a5d592726
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 710617dc65666c1eec32f4a6d1e7c4ac15c3aab6ce295548d59edaefacd86863ed2b716246bceb250dd9f7f67c4e3a54701366a3c4c65a6b69321b6df3024587
|
|
7
|
+
data.tar.gz: a54ee5f4a6cf7ff02c35f6239ab63e805404db1610fa470358ac382bf4870b1fb8176a0990e5a01a24195275e6c603fe44b2d124b9c6829765f222872f874a10
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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`, `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-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
|
|
|
@@ -25,6 +25,40 @@ mmsearch 'd 2026-05'
|
|
|
25
25
|
mmsearch 'f acme' 'id flags subject from' --limit 20 --no-align
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
**Quicksearch syntax.** The search-string is a list of specs combined with **AND** (`or`/parens not yet supported). Wrap multi-word terms in `"double quotes"`.
|
|
29
|
+
|
|
30
|
+
| Modifier | Scope |
|
|
31
|
+
|---|---|
|
|
32
|
+
| _bare term_ | From/To/Cc/Subject **or body** contains (the UI quicksearch default). `--headers-only` skips the body scan. |
|
|
33
|
+
| `f <term>` | From contains. |
|
|
34
|
+
| `t <term>` | To/Cc (recipients) contains. |
|
|
35
|
+
| `c <term>` | Cc contains. |
|
|
36
|
+
| `s <term>` | Subject contains. |
|
|
37
|
+
| `a <term>` | Any address header contains. |
|
|
38
|
+
| `b <term>` | Body (plain text) contains. |
|
|
39
|
+
| `m <term>` | Common headers OR body. |
|
|
40
|
+
| `d <date>` | Received date: `Y`, `Y-M`, `Y-M-D`, or relative `1d`/`2w`/`3m`/`1y`. |
|
|
41
|
+
| `T <tag>` | Tags / IMAP keywords (`K` is a synonym). |
|
|
42
|
+
| `!<value>` | Negate, e.g. `f !smith` = From does NOT contain smith. |
|
|
43
|
+
|
|
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.
|
|
45
|
+
|
|
46
|
+
**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
|
+
|
|
48
|
+
| Field | What it shows |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `id` | The `.eml` body-part ID (always emitted first). |
|
|
51
|
+
| `path` | Absolute file path. |
|
|
52
|
+
| `mailbox` | `<account>/<mailbox>` relative to the IMAP root. |
|
|
53
|
+
| `from`, `to`, `cc`, `bcc`, `reply-to` | Address headers (joined with `; `). |
|
|
54
|
+
| `subject` | Decoded subject. |
|
|
55
|
+
| `date` / `time` | `YYYY-MM-DD` / `HH:MM` (sender's timezone). |
|
|
56
|
+
| `message-id` | The `Message-ID` header. |
|
|
57
|
+
| `direction` | `→` outbound, `←` inbound (rendered as `dir`). |
|
|
58
|
+
| `party` | The other party — recipient(s) if outbound, sender if inbound. |
|
|
59
|
+
| `flags` | Two chars: position 1 archive-state (`A` archived / `P` primary), position 2 read-state (`R` read / `U` unread) — e.g. `AR`, `PU`. |
|
|
60
|
+
| `read` / `archive` | Standalone versions of the two `flags` positions. |
|
|
61
|
+
|
|
28
62
|
### `mmmessage` — read one message
|
|
29
63
|
|
|
30
64
|
```bash
|
|
@@ -146,12 +180,49 @@ EOF
|
|
|
146
180
|
mm-send -t friend@example.com -s "Photos" /path/to/photo1.jpg /path/to/photo2.jpg <<<"See attached."
|
|
147
181
|
```
|
|
148
182
|
|
|
183
|
+
#### Replies and threading
|
|
184
|
+
|
|
185
|
+
MailMate auto-generates the outgoing `Message-ID` for every send — never the caller's job. `In-Reply-To` and `References` are pure pass-through: whatever you set via `--header` ships verbatim, and **what you don't set is absent**. A reply with a `Re: …` subject but no threading headers shows up as a brand-new conversation in modern mail clients (they thread on headers, not subjects).
|
|
186
|
+
|
|
187
|
+
To make a reply land in-thread, pass both headers:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
mm-send -f you@x -t them@y -s "Re: foo" \
|
|
191
|
+
--header "In-Reply-To: <parent-message-id@domain>" \
|
|
192
|
+
--header "References: <root-mid> <parent-mid>" \
|
|
193
|
+
--send-now <<<"body"
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Construct `References` as the source message's `References` header (if any) with the source's `Message-ID` appended. If the source is itself a thread root with no `References`, just use its `Message-ID` alone.
|
|
197
|
+
|
|
198
|
+
The same passthrough applies to the `mailmate-mcp` `send` tool — see the `from`, `in_reply_to`, and `references` fields.
|
|
199
|
+
|
|
200
|
+
### `mm-draft` — compose without sending
|
|
201
|
+
|
|
202
|
+
`mm-draft` is identical to `mm-send` in every way except one: it **never sends**. It refuses the `--send-now` flag with a nonzero exit, so a "compose this but don't send it" instruction can't be silently defeated by passing the flag that flips `emate mailto` from draft-pause to send. Without `--send-now` the two commands already behave identically (open a draft window in MailMate and wait) — `mm-draft` just removes the ability to send at all.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Opens a draft in MailMate; never sends.
|
|
206
|
+
echo "Quick **markdown** body." | mm-draft -t friend@example.com -s "Hello"
|
|
207
|
+
|
|
208
|
+
# Threading headers and attachments work exactly as in mm-send.
|
|
209
|
+
mm-draft -f you@x -t them@y -s "Re: foo" \
|
|
210
|
+
--header "In-Reply-To: <parent-message-id@domain>" \
|
|
211
|
+
--header "References: <root-mid> <parent-mid>" <<<"body"
|
|
212
|
+
|
|
213
|
+
# Passing --send-now is refused (exit 2):
|
|
214
|
+
mm-draft -t friend@example.com -s "nope" --send-now <<<"body"
|
|
215
|
+
# → mm-draft: refusing --send-now — mm-draft only ever creates drafts. Use mm-send to send.
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Reach for `mm-draft` (or the `mailmate-mcp` `draft` tool) whenever the instruction is "write/compose but don't send" — it's the unambiguous, can't-misfire choice. The `draft` MCP tool mirrors `send` minus the `send_now` field.
|
|
219
|
+
|
|
149
220
|
### Why the names
|
|
150
221
|
|
|
151
222
|
The `mm` prefix is for tab completion: typing `mm<tab>` in a shell lists every command in the toolkit. The dash matters:
|
|
152
223
|
|
|
153
224
|
- **`mm<name>`** (no dash) — **read** operations. `mmsearch`, `mmmessage`, `mmopen`, `mmtags`, `mmdiscover` observe MailMate's on-disk state without changing it. (`mmopen` activates MailMate's UI but doesn't modify any message.)
|
|
154
|
-
- **`mm-<name>`** (with dash) — **write** operations. `mm-modify`, `mm-send` change state (or send mail). `mm-mailboxes` is an exception: read-only, but uses the dash to keep `mmm<tab>` free for the daily-driver `mmmessage`. Typing `mm-<tab>` filters to the write-leaning commands.
|
|
225
|
+
- **`mm-<name>`** (with dash) — **write** operations. `mm-modify`, `mm-send`, `mm-draft` change state (or compose/send mail). `mm-mailboxes` is an exception: read-only, but uses the dash to keep `mmm<tab>` free for the daily-driver `mmmessage`. Typing `mm-<tab>` filters to the write-leaning commands. (`mm-draft` only ever opens a draft — it never sends.)
|
|
155
226
|
|
|
156
227
|
## Limitations
|
|
157
228
|
|
|
@@ -278,6 +349,7 @@ Restart Claude Desktop after any change to server code or config.
|
|
|
278
349
|
| `mmtags` | List user tags applied to messages (with counts) or defined in Preferences. |
|
|
279
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. |
|
|
280
351
|
| `mm-send` | Send mail through `emate` with a markdown body on stdin. |
|
|
352
|
+
| `mm-draft` | Like `mm-send`, but only ever opens a draft — refuses `--send-now` (exit 2). |
|
|
281
353
|
| `mmdiscover` | First-run bootstrap; (re-)writes the user config from MailMate's plists. |
|
|
282
354
|
|
|
283
355
|
Each command takes `--help` for usage. Tab-completion: `mm<tab>` lists every command; `mms<tab>` → `mmsearch`; `mmm<tab>` → `mmmessage`; `mm-<tab>` lists the write-side commands.
|
data/exe/mm-draft
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mailmate/cli/send"
|
|
4
|
+
|
|
5
|
+
module Mailmate
|
|
6
|
+
module CLI
|
|
7
|
+
# `mm-draft` — identical to `mm-send` but guaranteed never to actually
|
|
8
|
+
# send. It refuses `--send-now` with a nonzero exit, so a "compose this
|
|
9
|
+
# but don't send it" instruction can't be silently defeated by the flag
|
|
10
|
+
# that flips `emate mailto` from draft-pause to send. Without `--send-now`
|
|
11
|
+
# both commands behave identically (open a draft window in MailMate); the
|
|
12
|
+
# only difference is that mm-draft *cannot* be talked into sending.
|
|
13
|
+
# @api private
|
|
14
|
+
module Draft
|
|
15
|
+
extend self
|
|
16
|
+
|
|
17
|
+
NOTE = <<~NOTE
|
|
18
|
+
mm-draft — same as `mm-send`, but it only ever opens a draft and refuses
|
|
19
|
+
`--send-now`. Use `mm-send` when you actually want to send. All other
|
|
20
|
+
flags pass through to `emate mailto` exactly as in mm-send (its help
|
|
21
|
+
follows below).
|
|
22
|
+
|
|
23
|
+
NOTE
|
|
24
|
+
|
|
25
|
+
# Returns the exit status. Refuses `--send-now` (exit 2); otherwise
|
|
26
|
+
# delegates verbatim to `Mailmate::CLI::Send`.
|
|
27
|
+
def run(argv)
|
|
28
|
+
if argv.include?("--send-now")
|
|
29
|
+
warn "mm-draft: refusing --send-now — mm-draft only ever creates drafts. Use mm-send to send."
|
|
30
|
+
return 2
|
|
31
|
+
end
|
|
32
|
+
warn NOTE if argv.include?("--help") || argv.include?("-h")
|
|
33
|
+
Send.run(argv)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/mailmate/cli/message.rb
CHANGED
|
@@ -89,6 +89,8 @@ module Mailmate
|
|
|
89
89
|
$stdout.puts "subject: #{mail.subject}"
|
|
90
90
|
$stdout.puts "date: #{Mailmate.localize(mail.date)&.iso8601}"
|
|
91
91
|
$stdout.puts "message-id: #{mail.message_id}"
|
|
92
|
+
thread_id = Mailmate::Attributes.thread_id_for(mail)
|
|
93
|
+
$stdout.puts "thread-id: #{thread_id}" if thread_id
|
|
92
94
|
if mail.attachments.any?
|
|
93
95
|
$stdout.puts "attachments:"
|
|
94
96
|
mail.attachments.each do |a|
|
data/lib/mailmate/cli/modify.rb
CHANGED
|
@@ -256,6 +256,13 @@ module Mailmate
|
|
|
256
256
|
driver = Mailmate::AppleScriptDriver.new(dry_run: opts[:dry_run])
|
|
257
257
|
mid_url = Mailmate::MidUrl.for(message_id)
|
|
258
258
|
|
|
259
|
+
# Opening the `mid:` URL displays the message in a viewer, which
|
|
260
|
+
# causes MailMate to mark it `\Seen`. If the caller's action chain
|
|
261
|
+
# neither sets nor clears the read state, snapshot it first and
|
|
262
|
+
# restore after the open so this side-effect doesn't silently
|
|
263
|
+
# change read state. Skip in dry-run — no real open happens.
|
|
264
|
+
preserve_unread = should_preserve_unread?(eml_id, actions, opts)
|
|
265
|
+
|
|
259
266
|
windows_before = driver.window_ids
|
|
260
267
|
driver.open_url(mid_url)
|
|
261
268
|
# Active wait: poll for the new viewer window instead of sleeping a
|
|
@@ -264,6 +271,15 @@ module Mailmate
|
|
|
264
271
|
# the timeout for this wait rather than the sleep duration.
|
|
265
272
|
new_windows = opts[:dry_run] ? [] : wait_for_new_window(driver, windows_before, timeout: opts[:settle])
|
|
266
273
|
|
|
274
|
+
# Restore unread BEFORE user actions: a subsequent move / archive /
|
|
275
|
+
# delete moves the message out of the viewer's selection, after
|
|
276
|
+
# which a `markAsUnread:` would land on whatever MailMate selected
|
|
277
|
+
# next (or be a silent no-op).
|
|
278
|
+
if preserve_unread
|
|
279
|
+
$stdout.puts "preserve-read-state: re-marking #{eml_id}.eml unread (opening the mid: URL marks it read)"
|
|
280
|
+
driver.perform("markAsUnread:")
|
|
281
|
+
end
|
|
282
|
+
|
|
267
283
|
# Send each action's selector without sleeping between them.
|
|
268
284
|
# AppleEvents queue per-app and process in order, so a subsequent
|
|
269
285
|
# `close window id N` will execute only after `perform` is committed.
|
|
@@ -312,6 +328,17 @@ module Mailmate
|
|
|
312
328
|
[]
|
|
313
329
|
end
|
|
314
330
|
|
|
331
|
+
# True iff we should re-mark the message unread after opening it.
|
|
332
|
+
# - Skip when any user action already touches read state (read/unread)
|
|
333
|
+
# — the user's chain wins.
|
|
334
|
+
# - Skip in dry-run — no real open, so no real read-flip to undo.
|
|
335
|
+
# - Otherwise: read the index; preserve only if currently unread.
|
|
336
|
+
def should_preserve_unread?(eml_id, actions, opts)
|
|
337
|
+
return false if opts[:dry_run]
|
|
338
|
+
return false if actions.any? { |name, _, _| name == "read" || name == "unread" }
|
|
339
|
+
!current_flags(eml_id).include?("\\Seen")
|
|
340
|
+
end
|
|
341
|
+
|
|
315
342
|
def current_flags(eml_id)
|
|
316
343
|
# AppleScript actions write the index asynchronously — bust just the
|
|
317
344
|
# #flags cache to pick up the latest values without throwing away
|
data/lib/mailmate/cli/send.rb
CHANGED
|
@@ -12,6 +12,35 @@ module Mailmate
|
|
|
12
12
|
|
|
13
13
|
EMATE_PATH = "/Applications/MailMate.app/Contents/Resources/emate"
|
|
14
14
|
|
|
15
|
+
PREAMBLE = <<~PREAMBLE
|
|
16
|
+
mm-send — thin wrapper around `emate mailto` with `--markup markdown` enforced.
|
|
17
|
+
Body is read from stdin. All other flags pass through to emate (its help follows).
|
|
18
|
+
|
|
19
|
+
Replies and threading
|
|
20
|
+
MailMate auto-generates the outgoing Message-ID — never your job.
|
|
21
|
+
In-Reply-To and References are PURE PASS-THROUGH: whatever you set via
|
|
22
|
+
`--header` ships verbatim; what you don't set is absent (and recipients'
|
|
23
|
+
clients will see the message as a fresh thread, no matter how `Re:` the
|
|
24
|
+
subject looks). To make a reply land in-thread, pass both:
|
|
25
|
+
|
|
26
|
+
mm-send -f you@x -t them@y -s "Re: foo" \\
|
|
27
|
+
--header "In-Reply-To: <parent-message-id@domain>" \\
|
|
28
|
+
--header "References: <root-mid> <parent-mid>" \\
|
|
29
|
+
--send-now <<<"body"
|
|
30
|
+
|
|
31
|
+
References is constructed as the source message's References header (if
|
|
32
|
+
any) with the source's Message-ID appended. If the source is a thread
|
|
33
|
+
root with no References, just use its Message-ID alone.
|
|
34
|
+
|
|
35
|
+
Identity selection
|
|
36
|
+
`-f <address>` picks which of MailMate's configured identities sends.
|
|
37
|
+
Without `-f`, MailMate uses its default identity. See `mmdiscover` to
|
|
38
|
+
list available addresses.
|
|
39
|
+
|
|
40
|
+
──────────────────────────── emate help follows ────────────────────────────
|
|
41
|
+
|
|
42
|
+
PREAMBLE
|
|
43
|
+
|
|
15
44
|
# Returns the exit status of the spawned `emate` invocation. Uses
|
|
16
45
|
# `system` (not `exec`) so the caller — and the test suite — can
|
|
17
46
|
# actually observe the result.
|
|
@@ -21,6 +50,7 @@ module Mailmate
|
|
|
21
50
|
warn "mm-send: emate not found at #{EMATE_PATH}. Is MailMate installed?"
|
|
22
51
|
return 1
|
|
23
52
|
end
|
|
53
|
+
warn PREAMBLE if argv.include?("--help") || argv.include?("-h")
|
|
24
54
|
system(EMATE_PATH, "mailto", "--markup", "markdown", *argv)
|
|
25
55
|
$?.exitstatus
|
|
26
56
|
end
|
data/lib/mailmate/mcp.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "mailmate/cli/search"
|
|
|
8
8
|
require "mailmate/cli/message"
|
|
9
9
|
require "mailmate/cli/modify"
|
|
10
10
|
require "mailmate/cli/send"
|
|
11
|
+
require "mailmate/cli/draft"
|
|
11
12
|
require "mailmate/cli/open"
|
|
12
13
|
require "mailmate/cli/mailboxes"
|
|
13
14
|
require "mailmate/cli/tags"
|
|
@@ -17,7 +18,7 @@ require "mailmate/mid_url"
|
|
|
17
18
|
|
|
18
19
|
module Mailmate
|
|
19
20
|
# Stdio MCP server (JSON-RPC 2.0, line-delimited). Exposes the gem's CLIs —
|
|
20
|
-
# search, message, modify, send — plus a resolve_id helper that round-trips
|
|
21
|
+
# search, message, modify, send, draft — plus a resolve_id helper that round-trips
|
|
21
22
|
# between local eml-id, RFC Message-ID, and the cross-machine message:// URL.
|
|
22
23
|
#
|
|
23
24
|
# In-process: each tool call runs the corresponding `Mailmate::CLI::*.run`
|
|
@@ -29,6 +30,46 @@ module Mailmate
|
|
|
29
30
|
PROTOCOL_VERSION = "2024-11-05"
|
|
30
31
|
SERVER_NAME = "mailmate"
|
|
31
32
|
|
|
33
|
+
# Server-level guidance returned from `initialize`. This is the gem's
|
|
34
|
+
# operational doctrine — the things a caller must know to drive MailMate
|
|
35
|
+
# correctly that don't fit in a single tool's description. It travels with
|
|
36
|
+
# the gem so the MCP is self-sufficient (no companion skill required).
|
|
37
|
+
INSTRUCTIONS = <<~INSTRUCTIONS.strip
|
|
38
|
+
Read and act on MailMate's local mail store on macOS. `search`, `message`,
|
|
39
|
+
`resolve_id`, and `list_*` are read-only; `send`, `draft`, `modify`, and
|
|
40
|
+
`open` require MailMate to be running (they drive the app).
|
|
41
|
+
|
|
42
|
+
Composing (send / draft)
|
|
43
|
+
- Bodies are Markdown; MailMate renders them to HTML on the way out. For
|
|
44
|
+
that to reach recipients, MailMate → Preferences → Composer must have
|
|
45
|
+
"Preview: Display" = Always and "Replying/Forwarding HTML" = Always
|
|
46
|
+
embed — otherwise recipients get plain text. These are global, one-time
|
|
47
|
+
settings.
|
|
48
|
+
- Set `from` explicitly. When the To: address matches one of the user's
|
|
49
|
+
own accounts, MailMate may otherwise send from that account.
|
|
50
|
+
- Prefer `draft` over `send` whenever the user said "don't send" / "just
|
|
51
|
+
draft it" — `draft` physically cannot send, so it's the safe choice.
|
|
52
|
+
`send` also opens a draft and waits unless you pass `send_now: true`.
|
|
53
|
+
- Threading: set BOTH `in_reply_to` and `references`. A "Re:" subject
|
|
54
|
+
alone does not thread in modern clients. MailMate generates the
|
|
55
|
+
outgoing Message-ID itself.
|
|
56
|
+
|
|
57
|
+
Modifying (modify)
|
|
58
|
+
- Drives MailMate's UI via AppleScript: it briefly takes focus, calls are
|
|
59
|
+
serial per app, and each call costs a few seconds (flag/tag writes are
|
|
60
|
+
async). Batch multiple actions into ONE call rather than many.
|
|
61
|
+
- Opening a message marks it \\Seen; read state is auto-preserved unless
|
|
62
|
+
your action chain itself includes read/unread.
|
|
63
|
+
- A Message-ID can live in several mailboxes (Sent + Received copies,
|
|
64
|
+
Gmail label copies). When it does, the action may land on a different
|
|
65
|
+
copy than the id you targeted.
|
|
66
|
+
|
|
67
|
+
Identifiers
|
|
68
|
+
- Tools accept a local eml-id (an integer; per-machine, NOT portable) or
|
|
69
|
+
an RFC Message-ID (portable across machines). Use `resolve_id` to
|
|
70
|
+
convert between them and to mint a cross-machine message:// URL.
|
|
71
|
+
INSTRUCTIONS
|
|
72
|
+
|
|
32
73
|
TOOLS = [
|
|
33
74
|
{
|
|
34
75
|
name: "search",
|
|
@@ -36,15 +77,31 @@ module Mailmate
|
|
|
36
77
|
Search MailMate's .eml files using MailMate's quicksearch syntax.
|
|
37
78
|
Returns column-aligned CSV. Same engine as the `mmsearch` CLI.
|
|
38
79
|
|
|
39
|
-
|
|
80
|
+
Query = space-separated specs combined with AND (no or/parens yet).
|
|
81
|
+
Quote multi-word terms ("rent due"). Modifiers:
|
|
82
|
+
(bare term) From/To/Cc/Subject OR body contains
|
|
83
|
+
f <t> From t <t> To/Cc c <t> Cc s <t> Subject
|
|
84
|
+
a <t> any address header b <t> body m <t> headers OR body
|
|
85
|
+
d <date> received: Y, Y-M, Y-M-D, or relative 1d/2w/3m/1y
|
|
86
|
+
T <tag> tag / IMAP keyword (K is a synonym)
|
|
87
|
+
!<value> negate, e.g. f !smith (From does NOT contain smith)
|
|
88
|
+
The `mailbox` arg also accepts a smart-mailbox name (e.g. Medium,
|
|
89
|
+
Whisper, Personal Inbox) whose filter is ANDed into the search.
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
40
92
|
query="f medium d 7d" from Medium in the last 7 days
|
|
41
|
-
query="T robot" tagged "robot"
|
|
93
|
+
query="T robot" tagged "robot"
|
|
42
94
|
query="s 'rent due' !draft" subject has 'rent due', not 'draft'
|
|
43
|
-
query="d 2026-05" received in May 2026
|
|
44
95
|
|
|
45
|
-
Fields default to: flags date time direction party subject
|
|
46
|
-
Prefix
|
|
47
|
-
|
|
96
|
+
Fields default to: flags date time direction party subject.
|
|
97
|
+
Prefix with "+" to add to the defaults ("+tags +mailbox"); a bare
|
|
98
|
+
list replaces them (id is always the first column). Meanings:
|
|
99
|
+
flags 2 chars — archive-state (A archived / P primary) then
|
|
100
|
+
read-state (R read / U unread), e.g. AR, PU
|
|
101
|
+
direction → outbound / ← inbound party the other party
|
|
102
|
+
read R/U archive A/P (standalone flag columns)
|
|
103
|
+
Other fields (from/to/cc/bcc/reply-to/subject/date/time/message-id/
|
|
104
|
+
path/mailbox) are the obvious header or location values.
|
|
48
105
|
DESC
|
|
49
106
|
inputSchema: {
|
|
50
107
|
type: "object",
|
|
@@ -118,22 +175,45 @@ module Mailmate
|
|
|
118
175
|
},
|
|
119
176
|
{
|
|
120
177
|
name: "send",
|
|
121
|
-
description: "Send mail via MailMate's `emate` (markdown body). Recipients and subject via fields; body is the markdown source.",
|
|
178
|
+
description: "Send mail via MailMate's `emate` (markdown body). Recipients and subject via fields; body is the markdown source. For replies, set `in_reply_to` and `references` so recipients' clients thread the message — without them a `Re:` subject alone is not enough. MailMate generates the outgoing Message-ID automatically.",
|
|
122
179
|
inputSchema: {
|
|
123
180
|
type: "object",
|
|
124
181
|
properties: {
|
|
182
|
+
from: { type: "string", description: "Sender identity (one of MailMate's configured addresses; see mmdiscover). If omitted, MailMate uses its default identity." },
|
|
125
183
|
to: { type: "string", description: "Recipient(s), comma-separated." },
|
|
126
184
|
cc: { type: "string", description: "CC recipient(s), comma-separated." },
|
|
127
185
|
bcc: { type: "string", description: "BCC recipient(s), comma-separated." },
|
|
128
186
|
subject: { type: "string", description: "Subject line." },
|
|
129
187
|
body: { type: "string", description: "Markdown body." },
|
|
130
188
|
attachments: { type: "array", items: { type: "string" }, description: "Absolute paths to files to attach." },
|
|
189
|
+
in_reply_to: { type: "string", description: "Message-ID of the parent message (with or without angle brackets). Sets the In-Reply-To header on the outgoing message so recipients' clients thread it correctly." },
|
|
190
|
+
references: { type: "string", description: "Space-separated chain of Message-IDs (with angle brackets). Conventionally: parent's References header + parent's Message-ID. Required alongside in_reply_to for clean threading in deep chains." },
|
|
131
191
|
send_now: { type: "boolean", description: "Send immediately (skip the Drafts pause)." },
|
|
132
192
|
},
|
|
133
193
|
required: %w[to subject body],
|
|
134
194
|
additionalProperties: false,
|
|
135
195
|
},
|
|
136
196
|
},
|
|
197
|
+
{
|
|
198
|
+
name: "draft",
|
|
199
|
+
description: "Compose a draft via MailMate's `emate` (markdown body) — IDENTICAL to `send` but it never sends: the draft opens in MailMate and waits. There is no `send_now` option; use this whenever the instruction is 'write/compose but don't send'. For replies, set `in_reply_to` and `references` so the draft threads correctly. MailMate generates the outgoing Message-ID automatically.",
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: "object",
|
|
202
|
+
properties: {
|
|
203
|
+
from: { type: "string", description: "Sender identity (one of MailMate's configured addresses; see mmdiscover). If omitted, MailMate uses its default identity." },
|
|
204
|
+
to: { type: "string", description: "Recipient(s), comma-separated." },
|
|
205
|
+
cc: { type: "string", description: "CC recipient(s), comma-separated." },
|
|
206
|
+
bcc: { type: "string", description: "BCC recipient(s), comma-separated." },
|
|
207
|
+
subject: { type: "string", description: "Subject line." },
|
|
208
|
+
body: { type: "string", description: "Markdown body." },
|
|
209
|
+
attachments: { type: "array", items: { type: "string" }, description: "Absolute paths to files to attach." },
|
|
210
|
+
in_reply_to: { type: "string", description: "Message-ID of the parent message (with or without angle brackets). Sets the In-Reply-To header so recipients' clients thread it correctly." },
|
|
211
|
+
references: { type: "string", description: "Space-separated chain of Message-IDs (with angle brackets). Conventionally: parent's References header + parent's Message-ID. Required alongside in_reply_to for clean threading in deep chains." },
|
|
212
|
+
},
|
|
213
|
+
required: %w[to subject body],
|
|
214
|
+
additionalProperties: false,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
137
217
|
{
|
|
138
218
|
name: "open",
|
|
139
219
|
description: "Open one MailMate message in MailMate's UI (activates the window). Accepts any of the id forms `resolve_id` takes. Read-side semantically, but does shift focus to MailMate.",
|
|
@@ -223,6 +303,7 @@ module Mailmate
|
|
|
223
303
|
protocolVersion: PROTOCOL_VERSION,
|
|
224
304
|
capabilities: { tools: {} },
|
|
225
305
|
serverInfo: { name: SERVER_NAME, version: Mailmate::VERSION },
|
|
306
|
+
instructions: INSTRUCTIONS,
|
|
226
307
|
}))
|
|
227
308
|
when "notifications/initialized", "notifications/cancelled"
|
|
228
309
|
# notifications — no response
|
|
@@ -246,6 +327,7 @@ module Mailmate
|
|
|
246
327
|
when "message" then call_message(args)
|
|
247
328
|
when "modify" then call_modify(args)
|
|
248
329
|
when "send" then call_send(args)
|
|
330
|
+
when "draft" then call_draft(args)
|
|
249
331
|
when "open" then call_open(args)
|
|
250
332
|
when "list_mailboxes" then call_list_mailboxes(args)
|
|
251
333
|
when "list_tags" then call_list_tags(args)
|
|
@@ -289,14 +371,40 @@ module Mailmate
|
|
|
289
371
|
end
|
|
290
372
|
|
|
291
373
|
def call_send(args)
|
|
374
|
+
argv = compose_argv(args)
|
|
375
|
+
argv << "--send-now" if args["send_now"]
|
|
376
|
+
with_stdin(args["body"].to_s) { run_cli(Mailmate::CLI::Send, argv) }
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# `draft` mirrors `send` but never sends — it has no send_now option and
|
|
380
|
+
# routes through CLI::Draft, which refuses `--send-now` outright.
|
|
381
|
+
def call_draft(args)
|
|
382
|
+
argv = compose_argv(args)
|
|
383
|
+
with_stdin(args["body"].to_s) { run_cli(Mailmate::CLI::Draft, argv) }
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Shared message-composition argv for both send and draft (everything bar
|
|
387
|
+
# the body, which is piped on stdin, and the send-only `--send-now`).
|
|
388
|
+
def compose_argv(args)
|
|
292
389
|
argv = []
|
|
390
|
+
argv.push("-f", args["from"].to_s) if args["from"]
|
|
293
391
|
argv.push("-t", args["to"].to_s) if args["to"]
|
|
294
392
|
argv.push("-c", args["cc"].to_s) if args["cc"]
|
|
295
393
|
argv.push("-b", args["bcc"].to_s) if args["bcc"]
|
|
296
394
|
argv.push("-s", args["subject"].to_s) if args["subject"]
|
|
297
|
-
argv
|
|
395
|
+
argv.push("--header", "In-Reply-To: #{bracket_mid(args["in_reply_to"])}") if args["in_reply_to"]
|
|
396
|
+
argv.push("--header", "References: #{args["references"]}") if args["references"]
|
|
298
397
|
Array(args["attachments"]).each { |p| argv << p.to_s }
|
|
299
|
-
|
|
398
|
+
argv
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Wrap a bare Message-ID in `<…>` if it doesn't already have them. Both
|
|
402
|
+
# forms are valid input to the MCP for ergonomics; the header value
|
|
403
|
+
# going on the wire needs the brackets per RFC 5322.
|
|
404
|
+
def bracket_mid(id)
|
|
405
|
+
s = id.to_s.strip
|
|
406
|
+
return s if s.start_with?("<") && s.end_with?(">")
|
|
407
|
+
"<#{s}>"
|
|
300
408
|
end
|
|
301
409
|
|
|
302
410
|
def call_open(args)
|
data/lib/mailmate/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mailmate
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Murphy-Dye
|
|
@@ -94,6 +94,7 @@ email:
|
|
|
94
94
|
- brian@murphydye.com
|
|
95
95
|
executables:
|
|
96
96
|
- mailmate-mcp
|
|
97
|
+
- mm-draft
|
|
97
98
|
- mm-mailboxes
|
|
98
99
|
- mm-modify
|
|
99
100
|
- mm-send
|
|
@@ -109,6 +110,7 @@ files:
|
|
|
109
110
|
- README.md
|
|
110
111
|
- config.yml.example
|
|
111
112
|
- exe/mailmate-mcp
|
|
113
|
+
- exe/mm-draft
|
|
112
114
|
- exe/mm-mailboxes
|
|
113
115
|
- exe/mm-modify
|
|
114
116
|
- exe/mm-send
|
|
@@ -122,6 +124,7 @@ files:
|
|
|
122
124
|
- lib/mailmate/ast.rb
|
|
123
125
|
- lib/mailmate/attributes.rb
|
|
124
126
|
- lib/mailmate/cli/discover.rb
|
|
127
|
+
- lib/mailmate/cli/draft.rb
|
|
125
128
|
- lib/mailmate/cli/mailboxes.rb
|
|
126
129
|
- lib/mailmate/cli/message.rb
|
|
127
130
|
- lib/mailmate/cli/modify.rb
|
|
@@ -166,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
166
169
|
- !ruby/object:Gem::Version
|
|
167
170
|
version: '0'
|
|
168
171
|
requirements: []
|
|
169
|
-
rubygems_version:
|
|
172
|
+
rubygems_version: 4.0.12
|
|
170
173
|
specification_version: 4
|
|
171
174
|
summary: Ruby toolkit for MailMate on macOS — search, read, modify, send, and smart-mailbox
|
|
172
175
|
evaluation
|