sup 0.19.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.
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
+