mailmate 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +212 -0
- data/config.yml.example +21 -0
- data/exe/mm-modify +8 -0
- data/exe/mm-send +8 -0
- data/exe/mmdiscover +8 -0
- data/exe/mmmessage +8 -0
- data/exe/mmsearch +8 -0
- data/lib/mailmate/applescript_driver.rb +103 -0
- data/lib/mailmate/ast.rb +33 -0
- data/lib/mailmate/attributes.rb +289 -0
- data/lib/mailmate/cli/discover.rb +170 -0
- data/lib/mailmate/cli/message.rb +109 -0
- data/lib/mailmate/cli/modify.rb +190 -0
- data/lib/mailmate/cli/search.rb +609 -0
- data/lib/mailmate/cli/send.rb +29 -0
- data/lib/mailmate/config.rb +134 -0
- data/lib/mailmate/duplicate_scanner.rb +65 -0
- data/lib/mailmate/eml_lookup.rb +86 -0
- data/lib/mailmate/evaluator.rb +93 -0
- data/lib/mailmate/filter_classifier.rb +123 -0
- data/lib/mailmate/header_reader.rb +74 -0
- data/lib/mailmate/identity.rb +35 -0
- data/lib/mailmate/index_reader.rb +126 -0
- data/lib/mailmate/lexer.rb +136 -0
- data/lib/mailmate/mailbox_graph.rb +77 -0
- data/lib/mailmate/message.rb +31 -0
- data/lib/mailmate/mid_url.rb +23 -0
- data/lib/mailmate/operators.rb +110 -0
- data/lib/mailmate/parser.rb +218 -0
- data/lib/mailmate/platform_error.rb +21 -0
- data/lib/mailmate/source_resolver.rb +104 -0
- data/lib/mailmate/var_resolver.rb +108 -0
- data/lib/mailmate/version.rb +5 -0
- data/lib/mailmate.rb +73 -0
- metadata +146 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 79154ae28a8d2c2aaf816e6719da7ee549f59db9a9425c19fd14971fa17c2752
|
|
4
|
+
data.tar.gz: 3ebd8963699820c5a10a777ea1d0013108301f268bd4a59939e5a9cf54649747
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0f709dfccd86d6abb7e82ec1da45fc7f84745a6967f7e9fdaa90b1b90ae015671271102016f03aea80262ce8ff6ca109d7d8b5365d012c95ab4cc57c102e43ed
|
|
7
|
+
data.tar.gz: 1da1debaba2cb1260b8b033efb742ec45bca0b37c5b041f87492367bbf0f9efda29cf117723cd1bf027d7b84ccfd4947f8dc84d545759b36ad8f346b0eb4121c
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brian Murphy-Dye
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# mailmate
|
|
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`, `mm-modify`, `mm-send`, `mmdiscover`) for searching, reading, modifying, and sending mail.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Example usage
|
|
8
|
+
|
|
9
|
+
### `mmsearch` — find messages
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Default: today's mail, all mailboxes
|
|
13
|
+
mmsearch
|
|
14
|
+
|
|
15
|
+
# From "Medium" in the last 7 days
|
|
16
|
+
mmsearch 'f medium d 7d'
|
|
17
|
+
|
|
18
|
+
# Subject contains "rent due", not the word "draft"
|
|
19
|
+
mmsearch 's "rent due" !draft'
|
|
20
|
+
|
|
21
|
+
# Received in May 2026
|
|
22
|
+
mmsearch 'd 2026-05'
|
|
23
|
+
|
|
24
|
+
# Custom columns + cap results + raw CSV (no padding)
|
|
25
|
+
mmsearch 'f acme' 'id flags subject from' --limit 20 --no-align
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### `mmmessage` — read one message
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# By local eml-id (the integer Msg ID column in MailMate)
|
|
32
|
+
mmmessage 183715
|
|
33
|
+
|
|
34
|
+
# By portable Message-ID (quote it — the angle brackets are shell metacharacters)
|
|
35
|
+
mmmessage '<CA+abc123@mail.example.com>'
|
|
36
|
+
|
|
37
|
+
# Raw .eml bytes (e.g. to pipe into `mail` parsers)
|
|
38
|
+
mmmessage 183715 --raw
|
|
39
|
+
|
|
40
|
+
# Body only, no headers block
|
|
41
|
+
mmmessage 183715 --text-only
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `mm-modify` — change message state
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Mark a message read, flag it, and archive it — one open/wait cycle
|
|
48
|
+
mm-modify 183715 read flag archive
|
|
49
|
+
|
|
50
|
+
# Add a tag
|
|
51
|
+
mm-modify 183715 tag urgent
|
|
52
|
+
|
|
53
|
+
# Dry-run first
|
|
54
|
+
mm-modify 183715 archive --dry-run
|
|
55
|
+
|
|
56
|
+
# Verify the new flags after acting
|
|
57
|
+
mm-modify 183715 read --verify
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `mm-send` — send mail
|
|
61
|
+
|
|
62
|
+
`mm-send` is a thin wrapper around MailMate's bundled `emate mailto`, with `--markup markdown` enforced. The body is read from stdin.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# One-liner
|
|
66
|
+
echo "Quick **markdown** body." | mm-send -t friend@example.com -s "Hello"
|
|
67
|
+
|
|
68
|
+
# Heredoc with cc + send-now
|
|
69
|
+
mm-send -t friend@example.com -c cc@example.com -s "Status update" --send-now <<'EOF'
|
|
70
|
+
## Update
|
|
71
|
+
|
|
72
|
+
- shipped the thing
|
|
73
|
+
- on to the next
|
|
74
|
+
EOF
|
|
75
|
+
|
|
76
|
+
# Attach files (positional args after options)
|
|
77
|
+
mm-send -t friend@example.com -s "Photos" /path/to/photo1.jpg /path/to/photo2.jpg <<<"See attached."
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Why the names
|
|
81
|
+
|
|
82
|
+
The `mm` prefix is for tab completion: typing `mm<tab>` in a shell lists every command in the toolkit. The dash matters:
|
|
83
|
+
|
|
84
|
+
- **`mm<name>`** (no dash) — **read** operations. `mmsearch`, `mmmessage`, `mmdiscover` only observe MailMate's on-disk state.
|
|
85
|
+
- **`mm-<name>`** (with dash) — **write** operations. `mm-modify`, `mm-send` change state (or send mail). Typing `mm-<tab>` filters to just the write commands so you can see at a glance what mutates.
|
|
86
|
+
|
|
87
|
+
## Limitations
|
|
88
|
+
|
|
89
|
+
A few rough edges to be aware of:
|
|
90
|
+
|
|
91
|
+
1. **Search is slow against this wrapper, even though MailMate itself is fast.** MailMate has a fantastic search engine — its native quicksearch is near-instant even against large stores — but `mmsearch` doesn't yet route through it. The current implementation uses index prefilters plus direct `.eml` walks, which works but is orders of magnitude slower than MailMate's own UI search, and **painfully** slow once a body-text term (`b <term>`, a bare term that falls through to body matching, or a `--no-headers-only` query) disables the prefilter. Prefer narrowing with `f`/`t`/`s`/`d` first, and use `--headers-only` when you don't need the body matched. Finding a way to drive MailMate's native engine from the outside is open work.
|
|
92
|
+
|
|
93
|
+
2. **Bulk `mm-modify` takes over the whole computer, not just MailMate.** Each invocation opens a message-viewer window via the `mid:` URL, runs AppleScript key-binding selectors against it, then closes the window. Two things follow from that:
|
|
94
|
+
- **Focus is stolen.** When the `mid:` URL fires, macOS brings MailMate forward and the spawned message-viewer window takes keyboard focus. Anything you were typing into another app goes to MailMate instead.
|
|
95
|
+
- **The close at the end can close the wrong window.** `mm-modify` ends by sending the standard "close window" keystroke. If focus has drifted (or the next app's window has come forward in the meantime), that keystroke lands on **your** window — your editor, your browser tab — not MailMate's viewer.
|
|
96
|
+
|
|
97
|
+
For one-off changes this is just annoying; for a loop of hundreds of messages it makes the machine unusable while it runs. Batch multiple actions into one `mm-modify` invocation when you can — they share a single open/close cycle. The `--keep-window` flag avoids the close-keystroke entirely if you don't mind cleaning up viewers manually.
|
|
98
|
+
|
|
99
|
+
3. **`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.
|
|
100
|
+
|
|
101
|
+
4. **MailMate must be running.** Anything that goes through `mm-modify` requires MailMate open and unblocked by modal dialogs. `mm-send` likewise needs MailMate running — `emate mailto` opens a draft window in the running MailMate process, so without MailMate up there's nowhere for the draft to land (this is true with or without `--send-now`). Headless / unattended use isn't supported.
|
|
102
|
+
|
|
103
|
+
5. **Single-account `mm-send` defaults.** `mm-send` passes flags straight through to `emate mailto`. If you have multiple identities configured in MailMate and don't pass `-f`, MailMate picks the default identity — there's no opinionated multi-account routing in the wrapper.
|
|
104
|
+
|
|
105
|
+
## Status
|
|
106
|
+
|
|
107
|
+
Pre-1.0 (0.x). Breaking changes allowed without version bumps. See [`docs/roadmap/Mailmate gem.md`](../claude/people/docs/roadmap/Mailmate%20gem.md) in the sibling `people` repo for the design history and remaining work.
|
|
108
|
+
|
|
109
|
+
## Install
|
|
110
|
+
|
|
111
|
+
For development (no `gem install` needed):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
git clone <this repo> ~/code/claude/mailmate
|
|
115
|
+
echo 'export PATH="$HOME/code/claude/mailmate/exe:$PATH"' >> ~/.zshrc
|
|
116
|
+
source ~/.zshrc
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Then bootstrap your config:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
mmdiscover
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`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.
|
|
126
|
+
|
|
127
|
+
## Commands
|
|
128
|
+
|
|
129
|
+
| Command | What it does |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `mmsearch` | List messages matching a quicksearch expression. Output is aligned CSV. |
|
|
132
|
+
| `mmmessage` | Print one message by `.eml` id (decoded headers + plain-text body). |
|
|
133
|
+
| `mm-modify` | Mark read/flag/tag/archive/move a message via AppleScript. |
|
|
134
|
+
| `mm-send` | Send mail through `emate` with a markdown body on stdin. |
|
|
135
|
+
| `mmdiscover` | First-run bootstrap; (re-)writes the user config from MailMate's plists. |
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
## eml-id vs Message-ID
|
|
140
|
+
|
|
141
|
+
The CLI tools take an `eml-id` — the integer filename of MailMate's `.eml` storage (the same value as the **Msg ID** column in MailMate's UI, internally MailMate's `#body-part-id`). It's a counter MailMate maintains locally; **eml-ids are NOT portable across machines.** The same RFC `Message-ID:` will have a different eml-id on every install. If you need a cross-machine reference, use the message's `Message-ID:` header (which `mmmessage <id>` prints). The `mid:%3C<message-id>%3E` URL scheme works portably for the same reason.
|
|
142
|
+
|
|
143
|
+
## Library
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
require "mailmate"
|
|
147
|
+
|
|
148
|
+
# Parse and evaluate a MailMate smart-mailbox filter
|
|
149
|
+
ast = Mailmate.compile_filter("from.name = 'Medium' and #date-received > '1 days ago'")
|
|
150
|
+
# ... feed `ast` to Mailmate::Evaluator ...
|
|
151
|
+
|
|
152
|
+
# Read the binary `#flags` index
|
|
153
|
+
reader = Mailmate::IndexReader.for("#flags")
|
|
154
|
+
reader.flags_for(180644) # → ["\\Seen", "$Forwarded"]
|
|
155
|
+
|
|
156
|
+
# Configuration
|
|
157
|
+
Mailmate.config.app_support_dir # → expanded path
|
|
158
|
+
Mailmate::Identity.mine?("brian@example.com") # → true if in identities
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Using from a MailMate bundle
|
|
162
|
+
|
|
163
|
+
`mmdiscover` writes `~/.config/mailmate/bundle_loader.rb` — a one-line bootstrap that lets MailMate bundle handlers find the gem. Every handler then does:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
#!/usr/bin/env ruby
|
|
167
|
+
load File.expand_path("~/.config/mailmate/bundle_loader.rb")
|
|
168
|
+
require "mailmate"
|
|
169
|
+
|
|
170
|
+
# ... use Mailmate::IndexReader, Mailmate::HeaderReader, Mailmate::Identity, etc.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The bootstrap file is the only place that knows the gem's path on disk, so individual bundles stay portable across machines — copy a `.mmBundle/` to another Mac, run `mmdiscover` there, and the bundle works.
|
|
174
|
+
|
|
175
|
+
### Sample bundle
|
|
176
|
+
|
|
177
|
+
The gem ships a working sample at `~/Library/Application Support/MailMate/Bundles/Mailmate.mmBundle/`:
|
|
178
|
+
|
|
179
|
+
- **`Commands/Inbox Note.mmCommand`** — declares input (canonical body), env vars (from / subject / date / message-id), and output type (actions JSON).
|
|
180
|
+
- **`Support/bin/inbox_note.rb`** — the handler. Reads body from stdin, headers from env, uses `Mailmate::Identity.mine?` to decide inbound/outbound, writes a markdown note to `~/code/claude/people/projects/email/inbox/<YYYY>/<MM>/`, and returns `moveMessage` (archive) + `notify` actions.
|
|
181
|
+
|
|
182
|
+
To enable: restart MailMate (or use "Reload Bundles" in the Command menu). The "→ Inbox Note" entry will appear in `Command → Mailmate gem bundle`. Override the output directory by setting `MAILMATE_INBOX_DIR` in the `.mmCommand`'s `environment` block.
|
|
183
|
+
|
|
184
|
+
See `~/code/claude/people/projects/email/mailmate-bundles.md` for the bundle plist mechanics in full.
|
|
185
|
+
|
|
186
|
+
## Configuration
|
|
187
|
+
|
|
188
|
+
Loading order: built-in defaults → `~/.config/mailmate/config.yml` → environment variables (override YAML).
|
|
189
|
+
|
|
190
|
+
Available settings:
|
|
191
|
+
|
|
192
|
+
| Key (YAML) | Env var | Default |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `app_support_dir` | `MAILMATE_APP_SUPPORT_DIR` | `~/Library/Application Support/MailMate` |
|
|
195
|
+
| `identities` (array) | `MAILMATE_IDENTITIES` (comma-separated) | `[]` |
|
|
196
|
+
|
|
197
|
+
A sample `config.yml.example` ships in the repo with placeholder values.
|
|
198
|
+
|
|
199
|
+
## Tests
|
|
200
|
+
|
|
201
|
+
Two suites:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
rake test # hermetic — no MailMate required, runs anywhere
|
|
205
|
+
rake test:live # live — runs against your actual MailMate install
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`rake test:live` smoke-tests every smart mailbox in your `Mailboxes.plist`, decodes every `Database.noindex/Headers/*` index, and verifies one message round-trips through `EmlLookup` → `HeaderReader` → `MidUrl`. It's user-runnable so you can verify the gem works on your machine.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT. See `LICENSE.txt`.
|
data/config.yml.example
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Mailmate gem configuration. Copy to ~/.config/mailmate/config.yml and edit,
|
|
2
|
+
# or run `mmdiscover` to have it generated for you from MailMate's own
|
|
3
|
+
# Sources.plist and Identities.plist.
|
|
4
|
+
|
|
5
|
+
# Optional. Override only if MailMate isn't installed at the default location.
|
|
6
|
+
# The loader expands ~ before use.
|
|
7
|
+
# app_support_dir: ~/Library/Application Support/MailMate
|
|
8
|
+
|
|
9
|
+
# Required for the "is this email mine?" check used by the search CLI's
|
|
10
|
+
# `direction` and `party` output fields. List every address you send mail
|
|
11
|
+
# from across all configured MailMate accounts (Gmail, iCloud, aliases, etc.).
|
|
12
|
+
identities:
|
|
13
|
+
- me@example.com
|
|
14
|
+
- me@example.org
|
|
15
|
+
|
|
16
|
+
# Optional. How to display dates and times in `mmsearch` / `mmmessage`.
|
|
17
|
+
# Unset (default): use the system's local time zone — handles DST automatically.
|
|
18
|
+
# Set to a fixed offset string (e.g. "-07:00" for MST, "-05:00" for EST) to
|
|
19
|
+
# pin a specific zone regardless of the system. IANA names (e.g.
|
|
20
|
+
# "America/Denver") are not supported; use the system zone for DST handling.
|
|
21
|
+
# display_timezone: "-07:00"
|
data/exe/mm-modify
ADDED
data/exe/mm-send
ADDED
data/exe/mmdiscover
ADDED
data/exe/mmmessage
ADDED
data/exe/mmsearch
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module Mailmate
|
|
6
|
+
# @api public
|
|
7
|
+
#
|
|
8
|
+
# Build and run AppleScript calls against MailMate. macOS-only.
|
|
9
|
+
#
|
|
10
|
+
# A class (not a module like the stateless utilities `HeaderReader`,
|
|
11
|
+
# `MidUrl`, `Identity`, etc.) because each driver carries per-invocation
|
|
12
|
+
# state — `dry_run`, `output`, `errput`. The rule throughout the gem:
|
|
13
|
+
# stateless surface → module with `extend self`; state-bearing → class.
|
|
14
|
+
class AppleScriptDriver
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
attr_reader :dry_run
|
|
18
|
+
|
|
19
|
+
def initialize(dry_run: false, output: $stdout, errput: $stderr)
|
|
20
|
+
@dry_run = dry_run
|
|
21
|
+
@output = output
|
|
22
|
+
@errput = errput
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Drive a MailMate selector against the current selection.
|
|
26
|
+
# `selector` is the key-binding selector name (`"markAsRead:"`,
|
|
27
|
+
# `"setTag:"`, etc.). `args` are positional arguments passed alongside.
|
|
28
|
+
def perform(selector, *args)
|
|
29
|
+
Mailmate::PlatformError.check_darwin!(component: "AppleScriptDriver") unless dry_run
|
|
30
|
+
script = build_perform_script(selector, args)
|
|
31
|
+
run_apple_script(script)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Open a URL in MailMate (used for `mid:` URLs to select a message).
|
|
35
|
+
def open_url(url)
|
|
36
|
+
Mailmate::PlatformError.check_darwin!(component: "AppleScriptDriver") unless dry_run
|
|
37
|
+
if dry_run
|
|
38
|
+
@output.puts "DRY: open -a MailMate #{url.inspect}"
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
success = system("open", "-a", "MailMate", url)
|
|
42
|
+
raise Error, "open command failed for #{url}" unless success
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Return the array of MailMate's current window IDs (integers). Empty
|
|
46
|
+
# array if MailMate isn't running or `osascript` errors.
|
|
47
|
+
def window_ids
|
|
48
|
+
return [] if dry_run
|
|
49
|
+
out = `osascript -e 'tell application "MailMate" to get id of every window' 2>&1`
|
|
50
|
+
return [] unless $?.success?
|
|
51
|
+
out.strip.split(",").map { |s| s.strip.to_i }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Close every window in `ids`. No-op for IDs that no longer exist.
|
|
55
|
+
def close_windows(ids)
|
|
56
|
+
return if dry_run
|
|
57
|
+
Array(ids).each do |id|
|
|
58
|
+
`osascript -e 'tell application "MailMate" to close window id #{id}' >/dev/null 2>&1`
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Internal — build the `tell application "MailMate" to perform { ... }`
|
|
63
|
+
# script with proper quoting. AppleScript string literals require:
|
|
64
|
+
# \ → \\ (single backslash becomes \\, otherwise it interprets
|
|
65
|
+
# \b / \n / \t / etc. as control-character escapes)
|
|
66
|
+
# " → \" (terminates the string otherwise)
|
|
67
|
+
# Newlines and tabs in args are rejected — they have no defensible meaning
|
|
68
|
+
# inside an AppleScript single-line script and almost certainly indicate
|
|
69
|
+
# data the caller doesn't want injected.
|
|
70
|
+
def build_perform_script(selector, args)
|
|
71
|
+
escaped_selector = applescript_escape(selector.to_s, allow_controls: false)
|
|
72
|
+
if args.empty?
|
|
73
|
+
%(tell application "MailMate" to perform {"#{escaped_selector}"})
|
|
74
|
+
else
|
|
75
|
+
list = args.map { |a| %("#{applescript_escape(a.to_s)}") }.join(", ")
|
|
76
|
+
%(tell application "MailMate" to perform {"#{escaped_selector}", #{list}})
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Escape a Ruby string for inclusion inside an AppleScript double-quoted
|
|
81
|
+
# string literal. Order matters: backslash first, then quote. Newlines/
|
|
82
|
+
# tabs are rejected unless `allow_controls: true`.
|
|
83
|
+
def applescript_escape(s, allow_controls: false)
|
|
84
|
+
if !allow_controls && s.match?(/[\r\n\t]/)
|
|
85
|
+
raise Error, "AppleScript arg contains control character (\\r/\\n/\\t): #{s.inspect}"
|
|
86
|
+
end
|
|
87
|
+
s.gsub("\\", "\\\\\\\\").gsub('"', '\\"')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def run_apple_script(script)
|
|
93
|
+
if dry_run
|
|
94
|
+
@output.puts "DRY: osascript -e #{script.inspect}"
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
output = `osascript -e #{Shellwords.escape(script)} 2>&1`
|
|
98
|
+
status = $?.exitstatus
|
|
99
|
+
@errput.puts "[osascript] #{output.strip}" unless output.strip.empty?
|
|
100
|
+
raise Error, "osascript exited #{status}: #{script}" unless status.zero?
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/mailmate/ast.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# AST nodes for MailMate filter expressions. All nodes implement #inspect for
|
|
4
|
+
# debug output and are evaluated by Mailmate::Evaluator.
|
|
5
|
+
|
|
6
|
+
module Mailmate
|
|
7
|
+
# @api private
|
|
8
|
+
module AST
|
|
9
|
+
AndNode = Struct.new(:children) { def inspect; "And(#{children.map(&:inspect).join(", ")})"; end }
|
|
10
|
+
OrNode = Struct.new(:children) { def inspect; "Or(#{children.map(&:inspect).join(", ")})"; end }
|
|
11
|
+
NotNode = Struct.new(:child) { def inspect; "Not(#{child.inspect})"; end }
|
|
12
|
+
|
|
13
|
+
# path: array of strings, e.g. ["from", "name"] or ["#any-address"]
|
|
14
|
+
# op: one of "=", "!=", "~", "!~", "<", "<=", ">", ">="
|
|
15
|
+
# flags: array of single-letter strings, e.g. ["c"], ["c", "a"], or []
|
|
16
|
+
# value: a *Node (LiteralStringNode, NumberNode, RelativeDateNode, AbsoluteDateNode, VarRefNode)
|
|
17
|
+
CompareNode = Struct.new(:path, :op, :flags, :value) do
|
|
18
|
+
def inspect; "Compare(#{path.join(".")} #{op}#{flags.empty? ? "" : "[#{flags.join}]"} #{value.inspect})"; end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
ExistsNode = Struct.new(:path) { def inspect; "Exists(#{path.join(".")})"; end }
|
|
22
|
+
|
|
23
|
+
LiteralStringNode = Struct.new(:value) { def inspect; "Str(#{value.inspect})"; end }
|
|
24
|
+
NumberNode = Struct.new(:value) { def inspect; "Num(#{value})"; end }
|
|
25
|
+
|
|
26
|
+
# n: integer; unit: :day, :week, :month, :year
|
|
27
|
+
RelativeDateNode = Struct.new(:n, :unit) { def inspect; "Rel(#{n} #{unit}s ago)"; end }
|
|
28
|
+
AbsoluteDateNode = Struct.new(:time) { def inspect; "Abs(#{time.iso8601})"; end }
|
|
29
|
+
|
|
30
|
+
# Stage C placeholder: $SENT.from.address style references.
|
|
31
|
+
VarRefNode = Struct.new(:var, :path) { def inspect; "Var($#{var}.#{path.join(".")})"; end }
|
|
32
|
+
end
|
|
33
|
+
end
|