sup 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +12 -0
  4. data/CONTRIBUTORS +84 -0
  5. data/Gemfile +3 -0
  6. data/HACKING +42 -0
  7. data/History.txt +361 -0
  8. data/LICENSE +280 -0
  9. data/README.md +70 -0
  10. data/Rakefile +12 -0
  11. data/ReleaseNotes +231 -0
  12. data/bin/sup +434 -0
  13. data/bin/sup-add +118 -0
  14. data/bin/sup-config +243 -0
  15. data/bin/sup-dump +43 -0
  16. data/bin/sup-import-dump +101 -0
  17. data/bin/sup-psych-ify-config-files +21 -0
  18. data/bin/sup-recover-sources +87 -0
  19. data/bin/sup-sync +210 -0
  20. data/bin/sup-sync-back-maildir +127 -0
  21. data/bin/sup-tweak-labels +140 -0
  22. data/contrib/colorpicker.rb +100 -0
  23. data/contrib/completion/_sup.zsh +114 -0
  24. data/devel/console.sh +3 -0
  25. data/devel/count-loc.sh +3 -0
  26. data/devel/load-index.rb +9 -0
  27. data/devel/profile.rb +12 -0
  28. data/devel/start-console.rb +5 -0
  29. data/doc/FAQ.txt +119 -0
  30. data/doc/Hooks.txt +79 -0
  31. data/doc/Philosophy.txt +69 -0
  32. data/lib/sup.rb +467 -0
  33. data/lib/sup/account.rb +90 -0
  34. data/lib/sup/buffer.rb +768 -0
  35. data/lib/sup/colormap.rb +239 -0
  36. data/lib/sup/contact.rb +67 -0
  37. data/lib/sup/crypto.rb +461 -0
  38. data/lib/sup/draft.rb +119 -0
  39. data/lib/sup/hook.rb +159 -0
  40. data/lib/sup/horizontal_selector.rb +59 -0
  41. data/lib/sup/idle.rb +42 -0
  42. data/lib/sup/index.rb +882 -0
  43. data/lib/sup/interactive_lock.rb +89 -0
  44. data/lib/sup/keymap.rb +140 -0
  45. data/lib/sup/label.rb +87 -0
  46. data/lib/sup/logger.rb +77 -0
  47. data/lib/sup/logger/singleton.rb +10 -0
  48. data/lib/sup/maildir.rb +257 -0
  49. data/lib/sup/mbox.rb +187 -0
  50. data/lib/sup/message.rb +803 -0
  51. data/lib/sup/message_chunks.rb +328 -0
  52. data/lib/sup/mode.rb +140 -0
  53. data/lib/sup/modes/buffer_list_mode.rb +50 -0
  54. data/lib/sup/modes/completion_mode.rb +55 -0
  55. data/lib/sup/modes/compose_mode.rb +38 -0
  56. data/lib/sup/modes/console_mode.rb +125 -0
  57. data/lib/sup/modes/contact_list_mode.rb +148 -0
  58. data/lib/sup/modes/edit_message_async_mode.rb +110 -0
  59. data/lib/sup/modes/edit_message_mode.rb +728 -0
  60. data/lib/sup/modes/file_browser_mode.rb +109 -0
  61. data/lib/sup/modes/forward_mode.rb +82 -0
  62. data/lib/sup/modes/help_mode.rb +19 -0
  63. data/lib/sup/modes/inbox_mode.rb +85 -0
  64. data/lib/sup/modes/label_list_mode.rb +138 -0
  65. data/lib/sup/modes/label_search_results_mode.rb +38 -0
  66. data/lib/sup/modes/line_cursor_mode.rb +203 -0
  67. data/lib/sup/modes/log_mode.rb +57 -0
  68. data/lib/sup/modes/person_search_results_mode.rb +12 -0
  69. data/lib/sup/modes/poll_mode.rb +19 -0
  70. data/lib/sup/modes/reply_mode.rb +228 -0
  71. data/lib/sup/modes/resume_mode.rb +52 -0
  72. data/lib/sup/modes/scroll_mode.rb +252 -0
  73. data/lib/sup/modes/search_list_mode.rb +204 -0
  74. data/lib/sup/modes/search_results_mode.rb +59 -0
  75. data/lib/sup/modes/text_mode.rb +76 -0
  76. data/lib/sup/modes/thread_index_mode.rb +1033 -0
  77. data/lib/sup/modes/thread_view_mode.rb +941 -0
  78. data/lib/sup/person.rb +134 -0
  79. data/lib/sup/poll.rb +272 -0
  80. data/lib/sup/rfc2047.rb +56 -0
  81. data/lib/sup/search.rb +110 -0
  82. data/lib/sup/sent.rb +58 -0
  83. data/lib/sup/service/label_service.rb +45 -0
  84. data/lib/sup/source.rb +244 -0
  85. data/lib/sup/tagger.rb +50 -0
  86. data/lib/sup/textfield.rb +253 -0
  87. data/lib/sup/thread.rb +452 -0
  88. data/lib/sup/time.rb +93 -0
  89. data/lib/sup/undo.rb +38 -0
  90. data/lib/sup/update.rb +30 -0
  91. data/lib/sup/util.rb +747 -0
  92. data/lib/sup/util/ncurses.rb +274 -0
  93. data/lib/sup/util/path.rb +9 -0
  94. data/lib/sup/util/query.rb +17 -0
  95. data/lib/sup/util/uri.rb +15 -0
  96. data/lib/sup/version.rb +3 -0
  97. data/sup.gemspec +53 -0
  98. data/test/dummy_source.rb +61 -0
  99. data/test/gnupg_test_home/gpg.conf +1 -0
  100. data/test/gnupg_test_home/pubring.gpg +0 -0
  101. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  102. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  103. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  104. data/test/gnupg_test_home/secring.gpg +0 -0
  105. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
  106. data/test/gnupg_test_home/trustdb.gpg +0 -0
  107. data/test/integration/test_label_service.rb +18 -0
  108. data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
  109. data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
  110. data/test/messages/missing-line.eml +9 -0
  111. data/test/test_crypto.rb +109 -0
  112. data/test/test_header_parsing.rb +168 -0
  113. data/test/test_helper.rb +7 -0
  114. data/test/test_message.rb +532 -0
  115. data/test/test_messages_dir.rb +147 -0
  116. data/test/test_yaml_migration.rb +85 -0
  117. data/test/test_yaml_regressions.rb +17 -0
  118. data/test/unit/service/test_label_service.rb +19 -0
  119. data/test/unit/test_horizontal_selector.rb +40 -0
  120. data/test/unit/util/test_query.rb +46 -0
  121. data/test/unit/util/test_string.rb +57 -0
  122. data/test/unit/util/test_uri.rb +19 -0
  123. metadata +423 -0
@@ -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
+