sup 0.22.1 → 0.23

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 (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>"]]]