sup 0.9.1 → 0.10

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 (46) hide show
  1. data/CONTRIBUTORS +10 -6
  2. data/History.txt +11 -0
  3. data/ReleaseNotes +10 -0
  4. data/bin/sup +55 -19
  5. data/bin/sup-add +18 -8
  6. data/bin/sup-config +2 -2
  7. data/bin/sup-convert-ferret-index +84 -0
  8. data/bin/sup-dump +4 -3
  9. data/bin/sup-sync +4 -3
  10. data/bin/sup-sync-back +3 -2
  11. data/bin/sup-tweak-labels +3 -3
  12. data/lib/sup.rb +35 -4
  13. data/lib/sup/buffer.rb +12 -6
  14. data/lib/sup/colormap.rb +1 -0
  15. data/lib/sup/crypto.rb +76 -55
  16. data/lib/sup/ferret_index.rb +6 -1
  17. data/lib/sup/index.rb +62 -8
  18. data/lib/sup/logger.rb +2 -1
  19. data/lib/sup/maildir.rb +4 -2
  20. data/lib/sup/mbox/loader.rb +4 -3
  21. data/lib/sup/message-chunks.rb +9 -7
  22. data/lib/sup/message.rb +29 -27
  23. data/lib/sup/mode.rb +11 -4
  24. data/lib/sup/modes/buffer-list-mode.rb +5 -0
  25. data/lib/sup/modes/console-mode.rb +4 -0
  26. data/lib/sup/modes/edit-message-mode.rb +4 -2
  27. data/lib/sup/modes/file-browser-mode.rb +1 -1
  28. data/lib/sup/modes/inbox-mode.rb +18 -1
  29. data/lib/sup/modes/label-list-mode.rb +44 -3
  30. data/lib/sup/modes/text-mode.rb +1 -1
  31. data/lib/sup/modes/thread-index-mode.rb +63 -52
  32. data/lib/sup/modes/thread-view-mode.rb +68 -7
  33. data/lib/sup/poll.rb +20 -5
  34. data/lib/sup/source.rb +1 -0
  35. data/lib/sup/thread.rb +1 -1
  36. data/lib/sup/util.rb +49 -11
  37. data/lib/sup/xapian_index.rb +151 -112
  38. metadata +4 -10
  39. data/lib/sup/hook.rb.BACKUP.8625.rb +0 -158
  40. data/lib/sup/hook.rb.BACKUP.8681.rb +0 -158
  41. data/lib/sup/hook.rb.BASE.8625.rb +0 -155
  42. data/lib/sup/hook.rb.BASE.8681.rb +0 -155
  43. data/lib/sup/hook.rb.LOCAL.8625.rb +0 -142
  44. data/lib/sup/hook.rb.LOCAL.8681.rb +0 -142
  45. data/lib/sup/hook.rb.REMOTE.8625.rb +0 -145
  46. data/lib/sup/hook.rb.REMOTE.8681.rb +0 -145
data/lib/sup.rb CHANGED
@@ -37,7 +37,7 @@ class Module
37
37
  end
38
38
 
39
39
  module Redwood
40
- VERSION = "0.9.1"
40
+ VERSION = "0.10"
41
41
 
42
42
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
43
43
  CONFIG_FN = File.join(BASE_DIR, "config.yaml")
@@ -54,7 +54,7 @@ module Redwood
54
54
  YAML_DOMAIN = "masanjin.net"
55
55
  YAML_DATE = "2006-10-01"
56
56
 
57
- DEFAULT_INDEX = 'ferret'
57
+ DEFAULT_NEW_INDEX_TYPE = 'xapian'
58
58
 
59
59
  ## record exceptions thrown in threads nicely
60
60
  @exceptions = []
@@ -188,8 +188,38 @@ EOM
188
188
  end
189
189
  end
190
190
 
191
+ ## to be called by entry points in bin/, to ensure that
192
+ ## their versions match up against the library versions.
193
+ ##
194
+ ## this is a perennial source of bug reports from people
195
+ ## who both use git and have a gem version installed.
196
+ def check_library_version_against v
197
+ unless Redwood::VERSION == v
198
+ $stderr.puts <<EOS
199
+ Error: version mismatch!
200
+ The sup executable is at version #{v.inspect}.
201
+ The sup libraries are at version #{Redwood::VERSION.inspect}.
202
+
203
+ Your development environment may be picking up code from a
204
+ rubygems installation of sup.
205
+
206
+ If you're running from git with a commandline like
207
+
208
+ ruby -Ilib #{$0}
209
+
210
+ try this instead:
211
+
212
+ RUBY_INVOCATION="ruby -Ilib" ruby -Ilib #{$0}
213
+
214
+ You can also try `gem uninstall sup` and removing all Sup rubygems.
215
+
216
+ EOS
217
+ abort
218
+ end
219
+ end
220
+
191
221
  module_function :save_yaml_obj, :load_yaml_obj, :start, :finish,
192
- :report_broken_sources
222
+ :report_broken_sources, :check_library_version_against
193
223
  end
194
224
 
195
225
  ## set up default configuration file
@@ -229,7 +259,8 @@ else
229
259
  :confirm_top_posting => true,
230
260
  :discard_snippets_from_encrypted_messages => false,
231
261
  :default_attachment_save_dir => "",
232
- :sent_source => "sup://sent"
262
+ :sent_source => "sup://sent",
263
+ :poll_interval => 300
233
264
  }
234
265
  begin
235
266
  FileUtils.mkdir_p Redwood::BASE_DIR
@@ -1,6 +1,11 @@
1
1
  require 'etc'
2
2
  require 'thread'
3
- require 'ncurses'
3
+
4
+ begin
5
+ require 'ncursesw'
6
+ rescue LoadError
7
+ require 'ncurses'
8
+ end
4
9
 
5
10
  if defined? Ncurses
6
11
  module Ncurses
@@ -470,7 +475,7 @@ EOS
470
475
  end
471
476
  end
472
477
 
473
- def ask_for_filename domain, question, default=nil
478
+ def ask_for_filename domain, question, default=nil, allow_directory=false
474
479
  answer = ask domain, question, default do |s|
475
480
  if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
476
481
  full = $1
@@ -495,7 +500,7 @@ EOS
495
500
  answer =
496
501
  if answer.empty?
497
502
  spawn_modal "file browser", FileBrowserMode.new
498
- elsif File.directory?(answer)
503
+ elsif File.directory?(answer) && !allow_directory
499
504
  spawn_modal "file browser", FileBrowserMode.new(answer)
500
505
  else
501
506
  File.expand_path answer
@@ -530,7 +535,7 @@ EOS
530
535
  end
531
536
 
532
537
  def ask_for_contacts domain, question, default_contacts=[]
533
- default = default_contacts.map { |s| s.to_s }.join(" ")
538
+ default = default_contacts.is_a?(String) ? default_contacts : default_contacts.map { |s| s.to_s }.join(", ")
534
539
  default += " " unless default.empty?
535
540
 
536
541
  recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
@@ -604,7 +609,7 @@ EOS
604
609
  def ask_getch question, accept=nil
605
610
  raise "impossible!" if @asking
606
611
 
607
- accept = accept.split(//).map { |x| x[0] } if accept
612
+ accept = accept.split(//).map { |x| x.ord } if accept
608
613
 
609
614
  status, title = get_status_and_title @focus_buf
610
615
  Ncurses.sync do
@@ -640,7 +645,7 @@ EOS
640
645
  ## returns true (y), false (n), or nil (ctrl-g / cancel)
641
646
  def ask_yes_or_no question
642
647
  case(r = ask_getch question, "ynYN")
643
- when ?y, ?Y
648
+ when ?y.ord, ?Y.ord
644
649
  true
645
650
  when nil
646
651
  nil
@@ -755,6 +760,7 @@ EOS
755
760
  end
756
761
 
757
762
  private
763
+
758
764
  def default_status_bar buf
759
765
  " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
760
766
  end
@@ -50,6 +50,7 @@ class Colormap
50
50
  :system_buf => { :fg => "blue", :bg => "default" },
51
51
  :regular_buf => { :fg => "white", :bg => "default" },
52
52
  :modified_buffer => { :fg => "yellow", :bg => "default", :attrs => ["bold"] },
53
+ :date => { :fg => "white", :bg => "default"},
53
54
  }
54
55
 
55
56
  def initialize
@@ -15,15 +15,14 @@ class CryptoManager
15
15
  @mutex = Mutex.new
16
16
 
17
17
  bin = `which gpg`.chomp
18
- @cmd =
19
- case bin
20
- when /\S/
21
- debug "crypto: detected gpg binary in #{bin}"
22
- "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
23
- else
24
- debug "crypto: no gpg binary detected"
25
- nil
26
- end
18
+ @cmd = case bin
19
+ when /\S/
20
+ debug "crypto: detected gpg binary in #{bin}"
21
+ "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
22
+ else
23
+ debug "crypto: no gpg binary detected"
24
+ nil
25
+ end
27
26
  end
28
27
 
29
28
  def have_crypto?; !@cmd.nil? end
@@ -33,15 +32,19 @@ class CryptoManager
33
32
  payload_fn.write format_payload(payload)
34
33
  payload_fn.close
35
34
 
36
- output = run_gpg "--output - --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}"
35
+ sig_fn = Tempfile.new "redwood.signature"; sig_fn.close
37
36
 
38
- raise Error, (output || "gpg command failed: #{cmd}") unless $?.success?
37
+ message = run_gpg "--output #{sig_fn.path} --yes --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}", :interactive => true
38
+ unless $?.success?
39
+ info "Error while running gpg: #{message}"
40
+ raise Error, "GPG command failed. See log for details."
41
+ end
39
42
 
40
43
  envelope = RMail::Message.new
41
44
  envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha1'
42
45
 
43
46
  envelope.add_part payload
44
- signature = RMail::Message.make_attachment output, "application/pgp-signature", nil, "signature.asc"
47
+ signature = RMail::Message.make_attachment IO.read(sig_fn.path), "application/pgp-signature", nil, "signature.asc"
45
48
  envelope.add_part signature
46
49
  envelope
47
50
  end
@@ -51,15 +54,20 @@ class CryptoManager
51
54
  payload_fn.write format_payload(payload)
52
55
  payload_fn.close
53
56
 
57
+ encrypted_fn = Tempfile.new "redwood.encrypted"; encrypted_fn.close
58
+
54
59
  recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ")
55
60
  sign_opts = sign ? "--sign --local-user '#{from}'" : ""
56
- gpg_output = run_gpg "--output - --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}"
57
- raise Error, (gpg_output || "gpg command failed: #{cmd}") unless $?.success?
61
+ message = run_gpg "--output #{encrypted_fn.path} --yes --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}", :interactive => true
62
+ unless $?.success?
63
+ info "Error while running gpg: #{message}"
64
+ raise Error, "GPG command failed. See log for details."
65
+ end
58
66
 
59
67
  encrypted_payload = RMail::Message.new
60
68
  encrypted_payload.header["Content-Type"] = "application/octet-stream"
61
69
  encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"'
62
- encrypted_payload.body = gpg_output
70
+ encrypted_payload.body = IO.read(encrypted_fn.path)
63
71
 
64
72
  control = RMail::Message.new
65
73
  control.header["Content-Type"] = "application/pgp-encrypted"
@@ -111,46 +119,50 @@ class CryptoManager
111
119
  payload_fn.write payload.to_s
112
120
  payload_fn.close
113
121
 
114
- output = run_gpg "--decrypt #{payload_fn.path}"
122
+ output_fn = Tempfile.new "redwood.output"
123
+ output_fn.close
115
124
 
116
- if $?.success?
117
- decrypted_payload, sig_lines = if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
118
- [$1, $2]
119
- else
120
- [output, nil]
121
- end
125
+ message = run_gpg "--output #{output_fn.path} --yes --decrypt #{payload_fn.path}", :interactive => true
122
126
 
123
- sig = if sig_lines # encrypted & signed
124
- if sig_lines =~ /^gpg: (Good signature from .*$)/
125
- Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
126
- else
127
- Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
128
- end
129
- end
127
+ unless $?.success?
128
+ info "Error while running gpg: #{message}"
129
+ return Chunk::CryptoNotice.new(:invalid, "This message could not be decrypted", message.split("\n"))
130
+ end
130
131
 
131
- # This is gross. This decrypted payload could very well be a multipart
132
- # element itself, as opposed to a simple payload. For example, a
133
- # multipart/signed element, like those generated by Mutt when encrypting
134
- # and signing a message (instead of just clearsigning the body).
135
- # Supposedly, decrypted_payload being a multipart element ought to work
136
- # out nicely because Message::multipart_encrypted_to_chunks() runs the
137
- # decrypted message through message_to_chunks() again to get any
138
- # children. However, it does not work as intended because these inner
139
- # payloads need not carry a MIME-Version header, yet they are fed to
140
- # RMail as a top-level message, for which the MIME-Version header is
141
- # required. This causes for the part not to be detected as multipart,
142
- # hence being shown as an attachment. If we detect this is happening,
143
- # we force the decrypted payload to be interpreted as MIME.
144
- msg = RMail::Parser.read(decrypted_payload)
145
- if msg.header.content_type =~ %r{^multipart/} and not msg.multipart?
146
- decrypted_payload = "MIME-Version: 1.0\n" + decrypted_payload
147
- msg = RMail::Parser.read(decrypted_payload)
148
- end
149
- notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
150
- [notice, sig, msg]
151
- else
152
- Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
132
+ output = IO.read output_fn.path
133
+ output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding
134
+
135
+ ## there's probably a better way to do this, but we're using the output to
136
+ ## look for a valid signature being present.
137
+
138
+ sig = case message
139
+ when /^gpg: (Good signature from .*$)/i
140
+ Chunk::CryptoNotice.new :valid, $1, message.split("\n")
141
+ when /^gpg: (Bad signature from .*$)/i
142
+ Chunk::CryptoNotice.new :invalid, $1, message.split("\n")
153
143
  end
144
+
145
+ # This is gross. This decrypted payload could very well be a multipart
146
+ # element itself, as opposed to a simple payload. For example, a
147
+ # multipart/signed element, like those generated by Mutt when encrypting
148
+ # and signing a message (instead of just clearsigning the body).
149
+ # Supposedly, decrypted_payload being a multipart element ought to work
150
+ # out nicely because Message::multipart_encrypted_to_chunks() runs the
151
+ # decrypted message through message_to_chunks() again to get any
152
+ # children. However, it does not work as intended because these inner
153
+ # payloads need not carry a MIME-Version header, yet they are fed to
154
+ # RMail as a top-level message, for which the MIME-Version header is
155
+ # required. This causes for the part not to be detected as multipart,
156
+ # hence being shown as an attachment. If we detect this is happening,
157
+ # we force the decrypted payload to be interpreted as MIME.
158
+ msg = RMail::Parser.read output
159
+ if msg.header.content_type =~ %r{^multipart/} && !msg.multipart?
160
+ output = "MIME-Version: 1.0\n" + output
161
+ output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding
162
+ msg = RMail::Parser.read output
163
+ end
164
+ notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
165
+ [notice, sig, msg]
154
166
  end
155
167
 
156
168
  private
@@ -169,10 +181,19 @@ private
169
181
  payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
170
182
  end
171
183
 
172
- def run_gpg args
173
- cmd = "#{@cmd} #{args} 2> /dev/null"
174
- output = `#{cmd}`
175
- output
184
+ def run_gpg args, opts={}
185
+ cmd = "#{@cmd} #{args}"
186
+ if opts[:interactive] && BufferManager.instantiated?
187
+ output_fn = Tempfile.new "redwood.output"
188
+ output_fn.close
189
+ cmd += " > #{output_fn.path} 2> /dev/null"
190
+ debug "crypto: running: #{cmd}"
191
+ BufferManager.shell_out cmd
192
+ IO.read(output_fn.path) rescue "can't read output"
193
+ else
194
+ debug "crypto: running: #{cmd}"
195
+ `#{cmd} 2> /dev/null`
196
+ end
176
197
  end
177
198
  end
178
199
  end
@@ -11,6 +11,8 @@ Variables:
11
11
  subs: The string being searched.
12
12
  EOS
13
13
 
14
+ def is_a_deprecated_ferret_index?; true end
15
+
14
16
  def initialize dir=BASE_DIR
15
17
  super
16
18
 
@@ -127,8 +129,11 @@ EOS
127
129
  :from => (m.from ? m.from.indexable_content : ""),
128
130
  :to => (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" "),
129
131
 
132
+ ## always overwrite :refs.
133
+ ## these might have changed due to manual thread joining.
134
+ :refs => (m.refs + m.replytos).uniq.join(" "),
135
+
130
136
  :subject => (entry[:subject] || wrap_subj(Message.normalize_subj(m.subj))),
131
- :refs => (entry[:refs] || (m.refs + m.replytos).uniq.join(" ")),
132
137
  }
133
138
 
134
139
  @index_mutex.synchronize do
@@ -6,7 +6,7 @@ begin
6
6
  require 'chronic'
7
7
  $have_chronic = true
8
8
  rescue LoadError => e
9
- debug "optional 'chronic' library not found; date-time query restrictions disabled"
9
+ debug "No 'chronic' gem detected. Install it for date/time query restrictions."
10
10
  $have_chronic = false
11
11
  end
12
12
 
@@ -23,11 +23,15 @@ class BaseIndex
23
23
  def method_missing m; @h[m.to_s] end
24
24
  end
25
25
 
26
+ def is_a_deprecated_ferret_index?; false end
27
+
26
28
  include Singleton
27
29
 
28
30
  def initialize dir=BASE_DIR
29
31
  @dir = dir
30
32
  @lock = Lockfile.new lockfile, :retries => 0, :max_age => nil
33
+ @sync_worker = nil
34
+ @sync_queue = Queue.new
31
35
  end
32
36
 
33
37
  def lockfile; File.join @dir, "lock" end
@@ -172,15 +176,65 @@ class BaseIndex
172
176
  def parse_query s
173
177
  unimplemented
174
178
  end
179
+
180
+ def save_thread t
181
+ t.each_dirty_message do |m|
182
+ if @sync_worker
183
+ @sync_queue << m
184
+ else
185
+ update_message_state m
186
+ end
187
+ m.clear_dirty
188
+ end
189
+ end
190
+
191
+ def start_sync_worker
192
+ @sync_worker = Redwood::reporting_thread('index sync') { run_sync_worker }
193
+ end
194
+
195
+ def stop_sync_worker
196
+ return unless worker = @sync_worker
197
+ @sync_worker = nil
198
+ @sync_queue << :die
199
+ worker.join
200
+ end
201
+
202
+ def run_sync_worker
203
+ while m = @sync_queue.deq
204
+ return if m == :die
205
+ update_message_state m
206
+ # Necessary to keep Xapian calls from lagging the UI too much.
207
+ sleep 0.03
208
+ end
209
+ end
175
210
  end
176
211
 
177
- index_name = ENV['SUP_INDEX'] || $config[:index] || DEFAULT_INDEX
178
- case index_name
179
- when "xapian"; require "sup/xapian_index"
180
- when "ferret"; require "sup/ferret_index"
181
- else fail "unknown index type #{index_name.inspect}"
212
+ ## just to make the backtraces even more insane, here we engage in yet more
213
+ ## method_missing metaprogramming so that Index.init(index_type_name) will
214
+ ## magically make Index act like the correct Index class.
215
+ class Index
216
+ def self.init type=nil
217
+ ## determine the index type from the many possible ways of setting it
218
+ type = (type == "auto" ? nil : type) ||
219
+ ENV['SUP_INDEX'] ||
220
+ $config[:index] ||
221
+ (File.exist?(File.join(BASE_DIR, "xapian")) && "xapian") || ## PRIORITIZE THIS
222
+ (File.exist?(File.join(BASE_DIR, "ferret")) && "ferret") || ## deprioritize this
223
+ DEFAULT_NEW_INDEX_TYPE
224
+ begin
225
+ require "sup/#{type}_index"
226
+ @klass = Redwood.const_get "#{type.capitalize}Index"
227
+ @obj = @klass.init
228
+ rescue LoadError, NameError => e
229
+ raise "unknown index type #{type.inspect}: #{e.message}"
230
+ end
231
+ debug "using #{type} index"
232
+ @obj
233
+ end
234
+
235
+ def self.instance; @obj end
236
+ def self.method_missing m, *a, &b; @obj.send(m, *a, &b) end
237
+ def self.const_missing x; @obj.class.const_get(x) end
182
238
  end
183
- Index = Redwood.const_get "#{index_name.capitalize}Index"
184
- debug "using index #{Index.name}"
185
239
 
186
240
  end
@@ -14,13 +14,14 @@ class Logger
14
14
 
15
15
  def initialize level=nil
16
16
  level ||= ENV["SUP_LOG_LEVEL"] || "info"
17
- @level = LEVELS.index(level) or raise ArgumentError, "invalid log level #{level.inspect}: should be one of #{LEVELS * ', '}"
17
+ self.level = level
18
18
  @mutex = Mutex.new
19
19
  @buf = StringIO.new
20
20
  @sinks = []
21
21
  end
22
22
 
23
23
  def level; LEVELS[@level] end
24
+ def level=(level); @level = LEVELS.index(level) || raise(ArgumentError, "invalid log level #{level.inspect}: should be one of #{LEVELS * ', '}"); end
24
25
 
25
26
  def add_sink s, copy_current=true
26
27
  @mutex.synchronize do