mailmate 1.1.0 → 1.3.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 +59 -6
- data/exe/mm-draft +8 -0
- data/lib/mailmate/applescript_driver.rb +9 -2
- data/lib/mailmate/cli/draft.rb +37 -0
- data/lib/mailmate/cli/message.rb +2 -0
- data/lib/mailmate/cli/modify.rb +58 -8
- data/lib/mailmate/cli/open.rb +8 -4
- data/lib/mailmate/cli/send.rb +30 -0
- data/lib/mailmate/mcp.rb +55 -4
- 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: '048aab794be796fae6993d06f902be520dd044a3e09b65ea8f673a127c670de0'
|
|
4
|
+
data.tar.gz: 17f3dcd902490b4077676f61c50e5c600b14382a6b753046d75e5e4b7d76f611
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a05405be8bf33235663c439d96af6afab59c9dbdd6a1a238d3746affe964c1bb4cbc9e7865a1a5c1f69276fe92598605f5814bc3cd3b31e3607c7e89d5957791
|
|
7
|
+
data.tar.gz: d722a40a671b8d44b5106a6a094cbf78d7886c7d85d1a048dabe8767453e235d3272ead4361c4655b2b2aef562a26659a77a810863d14760fe3f85b41f176859
|
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
|
|
|
@@ -60,6 +60,10 @@ mmopen 'message://%3Cabc%40example.com%3E'
|
|
|
60
60
|
|
|
61
61
|
# Print the mid: URL instead of opening it (useful in pipelines)
|
|
62
62
|
mmopen 183715 --print
|
|
63
|
+
|
|
64
|
+
# Spawn the viewer in the background — MailMate stays where it is and
|
|
65
|
+
# your keyboard focus is not stolen. Useful when scripting / cross-app.
|
|
66
|
+
mmopen 183715 --background # also: -g
|
|
63
67
|
```
|
|
64
68
|
|
|
65
69
|
### `mm-mailboxes` — list accounts and mailboxes
|
|
@@ -113,6 +117,11 @@ mm-modify 183715 archive --dry-run
|
|
|
113
117
|
|
|
114
118
|
# Verify the new flags after acting
|
|
115
119
|
mm-modify 183715 read --verify
|
|
120
|
+
|
|
121
|
+
# As of 1.2.0, `--verify` works in `--dry-run` mode too — pair them as a
|
|
122
|
+
# post-hoc state probe (run the action first, then re-run with --dry-run
|
|
123
|
+
# --verify to confirm the change took effect).
|
|
124
|
+
mm-modify 183715 read --dry-run --verify
|
|
116
125
|
```
|
|
117
126
|
|
|
118
127
|
**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.
|
|
@@ -137,22 +146,63 @@ EOF
|
|
|
137
146
|
mm-send -t friend@example.com -s "Photos" /path/to/photo1.jpg /path/to/photo2.jpg <<<"See attached."
|
|
138
147
|
```
|
|
139
148
|
|
|
149
|
+
#### Replies and threading
|
|
150
|
+
|
|
151
|
+
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).
|
|
152
|
+
|
|
153
|
+
To make a reply land in-thread, pass both headers:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
mm-send -f you@x -t them@y -s "Re: foo" \
|
|
157
|
+
--header "In-Reply-To: <parent-message-id@domain>" \
|
|
158
|
+
--header "References: <root-mid> <parent-mid>" \
|
|
159
|
+
--send-now <<<"body"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
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.
|
|
163
|
+
|
|
164
|
+
The same passthrough applies to the `mailmate-mcp` `send` tool — see the `from`, `in_reply_to`, and `references` fields.
|
|
165
|
+
|
|
166
|
+
### `mm-draft` — compose without sending
|
|
167
|
+
|
|
168
|
+
`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.
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# Opens a draft in MailMate; never sends.
|
|
172
|
+
echo "Quick **markdown** body." | mm-draft -t friend@example.com -s "Hello"
|
|
173
|
+
|
|
174
|
+
# Threading headers and attachments work exactly as in mm-send.
|
|
175
|
+
mm-draft -f you@x -t them@y -s "Re: foo" \
|
|
176
|
+
--header "In-Reply-To: <parent-message-id@domain>" \
|
|
177
|
+
--header "References: <root-mid> <parent-mid>" <<<"body"
|
|
178
|
+
|
|
179
|
+
# Passing --send-now is refused (exit 2):
|
|
180
|
+
mm-draft -t friend@example.com -s "nope" --send-now <<<"body"
|
|
181
|
+
# → mm-draft: refusing --send-now — mm-draft only ever creates drafts. Use mm-send to send.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
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.
|
|
185
|
+
|
|
140
186
|
### Why the names
|
|
141
187
|
|
|
142
188
|
The `mm` prefix is for tab completion: typing `mm<tab>` in a shell lists every command in the toolkit. The dash matters:
|
|
143
189
|
|
|
144
190
|
- **`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.)
|
|
145
|
-
- **`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.
|
|
191
|
+
- **`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.)
|
|
146
192
|
|
|
147
193
|
## Limitations
|
|
148
194
|
|
|
149
195
|
A few rough edges to be aware of:
|
|
150
196
|
|
|
151
|
-
1. **Non-move `mm-modify` actions
|
|
152
|
-
|
|
153
|
-
|
|
197
|
+
1. **Non-move `mm-modify` actions briefly spawn a MailMate viewer window.** Same-account `move` actions use a fast path — a direct `.eml` rename on disk — so they're entirely silent. Everything else (`read`, `flag`, `tag`, `archive`, `junk`, `delete`, etc.) drives MailMate's URL handler: each invocation opens a message-viewer window via the `mid:` URL in the background, runs AppleScript key-binding selectors against it, then closes the window.
|
|
198
|
+
|
|
199
|
+
**As of 1.2.0 this runs in the background.** MailMate is not brought to the foreground, your keyboard focus stays in whatever app you were using, and you can keep working — even during bulk loops. The viewer windows still appear briefly in MailMate's own window list before closing, but they don't take over your screen. The previous behavior (full-screen takeover, close-keystroke landing on the wrong window) is gone.
|
|
154
200
|
|
|
155
|
-
|
|
201
|
+
Two residual caveats:
|
|
202
|
+
- **Don't bring MailMate to the foreground during a run.** If you click into MailMate or Cmd-Tab to it while `mm-modify` is mid-flight, MailMate's responder chain shifts and subsequent perform-selectors may misbehave. Wait for the run to finish.
|
|
203
|
+
- **MailMate must be running.** See #3.
|
|
204
|
+
|
|
205
|
+
The `--keep-window` flag leaves the spawned viewers open if you want to inspect them. Batch multiple actions into one `mm-modify` invocation when you can — they share a single open/close cycle (or skip it entirely for pure-move).
|
|
156
206
|
|
|
157
207
|
2. **`eml-id` is machine-local; prefer `Message-ID:`.** The integer eml-id (also shown as MailMate's "Msg ID" column) is just the filename of the `.eml` on disk and differs on every install — copy/pasting an eml-id from your desktop to your laptop will refer to a different message (or none at all). For anything you want to keep, store the RFC `Message-ID:` header (which `mmmessage` prints) and pass that to the CLIs. The `mid:%3C<message-id>%3E` URL scheme works portably for the same reason.
|
|
158
208
|
|
|
@@ -162,6 +212,8 @@ A few rough edges to be aware of:
|
|
|
162
212
|
|
|
163
213
|
## Status
|
|
164
214
|
|
|
215
|
+
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.
|
|
216
|
+
|
|
165
217
|
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.
|
|
166
218
|
|
|
167
219
|
1.0.0 — initial public release; API stable from this point. Breaking changes bump the major version going forward.
|
|
@@ -263,6 +315,7 @@ Restart Claude Desktop after any change to server code or config.
|
|
|
263
315
|
| `mmtags` | List user tags applied to messages (with counts) or defined in Preferences. |
|
|
264
316
|
| `mm-modify` | Mark read/flag/tag/archive a message via AppleScript; same-account `move` uses a fast `.eml`-rename path with no UI takeover. |
|
|
265
317
|
| `mm-send` | Send mail through `emate` with a markdown body on stdin. |
|
|
318
|
+
| `mm-draft` | Like `mm-send`, but only ever opens a draft — refuses `--send-now` (exit 2). |
|
|
266
319
|
| `mmdiscover` | First-run bootstrap; (re-)writes the user config from MailMate's plists. |
|
|
267
320
|
|
|
268
321
|
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
|
@@ -32,13 +32,20 @@ module Mailmate
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Open a URL in MailMate (used for `mid:` URLs to select a message).
|
|
35
|
+
# Uses `open -g -a MailMate` so the URL is dispatched to MailMate's URL
|
|
36
|
+
# handler without bringing MailMate to the foreground — keeping the
|
|
37
|
+
# user's keyboard focus where it was. The viewer window is still spawned
|
|
38
|
+
# for `mid:` URLs. The AppleScript-native alternative
|
|
39
|
+
# (`tell application "MailMate" to open location`) was tried but is
|
|
40
|
+
# synchronous and can hang on `mid:` URLs while MailMate processes the
|
|
41
|
+
# spawn — `open -g` is asynchronous and avoids the wait.
|
|
35
42
|
def open_url(url)
|
|
36
43
|
Mailmate::PlatformError.check_darwin!(component: "AppleScriptDriver") unless dry_run
|
|
37
44
|
if dry_run
|
|
38
|
-
@output.puts "DRY: open -a MailMate #{url.inspect}"
|
|
45
|
+
@output.puts "DRY: open -g -a MailMate #{url.inspect}"
|
|
39
46
|
return
|
|
40
47
|
end
|
|
41
|
-
success = system("open", "-a", "MailMate", url)
|
|
48
|
+
success = system("/usr/bin/open", "-g", "-a", "MailMate", url)
|
|
42
49
|
raise Error, "open command failed for #{url}" unless success
|
|
43
50
|
end
|
|
44
51
|
|
|
@@ -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
|
@@ -98,9 +98,9 @@ module Mailmate
|
|
|
98
98
|
mute Toggle mute state
|
|
99
99
|
delete Delete (move to trash)
|
|
100
100
|
BANNER
|
|
101
|
-
o.on("--verify", "
|
|
101
|
+
o.on("--verify", "Print the message's current flags. Works with --dry-run as a post-hoc 'check state' probe (run the action first, then re-run with --dry-run --verify).") { opts[:verify] = true }
|
|
102
102
|
o.on("--dry-run", "Print the actions; don't run") { opts[:dry_run] = true }
|
|
103
|
-
o.on("--settle SECONDS", Float, "
|
|
103
|
+
o.on("--settle SECONDS", Float, "Timeout for waiting for MailMate's viewer window to spawn after open_url (default 3.5)") { |s| opts[:settle] = s }
|
|
104
104
|
o.on("--keep-window", "Don't close the spawned message-viewer window") { opts[:keep_window] = true }
|
|
105
105
|
end
|
|
106
106
|
parser.parse!(argv)
|
|
@@ -256,11 +256,33 @@ 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
|
-
|
|
262
|
-
|
|
268
|
+
# Active wait: poll for the new viewer window instead of sleeping a
|
|
269
|
+
# fixed `settle`. Windows typically spawn in 200-500ms; the previous
|
|
270
|
+
# fixed 3.5s sleep was wildly conservative. `--settle` now controls
|
|
271
|
+
# the timeout for this wait rather than the sleep duration.
|
|
272
|
+
new_windows = opts[:dry_run] ? [] : wait_for_new_window(driver, windows_before, timeout: opts[:settle])
|
|
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
|
|
263
282
|
|
|
283
|
+
# Send each action's selector without sleeping between them.
|
|
284
|
+
# AppleEvents queue per-app and process in order, so a subsequent
|
|
285
|
+
# `close window id N` will execute only after `perform` is committed.
|
|
264
286
|
actions.each do |name, selector, args|
|
|
265
287
|
case selector
|
|
266
288
|
when :ensure_flagged, :ensure_not_flagged
|
|
@@ -271,16 +293,19 @@ module Mailmate
|
|
|
271
293
|
$stdout.puts "#{name}: already #{want ? "flagged" : "not flagged"} — no-op"
|
|
272
294
|
else
|
|
273
295
|
driver.perform("toggleFlag:")
|
|
274
|
-
sleep(opts[:settle]) unless opts[:dry_run]
|
|
275
296
|
end
|
|
276
297
|
else
|
|
277
298
|
driver.perform(selector, *args)
|
|
278
|
-
sleep(opts[:settle]) unless opts[:dry_run]
|
|
279
299
|
end
|
|
280
300
|
end
|
|
281
301
|
|
|
282
|
-
|
|
283
|
-
|
|
302
|
+
# --verify is now permitted in --dry-run mode so callers can use
|
|
303
|
+
# `mm-modify <id> <any-action> --dry-run --verify` as a post-hoc
|
|
304
|
+
# "what's the current flag state?" probe after a separate action run.
|
|
305
|
+
# In non-dry-run mode, a short fixed wait gives MailMate time to
|
|
306
|
+
# flush #flags before we read it back; in dry-run no wait is needed.
|
|
307
|
+
if opts[:verify]
|
|
308
|
+
sleep(1) unless opts[:dry_run]
|
|
284
309
|
$stdout.puts "Flags now: #{current_flags(eml_id).inspect}"
|
|
285
310
|
end
|
|
286
311
|
|
|
@@ -289,6 +314,31 @@ module Mailmate
|
|
|
289
314
|
end
|
|
290
315
|
end
|
|
291
316
|
|
|
317
|
+
# Poll for a new MailMate window appearing in `driver.window_ids` that
|
|
318
|
+
# isn't in `windows_before`. Returns the set of new window IDs, or an
|
|
319
|
+
# empty array if `timeout` elapses without a new window appearing.
|
|
320
|
+
def wait_for_new_window(driver, windows_before, timeout:, poll: 0.05)
|
|
321
|
+
deadline = Time.now + timeout
|
|
322
|
+
loop do
|
|
323
|
+
diff = driver.window_ids - windows_before
|
|
324
|
+
return diff unless diff.empty?
|
|
325
|
+
break if Time.now >= deadline
|
|
326
|
+
sleep(poll)
|
|
327
|
+
end
|
|
328
|
+
[]
|
|
329
|
+
end
|
|
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
|
+
|
|
292
342
|
def current_flags(eml_id)
|
|
293
343
|
# AppleScript actions write the index asynchronously — bust just the
|
|
294
344
|
# #flags cache to pick up the latest values without throwing away
|
data/lib/mailmate/cli/open.rb
CHANGED
|
@@ -44,26 +44,30 @@ module Mailmate
|
|
|
44
44
|
return 0
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
cmd = ["/usr/bin/open"]
|
|
48
|
+
cmd << "-g" if opts[:background]
|
|
49
|
+
cmd << url
|
|
50
|
+
system(*cmd)
|
|
48
51
|
$?.exitstatus
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
def parse_options(argv)
|
|
52
|
-
opts = { print_only: false }
|
|
55
|
+
opts = { print_only: false, background: false }
|
|
53
56
|
OptionParser.new do |o|
|
|
54
|
-
o.banner = "Usage: mmopen <id> [--print]"
|
|
57
|
+
o.banner = "Usage: mmopen <id> [--print] [--background|-g]"
|
|
55
58
|
o.separator ""
|
|
56
59
|
o.separator "Open a MailMate message in MailMate's UI. <id> can be a local"
|
|
57
60
|
o.separator "eml-id, an RFC Message-ID (with or without angle brackets), or"
|
|
58
61
|
o.separator "a message://… or mid:… URL."
|
|
59
62
|
o.on("--print", "Print the mid: URL instead of opening it (for piping)") { opts[:print_only] = true }
|
|
63
|
+
o.on("-g", "--background", "Open without bringing MailMate to the foreground") { opts[:background] = true }
|
|
60
64
|
end.parse!(argv)
|
|
61
65
|
opts
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
def usage_error(msg)
|
|
65
69
|
warn "mmopen: #{msg}"
|
|
66
|
-
warn "Usage: mmopen <id> [--print]"
|
|
70
|
+
warn "Usage: mmopen <id> [--print] [--background|-g]"
|
|
67
71
|
2
|
|
68
72
|
end
|
|
69
73
|
end
|
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`
|
|
@@ -118,22 +119,45 @@ module Mailmate
|
|
|
118
119
|
},
|
|
119
120
|
{
|
|
120
121
|
name: "send",
|
|
121
|
-
description: "Send mail via MailMate's `emate` (markdown body). Recipients and subject via fields; body is the markdown source.",
|
|
122
|
+
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
123
|
inputSchema: {
|
|
123
124
|
type: "object",
|
|
124
125
|
properties: {
|
|
126
|
+
from: { type: "string", description: "Sender identity (one of MailMate's configured addresses; see mmdiscover). If omitted, MailMate uses its default identity." },
|
|
125
127
|
to: { type: "string", description: "Recipient(s), comma-separated." },
|
|
126
128
|
cc: { type: "string", description: "CC recipient(s), comma-separated." },
|
|
127
129
|
bcc: { type: "string", description: "BCC recipient(s), comma-separated." },
|
|
128
130
|
subject: { type: "string", description: "Subject line." },
|
|
129
131
|
body: { type: "string", description: "Markdown body." },
|
|
130
132
|
attachments: { type: "array", items: { type: "string" }, description: "Absolute paths to files to attach." },
|
|
133
|
+
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." },
|
|
134
|
+
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
135
|
send_now: { type: "boolean", description: "Send immediately (skip the Drafts pause)." },
|
|
132
136
|
},
|
|
133
137
|
required: %w[to subject body],
|
|
134
138
|
additionalProperties: false,
|
|
135
139
|
},
|
|
136
140
|
},
|
|
141
|
+
{
|
|
142
|
+
name: "draft",
|
|
143
|
+
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.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
from: { type: "string", description: "Sender identity (one of MailMate's configured addresses; see mmdiscover). If omitted, MailMate uses its default identity." },
|
|
148
|
+
to: { type: "string", description: "Recipient(s), comma-separated." },
|
|
149
|
+
cc: { type: "string", description: "CC recipient(s), comma-separated." },
|
|
150
|
+
bcc: { type: "string", description: "BCC recipient(s), comma-separated." },
|
|
151
|
+
subject: { type: "string", description: "Subject line." },
|
|
152
|
+
body: { type: "string", description: "Markdown body." },
|
|
153
|
+
attachments: { type: "array", items: { type: "string" }, description: "Absolute paths to files to attach." },
|
|
154
|
+
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." },
|
|
155
|
+
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." },
|
|
156
|
+
},
|
|
157
|
+
required: %w[to subject body],
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
137
161
|
{
|
|
138
162
|
name: "open",
|
|
139
163
|
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.",
|
|
@@ -246,6 +270,7 @@ module Mailmate
|
|
|
246
270
|
when "message" then call_message(args)
|
|
247
271
|
when "modify" then call_modify(args)
|
|
248
272
|
when "send" then call_send(args)
|
|
273
|
+
when "draft" then call_draft(args)
|
|
249
274
|
when "open" then call_open(args)
|
|
250
275
|
when "list_mailboxes" then call_list_mailboxes(args)
|
|
251
276
|
when "list_tags" then call_list_tags(args)
|
|
@@ -289,14 +314,40 @@ module Mailmate
|
|
|
289
314
|
end
|
|
290
315
|
|
|
291
316
|
def call_send(args)
|
|
317
|
+
argv = compose_argv(args)
|
|
318
|
+
argv << "--send-now" if args["send_now"]
|
|
319
|
+
with_stdin(args["body"].to_s) { run_cli(Mailmate::CLI::Send, argv) }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# `draft` mirrors `send` but never sends — it has no send_now option and
|
|
323
|
+
# routes through CLI::Draft, which refuses `--send-now` outright.
|
|
324
|
+
def call_draft(args)
|
|
325
|
+
argv = compose_argv(args)
|
|
326
|
+
with_stdin(args["body"].to_s) { run_cli(Mailmate::CLI::Draft, argv) }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Shared message-composition argv for both send and draft (everything bar
|
|
330
|
+
# the body, which is piped on stdin, and the send-only `--send-now`).
|
|
331
|
+
def compose_argv(args)
|
|
292
332
|
argv = []
|
|
333
|
+
argv.push("-f", args["from"].to_s) if args["from"]
|
|
293
334
|
argv.push("-t", args["to"].to_s) if args["to"]
|
|
294
335
|
argv.push("-c", args["cc"].to_s) if args["cc"]
|
|
295
336
|
argv.push("-b", args["bcc"].to_s) if args["bcc"]
|
|
296
337
|
argv.push("-s", args["subject"].to_s) if args["subject"]
|
|
297
|
-
argv
|
|
338
|
+
argv.push("--header", "In-Reply-To: #{bracket_mid(args["in_reply_to"])}") if args["in_reply_to"]
|
|
339
|
+
argv.push("--header", "References: #{args["references"]}") if args["references"]
|
|
298
340
|
Array(args["attachments"]).each { |p| argv << p.to_s }
|
|
299
|
-
|
|
341
|
+
argv
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Wrap a bare Message-ID in `<…>` if it doesn't already have them. Both
|
|
345
|
+
# forms are valid input to the MCP for ergonomics; the header value
|
|
346
|
+
# going on the wire needs the brackets per RFC 5322.
|
|
347
|
+
def bracket_mid(id)
|
|
348
|
+
s = id.to_s.strip
|
|
349
|
+
return s if s.start_with?("<") && s.end_with?(">")
|
|
350
|
+
"<#{s}>"
|
|
300
351
|
end
|
|
301
352
|
|
|
302
353
|
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.3.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
|