mailmate 1.8.0 → 1.8.1

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: 7d4531494a2e95ee32395c937a293085b055c6868327f9914702da62a7b039c6
4
- data.tar.gz: 3251b2608c1ede51d85425c205a274993d51b58b9cace6f04eb53981a04345bb
3
+ metadata.gz: 2563290d07b7e90281a78c4161337f8fe94a21d4cd1f42501946a6c8107be5da
4
+ data.tar.gz: 04600cc5e50d4a4518ddcd27cc210d357decfeefb32e8adc1e3bb1624af17781
5
5
  SHA512:
6
- metadata.gz: 4a162097210b4578cc6118e70607258523cbf1b0a82e50a6c50b9b10a3a30bef05c5dd394ab50204920d7967bb9103073a457259f1e4ec570920daff09b1031e
7
- data.tar.gz: 84fa48ec99f5e9df866899d4c805df8de7c1b427fd8ba868732baf79ad9c9b6c3c55cae92e00fec928ecc3c43fca7cd4572d8f2828f4a270494310dbdd7c97f7
6
+ metadata.gz: d4619d78ed6224df84378ad42cff61865653e32e463b8c8b3ad1349549f32c8bf74c6635e64f377653231d3a93afc087fba18cf6da9ad2897fe97c68e096d58a
7
+ data.tar.gz: a1437ae600957dedc21446c151cbc6061d1def590693f83359fcd79f5d1dd36e57bcbad4ab9ecb85a7621910119a14d30b21f97eb4b90aebc826b3dc9a0f873e
@@ -69,8 +69,24 @@ module Mailmate
69
69
  date sent received time
70
70
  from to cc bcc subject body
71
71
  in label folder mailbox category filename
72
+ date_last_viewed
72
73
  ].freeze
73
74
 
75
+ # MailMate's OWN attribute vocabulary, minus the leading `#` — a model
76
+ # that has seen the engine's internals or MailMate's docs reaches for
77
+ # `date-received:` in good faith (dogfood 2026-08-13: an agent queried
78
+ # `date-received:8/13/2026-8/14/2026`, got zero rows with no advisory,
79
+ # and reported the mailbox empty). Keys are normalized (`-` → `_`,
80
+ # lowercased) and then aliased to the base foreign key here, so a
81
+ # compound key flows through the SAME translation and flagging pipeline
82
+ # as its base — nothing downstream knows compounds exist.
83
+ # `date_last_viewed` has no faithful `d` equivalent (viewed ≠ received),
84
+ # so it is only flagged (via FOREIGN_KEYS above), never translated.
85
+ KEY_ALIASES = {
86
+ "date_received" => "date", "received_date" => "date", "datereceived" => "date",
87
+ "date_sent" => "sent", "sent_date" => "sent", "datesent" => "sent", "sentdate" => "sent",
88
+ }.freeze
89
+
74
90
  # Spec placeholders for the zero-result hint, for foreign keys whose value
75
91
  # translate() could NOT rewrite (e.g. `after:8am`). Keys absent here still
76
92
  # get flagged, just without a suggested rewrite.
@@ -101,6 +117,8 @@ module Mailmate
101
117
  ["newer_than:2d / older_than:2w", "d 2d / d !2w"],
102
118
  ["after:2026-05 or since:2026-05", "d >=2026-05"],
103
119
  ["before:2026-08 / until:2026-08", "d <2026-08 / d <=2026-08"],
120
+ ["date:8/13/2026-8/14/2026 (also A..B)", "d >=2026-08-13 d <=2026-08-14"],
121
+ ["date-received: / date-sent: (MailMate heads)", "date: / sent:"],
104
122
  ].freeze
105
123
 
106
124
  module_function
@@ -123,7 +141,7 @@ module Mailmate
123
141
  # One token: an (optionally key:-prefixed) quoted string, or a bare run
124
142
  # of non-space. Quoted regions survive as single tokens so translate()
125
143
  # can leave a deliberate literal search (`s "date:today"`) alone.
126
- TOKEN_RX = /(?:[-!]?[A-Za-z_]+:)?"[^"]*"|(?:[-!]?[A-Za-z_]+:)?'[^']*'|\S+/
144
+ TOKEN_RX = /(?:[-!]?[A-Za-z_][A-Za-z_-]*:)?"[^"]*"|(?:[-!]?[A-Za-z_][A-Za-z_-]*:)?'[^']*'|\S+/
127
145
 
128
146
  # Rewrite foreign `key:value` tokens to their exact quicksearch
129
147
  # equivalent, leaving everything else byte-for-byte intact. Returns
@@ -170,8 +188,8 @@ module Mailmate
170
188
  # search for the literal text `s "date:today"` is not a mistake.
171
189
  def foreign_tokens(query)
172
190
  unquoted = query.to_s.gsub(/"[^"]*"|'[^']*'/, " ")
173
- unquoted.scan(/(?<![\w-])!?([A-Za-z_]+):(\S*)/).filter_map do |key, value|
174
- k = key.downcase
191
+ unquoted.scan(/(?<![\w-])!?([A-Za-z_][A-Za-z_-]*):(\S*)/).filter_map do |key, value|
192
+ k = normalize_key(key)
175
193
  next unless FOREIGN_KEYS.include?(k)
176
194
  ["#{key}:#{value}", k]
177
195
  end.uniq { |_token, k| k }
@@ -198,13 +216,21 @@ module Mailmate
198
216
 
199
217
  # ---- translation internals -------------------------------------------
200
218
 
219
+ # `Date-Received` / `date_received` / `datereceived` all mean the same
220
+ # thing to the person typing them; collapse to one spelling, then map
221
+ # MailMate-head aliases onto their base key.
222
+ def normalize_key(raw)
223
+ k = raw.downcase.tr("-", "_")
224
+ KEY_ALIASES.fetch(k, k)
225
+ end
226
+
201
227
  # nil = not a rewritable token (not key:value, unknown key, or a value
202
228
  # with no faithful equivalent).
203
229
  def translate_token(token, today, european = false)
204
- m = token.match(/\A(?<neg>[-!])?(?<key>[A-Za-z_]+):(?<value>.+)\z/m)
230
+ m = token.match(/\A(?<neg>[-!])?(?<key>[A-Za-z_][A-Za-z_-]*):(?<value>.+)\z/m)
205
231
  return nil unless m
206
232
 
207
- key = m[:key].downcase
233
+ key = normalize_key(m[:key])
208
234
  value = unquote(m[:value])
209
235
  return nil if value.empty?
210
236
 
@@ -255,11 +281,51 @@ module Mailmate
255
281
  return translate_after(v.delete_suffix("-today"), today, european) if v.end_with?("-today")
256
282
  return "d #{v}" if v =~ /\A\d+[dwmy]\z/
257
283
 
258
- (period = normalize_period(v, european)) && "d #{period}"
284
+ if (period = normalize_period(v, european))
285
+ "d #{period}"
286
+ else
287
+ translate_range(v, today, european)
288
+ end
289
+ end
290
+
291
+ # A two-sided absolute range (`8/13/2026-8/14/2026`, `2026-08-13..2026-08-14`)
292
+ # → the comparison pair `d >=A d <=B`. Inclusive on both ends: "the 13th
293
+ # to the 14th" includes the 14th in plain reading. Only fires when BOTH
294
+ # sides resolve — a side we can't parse means this isn't a range we
295
+ # understand, and no translation beats a wrong one. A reversed range
296
+ # still translates: the engine rejects an impossible date combination
297
+ # with an explicit error, which is exactly the visibility we want.
298
+ def translate_range(value, today, european)
299
+ bounds =
300
+ if value.include?("..")
301
+ a, b = value.split("..", 2)
302
+ [resolve_bound(a, today, european), resolve_bound(b, today, european)]
303
+ else
304
+ # Dates carry hyphens themselves (2026-08-13), so try each hyphen
305
+ # as the separator and take the first split where both sides parse.
306
+ value.length.times.filter_map { |i|
307
+ next unless value[i] == "-"
308
+ left = resolve_bound(value[0...i], today, european)
309
+ right = resolve_bound(value[(i + 1)..], today, european)
310
+ [left, right] if left && right
311
+ }.first
312
+ end
313
+ return nil unless bounds&.all?
314
+
315
+ "d >=#{bounds[0]} d <=#{bounds[1]}"
316
+ end
317
+
318
+ # One end of a range: a keyword day or any absolute period.
319
+ def resolve_bound(value, today, european)
320
+ v = value.to_s.strip.downcase
321
+ return today.strftime("%Y-%m-%d") if v == "today"
322
+ return (today - 1).strftime("%Y-%m-%d") if v == "yesterday"
323
+
324
+ normalize_period(v, european)
259
325
  end
260
326
 
261
327
  # Gmail's after: includes the named day; since: likewise → >=.
262
- def translate_after(value, today, european = false)
328
+ def translate_after(value, _today, european = false)
263
329
  v = value.downcase
264
330
  return "d 1d" if v == "today"
265
331
  return "d 2d" if v == "yesterday"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mailmate
4
- VERSION = "1.8.0"
4
+ VERSION = "1.8.1"
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: 1.8.0
4
+ version: 1.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Murphy-Dye