mailmate 0.1.0 → 0.2.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: 79154ae28a8d2c2aaf816e6719da7ee549f59db9a9425c19fd14971fa17c2752
4
- data.tar.gz: 3ebd8963699820c5a10a777ea1d0013108301f268bd4a59939e5a9cf54649747
3
+ metadata.gz: 52941d76f7875e2944527026ed3d8584eb15e718dded7c7db5b75cad880be7d2
4
+ data.tar.gz: bb5f9463d23b07f6b6755c1c1c54326a060b9bd5680554399ff97f23137f7b6c
5
5
  SHA512:
6
- metadata.gz: 0f709dfccd86d6abb7e82ec1da45fc7f84745a6967f7e9fdaa90b1b90ae015671271102016f03aea80262ce8ff6ca109d7d8b5365d012c95ab4cc57c102e43ed
7
- data.tar.gz: 1da1debaba2cb1260b8b033efb742ec45bca0b37c5b041f87492367bbf0f9efda29cf117723cd1bf027d7b84ccfd4947f8dc84d545759b36ad8f346b0eb4121c
6
+ metadata.gz: ac99eb6fee19f727a953228a599f5ec29d741339776388547ff514403a838c990635c586881dfee5750c3a0c6a72f6c66bdcc513f3dba4ed85eb0e980b588357
7
+ data.tar.gz: 6a293bf66d563b80d9f9a0e2f0808fa32db6ba4bd40bea65fbb364a73e0fbef3d0aa715b284ad07f464a7cbb695825da4d2f614903699f1d3e527101e7135edc
data/README.md CHANGED
@@ -108,12 +108,8 @@ Pre-1.0 (0.x). Breaking changes allowed without version bumps. See [`docs/roadma
108
108
 
109
109
  ## Install
110
110
 
111
- For development (no `gem install` needed):
112
-
113
111
  ```bash
114
- git clone <this repo> ~/code/claude/mailmate
115
- echo 'export PATH="$HOME/code/claude/mailmate/exe:$PATH"' >> ~/.zshrc
116
- source ~/.zshrc
112
+ gem install mailmate
117
113
  ```
118
114
 
119
115
  Then bootstrap your config:
@@ -124,6 +120,18 @@ mmdiscover
124
120
 
125
121
  `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
122
 
123
+ ### From source (development)
124
+
125
+ If you're hacking on the gem itself, skip `gem install` and put the repo's `exe/` on your `PATH`:
126
+
127
+ ```bash
128
+ git clone <this repo> ~/code/claude/mailmate
129
+ echo 'export PATH="$HOME/code/claude/mailmate/exe:$PATH"' >> ~/.zshrc
130
+ source ~/.zshrc
131
+ ```
132
+
133
+ Then `mmdiscover` as above.
134
+
127
135
  ## Commands
128
136
 
129
137
  | Command | What it does |
@@ -25,7 +25,8 @@ module Mailmate
25
25
  HEADER_FIELDS = %i[from recipients cc subject address_any any].freeze
26
26
 
27
27
  VALID_FIELDS = %w[id path mailbox from to cc bcc reply-to subject date time
28
- message-id direction party flags read archive].freeze
28
+ message-id references in-reply-to
29
+ direction party flags read archive tags keywords].freeze
29
30
 
30
31
  HEADER_LABELS = {
31
32
  "direction" => "dir",
@@ -39,8 +40,11 @@ module Mailmate
39
40
  "read" => :index,
40
41
  "archive" => :index,
41
42
  "flags" => :index,
43
+ "tags" => :index,
44
+ "keywords" => :index,
42
45
  "from" => :header, "to" => :header, "cc" => :header, "bcc" => :header,
43
46
  "reply-to" => :header, "subject" => :header, "message-id" => :header,
47
+ "references" => :header, "in-reply-to" => :header,
44
48
  "direction" => :header, "party" => :header,
45
49
  }.freeze
46
50
 
@@ -57,8 +61,10 @@ module Mailmate
57
61
  parser.parse!(argv)
58
62
 
59
63
  search_string = argv[0] || DEFAULT_SEARCH
60
- fields_arg = (opts[:fields] || argv[1] || DEFAULT_FIELDS).to_s
61
- extra_fields = fields_arg.strip.split(/\s+/).reject(&:empty?)
64
+ fields_arg = (opts[:fields] || argv[1] || DEFAULT_FIELDS).to_s.strip
65
+ # `+...` means "defaults plus these"; bare list replaces defaults.
66
+ fields_arg = "#{DEFAULT_FIELDS} #{fields_arg[1..]}" if fields_arg.start_with?("+")
67
+ extra_fields = fields_arg.split(/\s+/).reject(&:empty?)
62
68
  fields = (["id"] + extra_fields).uniq
63
69
 
64
70
  imap_root = Mailmate.config.imap_root
@@ -105,7 +111,7 @@ module Mailmate
105
111
  :index
106
112
  elsif specs.any? { |field, _, _| (field == :body || field == :message_or_body) && !opts[:headers_only] }
107
113
  :full
108
- elsif specs.all? { |field, _, _| field == :date }
114
+ elsif specs.all? { |field, _, _| %i[date tag keyword].include?(field) }
109
115
  :index
110
116
  else
111
117
  :header
@@ -156,8 +162,9 @@ module Mailmate
156
162
  o.separator ""
157
163
  o.separator "POSITIONAL ARGS"
158
164
  o.separator " search-string Quicksearch expression. Default: 'd 1d'. Pass '' to disable."
159
- o.separator " fields Space-separated columns to add (id is always first)."
165
+ o.separator " fields Space-separated columns to show (id is always first)."
160
166
  o.separator " Default: 'flags date time direction party subject'."
167
+ o.separator " Prefix with '+' to add to the defaults: '+tags' = defaults + tags."
161
168
  o.separator ""
162
169
  o.separator "OPTIONS"
163
170
  o.on("--mailbox X", "Mailbox to search (default: all)") { |v| opts[:mailbox] = v }
@@ -188,6 +195,29 @@ module Mailmate
188
195
  o.separator " mmsearch 'f medium d 7d' from Medium in last 7 days"
189
196
  o.separator " mmsearch 's \"rent due\" !draft' subject has rent due, no 'draft'"
190
197
  o.separator " mmsearch 'd 2026-05' received in May 2026"
198
+ o.separator ""
199
+ o.separator "FIELDS (for the fields argument / --fields)"
200
+ o.separator " id eml-id (always included as first column)"
201
+ o.separator " path full path to the .eml file"
202
+ o.separator " mailbox account/mailbox path (no /Messages/<id>.eml suffix)"
203
+ o.separator " from From header"
204
+ o.separator " to To header"
205
+ o.separator " cc Cc header"
206
+ o.separator " bcc Bcc header"
207
+ o.separator " reply-to Reply-To header"
208
+ o.separator " subject Subject header"
209
+ o.separator " message-id RFC Message-ID header"
210
+ o.separator " references RFC References header (space-joined when multiple)"
211
+ o.separator " in-reply-to RFC In-Reply-To header"
212
+ o.separator " date received date, YYYY-MM-DD (local time)"
213
+ o.separator " time received time, HH:MM (local time)"
214
+ o.separator " direction '→' outbound, '←' inbound (column header: 'dir')"
215
+ o.separator " party counterparty (recipients if outbound, sender if inbound)"
216
+ o.separator " flags archive + read combined, e.g. 'AR', 'PU'"
217
+ o.separator " read 'R' read or 'U' unread (column header: 'r')"
218
+ o.separator " archive 'A' archived or 'P' present elsewhere (column header: 'a')"
219
+ o.separator " tags user tags (IMAP keywords), comma-joined; system flags (\\… , $…) excluded"
220
+ o.separator " keywords raw IMAP keyword list (incl. \\Seen, \\Draft, \\Flagged, \$Forwarded, user tags)"
191
221
  end
192
222
  end
193
223
 
@@ -359,11 +389,19 @@ module Mailmate
359
389
  when :address_any
360
390
  [mail[:from], mail[:to], mail[:cc], mail[:reply_to], mail[:sender]]
361
391
  .compact.map { |h| h.value.to_s }.join(" ").downcase
362
- when :tag, :keyword
363
- [mail["x-keywords"]&.value, mail["keywords"]&.value].compact.join(" ").downcase
364
392
  end
365
393
  end
366
394
 
395
+ # MailMate stores user tags as IMAP keywords in the `#flags` index — not
396
+ # as `X-Keywords`/`Keywords` headers in the .eml — so tag matching has to
397
+ # go through the index, not the parsed mail. Strips `\…` (RFC) and `$…`
398
+ # (Thunderbird/Apple) system flags so substring matches only hit user tags.
399
+ def tag_value(eml_id)
400
+ return "" unless eml_id
401
+ flags = (Mailmate::IndexReader.for("#flags").flags_for(eml_id.to_i) rescue [])
402
+ flags.reject { |f| f.start_with?("\\", "$") }.join(" ").downcase
403
+ end
404
+
367
405
  def text_body(mail)
368
406
  (mail.text_part&.decoded || mail.body.decoded).to_s.force_encoding("UTF-8").scrub.downcase
369
407
  rescue StandardError
@@ -374,8 +412,10 @@ module Mailmate
374
412
  specs.all? do |field, term, negate|
375
413
  hit =
376
414
  case field
377
- when :from, :recipients, :cc, :subject, :address_any, :tag, :keyword
415
+ when :from, :recipients, :cc, :subject, :address_any
378
416
  field_value(mail, field).include?(term)
417
+ when :tag, :keyword
418
+ tag_value(eml_id).include?(term)
379
419
  when :body
380
420
  headers_only ? false : text_body(mail).include?(term)
381
421
  when :message_or_body
@@ -485,14 +525,21 @@ module Mailmate
485
525
  archive = path.include?("/Archive.mailbox/") ? "A" : "P"
486
526
  seen = (Mailmate::IndexReader.for("#flags").flags_for(eml_id.to_i) rescue []).include?("\\Seen")
487
527
  "#{archive}#{seen ? 'R' : 'U'}"
528
+ when "tags"
529
+ flags = (Mailmate::IndexReader.for("#flags").flags_for(eml_id.to_i) rescue [])
530
+ flags.reject { |f| f.start_with?("\\", "$") }.join(",")
531
+ when "keywords"
532
+ (Mailmate::IndexReader.for("#flags").flags_for(eml_id.to_i) rescue []).join(",")
488
533
  when "from" then mail ? Array(mail.from).join("; ") : nil
489
534
  when "to" then mail ? Array(mail.to).join("; ") : nil
490
535
  when "cc" then mail ? Array(mail.cc).join("; ") : nil
491
536
  when "bcc" then mail ? Array(mail.bcc).join("; ") : nil
492
537
  when "reply-to" then mail ? Array(mail.reply_to).join("; ") : nil
493
- when "subject" then mail&.subject.to_s
494
- when "message-id" then mail&.message_id.to_s
495
- when "direction" then mail ? (outbound?(path, mail) ? "→" : "←") : nil
538
+ when "subject" then mail&.subject.to_s
539
+ when "message-id" then mail&.message_id.to_s
540
+ when "references" then mail ? Array(mail.references).join(" ") : nil
541
+ when "in-reply-to" then mail ? Array(mail.in_reply_to).join(" ") : nil
542
+ when "direction" then mail ? (outbound?(path, mail) ? "→" : "←") : nil
496
543
  when "party" then mail ? party_for(mail, outbound?(path, mail)) : nil
497
544
  end.to_s
498
545
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mailmate
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Murphy-Dye