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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Mailmate
|
|
6
|
+
module CLI
|
|
7
|
+
# `mm-modify` — apply AppleScript-driven actions (read, flag, tag, archive,
|
|
8
|
+
# move, …) to a MailMate message by its eml-id.
|
|
9
|
+
#
|
|
10
|
+
# Ports mailmate-modify. Multiple actions in one invocation share a single
|
|
11
|
+
# open+wait cycle so chained operations are batched.
|
|
12
|
+
# @api private
|
|
13
|
+
module Modify
|
|
14
|
+
extend self
|
|
15
|
+
|
|
16
|
+
# action → [selector] or [selector, arg-count]. arg-count default 0.
|
|
17
|
+
ACTIONS = {
|
|
18
|
+
"read" => ["markAsRead:"],
|
|
19
|
+
"unread" => ["markAsUnread:"],
|
|
20
|
+
"flag" => [:ensure_flagged],
|
|
21
|
+
"unflag" => [:ensure_not_flagged],
|
|
22
|
+
"tag" => ["setTag:", 1],
|
|
23
|
+
"untag" => ["removeTag:", 1],
|
|
24
|
+
"clear-tags" => ["clearTags:"],
|
|
25
|
+
"archive" => ["archive:"],
|
|
26
|
+
"junk" => ["markAsJunk:"],
|
|
27
|
+
"not-junk" => ["markAsNotJunk:"],
|
|
28
|
+
"mute" => ["toggleMuteState:"],
|
|
29
|
+
"delete" => ["deleteMessage:"],
|
|
30
|
+
"move" => ["moveToMailbox:", 1],
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def run(argv)
|
|
34
|
+
opts, parser = parse_options(argv)
|
|
35
|
+
input = argv.shift
|
|
36
|
+
return usage_error(parser, "missing <id>") if input.nil? || input.empty?
|
|
37
|
+
return usage_error(parser, "no actions given") if argv.empty?
|
|
38
|
+
|
|
39
|
+
actions = parse_actions(argv, parser)
|
|
40
|
+
return 2 if actions.nil?
|
|
41
|
+
|
|
42
|
+
eml_id = Mailmate::EmlLookup.resolve_id(input)
|
|
43
|
+
if eml_id.nil? || eml_id.zero?
|
|
44
|
+
warn "Not found: #{input.inspect} (couldn't resolve as eml-id or Message-ID)"
|
|
45
|
+
return 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
path = Mailmate::EmlLookup.path_for(eml_id)
|
|
49
|
+
unless path
|
|
50
|
+
warn "Not found: #{eml_id}.eml"
|
|
51
|
+
return 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
message_id = Mailmate::HeaderReader.message_id(path)
|
|
55
|
+
unless message_id
|
|
56
|
+
warn "Could not find Message-ID in #{path}"
|
|
57
|
+
return 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
warn_on_duplicates(message_id, eml_id)
|
|
61
|
+
|
|
62
|
+
drive(eml_id, message_id, actions, opts)
|
|
63
|
+
0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_options(argv)
|
|
67
|
+
opts = { verify: false, dry_run: false, settle: 3.5, keep_window: false }
|
|
68
|
+
parser = OptionParser.new do |o|
|
|
69
|
+
o.banner = <<~BANNER
|
|
70
|
+
Usage: mm-modify <id> <action> [args...] [<action> [args...]]...
|
|
71
|
+
|
|
72
|
+
<id> can be either a local eml-id (e.g. 183715) or an RFC Message-ID
|
|
73
|
+
(with or without angle brackets, e.g. <abc@example.com>). Quote the
|
|
74
|
+
Message-ID in your shell so the < > aren't interpreted as redirection.
|
|
75
|
+
|
|
76
|
+
Selects the message in MailMate (via the `mid:` URL) and runs one or
|
|
77
|
+
more AppleScript key-binding selectors against the now-selected message.
|
|
78
|
+
Multiple actions share one open+wait cycle.
|
|
79
|
+
|
|
80
|
+
ACTIONS
|
|
81
|
+
read Mark seen (\\Seen)
|
|
82
|
+
unread Mark unseen
|
|
83
|
+
flag Ensure \\Flagged is set (no-op if already)
|
|
84
|
+
unflag Ensure \\Flagged is cleared (no-op if already)
|
|
85
|
+
tag <name> Set IMAP keyword <name> (e.g. urgent, $Followup)
|
|
86
|
+
untag <name> Remove IMAP keyword <name>
|
|
87
|
+
clear-tags Remove all keywords
|
|
88
|
+
archive Move to the archive mailbox
|
|
89
|
+
move <mailbox-uuid> Move to a specific mailbox (use UUID from MailMate)
|
|
90
|
+
junk / not-junk Mark as junk / not junk
|
|
91
|
+
mute Toggle mute state
|
|
92
|
+
delete Delete (move to trash)
|
|
93
|
+
BANNER
|
|
94
|
+
o.on("--verify", "After running, print the message's current flags") { opts[:verify] = true }
|
|
95
|
+
o.on("--dry-run", "Print the actions; don't run") { opts[:dry_run] = true }
|
|
96
|
+
o.on("--settle SECONDS", Float, "Sleep between operations (default 3.5)") { |s| opts[:settle] = s }
|
|
97
|
+
o.on("--keep-window", "Don't close the spawned message-viewer window") { opts[:keep_window] = true }
|
|
98
|
+
end
|
|
99
|
+
parser.parse!(argv)
|
|
100
|
+
[opts, parser]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_actions(argv, parser)
|
|
104
|
+
actions = []
|
|
105
|
+
i = 0
|
|
106
|
+
while i < argv.length
|
|
107
|
+
name = argv[i]
|
|
108
|
+
spec = ACTIONS[name]
|
|
109
|
+
unless spec
|
|
110
|
+
warn "mm-modify: unknown action #{name.inspect}"
|
|
111
|
+
warn parser.help
|
|
112
|
+
return nil
|
|
113
|
+
end
|
|
114
|
+
arg_count = spec.is_a?(Symbol) ? 0 : (spec[1] || 0)
|
|
115
|
+
args = argv[(i + 1)...(i + 1 + arg_count)] || []
|
|
116
|
+
if args.length < arg_count
|
|
117
|
+
warn "mm-modify: action '#{name}' needs #{arg_count} arg(s); got #{args.length}"
|
|
118
|
+
return nil
|
|
119
|
+
end
|
|
120
|
+
actions << [name, spec.first, args]
|
|
121
|
+
i += 1 + arg_count
|
|
122
|
+
end
|
|
123
|
+
actions
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def warn_on_duplicates(message_id, eml_id)
|
|
127
|
+
dup_ids = Mailmate::DuplicateScanner.eml_ids_for(message_id)
|
|
128
|
+
return unless dup_ids.size > 1
|
|
129
|
+
others = dup_ids.reject { |id| id == eml_id.to_i }
|
|
130
|
+
warn "WARNING: Message-ID has #{dup_ids.size} copies in MailMate's tree."
|
|
131
|
+
warn " You targeted #{eml_id}.eml but the action may land on:"
|
|
132
|
+
warn " #{others.join(", ")}"
|
|
133
|
+
warn " (MailMate picks one candidate when resolving `mid:` URLs;"
|
|
134
|
+
warn " the choice is not deterministic by .eml id.)"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def drive(eml_id, message_id, actions, opts)
|
|
138
|
+
driver = Mailmate::AppleScriptDriver.new(dry_run: opts[:dry_run])
|
|
139
|
+
mid_url = Mailmate::MidUrl.for(message_id)
|
|
140
|
+
|
|
141
|
+
windows_before = driver.window_ids
|
|
142
|
+
driver.open_url(mid_url)
|
|
143
|
+
sleep(opts[:settle]) unless opts[:dry_run]
|
|
144
|
+
new_windows = driver.window_ids - windows_before
|
|
145
|
+
|
|
146
|
+
actions.each do |name, selector, args|
|
|
147
|
+
case selector
|
|
148
|
+
when :ensure_flagged, :ensure_not_flagged
|
|
149
|
+
want = selector == :ensure_flagged
|
|
150
|
+
flags = opts[:dry_run] ? [] : current_flags(eml_id)
|
|
151
|
+
has = flags.include?("\\Flagged")
|
|
152
|
+
if has == want
|
|
153
|
+
$stdout.puts "#{name}: already #{want ? "flagged" : "not flagged"} — no-op"
|
|
154
|
+
else
|
|
155
|
+
driver.perform("toggleFlag:")
|
|
156
|
+
sleep(opts[:settle]) unless opts[:dry_run]
|
|
157
|
+
end
|
|
158
|
+
else
|
|
159
|
+
driver.perform(selector, *args)
|
|
160
|
+
sleep(opts[:settle]) unless opts[:dry_run]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if opts[:verify] && !opts[:dry_run]
|
|
165
|
+
sleep(opts[:settle])
|
|
166
|
+
$stdout.puts "Flags now: #{current_flags(eml_id).inspect}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
unless opts[:keep_window] || opts[:dry_run] || new_windows.empty?
|
|
170
|
+
driver.close_windows(new_windows)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def current_flags(eml_id)
|
|
175
|
+
# AppleScript actions write the index asynchronously — bust just the
|
|
176
|
+
# #flags cache to pick up the latest values without throwing away
|
|
177
|
+
# other warmed indexes (#message-id, #source) that this same
|
|
178
|
+
# invocation may still need.
|
|
179
|
+
Mailmate::IndexReader.reset!("#flags")
|
|
180
|
+
Mailmate::IndexReader.for("#flags").flags_for(eml_id.to_i)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def usage_error(parser, msg)
|
|
184
|
+
warn "mm-modify: #{msg}"
|
|
185
|
+
warn parser.help
|
|
186
|
+
2
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|