sup 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTORS +84 -0
- data/Gemfile +3 -0
- data/HACKING +42 -0
- data/History.txt +361 -0
- data/LICENSE +280 -0
- data/README.md +70 -0
- data/Rakefile +12 -0
- data/ReleaseNotes +231 -0
- data/bin/sup +434 -0
- data/bin/sup-add +118 -0
- data/bin/sup-config +243 -0
- data/bin/sup-dump +43 -0
- data/bin/sup-import-dump +101 -0
- data/bin/sup-psych-ify-config-files +21 -0
- data/bin/sup-recover-sources +87 -0
- data/bin/sup-sync +210 -0
- data/bin/sup-sync-back-maildir +127 -0
- data/bin/sup-tweak-labels +140 -0
- data/contrib/colorpicker.rb +100 -0
- data/contrib/completion/_sup.zsh +114 -0
- data/devel/console.sh +3 -0
- data/devel/count-loc.sh +3 -0
- data/devel/load-index.rb +9 -0
- data/devel/profile.rb +12 -0
- data/devel/start-console.rb +5 -0
- data/doc/FAQ.txt +119 -0
- data/doc/Hooks.txt +79 -0
- data/doc/Philosophy.txt +69 -0
- data/lib/sup.rb +467 -0
- data/lib/sup/account.rb +90 -0
- data/lib/sup/buffer.rb +768 -0
- data/lib/sup/colormap.rb +239 -0
- data/lib/sup/contact.rb +67 -0
- data/lib/sup/crypto.rb +461 -0
- data/lib/sup/draft.rb +119 -0
- data/lib/sup/hook.rb +159 -0
- data/lib/sup/horizontal_selector.rb +59 -0
- data/lib/sup/idle.rb +42 -0
- data/lib/sup/index.rb +882 -0
- data/lib/sup/interactive_lock.rb +89 -0
- data/lib/sup/keymap.rb +140 -0
- data/lib/sup/label.rb +87 -0
- data/lib/sup/logger.rb +77 -0
- data/lib/sup/logger/singleton.rb +10 -0
- data/lib/sup/maildir.rb +257 -0
- data/lib/sup/mbox.rb +187 -0
- data/lib/sup/message.rb +803 -0
- data/lib/sup/message_chunks.rb +328 -0
- data/lib/sup/mode.rb +140 -0
- data/lib/sup/modes/buffer_list_mode.rb +50 -0
- data/lib/sup/modes/completion_mode.rb +55 -0
- data/lib/sup/modes/compose_mode.rb +38 -0
- data/lib/sup/modes/console_mode.rb +125 -0
- data/lib/sup/modes/contact_list_mode.rb +148 -0
- data/lib/sup/modes/edit_message_async_mode.rb +110 -0
- data/lib/sup/modes/edit_message_mode.rb +728 -0
- data/lib/sup/modes/file_browser_mode.rb +109 -0
- data/lib/sup/modes/forward_mode.rb +82 -0
- data/lib/sup/modes/help_mode.rb +19 -0
- data/lib/sup/modes/inbox_mode.rb +85 -0
- data/lib/sup/modes/label_list_mode.rb +138 -0
- data/lib/sup/modes/label_search_results_mode.rb +38 -0
- data/lib/sup/modes/line_cursor_mode.rb +203 -0
- data/lib/sup/modes/log_mode.rb +57 -0
- data/lib/sup/modes/person_search_results_mode.rb +12 -0
- data/lib/sup/modes/poll_mode.rb +19 -0
- data/lib/sup/modes/reply_mode.rb +228 -0
- data/lib/sup/modes/resume_mode.rb +52 -0
- data/lib/sup/modes/scroll_mode.rb +252 -0
- data/lib/sup/modes/search_list_mode.rb +204 -0
- data/lib/sup/modes/search_results_mode.rb +59 -0
- data/lib/sup/modes/text_mode.rb +76 -0
- data/lib/sup/modes/thread_index_mode.rb +1033 -0
- data/lib/sup/modes/thread_view_mode.rb +941 -0
- data/lib/sup/person.rb +134 -0
- data/lib/sup/poll.rb +272 -0
- data/lib/sup/rfc2047.rb +56 -0
- data/lib/sup/search.rb +110 -0
- data/lib/sup/sent.rb +58 -0
- data/lib/sup/service/label_service.rb +45 -0
- data/lib/sup/source.rb +244 -0
- data/lib/sup/tagger.rb +50 -0
- data/lib/sup/textfield.rb +253 -0
- data/lib/sup/thread.rb +452 -0
- data/lib/sup/time.rb +93 -0
- data/lib/sup/undo.rb +38 -0
- data/lib/sup/update.rb +30 -0
- data/lib/sup/util.rb +747 -0
- data/lib/sup/util/ncurses.rb +274 -0
- data/lib/sup/util/path.rb +9 -0
- data/lib/sup/util/query.rb +17 -0
- data/lib/sup/util/uri.rb +15 -0
- data/lib/sup/version.rb +3 -0
- data/sup.gemspec +53 -0
- data/test/dummy_source.rb +61 -0
- data/test/gnupg_test_home/gpg.conf +1 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
- data/test/gnupg_test_home/trustdb.gpg +0 -0
- data/test/integration/test_label_service.rb +18 -0
- data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
- data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
- data/test/messages/missing-line.eml +9 -0
- data/test/test_crypto.rb +109 -0
- data/test/test_header_parsing.rb +168 -0
- data/test/test_helper.rb +7 -0
- data/test/test_message.rb +532 -0
- data/test/test_messages_dir.rb +147 -0
- data/test/test_yaml_migration.rb +85 -0
- data/test/test_yaml_regressions.rb +17 -0
- data/test/unit/service/test_label_service.rb +19 -0
- data/test/unit/test_horizontal_selector.rb +40 -0
- data/test/unit/util/test_query.rb +46 -0
- data/test/unit/util/test_string.rb +57 -0
- data/test/unit/util/test_uri.rb +19 -0
- metadata +423 -0
data/bin/sup-sync
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
|
+
|
5
|
+
require 'uri'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'trollop'
|
8
|
+
require "sup"
|
9
|
+
|
10
|
+
PROGRESS_UPDATE_INTERVAL = 15 # seconds
|
11
|
+
|
12
|
+
class Float
|
13
|
+
def to_s; sprintf '%.2f', self; end
|
14
|
+
def to_time_s; infinite? ? "unknown" : super end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Numeric
|
18
|
+
def to_time_s
|
19
|
+
i = to_i
|
20
|
+
sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Set
|
25
|
+
def to_s; to_a * ',' end
|
26
|
+
end
|
27
|
+
|
28
|
+
def time
|
29
|
+
startt = Time.now
|
30
|
+
yield
|
31
|
+
Time.now - startt
|
32
|
+
end
|
33
|
+
|
34
|
+
opts = Trollop::options do
|
35
|
+
version "sup-sync (sup #{Redwood::VERSION})"
|
36
|
+
banner <<EOS
|
37
|
+
Synchronizes the Sup index with one or more message sources by adding
|
38
|
+
messages, deleting messages, or changing message state in the index as
|
39
|
+
appropriate.
|
40
|
+
|
41
|
+
"Message state" means read/unread, archived/inbox, starred/unstarred,
|
42
|
+
and all user-defined labels on each message.
|
43
|
+
|
44
|
+
"Default source state" refers to any state that a source itself has
|
45
|
+
keeps about a message. Sup-sync uses this information when adding a
|
46
|
+
new message to the index. The source state is typically limited to
|
47
|
+
read/unread, archived/inbox status and a single label based on the
|
48
|
+
source name. Messages using the default source state are placed in
|
49
|
+
the inbox (i.e. not archived) and unstarred.
|
50
|
+
|
51
|
+
Usage:
|
52
|
+
sup-sync [options] <source>*
|
53
|
+
|
54
|
+
where <source>* is zero or more source URIs. If no sources are given,
|
55
|
+
sync from all usual sources. Supported source URI schemes can be seen
|
56
|
+
by running "sup-add --help".
|
57
|
+
|
58
|
+
Options controlling HOW message state is altered:
|
59
|
+
EOS
|
60
|
+
opt :asis, "If the message is already in the index, preserve its state. Otherwise, use default source state. (Default.)", :short => :none
|
61
|
+
opt :restore, "Restore message state from a dump file created with sup-dump. If a message is not in this dumpfile, act as --asis.", :type => String, :short => :none
|
62
|
+
opt :discard, "Discard any message state in the index and use the default source state. Dangerous!", :short => :none
|
63
|
+
opt :archive, "When using the default source state, mark messages as archived.", :short => "-x"
|
64
|
+
opt :read, "When using the default source state, mark messages as read."
|
65
|
+
opt :extra_labels, "When using the default source state, also apply these user-defined labels (a comma-separated list)", :default => "", :short => :none
|
66
|
+
|
67
|
+
text <<EOS
|
68
|
+
|
69
|
+
Other options:
|
70
|
+
EOS
|
71
|
+
opt :verbose, "Print message ids as they're processed."
|
72
|
+
opt :optimize, "As the final operation, optimize the index."
|
73
|
+
opt :all_sources, "Scan over all sources.", :short => :none
|
74
|
+
opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
|
75
|
+
opt :version, "Show version information", :short => :none
|
76
|
+
|
77
|
+
conflicts :asis, :restore, :discard
|
78
|
+
end
|
79
|
+
|
80
|
+
op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis
|
81
|
+
|
82
|
+
Redwood::start
|
83
|
+
index = Redwood::Index.init
|
84
|
+
|
85
|
+
restored_state = if opts[:restore]
|
86
|
+
dump = {}
|
87
|
+
puts "Loading state dump from #{opts[:restore]}..."
|
88
|
+
IO.foreach opts[:restore] do |l|
|
89
|
+
l =~ /^(\S+) \((.*?)\)$/ or raise "Can't read dump line: #{l.inspect}"
|
90
|
+
mid, labels = $1, $2
|
91
|
+
dump[mid] = labels.to_set_of_symbols
|
92
|
+
end
|
93
|
+
puts "Read #{dump.size} entries from dump file."
|
94
|
+
dump
|
95
|
+
else
|
96
|
+
{}
|
97
|
+
end
|
98
|
+
|
99
|
+
seen = {}
|
100
|
+
index.lock_interactively or exit
|
101
|
+
begin
|
102
|
+
index.load
|
103
|
+
|
104
|
+
if(s = Redwood::SourceManager.source_for Redwood::SentManager.source_uri)
|
105
|
+
Redwood::SentManager.source = s
|
106
|
+
else
|
107
|
+
Redwood::SourceManager.add_source Redwood::SentManager.default_source
|
108
|
+
end
|
109
|
+
|
110
|
+
sources = if opts[:all_sources]
|
111
|
+
Redwood::SourceManager.sources
|
112
|
+
elsif ARGV.empty?
|
113
|
+
Redwood::SourceManager.usual_sources
|
114
|
+
else
|
115
|
+
ARGV.map do |uri|
|
116
|
+
Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
sources.each do |source|
|
121
|
+
puts "Scanning #{source}..."
|
122
|
+
num_added = num_updated = num_deleted = num_scanned = num_restored = 0
|
123
|
+
last_info_time = start_time = Time.now
|
124
|
+
|
125
|
+
Redwood::PollManager.poll_from source do |action,m,old_m,progress|
|
126
|
+
num_scanned += 1
|
127
|
+
if action == :delete
|
128
|
+
num_deleted += 1
|
129
|
+
puts "Deleting #{m.id}" if opts[:verbose]
|
130
|
+
elsif action == :add
|
131
|
+
seen[m.id] = true
|
132
|
+
|
133
|
+
## tweak source labels according to commandline arguments if necessary
|
134
|
+
m.labels.delete :inbox if opts[:archive]
|
135
|
+
m.labels.delete :unread if opts[:read]
|
136
|
+
m.labels += opts[:extra_labels].to_set_of_symbols(",")
|
137
|
+
|
138
|
+
## decide what to do based on message labels and the operation we're performing
|
139
|
+
dothis = case
|
140
|
+
when (op == :restore) && restored_state[m.id]
|
141
|
+
if old_m && (old_m.labels != restored_state[m.id])
|
142
|
+
num_restored += 1
|
143
|
+
m.labels = restored_state[m.id]
|
144
|
+
:update_message_state
|
145
|
+
elsif old_m.nil?
|
146
|
+
num_restored += 1
|
147
|
+
m.labels = restored_state[m.id]
|
148
|
+
:add_message
|
149
|
+
else
|
150
|
+
# labels are the same; don't do anything
|
151
|
+
end
|
152
|
+
when op == :discard
|
153
|
+
if old_m && (old_m.labels != m.labels)
|
154
|
+
:update_message_state
|
155
|
+
else
|
156
|
+
# labels are the same; don't do anything
|
157
|
+
end
|
158
|
+
else
|
159
|
+
if old_m
|
160
|
+
:update_message
|
161
|
+
else
|
162
|
+
:add_message
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
## now, actually do the operation
|
167
|
+
case dothis
|
168
|
+
when :add_message
|
169
|
+
puts "Adding new message #{source}##{m.source_info} with labels #{m.labels}" if opts[:verbose]
|
170
|
+
num_added += 1
|
171
|
+
when :update_message
|
172
|
+
puts "Updating message #{source}##{m.source_info}; labels #{old_m.labels} => #{m.labels}; offset #{old_m.source_info} => #{m.source_info}" if opts[:verbose]
|
173
|
+
num_updated += 1
|
174
|
+
when :update_message_state
|
175
|
+
puts "Changing flags for #{source}##{m.source_info} from #{old_m.labels} to #{m.labels}" if opts[:verbose]
|
176
|
+
num_updated += 1
|
177
|
+
end
|
178
|
+
else fail "sup-sync cannot handle :update's"
|
179
|
+
end
|
180
|
+
|
181
|
+
if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
|
182
|
+
last_info_time = Time.now
|
183
|
+
elapsed = last_info_time - start_time
|
184
|
+
pctdone = progress * 100.0
|
185
|
+
remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
|
186
|
+
printf "## scanned %dm (~%.0f%%) @ %.1fm/s. %s elapsed, ~%s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
|
187
|
+
end
|
188
|
+
next if opts[:dry_run]
|
189
|
+
end
|
190
|
+
|
191
|
+
puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated}, deleted #{num_deleted} messages from #{source}."
|
192
|
+
puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
|
193
|
+
end
|
194
|
+
|
195
|
+
index.save
|
196
|
+
|
197
|
+
if opts[:optimize]
|
198
|
+
puts "Optimizing index..."
|
199
|
+
optt = time { index.optimize unless opts[:dry_run] }
|
200
|
+
puts "Optimized index of size #{index.size} in #{optt}s."
|
201
|
+
end
|
202
|
+
rescue Redwood::FatalSourceError => e
|
203
|
+
$stderr.puts "Sorry, I couldn't communicate with a source: #{e.message}"
|
204
|
+
rescue Exception => e
|
205
|
+
File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
|
206
|
+
raise
|
207
|
+
ensure
|
208
|
+
Redwood::finish
|
209
|
+
index.unlock
|
210
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'trollop'
|
8
|
+
require "sup"
|
9
|
+
|
10
|
+
opts = Trollop::options do
|
11
|
+
version "sup-sync-back-maildir (sup #{Redwood::VERSION})"
|
12
|
+
banner <<EOS
|
13
|
+
Export Xapian entries to Maildir sources on disk.
|
14
|
+
|
15
|
+
This script parses the Xapian entries for a given Maildir source and renames
|
16
|
+
(changes maildir flags) e-mail files on disk according to the labels stored in
|
17
|
+
the index. It will export all the changes you made in Sup to your
|
18
|
+
Maildirs so that they can be propagated to your IMAP server with e.g. offlineimap.
|
19
|
+
|
20
|
+
The script also merges some Maildir flags into Sup such
|
21
|
+
as R (replied) and P (passed, forwarded), for instance suppose you
|
22
|
+
have an e-mail file like this: foo_bar:2,FRS (flags are favorite,
|
23
|
+
replied, seen) and its Xapian entry has labels 'starred', the merge
|
24
|
+
operation will add the 'replied' label to the Xapian entry.
|
25
|
+
|
26
|
+
If you choose not to merge (-m) you will lose information ('replied'), and in
|
27
|
+
the previous example the file will be renamed to foo_bar:2,FS.
|
28
|
+
|
29
|
+
Running this script is *strongly* recommended when setting the
|
30
|
+
"sync_back_to_maildir" option from false to true in config.yaml or changing the
|
31
|
+
"sync_back" flag to true for a source in sources.yaml.
|
32
|
+
|
33
|
+
Usage:
|
34
|
+
sup-sync-back-maildir [options] <source>*
|
35
|
+
|
36
|
+
where <source>* is source URIs. If no source is given, the default behavior is
|
37
|
+
to sync back all Maildir sources marked as usual and that have not disabled
|
38
|
+
sync back using the configuration parameter sync_back = false in sources.yaml.
|
39
|
+
|
40
|
+
Options include:
|
41
|
+
EOS
|
42
|
+
opt :no_confirm, "Don't ask for confirmation before synchronizing", :default => false, :short => "n"
|
43
|
+
opt :no_merge, "Don't merge new supported Maildir flags (R and P)", :default => false, :short => "m"
|
44
|
+
opt :list_sources, "List your Maildir sources and exit", :default => false, :short => "l"
|
45
|
+
opt :unusual_sources_too, "Sync unusual sources too if no specific source information is given", :default => false, :short => "u"
|
46
|
+
end
|
47
|
+
|
48
|
+
def die msg
|
49
|
+
$stderr.puts "Error: #{msg}"
|
50
|
+
exit(-1)
|
51
|
+
end
|
52
|
+
|
53
|
+
Redwood::start true
|
54
|
+
index = Redwood::Index.init
|
55
|
+
index.lock_interactively or exit
|
56
|
+
index.load
|
57
|
+
|
58
|
+
## Force sync_back_to_maildir option otherwise nothing will happen
|
59
|
+
$config[:sync_back_to_maildir] = true
|
60
|
+
|
61
|
+
begin
|
62
|
+
sync_performed = []
|
63
|
+
sync_performed = File.readlines(Redwood::SYNC_OK_FN).collect { |e| e.strip }.find_all { |e| not e.empty? } if File.exists? Redwood::SYNC_OK_FN
|
64
|
+
sources = []
|
65
|
+
|
66
|
+
## Try to find out sources given in parameters
|
67
|
+
sources = ARGV.map do |uri|
|
68
|
+
s = Redwood::SourceManager.source_for(uri) or die "unknown source: #{uri}. Did you add it with sup-add first?"
|
69
|
+
s.is_a?(Redwood::Maildir) or die "#{uri} is not a Maildir source."
|
70
|
+
s.sync_back_enabled? or die "#{uri} has disabled sync back - check your configuration."
|
71
|
+
s
|
72
|
+
end unless opts[:list_sources]
|
73
|
+
|
74
|
+
## Otherwise, check all sources in sources.yaml
|
75
|
+
if sources.empty? or opts[:list_sources] == true
|
76
|
+
if opts[:unusual_sources_too]
|
77
|
+
sources = Redwood::SourceManager.sources.select do |s|
|
78
|
+
s.is_a? Redwood::Maildir and s.sync_back_enabled?
|
79
|
+
end
|
80
|
+
else
|
81
|
+
sources = Redwood::SourceManager.usual_sources.select do |s|
|
82
|
+
s.is_a? Redwood::Maildir and s.sync_back_enabled?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if opts[:list_sources] == true
|
88
|
+
sources.each do |s|
|
89
|
+
puts "id: #{s.id}, uri: #{s.uri}"
|
90
|
+
end
|
91
|
+
else
|
92
|
+
sources.each do |s|
|
93
|
+
if opts[:no_confirm] == false
|
94
|
+
print "Are you sure you want to synchronize '#{s.uri}'? (Y/n) "
|
95
|
+
next if STDIN.gets.chomp.downcase == 'n'
|
96
|
+
end
|
97
|
+
|
98
|
+
infos = Enumerator.new(index, :each_source_info, s.id).to_a
|
99
|
+
counter = 0
|
100
|
+
infos.each do |info|
|
101
|
+
print "\rSynchronizing '#{s.uri}'... #{((counter += 1)/infos.size.to_f*100).to_i}%"
|
102
|
+
index.each_message({:location => [s.id, info]}, false) do |m|
|
103
|
+
if opts[:no_merge] == false
|
104
|
+
m.merge_labels_from_locations [:replied, :forwarded]
|
105
|
+
end
|
106
|
+
|
107
|
+
if Redwood::Index.message_joining_killed? m
|
108
|
+
m.labels += [:killed]
|
109
|
+
end
|
110
|
+
|
111
|
+
index.save_message m
|
112
|
+
end
|
113
|
+
end
|
114
|
+
print "\n"
|
115
|
+
sync_performed << s.uri
|
116
|
+
end
|
117
|
+
## Write a flag file to tell sup that the synchronization has been performed
|
118
|
+
File.open(Redwood::SYNC_OK_FN, 'w') {|f| f.write(sync_performed.join("\n")) }
|
119
|
+
end
|
120
|
+
rescue Exception => e
|
121
|
+
File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
|
122
|
+
raise
|
123
|
+
ensure
|
124
|
+
index.save_index
|
125
|
+
Redwood::finish
|
126
|
+
index.unlock
|
127
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'trollop'
|
7
|
+
require "sup"
|
8
|
+
|
9
|
+
class Float
|
10
|
+
def to_s; sprintf '%.2f', self; end
|
11
|
+
def to_time_s
|
12
|
+
infinite? ? "unknown" : super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Numeric
|
17
|
+
def to_time_s
|
18
|
+
i = to_i
|
19
|
+
sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def time
|
24
|
+
startt = Time.now
|
25
|
+
yield
|
26
|
+
Time.now - startt
|
27
|
+
end
|
28
|
+
|
29
|
+
opts = Trollop::options do
|
30
|
+
version "sup-tweak-labels (sup #{Redwood::VERSION})"
|
31
|
+
banner <<EOS
|
32
|
+
Batch modification of message state for messages already in the index.
|
33
|
+
|
34
|
+
Usage:
|
35
|
+
sup-tweak-labels [options] <source>*
|
36
|
+
|
37
|
+
where <source>* is zero or more source URIs. Supported source URI schemes can
|
38
|
+
be seen by running "sup-add --help".
|
39
|
+
|
40
|
+
Options:
|
41
|
+
EOS
|
42
|
+
opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :default => ""
|
43
|
+
opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :default => ""
|
44
|
+
opt :query, "A Sup search query", :type => String
|
45
|
+
|
46
|
+
text <<EOS
|
47
|
+
|
48
|
+
Other options:
|
49
|
+
EOS
|
50
|
+
opt :verbose, "Print message ids as they're processed."
|
51
|
+
opt :very_verbose, "Print message names and subjects as they're processed."
|
52
|
+
opt :all_sources, "Scan over all sources.", :short => :none
|
53
|
+
opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
|
54
|
+
opt :no_sync_back, "Do not sync back to the original Maildir."
|
55
|
+
opt :version, "Show version information", :short => :none
|
56
|
+
end
|
57
|
+
opts[:verbose] = true if opts[:very_verbose]
|
58
|
+
|
59
|
+
add_labels = opts[:add].to_set_of_symbols ","
|
60
|
+
remove_labels = opts[:remove].to_set_of_symbols ","
|
61
|
+
|
62
|
+
Trollop::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
|
63
|
+
|
64
|
+
Redwood::start
|
65
|
+
index = Redwood::Index.init
|
66
|
+
index.lock_interactively or exit
|
67
|
+
|
68
|
+
begin
|
69
|
+
index.load
|
70
|
+
|
71
|
+
source_ids = if opts[:all_sources]
|
72
|
+
Redwood::SourceManager.sources
|
73
|
+
else
|
74
|
+
ARGV.map do |uri|
|
75
|
+
Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
|
76
|
+
end
|
77
|
+
end.map { |s| s.id }
|
78
|
+
Trollop::die "nothing to do: no sources" if source_ids.empty?
|
79
|
+
|
80
|
+
query = "(" + source_ids.map { |id| "source_id:#{id}" }.join(" OR ") + ")"
|
81
|
+
if add_labels.empty?
|
82
|
+
## if all we're doing is removing labels, we can further restrict the
|
83
|
+
## query to only messages with those labels
|
84
|
+
query += " (" + remove_labels.map { |l| "label:#{l}" }.join(" OR ") + ")"
|
85
|
+
end
|
86
|
+
query += ' ' + opts[:query] if opts[:query]
|
87
|
+
|
88
|
+
parsed_query = index.parse_query query
|
89
|
+
parsed_query.merge! :load_spam => true, :load_deleted => true, :load_killed => true
|
90
|
+
ids = index.to_enum(:each_id, parsed_query)
|
91
|
+
num_total = index.num_results_for parsed_query
|
92
|
+
|
93
|
+
$stderr.puts "Found #{num_total} documents across #{source_ids.length} sources. Scanning..."
|
94
|
+
|
95
|
+
num_changed = num_scanned = 0
|
96
|
+
last_info_time = start_time = Time.now
|
97
|
+
ids.each do |id|
|
98
|
+
num_scanned += 1
|
99
|
+
|
100
|
+
m = index.build_message id
|
101
|
+
old_labels = m.labels.dup
|
102
|
+
|
103
|
+
m.labels += add_labels
|
104
|
+
m.labels -= remove_labels
|
105
|
+
|
106
|
+
unless m.labels == old_labels
|
107
|
+
num_changed += 1
|
108
|
+
puts "From #{m.from}, subject: #{m.subj}" if opts[:very_verbose]
|
109
|
+
puts "#{m.id}: {#{old_labels.to_a.join ','}} => {#{m.labels.to_a.join ','}}" if opts[:verbose]
|
110
|
+
puts if opts[:very_verbose]
|
111
|
+
unless opts[:dry_run]
|
112
|
+
index.update_message_state [m, false]
|
113
|
+
m.sync_back unless opts[:no_sync_back]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
if Time.now - last_info_time > 60
|
118
|
+
last_info_time = Time.now
|
119
|
+
elapsed = last_info_time - start_time
|
120
|
+
pctdone = 100.0 * num_scanned.to_f / num_total.to_f
|
121
|
+
remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
|
122
|
+
$stderr.puts "## #{num_scanned} (#{pctdone}%) read; #{elapsed.to_time_s} elapsed; #{remaining.to_time_s} remaining"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
$stderr.puts "Scanned #{num_scanned} / #{num_total} messages and changed #{num_changed}."
|
126
|
+
|
127
|
+
unless num_changed == 0
|
128
|
+
$stderr.puts "Optimizing index..."
|
129
|
+
index.optimize unless opts[:dry_run]
|
130
|
+
end
|
131
|
+
|
132
|
+
rescue Exception => e
|
133
|
+
File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
|
134
|
+
raise
|
135
|
+
ensure
|
136
|
+
index.save
|
137
|
+
Redwood::finish
|
138
|
+
index.unlock
|
139
|
+
end
|
140
|
+
|