mailmate 1.5.0 → 1.7.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.
data/lib/mailmate/mcp.rb CHANGED
@@ -74,25 +74,24 @@ module Mailmate
74
74
  TOOLS = [
75
75
  {
76
76
  name: "search",
77
+ title: "Search mail",
78
+ annotations: { title: "Search mail", readOnlyHint: true },
77
79
  description: <<~DESC.strip,
78
80
  Search MailMate's .eml files using MailMate's quicksearch syntax.
79
81
  Returns column-aligned CSV. Same engine as the `mmsearch` CLI.
80
82
 
81
- Query = space-separated specs combined with AND (no or/parens yet).
82
- Quote multi-word terms ("rent due"). Modifiers:
83
- (bare term) From/To/Cc/Subject OR body contains
84
- f <t> From t <t> To/Cc c <t> Cc s <t> Subject
85
- a <t> any address header b <t> body m <t> headers OR body
86
- d <date> received: Y, Y-M, Y-M-D, or relative 1d/2w/3m/1y
87
- T <tag> tag / IMAP keyword (K is a synonym)
88
- !<value> negate, e.g. f !smith (From does NOT contain smith)
89
- The `mailbox` arg also accepts a smart-mailbox name (e.g. Medium,
90
- Whisper, Personal Inbox) whose filter is ANDed into the search.
91
-
92
- Examples:
93
- query="f medium d 7d" from Medium in the last 7 days
94
- query="T robot" tagged "robot"
95
- query="s 'rent due' !draft" subject has 'rent due', not 'draft'
83
+ There is NO native key:value form write quicksearch directly using
84
+ the modifiers below. Familiar foreign tokens (`from:bob`,
85
+ `date:today`, `after:2026-08-01`, `older_than:2w`) are auto-translated
86
+ to quicksearch, and the rewrite is announced in the result — that
87
+ announcement means your query was translated, not that it failed.
88
+ Unrecognized keys (`is:unread`, `has:attachment`) are searched as
89
+ literal text and match nothing, silently.
90
+
91
+ #{Mailmate::SearchSyntax.reference(indent: " ")}
92
+
93
+ The `mailbox` arg also accepts a smart-mailbox name (e.g. Newsletters,
94
+ Receipts, Priority) whose filter is ANDed into the search.
96
95
 
97
96
  Fields default to: flags date time direction party subject.
98
97
  Prefix with "+" to add to the defaults ("+tags +mailbox"); a bare
@@ -122,12 +121,15 @@ module Mailmate
122
121
  limit: { type: "integer", description: "Stop after N matches." },
123
122
  headers_only: { type: "boolean", description: "Skip body matching (much faster on text searches)." },
124
123
  sort: { type: "string", enum: %w[asc desc none], description: "Sort by date+time. Default: asc." },
124
+ european: { type: "boolean", description: "Slash dates in the query are day-first (d 9/8/2026 = Aug 9). Default: month-first American." },
125
125
  },
126
126
  additionalProperties: false,
127
127
  },
128
128
  },
129
129
  {
130
130
  name: "message",
131
+ title: "Read message",
132
+ annotations: { title: "Read message", readOnlyHint: true },
131
133
  description: "Read one MailMate message. Accepts either local eml-id (digits) or RFC Message-ID (with or without angle brackets). Default output: headers block (incl. any user tags) + plain-text body. For HTML-only mail (most newsletters), pass markdown:true to get clean readable markdown instead of raw HTML — strongly preferred for reading and far more token-efficient; it's a no-op on plain-text messages.",
132
134
  inputSchema: {
133
135
  type: "object",
@@ -143,6 +145,8 @@ module Mailmate
143
145
  },
144
146
  {
145
147
  name: "modify",
148
+ title: "Modify message state",
149
+ annotations: { title: "Modify message state", readOnlyHint: false, destructiveHint: true },
146
150
  description: <<~DESC.strip,
147
151
  Apply state-change actions to a message via MailMate.
148
152
  NOTE: drives MailMate's UI via AppleScript — it briefly takes focus,
@@ -192,6 +196,8 @@ module Mailmate
192
196
  },
193
197
  {
194
198
  name: "verify",
199
+ title: "Verify modify results",
200
+ annotations: { title: "Verify modify results", readOnlyHint: true },
195
201
  description: <<~DESC.strip,
196
202
  Batch-confirm `modify` check-tickets (from check:"defer") against the
197
203
  #flags index in ONE flush-wait. Pass the tickets you collected from a
@@ -218,6 +224,8 @@ module Mailmate
218
224
  },
219
225
  {
220
226
  name: "send",
227
+ title: "Send mail",
228
+ annotations: { title: "Send mail", readOnlyHint: false, destructiveHint: true },
221
229
  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.",
222
230
  inputSchema: {
223
231
  type: "object",
@@ -239,6 +247,8 @@ module Mailmate
239
247
  },
240
248
  {
241
249
  name: "draft",
250
+ title: "Compose draft",
251
+ annotations: { title: "Compose draft", readOnlyHint: false, destructiveHint: false },
242
252
  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.",
243
253
  inputSchema: {
244
254
  type: "object",
@@ -259,6 +269,8 @@ module Mailmate
259
269
  },
260
270
  {
261
271
  name: "open",
272
+ title: "Open in MailMate",
273
+ annotations: { title: "Open in MailMate", readOnlyHint: false, destructiveHint: false },
262
274
  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.",
263
275
  inputSchema: {
264
276
  type: "object",
@@ -272,6 +284,8 @@ module Mailmate
272
284
  },
273
285
  {
274
286
  name: "list_mailboxes",
287
+ title: "List mailboxes",
288
+ annotations: { title: "List mailboxes", readOnlyHint: true },
275
289
  description: "Enumerate accounts, IMAP mailboxes (with optional message counts), and smart mailboxes MailMate has defined. Account names are decoded for display (`%40` → `@`).",
276
290
  inputSchema: {
277
291
  type: "object",
@@ -284,6 +298,8 @@ module Mailmate
284
298
  },
285
299
  {
286
300
  name: "list_tags",
301
+ title: "List tags",
302
+ annotations: { title: "List tags", readOnlyHint: true },
287
303
  description: "List user tags. Default: tags actually applied to messages, with usage counts (from MailMate's #flags index; system flags excluded). `defined: true`: tags MailMate has registered in Preferences → Tags (from Tags.plist).",
288
304
  inputSchema: {
289
305
  type: "object",
@@ -295,6 +311,8 @@ module Mailmate
295
311
  },
296
312
  {
297
313
  name: "resolve_id",
314
+ title: "Resolve message identifiers",
315
+ annotations: { title: "Resolve message identifiers", readOnlyHint: true },
298
316
  description: <<~DESC.strip,
299
317
  Look up a message and return all its identifiers:
300
318
  - eml_id local body-part id (changes per machine)
@@ -396,6 +414,7 @@ module Mailmate
396
414
  argv.push("--limit", args["limit"].to_i.to_s) if args["limit"]
397
415
  argv.push("--headers-only") if args["headers_only"]
398
416
  argv.push("--sort", args["sort"].to_s) if args["sort"]
417
+ argv.push("--european") if args["european"]
399
418
  # Positionals: search-string then fields. Only include if the caller
400
419
  # gave us either — otherwise let the CLI apply its defaults.
401
420
  if args.key?("query") || args["fields"]
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Mailmate
6
+ # THE one description of quicksearch syntax. Both surfaces that teach the
7
+ # syntax — `mmsearch --help` and the MCP `search` tool description — render
8
+ # from the tables here, so the two can no longer drift apart (they already
9
+ # had: the CLI said "Nd|Nw|Nm|Ny (relative), or Y, Y-M, Y-M-D" while the MCP
10
+ # said "Y, Y-M, Y-M-D, or relative 1d/2w/3m/1y" — same rules, two wordings,
11
+ # two things to remember to update).
12
+ #
13
+ # Downstream consumers should POINT at these surfaces rather than restate
14
+ # them. A copy of the syntax in someone else's system prompt is a copy that
15
+ # goes stale the next time a modifier is added here.
16
+ module SearchSyntax
17
+ # [spec, meaning]. Order is the teaching order, not alphabetical.
18
+ MODIFIERS = [
19
+ ["<term>", "common headers (from/to/cc/subject) OR body contains <term>"],
20
+ ["f <term>", "from contains"],
21
+ ["t <term>", "to/cc (recipients) contains"],
22
+ ["c <term>", "cc contains"],
23
+ ["s <term>", "subject contains"],
24
+ ["a <term>", "any address header contains"],
25
+ ["b <term>", "body contains"],
26
+ ["m <term>", "common headers OR body (same as a bare term)"],
27
+ ["d <date>", "received: Nh (rolling clock hours), Nd|Nw|Nm|Ny (N calendar units ending today; 1d = today), or Y, Y-M, Y-M-D"],
28
+ ["T <tag>", "tag / IMAP keyword contains (K is a synonym)"],
29
+ ].freeze
30
+
31
+ EXAMPLES = [
32
+ ["f substack d 7d", "from Substack in the last 7 days"],
33
+ ["s \"invoice due\" !draft", "subject has 'invoice due', not 'draft'"],
34
+ ["d 2026-05", "received in May 2026"],
35
+ ["d 2026-08-10", "received on one specific day"],
36
+ ["d 1d", "received today (the default); d 2d = yesterday + today"],
37
+ ["d 24h", "received in the last 24 hours (rolling, not calendar)"],
38
+ ["d >=2026-05 d <2026-08", "received May through July 2026"],
39
+ ["d 1h or 2026-08-09", "last hour, plus everything from Aug 9"],
40
+ ["T urgent", "tagged 'urgent'"],
41
+ ].freeze
42
+
43
+ RULES = [
44
+ "Specs combine with AND; `or` separates alternatives, and AND binds tighter",
45
+ "(no parens): (f bob or f ann) s invoice = f bob s invoice or f ann s invoice.",
46
+ "After `or`, a bare first term inherits the modifier in force: d 2024 or 2025.",
47
+ "Wrap multi-word terms in \"double quotes\" (also how to search the word \"or\").",
48
+ "Prefix an operand with ! to negate: f !smith = from does NOT contain smith.",
49
+ "Negation works on dates too: d !3d = received MORE than 3 days ago.",
50
+ "Absolute dates compare: d >2026-08 (after Aug), d <2026-08 (before), also >= <=.",
51
+ "Slash dates are month-first American: d 8/9/2026 = Aug 9 (day-first: --european).",
52
+ "An impossible date combination (d >2026 d <2025) is an error, not 0 results.",
53
+ ].freeze
54
+
55
+ # Search keys from OTHER mail systems (Gmail, Outlook, Apple Mail, IMAP
56
+ # dialects). Quicksearch has no `key:value` form at all, so a term like
57
+ # `date:today` is not a syntax error — it parses as a bare term and
58
+ # searches for the literal string "date:today" in headers and body, which
59
+ # matches nothing. That silence is the whole problem this list exists to
60
+ # break: an agent or a person gets an empty result set that is
61
+ # indistinguishable from "your mail really has nothing", and believes it.
62
+ FOREIGN_KEYS = %w[
63
+ after before older newer older_than newer_than on since until
64
+ date sent received time
65
+ from to cc bcc subject body
66
+ is has in label folder mailbox category filename
67
+ ].freeze
68
+
69
+ # Spec placeholders for the zero-result hint, for foreign keys whose value
70
+ # translate() could NOT rewrite (e.g. `after:8am`). Keys absent here still
71
+ # get flagged, just without a suggested rewrite.
72
+ EQUIVALENTS = {
73
+ "from" => "f <term>", "to" => "t <term>", "cc" => "c <term>",
74
+ "subject" => "s <term>", "body" => "b <term>", "label" => "T <tag>",
75
+ "date" => "d <date>", "sent" => "d <date>", "received" => "d <date>",
76
+ "on" => "d <date>", "after" => "d >=YYYY-MM-DD", "since" => "d >=YYYY-MM-DD",
77
+ "before" => "d <YYYY-MM-DD", "until" => "d <=YYYY-MM-DD",
78
+ "newer_than" => "d Nd", "older_than" => "d !Nd",
79
+ "newer" => "d Nd", "older" => "d !Nd",
80
+ }.freeze
81
+
82
+ # Foreign header-ish keys with a direct quicksearch spec. The value
83
+ # carries over unchanged, so these translate regardless of what it is.
84
+ HEADER_EQUIV = {
85
+ "from" => "f", "to" => "t", "cc" => "c",
86
+ "subject" => "s", "body" => "b", "label" => "T",
87
+ }.freeze
88
+
89
+ # The --help table for the translator. Symbolic, not computed — <N> is
90
+ # resolved against the current date at translation time.
91
+ TRANSLATIONS_HELP = [
92
+ ["from:bob (to: cc: subject: body: label:)", "f bob (t c s b T)"],
93
+ ["-from:bob or !from:bob", "f !bob"],
94
+ ["date:today / date:yesterday", "d 1d / d <that day>"],
95
+ ["date:2026-03-05 or date:3/5/2026 (M/D/Y)", "d 2026-03-05"],
96
+ ["newer_than:2d / older_than:2w", "d 2d / d !2w"],
97
+ ["after:2026-05 or since:2026-05", "d >=2026-05"],
98
+ ["before:2026-08 / until:2026-08", "d <2026-08 / d <=2026-08"],
99
+ ].freeze
100
+
101
+ module_function
102
+
103
+ # The shared syntax reference, indented for embedding. Used verbatim by
104
+ # `mmsearch --help` and by the MCP tool description.
105
+ def reference(indent: " ")
106
+ width = MODIFIERS.map { |spec, _| spec.length }.max
107
+ lines = []
108
+ RULES.each { |r| lines << "#{indent}#{r}" }
109
+ lines << ""
110
+ MODIFIERS.each { |spec, meaning| lines << "#{indent} #{spec.ljust(width)} #{meaning}" }
111
+ lines << ""
112
+ lines << "#{indent}Examples:"
113
+ ex_width = EXAMPLES.map { |q, _| q.length }.max
114
+ EXAMPLES.each { |q, meaning| lines << "#{indent} #{q.ljust(ex_width)} #{meaning}" }
115
+ lines.join("\n")
116
+ end
117
+
118
+ # One token: an (optionally key:-prefixed) quoted string, or a bare run
119
+ # of non-space. Quoted regions survive as single tokens so translate()
120
+ # can leave a deliberate literal search (`s "date:today"`) alone.
121
+ TOKEN_RX = /(?:[-!]?[A-Za-z_]+:)?"[^"]*"|(?:[-!]?[A-Za-z_]+:)?'[^']*'|\S+/
122
+
123
+ # Rewrite foreign `key:value` tokens to their exact quicksearch
124
+ # equivalent, leaving everything else byte-for-byte intact. Returns
125
+ # [query, notes] where notes is [[original_token, replacement], ...] —
126
+ # callers MUST surface the notes (stderr, tool result); a silent rewrite
127
+ # would show the reader a query that never ran.
128
+ #
129
+ # Only rewrites where the equivalence is exact. A foreign key whose value
130
+ # can't be translated faithfully (`after:8am`, `date:next week`) stays in
131
+ # the query as literal text, and zero_result_hint still flags it there.
132
+ def translate(query, today: Date.today, european: false)
133
+ notes = []
134
+ translated = query.to_s.gsub(TOKEN_RX) do |token|
135
+ replacement = translate_token(token, today, european)
136
+ notes << [token, replacement] if replacement
137
+ replacement || token
138
+ end
139
+ [translated, notes]
140
+ end
141
+
142
+ # The stderr/tool-result announcement for a rewritten query. nil when
143
+ # nothing was rewritten.
144
+ def translation_notice(notes)
145
+ return nil if notes.empty?
146
+
147
+ width = notes.map { |from, _| from.length }.max
148
+ lines = ["translated foreign search syntax to MailMate quicksearch (`mmsearch --help`):"]
149
+ notes.each { |from, to| lines << " #{from.ljust(width)} -> #{to}" }
150
+ lines.join("\n")
151
+ end
152
+
153
+ # The --help table, indented for embedding.
154
+ def translation_reference(indent: " ")
155
+ width = TRANSLATIONS_HELP.map { |from, _| from.length }.max
156
+ lines = TRANSLATIONS_HELP.map { |from, to| "#{indent} #{from.ljust(width)} -> #{to}" }
157
+ lines << ""
158
+ lines << "#{indent}Keys with no equivalent (is: has: in: filename: ...) are searched as"
159
+ lines << "#{indent}literal text; an empty result will call them out."
160
+ lines.join("\n")
161
+ end
162
+
163
+ # Foreign `key:value` tokens in a query, lowercased keys, in order of
164
+ # appearance and de-duplicated. Quoted regions are skipped: a deliberate
165
+ # search for the literal text `s "date:today"` is not a mistake.
166
+ def foreign_tokens(query)
167
+ unquoted = query.to_s.gsub(/"[^"]*"|'[^']*'/, " ")
168
+ unquoted.scan(/(?<![\w-])!?([A-Za-z_]+):(\S*)/).filter_map do |key, value|
169
+ k = key.downcase
170
+ next unless FOREIGN_KEYS.include?(k)
171
+ ["#{key}:#{value}", k]
172
+ end.uniq { |_token, k| k }
173
+ end
174
+
175
+ # The advisory a caller should print when a search matched NOTHING and the
176
+ # query carries foreign syntax. nil when there is nothing to say — an
177
+ # ordinary empty result stays silent, because polling for mail that has
178
+ # not arrived yet is a normal, correct thing to do.
179
+ def zero_result_hint(query)
180
+ tokens = foreign_tokens(query)
181
+ return nil if tokens.empty?
182
+
183
+ quoted = tokens.map { |token, _| "`#{token}`" }.join(", ")
184
+ lines = ["0 results, and #{quoted} #{tokens.size == 1 ? "is not" : "are not"} " \
185
+ "MailMate quicksearch syntax — it was searched for as literal text."]
186
+ tokens.each do |token, key|
187
+ eq = EQUIVALENTS[key] or next
188
+ lines << " #{token} -> #{eq}"
189
+ end
190
+ lines << "Run `mmsearch --help` for the full syntax."
191
+ lines.join("\n")
192
+ end
193
+
194
+ # ---- translation internals -------------------------------------------
195
+
196
+ # nil = not a rewritable token (not key:value, unknown key, or a value
197
+ # with no faithful equivalent).
198
+ def translate_token(token, today, european = false)
199
+ m = token.match(/\A(?<neg>[-!])?(?<key>[A-Za-z_]+):(?<value>.+)\z/m)
200
+ return nil unless m
201
+
202
+ key = m[:key].downcase
203
+ value = unquote(m[:value])
204
+ return nil if value.empty?
205
+
206
+ if (spec = HEADER_EQUIV[key])
207
+ negated = !m[:neg].nil?
208
+ # `f !"a b"` won't tokenize (the ! detaches the quotes) — leave a
209
+ # negated multi-word value alone rather than emit a broken spec.
210
+ return nil if negated && value =~ /\s/
211
+ operand = value =~ /\s/ ? "\"#{value}\"" : value
212
+ return "#{spec} #{negated ? "!" : ""}#{operand}"
213
+ end
214
+
215
+ # Date keys: Gmail has no negated date form, so a -/! prefix here means
216
+ # the caller is inventing syntax — don't guess at intent.
217
+ return nil if m[:neg]
218
+
219
+ case key
220
+ when "date", "on", "sent", "received", "time"
221
+ translate_point_date(value, today, european)
222
+ when "after", "since"
223
+ translate_after(value, today, european)
224
+ when "before"
225
+ translate_before(value, today, "<", european)
226
+ when "until"
227
+ translate_before(value, today, "<=", european)
228
+ when "newer_than", "newer"
229
+ (rel = parse_relative(value)) && "d #{rel}"
230
+ when "older_than", "older"
231
+ (rel = parse_relative(value)) && "d !#{rel}"
232
+ end
233
+ end
234
+
235
+ def unquote(value)
236
+ case value
237
+ when /\A"(.*)"\z/m, /\A'(.*)'\z/m then Regexp.last_match(1)
238
+ else value
239
+ end
240
+ end
241
+
242
+ # A day-, month-, or year-precision point in time. `d <period>` matches
243
+ # exactly that period in the engine, so these are exact.
244
+ def translate_point_date(value, today, european = false)
245
+ v = value.downcase
246
+ return "d 1d" if v == "today"
247
+ return "d #{(today - 1).strftime("%Y-%m-%d")}" if v == "yesterday"
248
+ # `date:8/10/2026-today` (seen in real transcripts): a range whose end
249
+ # is now IS an after-window.
250
+ return translate_after(v.delete_suffix("-today"), today, european) if v.end_with?("-today")
251
+ return "d #{v}" if v =~ /\A\d+[dwmy]\z/
252
+
253
+ (period = normalize_period(v, european)) && "d #{period}"
254
+ end
255
+
256
+ # Gmail's after: includes the named day; since: likewise → >=.
257
+ def translate_after(value, today, european = false)
258
+ v = value.downcase
259
+ return "d 1d" if v == "today"
260
+ return "d 2d" if v == "yesterday"
261
+
262
+ (period = normalize_period(v, european)) && "d >=#{period}"
263
+ end
264
+
265
+ # Gmail's before: excludes the named day → <. until: includes it → <=.
266
+ def translate_before(value, today, op, european = false)
267
+ v = value.downcase
268
+ v = today.strftime("%Y-%m-%d") if v == "today"
269
+ v = (today - 1).strftime("%Y-%m-%d") if v == "yesterday"
270
+
271
+ (period = normalize_period(v, european)) && "d #{op}#{period}"
272
+ end
273
+
274
+ # "2026", "2026-05", "2026-03-05", "3/5/2026", "2026/3/5" → the
275
+ # normalized absolute period string quicksearch expects, or nil.
276
+ def normalize_period(value, european = false)
277
+ if (day = parse_day(value, european))
278
+ day.strftime("%Y-%m-%d")
279
+ elsif value =~ %r{\A(\d{4})[-/.](\d{1,2})\z}
280
+ format("%04d-%02d", Regexp.last_match(1).to_i, Regexp.last_match(2).to_i)
281
+ elsif value =~ /\A\d{4}\z/
282
+ value
283
+ end
284
+ end
285
+
286
+ # Gmail relative units (d/m/y, plus w) carry over as-is: `d N<u>` uses
287
+ # the same calendar arithmetic.
288
+ def parse_relative(value)
289
+ value =~ /\A(\d+)\s*([dwmy])\z/ ? "#{Regexp.last_match(1)}#{Regexp.last_match(2)}" : nil
290
+ end
291
+
292
+ # Y-M-D (any of - / . separators), or slash-dates with a trailing
293
+ # 4-digit year — US M/D/Y by default, D/M/Y when european. Two-digit
294
+ # years are ambiguous across dialects — refused rather than guessed.
295
+ def parse_day(value, european = false)
296
+ parts = value.split(%r{[-/.]})
297
+ return nil unless parts.size == 3 && parts.all? { |p| p =~ /\A\d+\z/ }
298
+
299
+ y, m, d =
300
+ if parts[0].length == 4
301
+ [parts[0], parts[1], parts[2]]
302
+ elsif parts[2].length == 4
303
+ european ? [parts[2], parts[1], parts[0]] : [parts[2], parts[0], parts[1]]
304
+ end
305
+ return nil unless y
306
+
307
+ y, m, d = y.to_i, m.to_i, d.to_i
308
+ Date.valid_date?(y, m, d) ? Date.new(y, m, d) : nil
309
+ end
310
+ end
311
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mailmate
4
- VERSION = "1.5.0"
4
+ VERSION = "1.7.0"
5
5
  end
data/lib/mailmate.rb CHANGED
@@ -50,6 +50,7 @@ require_relative "mailmate/mailbox_graph"
50
50
  require_relative "mailmate/source_resolver"
51
51
  require_relative "mailmate/var_resolver"
52
52
  require_relative "mailmate/filter_classifier"
53
+ require_relative "mailmate/search_syntax"
53
54
 
54
55
  module Mailmate
55
56
  # First-run bootstrap. If ~/.config/mailmate/config.yml is missing,
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.5.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Murphy-Dye
@@ -153,6 +153,7 @@ files:
153
153
  - lib/mailmate/parser.rb
154
154
  - lib/mailmate/part_lookup.rb
155
155
  - lib/mailmate/platform_error.rb
156
+ - lib/mailmate/search_syntax.rb
156
157
  - lib/mailmate/source_resolver.rb
157
158
  - lib/mailmate/var_resolver.rb
158
159
  - lib/mailmate/version.rb