sup 0.22.1 → 0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -3
  3. data/.travis.yml +11 -6
  4. data/CONTRIBUTORS +13 -5
  5. data/Gemfile +2 -1
  6. data/History.txt +51 -0
  7. data/README.md +26 -5
  8. data/bin/sup +7 -5
  9. data/bin/sup-add +14 -14
  10. data/bin/sup-config +30 -44
  11. data/bin/sup-dump +2 -2
  12. data/bin/sup-import-dump +4 -4
  13. data/bin/sup-sync +3 -3
  14. data/bin/sup-sync-back-maildir +2 -2
  15. data/bin/sup-tweak-labels +5 -5
  16. data/ext/mkrf_conf_xapian.rb +1 -1
  17. data/lib/sup.rb +1 -0
  18. data/lib/sup/crypto.rb +17 -8
  19. data/lib/sup/hook.rb +8 -8
  20. data/lib/sup/index.rb +18 -5
  21. data/lib/sup/logger.rb +1 -1
  22. data/lib/sup/message.rb +20 -10
  23. data/lib/sup/message_chunks.rb +3 -2
  24. data/lib/sup/mode.rb +1 -0
  25. data/lib/sup/modes/contact_list_mode.rb +1 -0
  26. data/lib/sup/modes/reply_mode.rb +3 -1
  27. data/lib/sup/modes/thread_index_mode.rb +1 -1
  28. data/lib/sup/modes/thread_view_mode.rb +14 -11
  29. data/lib/sup/source.rb +1 -1
  30. data/lib/sup/util.rb +14 -19
  31. data/lib/sup/util/axe.rb +17 -0
  32. data/lib/sup/util/ncurses.rb +3 -3
  33. data/lib/sup/version.rb +10 -1
  34. data/sup.gemspec +7 -6
  35. data/test/fixtures/mailing-list-header.eml +80 -0
  36. data/test/fixtures/text-attachments-with-charset.eml +46 -0
  37. data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
  38. data/test/gnupg_test_home/gpg.conf +2 -1
  39. data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
  40. data/test/gnupg_test_home/pubring.gpg +0 -0
  41. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  42. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  43. data/test/gnupg_test_home/regen_keys.sh +69 -18
  44. data/test/gnupg_test_home/secring.gpg +0 -0
  45. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -22
  46. data/test/test_crypto.rb +2 -0
  47. data/test/test_message.rb +74 -0
  48. data/test/unit/util/test_query.rb +10 -4
  49. data/test/unit/util/test_string.rb +6 -0
  50. metadata +52 -38
  51. data/test/gnupg_test_home/key1.gen +0 -15
  52. data/test/gnupg_test_home/key2.gen +0 -15
  53. data/test/gnupg_test_home/key_ecc.gen +0 -13
@@ -3,12 +3,12 @@
3
3
  $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
4
 
5
5
  require 'xapian'
6
- require 'trollop'
6
+ require 'optimist'
7
7
  require 'set'
8
8
 
9
9
  BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
10
10
 
11
- $opts = Trollop::options do
11
+ $opts = Optimist::options do
12
12
  version "sup-dump"
13
13
  banner <<EOS
14
14
  Dumps all message state from the sup index to standard out. You can
@@ -3,7 +3,7 @@
3
3
  $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
4
 
5
5
  require 'uri'
6
- require 'trollop'
6
+ require 'optimist'
7
7
  require "sup"
8
8
 
9
9
  PROGRESS_UPDATE_INTERVAL = 15 # seconds
@@ -11,7 +11,7 @@ PROGRESS_UPDATE_INTERVAL = 15 # seconds
11
11
  class AbortExecution < SystemExit
12
12
  end
13
13
 
14
- opts = Trollop::options do
14
+ opts = Optimist::options do
15
15
  version "sup-import-dump (sup #{Redwood::VERSION})"
16
16
  banner <<EOS
17
17
  Imports message state previously exported by sup-dump into the index.
@@ -36,8 +36,8 @@ EOS
36
36
 
37
37
  conflicts :ignore_missing, :warn_missing, :abort_missing
38
38
  end
39
- Trollop::die "No dump file given" if ARGV.empty?
40
- Trollop::die "Extra arguments given" if ARGV.length > 1
39
+ Optimist::die "No dump file given" if ARGV.empty?
40
+ Optimist::die "Extra arguments given" if ARGV.length > 1
41
41
  dump_name = ARGV.shift
42
42
  missing_action = [:ignore_missing, :warn_missing, :abort_missing].find { |x| opts[x] } || :abort_missing
43
43
 
@@ -3,7 +3,7 @@
3
3
  $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
4
 
5
5
  require 'uri'
6
- require 'trollop'
6
+ require 'optimist'
7
7
  require "sup"
8
8
 
9
9
  PROGRESS_UPDATE_INTERVAL = 15 # seconds
@@ -30,7 +30,7 @@ def time
30
30
  Time.now - startt
31
31
  end
32
32
 
33
- opts = Trollop::options do
33
+ opts = Optimist::options do
34
34
  version "sup-sync (sup #{Redwood::VERSION})"
35
35
  banner <<EOS
36
36
  Synchronizes the Sup index with one or more message sources by adding
@@ -112,7 +112,7 @@ begin
112
112
  Redwood::SourceManager.usual_sources
113
113
  else
114
114
  ARGV.map do |uri|
115
- Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
115
+ Redwood::SourceManager.source_for uri or Optimist::die "Unknown source: #{uri}. Did you add it with sup-add first?"
116
116
  end
117
117
  end
118
118
 
@@ -3,10 +3,10 @@
3
3
 
4
4
  $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
5
5
 
6
- require 'trollop'
6
+ require 'optimist'
7
7
  require "sup"
8
8
 
9
- opts = Trollop::options do
9
+ opts = Optimist::options do
10
10
  version "sup-sync-back-maildir (sup #{Redwood::VERSION})"
11
11
  banner <<EOS
12
12
  Export Xapian entries to Maildir sources on disk.
@@ -2,7 +2,7 @@
2
2
 
3
3
  $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
4
 
5
- require 'trollop'
5
+ require 'optimist'
6
6
  require "sup"
7
7
 
8
8
  class Float
@@ -25,7 +25,7 @@ def time
25
25
  Time.now - startt
26
26
  end
27
27
 
28
- opts = Trollop::options do
28
+ opts = Optimist::options do
29
29
  version "sup-tweak-labels (sup #{Redwood::VERSION})"
30
30
  banner <<EOS
31
31
  Batch modification of message state for messages already in the index.
@@ -58,7 +58,7 @@ opts[:verbose] = true if opts[:very_verbose]
58
58
  add_labels = opts[:add].to_set_of_symbols ","
59
59
  remove_labels = opts[:remove].to_set_of_symbols ","
60
60
 
61
- Trollop::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
61
+ Optimist::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
62
62
 
63
63
  Redwood::start
64
64
  index = Redwood::Index.init
@@ -71,10 +71,10 @@ begin
71
71
  Redwood::SourceManager.sources
72
72
  else
73
73
  ARGV.map do |uri|
74
- Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
74
+ Redwood::SourceManager.source_for uri or Optimist::die "Unknown source: #{uri}. Did you add it with sup-add first?"
75
75
  end
76
76
  end.map { |s| s.id }
77
- Trollop::die "nothing to do: no sources" if source_ids.empty?
77
+ Optimist::die "nothing to do: no sources" if source_ids.empty?
78
78
 
79
79
  query = "(" + source_ids.map { |id| "source_id:#{id}" }.join(" OR ") + ")"
80
80
  if add_labels.empty?
@@ -16,7 +16,7 @@ begin
16
16
  if !RbConfig::CONFIG['arch'].include?('openbsd')
17
17
  # update version in Gemfile as well
18
18
  name = "xapian-ruby"
19
- version = "~> 1.2.15"
19
+ version = "~> 1.2"
20
20
 
21
21
  begin
22
22
  # try to load gem
data/lib/sup.rb CHANGED
@@ -331,6 +331,7 @@ EOM
331
331
  :poll_interval => 300,
332
332
  :wrap_width => 0,
333
333
  :slip_rows => 0,
334
+ :indent_spaces => 2,
334
335
  :col_jump => 2,
335
336
  :stem_language => "english",
336
337
  :sync_back_to_maildir => false,
@@ -127,18 +127,27 @@ EOS
127
127
  def sign from, to, payload
128
128
  return unknown_status(@not_working_reason) unless @not_working_reason.nil?
129
129
 
130
+ # We grab this from the GPG::Ctx below after signing, so that we can set
131
+ # micalg in Content-Type to match the hash algorithm GPG decided to use.
132
+ hash_algo = nil
133
+
130
134
  gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true}
131
135
  gpg_opts.merge!(gen_sign_user_opts(from))
132
136
  gpg_opts = HookManager.run("gpg-options",
133
137
  {:operation => "sign", :options => gpg_opts}) || gpg_opts
134
138
  begin
135
- if GPGME.respond_to?('detach_sign')
136
- sig = GPGME.detach_sign(format_payload(payload), gpg_opts)
137
- else
138
- crypto = GPGME::Crypto.new
139
- gpg_opts[:mode] = GPGME::SIG_MODE_DETACH
140
- sig = crypto.sign(format_payload(payload), gpg_opts).read
139
+ input = GPGME::Data.new(format_payload(payload))
140
+ output = GPGME::Data.new()
141
+ GPGME::Ctx.new(gpg_opts) do |ctx|
142
+ if gpg_opts[:signer]
143
+ signers = GPGME::Key.find(:secret, gpg_opts[:signer], :sign)
144
+ ctx.add_signer(*signers)
145
+ end
146
+ ctx.sign(input, output, GPGME::SIG_MODE_DETACH)
147
+ hash_algo = GPGME::hash_algo_name(ctx.sign_result.signatures[0].hash_algo)
141
148
  end
149
+ output.seek(0)
150
+ sig = output.read
142
151
  rescue GPGME::Error => exc
143
152
  raise Error, gpgme_exc_msg(exc.message)
144
153
  end
@@ -150,7 +159,7 @@ EOS
150
159
  end
151
160
 
152
161
  envelope = RMail::Message.new
153
- envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature'
162
+ envelope.header["Content-Type"] = "multipart/signed; protocol=application/pgp-signature; micalg=pgp-#{hash_algo.downcase}"
154
163
 
155
164
  envelope.add_part payload
156
165
  signature = RMail::Message.make_attachment sig, "application/pgp-signature", nil, "signature.asc"
@@ -233,7 +242,7 @@ EOS
233
242
  end
234
243
 
235
244
  if valid || !unknown
236
- summary_line = simplify_sig_line(verify_result.signatures[0].to_s, all_trusted)
245
+ summary_line = simplify_sig_line(verify_result.signatures[0].to_s.dup, all_trusted)
237
246
  end
238
247
 
239
248
  if all_output_lines.length == 0
@@ -109,20 +109,20 @@ class HookManager
109
109
  @descs[name] = desc
110
110
  end
111
111
 
112
- def print_hooks f=$stdout
113
- puts <<EOS
114
- Have #{HookManager.descs.size} registered hooks:
115
-
116
- EOS
117
-
118
- HookManager.descs.sort.each do |name, desc|
119
- f.puts <<EOS
112
+ def print_hooks pattern="", f=$stdout
113
+ matching_hooks = HookManager.descs.sort.keep_if {|name, desc| pattern.empty? or name.match(pattern)}.map do |name, desc|
114
+ <<EOS
120
115
  #{name}
121
116
  #{"-" * name.length}
122
117
  File: #{fn_for name}
123
118
  #{desc}
124
119
  EOS
125
120
  end
121
+
122
+ showing_str = matching_hooks.size == HookManager.descs.size ? "" : " (showing #{matching_hooks.size})"
123
+ f.puts "Have #{HookManager.descs.size} registered hooks#{showing_str}:"
124
+ f.puts
125
+ matching_hooks.each { |text| f.puts text }
126
126
  end
127
127
 
128
128
  def enabled? name; !hook_for(name).nil? end
@@ -516,19 +516,32 @@ EOS
516
516
  qp.stemmer = Xapian::Stem.new($config[:stem_language])
517
517
  qp.stemming_strategy = Xapian::QueryParser::STEM_SOME
518
518
  qp.default_op = Xapian::Query::OP_AND
519
- qp.add_valuerangeprocessor(Xapian::NumberValueRangeProcessor.new(DATE_VALUENO, 'date:', true))
520
- NORMAL_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_prefix k, v } }
521
- BOOLEAN_PREFIX.each { |k,info| info[:prefix].each { |v| qp.add_boolean_prefix k, v, info[:exclusive] } }
519
+ valuerangeprocessor = Xapian::NumberValueRangeProcessor.new(DATE_VALUENO,
520
+ 'date:', true)
521
+ qp.add_valuerangeprocessor(valuerangeprocessor)
522
+ NORMAL_PREFIX.each { |k,info| info[:prefix].each {
523
+ |v| qp.add_prefix k, v }
524
+ }
525
+ BOOLEAN_PREFIX.each { |k,info| info[:prefix].each {
526
+ |v| qp.add_boolean_prefix k, v, info[:exclusive] }
527
+ }
522
528
 
523
529
  begin
524
- xapian_query = qp.parse_query(subs, Xapian::QueryParser::FLAG_PHRASE|Xapian::QueryParser::FLAG_BOOLEAN|Xapian::QueryParser::FLAG_LOVEHATE|Xapian::QueryParser::FLAG_WILDCARD)
530
+ xapian_query = qp.parse_query(subs, Xapian::QueryParser::FLAG_PHRASE |
531
+ Xapian::QueryParser::FLAG_BOOLEAN |
532
+ Xapian::QueryParser::FLAG_LOVEHATE |
533
+ Xapian::QueryParser::FLAG_WILDCARD)
525
534
  rescue RuntimeError => e
526
535
  raise ParseError, "xapian query parser error: #{e}"
527
536
  end
528
537
 
529
538
  debug "parsed xapian query: #{Util::Query.describe(xapian_query, subs)}"
530
539
 
531
- raise ParseError if xapian_query.nil? or xapian_query.empty?
540
+ if xapian_query.nil? or xapian_query.empty?
541
+ raise ParseError, "couldn't parse \"#{s}\" as xapian query " \
542
+ "(special characters aren't indexed)"
543
+ end
544
+
532
545
  query[:qobj] = xapian_query
533
546
  query[:text] = s
534
547
  query
@@ -71,7 +71,7 @@ end
71
71
 
72
72
  ## include me to have top-level #debug, #info, etc. methods.
73
73
  module LogsStuff
74
- Logger::LEVELS.each { |l| define_method(l) { |s| Logger.instance.send(l, s) } }
74
+ Logger::LEVELS.each { |l| define_method(l) { |s, uplevel = 0| Logger.instance.send(l, s) } }
75
75
  end
76
76
 
77
77
  end
@@ -136,6 +136,11 @@ class Message
136
136
  header["list-post"] # just try the whole fucking thing
137
137
  end
138
138
  address && Person.from_address(address)
139
+ elsif header["mailing-list"]
140
+ address = if header["mailing-list"] =~ /list (.*?);/
141
+ $1
142
+ end
143
+ address && Person.from_address(address)
139
144
  elsif header["x-mailing-list"]
140
145
  Person.from_address header["x-mailing-list"]
141
146
  end
@@ -264,14 +269,14 @@ class Message
264
269
  parse_header rmsg.header
265
270
  message_to_chunks rmsg
266
271
  rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e
267
- warn "problem reading message #{id}"
272
+ warn_with_location "problem reading message #{id}"
268
273
  debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
269
274
 
270
275
  [Chunk::Text.new(error_message.split("\n"))]
271
276
 
272
277
  rescue Exception => e
273
278
 
274
- warn "problem reading message #{id}"
279
+ warn_with_location "problem reading message #{id}"
275
280
  debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
276
281
 
277
282
  raise e
@@ -404,19 +409,19 @@ private
404
409
 
405
410
  def multipart_signed_to_chunks m
406
411
  if m.body.size != 2
407
- warn "multipart/signed with #{m.body.size} parts (expecting 2)"
412
+ warn_with_location "multipart/signed with #{m.body.size} parts (expecting 2)"
408
413
  return
409
414
  end
410
415
 
411
416
  payload, signature = m.body
412
417
  if signature.multipart?
413
- warn "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
418
+ warn_with_location "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
414
419
  return
415
420
  end
416
421
 
417
422
  ## this probably will never happen
418
423
  if payload.header.content_type && payload.header.content_type.downcase == "application/pgp-signature"
419
- warn "multipart/signed with payload content type #{payload.header.content_type}"
424
+ warn_with_location "multipart/signed with payload content type #{payload.header.content_type}"
420
425
  return
421
426
  end
422
427
 
@@ -431,23 +436,23 @@ private
431
436
 
432
437
  def multipart_encrypted_to_chunks m
433
438
  if m.body.size != 2
434
- warn "multipart/encrypted with #{m.body.size} parts (expecting 2)"
439
+ warn_with_location "multipart/encrypted with #{m.body.size} parts (expecting 2)"
435
440
  return
436
441
  end
437
442
 
438
443
  control, payload = m.body
439
444
  if control.multipart?
440
- warn "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
445
+ warn_with_location "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
441
446
  return
442
447
  end
443
448
 
444
449
  if payload.header.content_type && payload.header.content_type.downcase != "application/octet-stream"
445
- warn "multipart/encrypted with payload content type #{payload.header.content_type}"
450
+ warn_with_location "multipart/encrypted with payload content type #{payload.header.content_type}"
446
451
  return
447
452
  end
448
453
 
449
454
  if control.header.content_type && control.header.content_type.downcase != "application/pgp-encrypted"
450
- warn "multipart/encrypted with control content type #{signature.header.content_type}"
455
+ warn_with_location "multipart/encrypted with control content type #{signature.header.content_type}"
451
456
  return
452
457
  end
453
458
 
@@ -691,7 +696,7 @@ private
691
696
  newstate = :quote
692
697
  elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE && !lines[(i+1)..-1].index { |l| l =~ /^-- $/ }
693
698
  newstate = :sig
694
- elsif line =~ BLOCK_QUOTE_PATTERN
699
+ elsif line =~ BLOCK_QUOTE_PATTERN && nextline !~ QUOTE_PATTERN
695
700
  newstate = :block_quote
696
701
  end
697
702
 
@@ -751,6 +756,11 @@ private
751
756
  end
752
757
  chunks
753
758
  end
759
+
760
+ def warn_with_location msg
761
+ warn msg
762
+ warn "Message is in #{location.source.uri} at #{location.info}"
763
+ end
754
764
  end
755
765
 
756
766
  class Location
@@ -128,7 +128,7 @@ EOS
128
128
 
129
129
  text = case @content_type
130
130
  when /^text\/plain\b/
131
- @raw_content
131
+ @raw_content.force_encoding(encoded_content.charset || 'US-ASCII')
132
132
  else
133
133
  HookManager.run "mime-decode", :content_type => @content_type,
134
134
  :filename => lambda { write_to_disk },
@@ -138,7 +138,7 @@ EOS
138
138
 
139
139
  @lines = nil
140
140
  if text
141
- text = text.transcode(encoded_content.charset || $encoding, text.encoding)
141
+ text = text.encode($encoding, :invalid => :replace, :undef => :replace)
142
142
  begin
143
143
  @lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
144
144
  rescue Encoding::CompatibilityError
@@ -160,6 +160,7 @@ EOS
160
160
  end
161
161
  end
162
162
  def safe_filename; Shellwords.escape(@filename).gsub("/", "_") end
163
+ def filesafe_filename; @filename.gsub("/", "_") end
163
164
 
164
165
  ## an attachment is exapndable if we've managed to decode it into
165
166
  ## something we can display inline. otherwise, it's viewable.
@@ -83,6 +83,7 @@ EOS
83
83
  ### helper functions
84
84
 
85
85
  def save_to_file fn, talk=true
86
+ FileUtils.mkdir_p File.dirname(fn)
86
87
  if File.exist? fn
87
88
  unless BufferManager.ask_yes_or_no "File \"#{fn}\" exists. Overwrite?"
88
89
  info "Not overwriting #{fn}"
@@ -108,6 +108,7 @@ class ContactListMode < LineCursorMode
108
108
  def load
109
109
  @num ||= (buffer.content_height * 2)
110
110
  @user_contacts = ContactManager.contacts_with_aliases
111
+ @user_contacts += (HookManager.run("extra-contact-addresses") || []).map { |addr| Person.from_address addr }
111
112
  num = [@num - @user_contacts.length, 0].max
112
113
  BufferManager.say("Loading #{num} contacts from index...") do
113
114
  recentc = Index.load_contacts AccountManager.user_emails, :num => num
@@ -36,6 +36,8 @@ Variables:
36
36
  [:#{REPLY_TYPES * ', :'}]
37
37
  The default behavior is equivalent to
38
38
  ([:list, :sender, :recipent] & modes)[0]
39
+ message: a message object representing the message being replied to
40
+ (useful values include message.is_list_message? and message.list_address)
39
41
  Return value:
40
42
  The reply mode you desire, or nil to use the default behavior.
41
43
  EOS
@@ -130,7 +132,7 @@ EOS
130
132
  types = REPLY_TYPES.select { |t| @headers.member?(t) }
131
133
  @type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
132
134
 
133
- hook_reply = HookManager.run "reply-to", :modes => types
135
+ hook_reply = HookManager.run "reply-to", :modes => types, :message => @m
134
136
 
135
137
  @type_selector.set_to(
136
138
  if types.include? type_arg
@@ -696,7 +696,7 @@ EOS
696
696
  @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
697
697
 
698
698
  update
699
- BufferManager.clear @mbid
699
+ BufferManager.clear @mbid if @mbid
700
700
  @mbid = nil
701
701
  BufferManager.draw_screen
702
702
  @ts.size - orig_size
@@ -10,8 +10,6 @@ class ThreadViewMode < LineCursorMode
10
10
  attr_accessor :state
11
11
  end
12
12
 
13
- INDENT_SPACES = 2 # how many spaces to indent child messages
14
-
15
13
  HookManager.register "detailed-headers", <<EOS
16
14
  Add or remove headers from the detailed header display of a message.
17
15
  Variables:
@@ -54,6 +52,7 @@ EOS
54
52
  k.add :toggle_detailed_header, "Toggle detailed header", 'h'
55
53
  k.add :show_header, "Show full message header", 'H'
56
54
  k.add :show_message, "Show full message (raw form)", 'V'
55
+ k.add :reload, "Update message in thread", '@'
57
56
  k.add :activate_chunk, "Expand/collapse or activate item", :enter
58
57
  k.add :expand_all_messages, "Expand/collapse all messages", 'E'
59
58
  k.add :edit_draft, "Edit draft", 'e'
@@ -127,6 +126,7 @@ EOS
127
126
  ## objects. @person_lines is a map from row #s to Person objects.
128
127
 
129
128
  def initialize thread, hidden_labels=[], index_mode=nil
129
+ @indent_spaces = $config[:indent_spaces]
130
130
  super :slip_rows => $config[:slip_rows]
131
131
  @thread = thread
132
132
  @hidden_labels = hidden_labels
@@ -205,6 +205,10 @@ EOS
205
205
  @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
206
206
  update
207
207
  end
208
+
209
+ def reload
210
+ update
211
+ end
208
212
 
209
213
  def reply type_arg=nil
210
214
  m = @message_lines[curpos] or return
@@ -389,7 +393,7 @@ EOS
389
393
  when Chunk::Attachment
390
394
  default_dir = $config[:default_attachment_save_dir]
391
395
  default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
392
- default_fn = File.expand_path File.join(default_dir, chunk.safe_filename)
396
+ default_fn = File.expand_path File.join(default_dir, chunk.filesafe_filename)
393
397
  fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
394
398
 
395
399
  # if user selects directory use file name from message
@@ -418,7 +422,7 @@ EOS
418
422
  num_errors = 0
419
423
  m.chunks.each do |chunk|
420
424
  next unless chunk.is_a?(Chunk::Attachment)
421
- fn = File.join(folder, chunk.safe_filename)
425
+ fn = File.join(folder, chunk.filesafe_filename)
422
426
  num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
423
427
  num += 1
424
428
  end
@@ -561,7 +565,7 @@ EOS
561
565
  l = @layout[m]
562
566
 
563
567
  ## boundaries of the message
564
- message_left = l.depth * INDENT_SPACES
568
+ message_left = l.depth * @indent_spaces
565
569
  message_right = message_left + l.width
566
570
 
567
571
  ## calculate leftmost colum
@@ -769,14 +773,13 @@ EOS
769
773
  # ]
770
774
 
771
775
  linetext = @text.slice(curpos, @text.length).flatten(1)
772
- .take_while{|d| d[0] == :text_color and d[1].strip != ""} # Only take up to the first "" alone on its line
776
+ .take_while{|d| [:text_color, :sig_color].include?(d[0]) and d[1].strip != ""} # Only take up to the first "" alone on its line
773
777
  .map{|d| d[1].strip}.join("").strip
774
778
 
775
779
  found = false
776
- (linetext || "").scan(URI::regexp).each do |matches|
780
+ URI.extract(linetext || "").each do |match|
777
781
  begin
778
- link = $& # ruby magic: $& is the whole regexp match
779
- u = URI.parse(link)
782
+ u = URI.parse(match)
780
783
  next unless u.absolute?
781
784
  next unless ["http", "https"].include?(u.scheme)
782
785
 
@@ -886,7 +889,7 @@ private
886
889
  (0 ... text.length).each do |i|
887
890
  @chunk_lines[@text.length + i] = c
888
891
  @message_lines[@text.length + i] = m
889
- lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * INDENT_SPACES)
892
+ lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * @indent_spaces)
890
893
  l.width = lw if lw > l.width
891
894
  end
892
895
  @text += text
@@ -991,7 +994,7 @@ private
991
994
 
992
995
  ## todo: check arguments on this overly complex function
993
996
  def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
994
- prefix = " " * INDENT_SPACES * depth
997
+ prefix = " " * @indent_spaces * depth
995
998
  case chunk
996
999
  when :fake_root
997
1000
  [[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]