sup 0.22.1 → 0.23
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -3
- data/.travis.yml +11 -6
- data/CONTRIBUTORS +13 -5
- data/Gemfile +2 -1
- data/History.txt +51 -0
- data/README.md +26 -5
- data/bin/sup +7 -5
- data/bin/sup-add +14 -14
- data/bin/sup-config +30 -44
- data/bin/sup-dump +2 -2
- data/bin/sup-import-dump +4 -4
- data/bin/sup-sync +3 -3
- data/bin/sup-sync-back-maildir +2 -2
- data/bin/sup-tweak-labels +5 -5
- data/ext/mkrf_conf_xapian.rb +1 -1
- data/lib/sup.rb +1 -0
- data/lib/sup/crypto.rb +17 -8
- data/lib/sup/hook.rb +8 -8
- data/lib/sup/index.rb +18 -5
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/message.rb +20 -10
- data/lib/sup/message_chunks.rb +3 -2
- data/lib/sup/mode.rb +1 -0
- data/lib/sup/modes/contact_list_mode.rb +1 -0
- data/lib/sup/modes/reply_mode.rb +3 -1
- data/lib/sup/modes/thread_index_mode.rb +1 -1
- data/lib/sup/modes/thread_view_mode.rb +14 -11
- data/lib/sup/source.rb +1 -1
- data/lib/sup/util.rb +14 -19
- data/lib/sup/util/axe.rb +17 -0
- data/lib/sup/util/ncurses.rb +3 -3
- data/lib/sup/version.rb +10 -1
- data/sup.gemspec +7 -6
- data/test/fixtures/mailing-list-header.eml +80 -0
- data/test/fixtures/text-attachments-with-charset.eml +46 -0
- data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
- data/test/gnupg_test_home/gpg.conf +2 -1
- data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/regen_keys.sh +69 -18
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -22
- data/test/test_crypto.rb +2 -0
- data/test/test_message.rb +74 -0
- data/test/unit/util/test_query.rb +10 -4
- data/test/unit/util/test_string.rb +6 -0
- metadata +52 -38
- data/test/gnupg_test_home/key1.gen +0 -15
- data/test/gnupg_test_home/key2.gen +0 -15
- data/test/gnupg_test_home/key_ecc.gen +0 -13
data/bin/sup-dump
CHANGED
@@ -3,12 +3,12 @@
|
|
3
3
|
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
4
|
|
5
5
|
require 'xapian'
|
6
|
-
require '
|
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 =
|
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
|
data/bin/sup-import-dump
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
4
|
|
5
5
|
require 'uri'
|
6
|
-
require '
|
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 =
|
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
|
-
|
40
|
-
|
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
|
|
data/bin/sup-sync
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
4
|
|
5
5
|
require 'uri'
|
6
|
-
require '
|
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 =
|
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
|
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
|
|
data/bin/sup-sync-back-maildir
CHANGED
@@ -3,10 +3,10 @@
|
|
3
3
|
|
4
4
|
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
5
5
|
|
6
|
-
require '
|
6
|
+
require 'optimist'
|
7
7
|
require "sup"
|
8
8
|
|
9
|
-
opts =
|
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.
|
data/bin/sup-tweak-labels
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
$:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
|
4
4
|
|
5
|
-
require '
|
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 =
|
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
|
-
|
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
|
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
|
-
|
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?
|
data/ext/mkrf_conf_xapian.rb
CHANGED
data/lib/sup.rb
CHANGED
data/lib/sup/crypto.rb
CHANGED
@@ -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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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"] =
|
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
|
data/lib/sup/hook.rb
CHANGED
@@ -109,20 +109,20 @@ class HookManager
|
|
109
109
|
@descs[name] = desc
|
110
110
|
end
|
111
111
|
|
112
|
-
def print_hooks f=$stdout
|
113
|
-
|
114
|
-
|
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
|
data/lib/sup/index.rb
CHANGED
@@ -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
|
-
|
520
|
-
|
521
|
-
|
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|
|
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
|
-
|
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
|
data/lib/sup/logger.rb
CHANGED
@@ -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
|
data/lib/sup/message.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/sup/message_chunks.rb
CHANGED
@@ -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.
|
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.
|
data/lib/sup/mode.rb
CHANGED
@@ -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
|
data/lib/sup/modes/reply_mode.rb
CHANGED
@@ -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
|
@@ -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.
|
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.
|
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 *
|
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]
|
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 || "").
|
780
|
+
URI.extract(linetext || "").each do |match|
|
777
781
|
begin
|
778
|
-
|
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 *
|
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 = " " *
|
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>"]]]
|