sup 0.0.8 → 0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (57) hide show
  1. data/HACKING +6 -36
  2. data/History.txt +11 -0
  3. data/Manifest.txt +5 -0
  4. data/README.txt +13 -31
  5. data/Rakefile +3 -3
  6. data/bin/sup +167 -89
  7. data/bin/sup-add +39 -29
  8. data/bin/sup-config +57 -31
  9. data/bin/sup-sync +60 -54
  10. data/bin/sup-sync-back +143 -0
  11. data/doc/FAQ.txt +56 -19
  12. data/doc/Philosophy.txt +34 -33
  13. data/doc/TODO +76 -46
  14. data/doc/UserGuide.txt +142 -122
  15. data/lib/sup.rb +76 -36
  16. data/lib/sup/account.rb +27 -19
  17. data/lib/sup/buffer.rb +130 -44
  18. data/lib/sup/contact.rb +1 -1
  19. data/lib/sup/draft.rb +1 -2
  20. data/lib/sup/imap.rb +64 -19
  21. data/lib/sup/index.rb +95 -16
  22. data/lib/sup/keymap.rb +1 -1
  23. data/lib/sup/label.rb +31 -5
  24. data/lib/sup/maildir.rb +7 -5
  25. data/lib/sup/mbox.rb +34 -15
  26. data/lib/sup/mbox/loader.rb +30 -12
  27. data/lib/sup/mbox/ssh-loader.rb +7 -5
  28. data/lib/sup/message.rb +93 -44
  29. data/lib/sup/modes/buffer-list-mode.rb +1 -1
  30. data/lib/sup/modes/completion-mode.rb +55 -0
  31. data/lib/sup/modes/compose-mode.rb +6 -25
  32. data/lib/sup/modes/contact-list-mode.rb +1 -1
  33. data/lib/sup/modes/edit-message-mode.rb +119 -29
  34. data/lib/sup/modes/file-browser-mode.rb +108 -0
  35. data/lib/sup/modes/forward-mode.rb +3 -20
  36. data/lib/sup/modes/inbox-mode.rb +9 -12
  37. data/lib/sup/modes/label-list-mode.rb +28 -46
  38. data/lib/sup/modes/label-search-results-mode.rb +1 -16
  39. data/lib/sup/modes/line-cursor-mode.rb +44 -5
  40. data/lib/sup/modes/person-search-results-mode.rb +1 -16
  41. data/lib/sup/modes/reply-mode.rb +18 -31
  42. data/lib/sup/modes/resume-mode.rb +6 -6
  43. data/lib/sup/modes/scroll-mode.rb +6 -5
  44. data/lib/sup/modes/search-results-mode.rb +6 -17
  45. data/lib/sup/modes/thread-index-mode.rb +70 -28
  46. data/lib/sup/modes/thread-view-mode.rb +65 -29
  47. data/lib/sup/person.rb +71 -30
  48. data/lib/sup/poll.rb +13 -4
  49. data/lib/sup/rfc2047.rb +61 -0
  50. data/lib/sup/sent.rb +7 -5
  51. data/lib/sup/source.rb +12 -9
  52. data/lib/sup/suicide.rb +36 -0
  53. data/lib/sup/tagger.rb +6 -6
  54. data/lib/sup/textfield.rb +76 -14
  55. data/lib/sup/thread.rb +97 -123
  56. data/lib/sup/util.rb +167 -1
  57. metadata +30 -5
data/bin/sup-add CHANGED
@@ -37,6 +37,7 @@ Options are:
37
37
  EOS
38
38
  opt :archive, "Automatically archive all new messages from these sources."
39
39
  opt :unusual, "Do not automatically poll these sources for new messages."
40
+ opt :labels, "A comma-separated set of labels to apply to all messages from this source", :type => String
40
41
  opt :force_new, "Create a new account for this source, even if one already exists."
41
42
  end
42
43
 
@@ -77,36 +78,45 @@ end
77
78
  $terminal.wrap_at = :auto
78
79
  Redwood::start
79
80
  index = Redwood::Index.new
80
- index.load
81
81
 
82
- ARGV.each do |uri|
83
- if !$opts[:force_new] && index.source_for(uri)
84
- say "Already know about #{uri}; skipping."
85
- next
86
- end
82
+ index.lock_or_die
83
+
84
+ begin
85
+ index.load_sources
87
86
 
88
- parsed_uri = URI(uri)
89
-
90
- source =
91
- case parsed_uri.scheme
92
- when "mbox+ssh"
93
- say "For SSH connections, if you will use public key authentication, you may leave the username and password blank."
94
- say ""
95
- username, password = get_login_info uri, index.sources
96
- Redwood::MBox::SSHLoader.new uri, username, password, nil, !$opts[:unusual], $opts[:archive]
97
- when "imap", "imaps"
98
- username, password = get_login_info uri, index.sources
99
- Redwood::IMAP.new uri, username, password, nil, !$opts[:unusual], $opts[:archive]
100
- when "maildir"
101
- Redwood::Maildir.new uri, nil, !$opts[:unusual], $opts[:archive]
102
- when "mbox"
103
- Redwood::MBox::Loader.new uri, nil, !$opts[:unusual], $opts[:archive]
104
- else
105
- Trollop::die "Unknown source type #{parsed_uri.scheme.inspect}"
87
+ ARGV.each do |uri|
88
+ labels = $opts[:labels] ? $opts[:labels].split(/\s*,\s*/).uniq : []
89
+
90
+ if !$opts[:force_new] && index.source_for(uri)
91
+ say "Already know about #{uri}; skipping."
92
+ next
106
93
  end
107
- say "Adding #{source}..."
108
- index.add_source source
109
- end
110
94
 
111
- index.save
112
- Redwood::finish
95
+ parsed_uri = URI(uri)
96
+ Trollop::die "no path component to uri: #{parsed_uri}" unless parsed_uri.path
97
+
98
+ source =
99
+ case parsed_uri.scheme
100
+ when "mbox+ssh"
101
+ say "For SSH connections, if you will use public key authentication, you may leave the username and password blank."
102
+ say ""
103
+ username, password = get_login_info uri, index.sources
104
+ Redwood::MBox::SSHLoader.new uri, username, password, nil, !$opts[:unusual], $opts[:archive], nil, labels
105
+ when "imap", "imaps"
106
+ username, password = get_login_info uri, index.sources
107
+ Redwood::IMAP.new uri, username, password, nil, !$opts[:unusual], $opts[:archive], nil, labels
108
+ when "maildir"
109
+ Redwood::Maildir.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
110
+ when "mbox"
111
+ Redwood::MBox::Loader.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
112
+ else
113
+ Trollop::die "Unknown source type #{parsed_uri.scheme.inspect}"
114
+ end
115
+ say "Adding #{source}..."
116
+ index.add_source source
117
+ end
118
+ ensure
119
+ index.save
120
+ index.unlock
121
+ Redwood::finish
122
+ end
data/bin/sup-config CHANGED
@@ -21,11 +21,11 @@ end #' stupid ruby-mode
21
21
 
22
22
  def axe q, default=nil
23
23
  ans =
24
- if default && !default.empty?
25
- ask "#{q} (enter for \"#{default}\"): "
26
- else
27
- ask "#{q}: "
28
- end
24
+ if default && !default.empty?
25
+ ask "#{q} (enter for \"#{default}\"): "
26
+ else
27
+ ask "#{q}: "
28
+ end
29
29
  ans.empty? ? default : ans
30
30
  end
31
31
 
@@ -42,7 +42,7 @@ def add_source
42
42
 
43
43
  say "Ok, adding a new source."
44
44
  choose do |menu|
45
- menu.prompt = "What type of mail source is it?"
45
+ menu.prompt = "What type of mail source is it? "
46
46
  menu.choice("mbox file") { type = :mbox }
47
47
  menu.choice("maildir directory") { type = :maildir }
48
48
  menu.choice("remote mbox file (accessible via ssh)") { type = :mboxssh }
@@ -52,34 +52,57 @@ def add_source
52
52
  end
53
53
 
54
54
  while true do
55
- say "Now for the details."
55
+ say "Ok, now for the details."
56
56
 
57
- components =
57
+ default_labels, components =
58
58
  case type
59
59
  when :mbox
60
- fn = axe "What's the full path to the mbox file?", ENV["MAIL"] #"srm
60
+ $last_fn ||= ENV["MAIL"]
61
+ fn = axe "What's the full path to the mbox file?", $last_fn #"srm
61
62
  return if fn.nil? || fn.empty?
62
- { :scheme => "mbox", :path => fn }
63
+
64
+ $last_fn = fn
65
+ [Redwood::MBox::Loader.suggest_labels_for(fn),
66
+ { :scheme => "mbox", :path => fn }]
63
67
  when :maildir
64
- fn = axe "What's the full path to the maildir directory?", ENV["MAIL"] #"srm
68
+ $last_fn ||= ENV["MAIL"]
69
+ fn = axe "What's the full path to the maildir directory?", $last_fn #"srm
65
70
  return if fn.nil? || fn.empty?
66
- { :scheme => "maildir", :path => fn }
71
+
72
+ $last_fn = fn
73
+ [Redwood::Maildir.suggest_labels_for(fn),
74
+ { :scheme => "maildir", :path => fn }]
67
75
  when :mboxssh
68
- srv = axe "What server is the mbox file located on?", $last_server
76
+ $last_server ||= "localhost"
77
+ srv = axe "What machine is the mbox file located on?", $last_server
69
78
  return if srv.nil? || srv.empty?
70
79
  $last_server = srv
71
- fn = axe "What's the full path to the mbox file?", ENV["MAIL"] #"srm
80
+
81
+ fn = axe "What's the path to the mbox file?", $last_fn #" stupid ruby-mode
72
82
  return if fn.nil? || fn.empty?
83
+ $last_fn = fn
73
84
  fn = "/#{fn}" # lame
74
- { :scheme => "mbox+ssh", :host => srv, :path => fn }
85
+ [Redwood::MBox::SSHLoader.suggest_labels_for(fn),
86
+ { :scheme => "mbox+ssh", :host => srv, :path => fn }]
75
87
  when :imap, :imaps
76
- srv = axe "What is the IMAP server?", $last_server
88
+ $last_server ||= "localhost"
89
+ srv = axe "What is the IMAP server (host, or host:port notation)?", $last_server
77
90
  return if srv.nil? || srv.empty?
78
91
  $last_server = srv
79
- fn = axe "What's the folder path?", "INBOX" #"srm
92
+
93
+ $last_folder ||= "INBOX"
94
+ fn = axe "What's the folder path?", $last_folder #"srm
80
95
  return if fn.nil? || fn.empty?
96
+ $last_folder = fn
97
+
81
98
  fn = "/#{fn}" # lame
82
- { :scheme => type.to_s, :host => srv, :path => fn }
99
+ if srv =~ /^(\w+):(\d+)$/
100
+ host, port = $1, $2.to_i
101
+ else
102
+ host, port = srv, nil
103
+ end
104
+ [Redwood::IMAP.suggest_labels_for(fn),
105
+ { :scheme => type.to_s, :host => host, :port => port, :path => fn }]
83
106
  end
84
107
 
85
108
  uri =
@@ -90,17 +113,27 @@ def add_source
90
113
  if axe_yes("Try again?") then next else return end
91
114
  end
92
115
 
93
- say "I'm going to add this source: #{uri}."
116
+ say "I'm going to add this source: #{uri}"
94
117
  unless axe("Does that look right?", "y") =~ /^y|yes$/i
95
118
  if axe_yes("Try again?") then next else return end
96
119
  end
97
120
 
98
121
  usual = axe_yes "Does this source ever receive new messages?", "y"
99
- archive = usual ? axe_yes("Should those new messages be automatically archived?") : false
122
+ archive = usual ? axe_yes("Should new messages be automatically archived? (I.e. not appear in your inbox, though still be accessible via search.)") : false
123
+
124
+ labels_str = axe("Enter any labels to be automatically added to all messages from this source, separated by spaces (or 'none')", default_labels.join(","))
125
+
126
+ labels =
127
+ if labels_str =~ /^\s*none\s*$/i
128
+ nil
129
+ else
130
+ labels_str.split(/\s+/)
131
+ end
100
132
 
101
133
  cmd = build_cmd "sup-add"
102
134
  cmd += " --unusual" unless usual
103
135
  cmd += " --archive" if archive
136
+ cmd += " --labels=#{labels.join(',')}" if labels && !labels.empty?
104
137
  cmd += " #{uri}"
105
138
 
106
139
  puts "Ok, trying to run \"#{cmd}\"..."
@@ -128,8 +161,7 @@ program. Get ready to be the envy of everyone in your internets
128
161
  with your amazing keyboarding skills! Jump from email to email with
129
162
  nary a click of the mouse!
130
163
 
131
- Just answer these simple questions and you'll be on your way! Press
132
- enter at any point to accept the default answer.
164
+ Just answer these simple questions and you'll be on your way.
133
165
 
134
166
  EOS
135
167
  #' stupid ruby-mode
@@ -137,7 +169,7 @@ EOS
137
169
  account = $config[:accounts][:default]
138
170
 
139
171
  name = axe "What's your name?", account[:name]
140
- email = axe "What's your email address?", account[:email] #'srm
172
+ email = axe "What's your (primary) email address?", account[:email] #'srm
141
173
 
142
174
  say "Ok, your header will look like this:"
143
175
  say " From: #{name} <#{email}>"
@@ -172,7 +204,7 @@ until done
172
204
 
173
205
  say "\n"
174
206
  choose do |menu|
175
- menu.prompt = "Your wish?"
207
+ menu.prompt = "Your wish? "
176
208
  menu.choice("Add a new source.") { add_source }
177
209
  menu.choice("Done adding sources!") { done = true }
178
210
  end
@@ -184,12 +216,6 @@ Ok. The final step is to import all your messages into the Sup index.
184
216
  Depending on how many messages are in the sources, this could take
185
217
  quite a while.
186
218
 
187
- IMPORTANT NOTE: this import will archive messages if the source is
188
- marked archival, and won't otherwise. It will preserve read/unread
189
- status as given by the source, and it will automatically add one label
190
- per source. All of this behavior can be controlled on per-source
191
- basis by running sup-sync manually.
192
-
193
219
  EOS
194
220
  #'
195
221
  if axe_yes "Run sup-sync to import all messages now?"
@@ -216,7 +242,7 @@ like you're ready to jack in to cyberspace there, cowboy.
216
242
 
217
243
  Just one last command:
218
244
 
219
- sup
245
+ #{build_cmd "sup"}
220
246
 
221
247
  Have fun!
222
248
  EOS
data/bin/sup-sync CHANGED
@@ -43,8 +43,8 @@ Usage:
43
43
  sup-sync [options] <source>*
44
44
 
45
45
  where <source>* is zero or more source URIs. If no sources are given,
46
- sync from all usual sources. All supported source URI schemes can
47
- be seen by running "sup-add --help".
46
+ sync from all usual sources. Supported source URI schemes can be seen
47
+ by running "sup-add --help".
48
48
 
49
49
  Options controlling WHICH messages sup-sync operates on:
50
50
  EOS
@@ -89,7 +89,6 @@ op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis
89
89
 
90
90
  Redwood::start
91
91
  index = Redwood::Index.new
92
- index.load
93
92
 
94
93
  restored_state =
95
94
  if opts[:restore]
@@ -106,24 +105,26 @@ restored_state =
106
105
  {}
107
106
  end
108
107
 
109
- sources = ARGV.map do |uri|
110
- uri = "mbox://#{uri}" unless uri =~ %r!://!
111
- index.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
112
- end
113
-
114
- sources = index.usual_sources if sources.empty?
115
- sources = index.sources if opts[:all_sources]
116
-
117
- unless target == :new
118
- if opts[:start_at]
119
- sources.each { |s| s.seek_to! opts[:start_at] }
120
- else
121
- sources.each { |s| s.reset! }
122
- end
123
- end
124
-
125
108
  seen = {}
109
+ index.lock_or_die
126
110
  begin
111
+ index.load
112
+
113
+ sources = ARGV.map do |uri|
114
+ index.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
115
+ end
116
+
117
+ sources = index.usual_sources if sources.empty?
118
+ sources = index.sources if opts[:all_sources]
119
+
120
+ unless target == :new
121
+ if opts[:start_at]
122
+ sources.each { |s| s.seek_to! opts[:start_at] }
123
+ else
124
+ sources.each { |s| s.reset! }
125
+ end
126
+ end
127
+
127
128
  sources.each do |source|
128
129
  $stderr.puts "Scanning #{source}..."
129
130
  num_added = num_updated = num_scanned = num_restored = 0
@@ -148,7 +149,7 @@ begin
148
149
 
149
150
  ## skip if we're operating on restored messages, and this one
150
151
  ## ain't.
151
- next if target == :restored && (!restored_state[m.id] || restored_state[m.id].sort_by { |s| s.to_s } == index_state.sort_by { |s| s.to_s })
152
+ next if target == :restored && (!restored_state[m.id] || (index_state && restored_state[m.id].sort_by { |s| s.to_s } == index_state.sort_by { |s| s.to_s }))
152
153
 
153
154
  ## m.labels is the default source labels. tweak these according
154
155
  ## to default source state modification flags.
@@ -177,7 +178,7 @@ begin
177
178
  elapsed = last_info_time - start_time
178
179
  pctdone = source.respond_to?(:pct_done) ? source.pct_done : 100.0 * (source.cur_offset.to_f - source.start_offset).to_f / (source.end_offset - source.start_offset).to_f
179
180
  remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
180
- $stderr.puts "## #{num_added + num_updated} (#{pctdone}% done) read; #{elapsed.to_time_s} elapsed; est. #{remaining.to_time_s} remaining (for this source)"
181
+ $stderr.puts "## #{num_scanned} (#{pctdone}%) read; #{elapsed.to_time_s} elapsed; #{remaining.to_time_s} remaining"
181
182
  end
182
183
 
183
184
  if index_state.nil?
@@ -193,43 +194,48 @@ begin
193
194
  $stderr.puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated} messages from #{source}."
194
195
  $stderr.puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
195
196
  end
197
+
198
+ ## delete any messages in the index that claim they're from one of
199
+ ## these sources, but that we didn't see.
200
+ ##
201
+ ## kinda crappy code here, because we delve directly into the Ferret
202
+ ## API.
203
+ ##
204
+ ## TODO: move this to Index, i suppose.
205
+
206
+
207
+ if target == :all || target == :changed
208
+ $stderr.puts "Deleting missing messages from the index..."
209
+ num_del, num_scanned = 0, 0
210
+ sources.each do |source|
211
+ raise "no source id for #{source}" unless source.id
212
+ q = "+source_id:#{source.id}"
213
+ q += " +source_info: >= #{opts[:start_at]}" if opts[:start_at]
214
+ index.index.search_each(q, :limit => :all) do |docid, score|
215
+ num_scanned += 1
216
+ mid = index.index[docid][:message_id]
217
+ unless seen[mid]
218
+ puts "Deleting #{mid}" if opts[:verbose]
219
+ index.index.delete docid unless opts[:dry_run]
220
+ num_del += 1
221
+ end
222
+ end
223
+ end
224
+ $stderr.puts "Deleted #{num_del} / #{num_scanned} messages"
225
+ end
226
+
227
+ if opts[:optimize]
228
+ $stderr.puts "Optimizing index..."
229
+ optt = time { index.index.optimize unless opts[:dry_run] }
230
+ $stderr.puts "Optimized index of size #{index.size} in #{optt}s."
231
+ end
232
+ rescue Redwood::FatalSourceError => e
233
+ $stderr.puts "Sorry, I couldn't communicate with a source: #{e.message}"
196
234
  rescue Exception => e
197
235
  File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
198
236
  raise
199
237
  ensure
200
238
  index.save
201
239
  Redwood::finish
202
- end
203
-
204
- ## delete any messages in the index that claim they're from one of
205
- ## these sources, but that we didn't see.
206
- ##
207
- ## kinda crappy code here, because we delve directly into the Ferret
208
- ## API.
209
- ##
210
- ## TODO: move this to Index, i suppose.
211
- if target == :all || target == :changed
212
- $stderr.puts "Deleting missing messages from the index..."
213
- num_del, num_scanned = 0, 0
214
- sources.each do |source|
215
- raise "no source id for #{source}" unless source.id
216
- q = "+source_id:#{source.id}"
217
- q += " +source_info: >= #{opts[:start_at]}" if opts[:start_at]
218
- index.index.search_each(q, :limit => :all) do |docid, score|
219
- num_scanned += 1
220
- mid = index.index[docid][:message_id]
221
- unless seen[mid]
222
- puts "Deleting #{mid}" if opts[:verbose]
223
- index.index.delete docid unless opts[:dry_run]
224
- num_del += 1
225
- end
226
- end
227
- end
228
- $stderr.puts "Deleted #{num_del} / #{num_scanned} messages"
229
- end
230
-
231
- if opts[:optimize]
232
- $stderr.puts "Optimizing index..."
233
- optt = time { index.index.optimize unless opts[:dry_run] }
234
- $stderr.puts "Optimized index of size #{index.size} in #{optt}s."
240
+ index.unlock
235
241
  end
data/bin/sup-sync-back ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'uri'
5
+ require 'tempfile'
6
+ require 'trollop'
7
+ require "sup"
8
+
9
+ ## save a message 'm' to an open file pointer 'fp'
10
+ def save m, fp
11
+ m.source.each_raw_full_message_line(m.source_info) { |l| fp.print l }
12
+ end
13
+
14
+ opts = Trollop::options do
15
+ version "sup-sync-back (sup #{Redwood::VERSION})"
16
+ banner <<EOS
17
+ Partially synchronizes a message source with the Sup index by deleting
18
+ or moving any messages from the source that are marked as deleted or
19
+ spam in the Sup index.
20
+
21
+ Currently only works with mbox sources.
22
+
23
+ Usage:
24
+ sup-sync-back [options] <source>*
25
+
26
+ where <source>* is zero or more source URIs. If no sources are given,
27
+ sync back all usual sources.
28
+
29
+ You probably want to run sup-sync --changed after this command.
30
+
31
+ Options include:
32
+ EOS
33
+ opt :delete_deleted, "Delete deleted messages.", :default => false, :short => "d"
34
+ opt :move_deleted, "Move deleted messages to a local mbox file.", :type => String, :short => :none
35
+ opt :delete_spam, "Delete spam messages.", :default => false, :short => "s"
36
+ opt :move_spam, "Move spam messages to a local mbox file.", :type => String, :short => :none
37
+ opt :verbose, "Print message ids as they're processed."
38
+ opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
39
+ opt :version, "Show version information", :short => :none
40
+
41
+ conflicts :delete_deleted, :move_deleted
42
+ conflicts :delete_spam, :move_spam
43
+ end
44
+
45
+ Redwood::start
46
+ index = Redwood::Index.new
47
+ index.lock_or_die
48
+
49
+ deleted_fp, spam_fp = nil
50
+ unless opts[:dry_run]
51
+ deleted_fp = File.open(opts[:move_deleted], "a") if opts[:move_deleted]
52
+ spam_fp = File.open(opts[:move_spam], "a") if opts[:move_spam]
53
+ end
54
+
55
+ begin
56
+ index.load
57
+
58
+ sources = ARGV.map do |uri|
59
+ s = index.source_for(uri) or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
60
+ s.is_a?(Redwood::MBox::Loader) or Trollop::die "#{uri} is not an mbox source."
61
+ s
62
+ end
63
+
64
+ if sources.empty?
65
+ sources = index.usual_sources.select { |s| s.is_a? Redwood::MBox::Loader }
66
+ end
67
+
68
+ modified_sources = []
69
+ sources.each do |source|
70
+ $stderr.puts "Scanning #{source}..."
71
+
72
+ unless ((opts[:delete_deleted] || opts[:move_deleted]) && index.has_any_from_source_with_label?(source, :deleted)) || ((opts[:delete_spam] || opts[:move_spam]) && index.has_any_from_source_with_label?(source, :spam))
73
+ $stderr.puts "Nothing to do from this source; skipping"
74
+ next
75
+ end
76
+
77
+ source.reset!
78
+ num_deleted = num_moved = num_scanned = 0
79
+
80
+ out_fp = Tempfile.new "sup-sync-back-#{source.id}"
81
+ Redwood::PollManager.add_messages_from source do |m, offset, entry|
82
+ num_scanned += 1
83
+
84
+ if entry
85
+ labels = entry[:label].split.map { |x| x.intern }.to_boolean_h
86
+
87
+ if labels.member? :deleted
88
+ if opts[:delete_deleted]
89
+ puts "Dropping deleted message #{source}##{offset}" if opts[:verbose]
90
+ num_deleted += 1
91
+ elsif opts[:move_deleted] && labels.member?(:deleted)
92
+ puts "Moving deleted message #{source}##{offset}" if opts[:verbose]
93
+ save m, deleted_fp unless opts[:dry_run]
94
+ num_moved += 1
95
+ end
96
+
97
+ elsif labels.member? :spam
98
+ if opts[:delete_spam]
99
+ puts "Deleting spam message #{source}##{offset}" if opts[:verbose]
100
+ num_deleted += 1
101
+ elsif opts[:move_spam] && labels.member?(:spam)
102
+ puts "Moving spam message #{source}##{offset}" if opts[:verbose]
103
+ save m, spam_fp unless opts[:dry_run]
104
+ num_moved += 1
105
+ end
106
+ else
107
+ save m, out_fp unless opts[:dry_run]
108
+ end
109
+ else
110
+ save m, out_fp unless opts[:dry_run]
111
+ end
112
+
113
+ nil # don't actually add anything!
114
+ end
115
+ $stderr.puts "Scanned #{num_scanned}, deleted #{num_deleted}, moved #{num_moved} messages from #{source}."
116
+ modified_sources << source if num_deleted > 0 || num_moved > 0
117
+ out_fp.close unless opts[:dry_run]
118
+
119
+ unless opts[:dry_run] || (num_deleted == 0 && num_moved == 0)
120
+ deleted_fp.flush if deleted_fp
121
+ spam_fp.flush if spam_fp
122
+ $stderr.puts "Moving #{out_fp.path} to #{source.file_path}"
123
+ FileUtils.mv out_fp.path, source.file_path
124
+ end
125
+ end
126
+
127
+ unless opts[:dry_run]
128
+ deleted_fp.close if deleted_fp
129
+ spam_fp.close if spam_fp
130
+ end
131
+
132
+ $stderr.puts "Done."
133
+ unless modified_sources.empty?
134
+ $stderr.puts "You should now run: sup-sync --changed #{modified_sources.join(' ')}"
135
+ end
136
+ rescue Exception => e
137
+ File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
138
+ raise
139
+ ensure
140
+ index.save
141
+ Redwood::finish
142
+ index.unlock
143
+ end