sup 0.20.0 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -1
- data/.travis.yml +11 -6
- data/CONTRIBUTORS +27 -15
- data/Gemfile +2 -1
- data/History.txt +84 -0
- data/README.md +26 -5
- data/Rakefile +0 -1
- data/ReleaseNotes +7 -0
- data/bin/sup +17 -30
- data/bin/sup-add +15 -16
- data/bin/sup-config +30 -45
- data/bin/sup-dump +2 -3
- data/bin/sup-import-dump +5 -6
- data/bin/sup-sync +3 -4
- data/bin/sup-sync-back-maildir +3 -4
- data/bin/sup-tweak-labels +6 -7
- data/contrib/colorpicker.rb +0 -2
- data/contrib/completion/_sup.bash +102 -0
- data/devel/profile.rb +0 -1
- data/ext/mkrf_conf_xapian.rb +1 -1
- data/lib/sup.rb +8 -8
- data/lib/sup/colormap.rb +5 -2
- data/lib/sup/contact.rb +4 -2
- data/lib/sup/crypto.rb +58 -16
- data/lib/sup/draft.rb +8 -8
- data/lib/sup/hook.rb +9 -9
- data/lib/sup/index.rb +20 -7
- data/lib/sup/label.rb +1 -1
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +2 -2
- data/lib/sup/mbox.rb +2 -2
- data/lib/sup/message.rb +26 -10
- data/lib/sup/message_chunks.rb +7 -4
- data/lib/sup/mode.rb +34 -28
- data/lib/sup/modes/contact_list_mode.rb +1 -0
- data/lib/sup/modes/edit_message_mode.rb +1 -1
- data/lib/sup/modes/forward_mode.rb +22 -3
- data/lib/sup/modes/line_cursor_mode.rb +1 -1
- data/lib/sup/modes/reply_mode.rb +3 -1
- data/lib/sup/modes/text_mode.rb +6 -1
- data/lib/sup/modes/thread_index_mode.rb +6 -2
- data/lib/sup/modes/thread_view_mode.rb +63 -18
- data/lib/sup/person.rb +68 -61
- data/lib/sup/search.rb +1 -1
- data/lib/sup/sent.rb +1 -1
- data/lib/sup/source.rb +1 -1
- data/lib/sup/util.rb +15 -94
- data/lib/sup/util/axe.rb +17 -0
- data/lib/sup/util/locale_fiddler.rb +24 -0
- data/lib/sup/util/ncurses.rb +3 -3
- data/lib/sup/version.rb +10 -1
- data/sup.gemspec +12 -10
- data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
- data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
- data/test/fixtures/blank-header-fields.eml +71 -0
- data/test/fixtures/contacts.txt +1 -0
- data/test/fixtures/mailing-list-header.eml +80 -0
- data/test/fixtures/malicious-attachment-names.eml +55 -0
- data/test/fixtures/missing-from-to.eml +18 -0
- data/test/{messages → fixtures}/missing-line.eml +0 -0
- data/test/fixtures/multi-part-2.eml +72 -0
- data/test/fixtures/multi-part.eml +61 -0
- data/test/fixtures/no-body.eml +18 -0
- data/test/fixtures/simple-message.eml +29 -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 +70 -16
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -22
- data/test/integration/test_maildir.rb +1 -1
- data/test/integration/test_mbox.rb +1 -1
- data/test/test_crypto.rb +14 -2
- data/test/test_header_parsing.rb +1 -1
- data/test/test_helper.rb +6 -3
- data/test/test_message.rb +115 -341
- data/test/test_messages_dir.rb +4 -28
- data/test/test_yaml_regressions.rb +1 -1
- data/test/unit/test_contact.rb +33 -0
- data/test/unit/test_locale_fiddler.rb +15 -0
- data/test/unit/test_person.rb +37 -0
- data/test/unit/util/test_query.rb +10 -4
- data/test/unit/util/test_string.rb +6 -0
- metadata +107 -43
- data/test/gnupg_test_home/key1.gen +0 -15
- data/test/gnupg_test_home/key2.gen +0 -15
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/maildir.rb
CHANGED
@@ -68,7 +68,7 @@ class Maildir < Source
|
|
68
68
|
File.safe_link tmp_path, new_path
|
69
69
|
stored = true
|
70
70
|
ensure
|
71
|
-
File.unlink tmp_path if File.
|
71
|
+
File.unlink tmp_path if File.exist? tmp_path
|
72
72
|
end
|
73
73
|
end #rescue Errno...
|
74
74
|
end #Dir.chdir
|
@@ -201,7 +201,7 @@ class Maildir < Source
|
|
201
201
|
def trashed? id; maildir_data(id)[2].include? "T"; end
|
202
202
|
|
203
203
|
def valid? id
|
204
|
-
File.
|
204
|
+
File.exist? File.join(@dir, id)
|
205
205
|
end
|
206
206
|
|
207
207
|
private
|
data/lib/sup/mbox.rb
CHANGED
@@ -115,7 +115,7 @@ class MBox < Source
|
|
115
115
|
end
|
116
116
|
|
117
117
|
def store_message date, from_email, &block
|
118
|
-
need_blank = File.
|
118
|
+
need_blank = File.exist?(@path) && !File.zero?(@path)
|
119
119
|
File.open(@path, "ab") do |f|
|
120
120
|
f.puts if need_blank
|
121
121
|
f.puts "From #{from_email} #{date.asctime}"
|
@@ -180,7 +180,7 @@ class MBox < Source
|
|
180
180
|
time = $1
|
181
181
|
begin
|
182
182
|
## hack -- make Time.parse fail when trying to substitute values from Time.now
|
183
|
-
Time.parse time, 0
|
183
|
+
Time.parse time, Time.at(0)
|
184
184
|
true
|
185
185
|
rescue NoMethodError, ArgumentError
|
186
186
|
warn "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
|
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
|
@@ -279,6 +284,12 @@ class Message
|
|
279
284
|
end
|
280
285
|
end
|
281
286
|
|
287
|
+
def reload_from_source!
|
288
|
+
@chunks = nil
|
289
|
+
load_from_source!
|
290
|
+
end
|
291
|
+
|
292
|
+
|
282
293
|
def error_message
|
283
294
|
<<EOS
|
284
295
|
#@snippet...
|
@@ -398,19 +409,19 @@ private
|
|
398
409
|
|
399
410
|
def multipart_signed_to_chunks m
|
400
411
|
if m.body.size != 2
|
401
|
-
|
412
|
+
warn_with_location "multipart/signed with #{m.body.size} parts (expecting 2)"
|
402
413
|
return
|
403
414
|
end
|
404
415
|
|
405
416
|
payload, signature = m.body
|
406
417
|
if signature.multipart?
|
407
|
-
|
418
|
+
warn_with_location "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
|
408
419
|
return
|
409
420
|
end
|
410
421
|
|
411
422
|
## this probably will never happen
|
412
423
|
if payload.header.content_type && payload.header.content_type.downcase == "application/pgp-signature"
|
413
|
-
|
424
|
+
warn_with_location "multipart/signed with payload content type #{payload.header.content_type}"
|
414
425
|
return
|
415
426
|
end
|
416
427
|
|
@@ -425,23 +436,23 @@ private
|
|
425
436
|
|
426
437
|
def multipart_encrypted_to_chunks m
|
427
438
|
if m.body.size != 2
|
428
|
-
|
439
|
+
warn_with_location "multipart/encrypted with #{m.body.size} parts (expecting 2)"
|
429
440
|
return
|
430
441
|
end
|
431
442
|
|
432
443
|
control, payload = m.body
|
433
444
|
if control.multipart?
|
434
|
-
|
445
|
+
warn_with_location "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
|
435
446
|
return
|
436
447
|
end
|
437
448
|
|
438
449
|
if payload.header.content_type && payload.header.content_type.downcase != "application/octet-stream"
|
439
|
-
|
450
|
+
warn_with_location "multipart/encrypted with payload content type #{payload.header.content_type}"
|
440
451
|
return
|
441
452
|
end
|
442
453
|
|
443
454
|
if control.header.content_type && control.header.content_type.downcase != "application/pgp-encrypted"
|
444
|
-
|
455
|
+
warn_with_location "multipart/encrypted with control content type #{signature.header.content_type}"
|
445
456
|
return
|
446
457
|
end
|
447
458
|
|
@@ -685,7 +696,7 @@ private
|
|
685
696
|
newstate = :quote
|
686
697
|
elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE && !lines[(i+1)..-1].index { |l| l =~ /^-- $/ }
|
687
698
|
newstate = :sig
|
688
|
-
elsif line =~ BLOCK_QUOTE_PATTERN
|
699
|
+
elsif line =~ BLOCK_QUOTE_PATTERN && nextline !~ QUOTE_PATTERN
|
689
700
|
newstate = :block_quote
|
690
701
|
end
|
691
702
|
|
@@ -745,6 +756,11 @@ private
|
|
745
756
|
end
|
746
757
|
chunks
|
747
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
|
748
764
|
end
|
749
765
|
|
750
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
|
@@ -159,6 +159,8 @@ EOS
|
|
159
159
|
"Attachment: #{filename} (#{content_type}; #{@raw_content.size.to_human_size})"
|
160
160
|
end
|
161
161
|
end
|
162
|
+
def safe_filename; Shellwords.escape(@filename).gsub("/", "_") end
|
163
|
+
def filesafe_filename; @filename.gsub("/", "_") end
|
162
164
|
|
163
165
|
## an attachment is exapndable if we've managed to decode it into
|
164
166
|
## something we can display inline. otherwise, it's viewable.
|
@@ -306,12 +308,13 @@ EOS
|
|
306
308
|
end
|
307
309
|
|
308
310
|
class CryptoNotice
|
309
|
-
attr_reader :lines, :status, :patina_text
|
311
|
+
attr_reader :lines, :status, :patina_text, :unknown_fingerprint
|
310
312
|
|
311
|
-
def initialize status, description, lines=[]
|
313
|
+
def initialize status, description, lines=[], unknown_fingerprint=nil
|
312
314
|
@status = status
|
313
315
|
@patina_text = description
|
314
316
|
@lines = lines
|
317
|
+
@unknown_fingerprint = unknown_fingerprint
|
315
318
|
end
|
316
319
|
|
317
320
|
def patina_color
|
data/lib/sup/mode.rb
CHANGED
@@ -46,7 +46,7 @@ class Mode
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def resolve_input c
|
49
|
-
ancestors.each do |klass| # try all keymaps in order of ancestry
|
49
|
+
self.class.ancestors.each do |klass| # try all keymaps in order of ancestry
|
50
50
|
next unless @@keymaps.member?(klass)
|
51
51
|
action = BufferManager.resolve_input_with_keymap c, @@keymaps[klass]
|
52
52
|
return action if action
|
@@ -62,7 +62,7 @@ class Mode
|
|
62
62
|
|
63
63
|
def help_text
|
64
64
|
used_keys = {}
|
65
|
-
ancestors.map do |klass|
|
65
|
+
self.class.ancestors.map do |klass|
|
66
66
|
km = @@keymaps[klass] or next
|
67
67
|
title = "Keybindings from #{Mode.make_name klass.name}"
|
68
68
|
s = <<EOS
|
@@ -83,7 +83,8 @@ EOS
|
|
83
83
|
### helper functions
|
84
84
|
|
85
85
|
def save_to_file fn, talk=true
|
86
|
-
|
86
|
+
FileUtils.mkdir_p File.dirname(fn)
|
87
|
+
if File.exist? fn
|
87
88
|
unless BufferManager.ask_yes_or_no "File \"#{fn}\" exists. Overwrite?"
|
88
89
|
info "Not overwriting #{fn}"
|
89
90
|
return
|
@@ -102,37 +103,42 @@ EOS
|
|
102
103
|
end
|
103
104
|
|
104
105
|
def pipe_to_process command
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
106
|
+
begin
|
107
|
+
Open3.popen3(command) do |input, output, error|
|
108
|
+
err, data, * = IO.select [error], [input], nil
|
109
|
+
|
110
|
+
unless err.empty?
|
111
|
+
message = err.first.read
|
112
|
+
if message =~ /^\s*$/
|
113
|
+
warn "error running #{command} (but no error message)"
|
114
|
+
BufferManager.flash "Error running #{command}!"
|
115
|
+
else
|
116
|
+
warn "error running #{command}: #{message}"
|
117
|
+
BufferManager.flash "Error: #{message}"
|
118
|
+
end
|
119
|
+
return nil, false
|
116
120
|
end
|
117
|
-
return
|
118
|
-
end
|
119
121
|
|
120
|
-
|
121
|
-
|
122
|
+
data = data.first
|
123
|
+
data.sync = false # buffer input
|
122
124
|
|
123
|
-
|
124
|
-
|
125
|
+
yield data
|
126
|
+
data.close # output will block unless input is closed
|
125
127
|
|
126
|
-
|
127
|
-
|
128
|
-
|
128
|
+
## BUG?: shows errors or output but not both....
|
129
|
+
data, * = IO.select [output, error], nil, nil
|
130
|
+
data = data.first
|
129
131
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
132
|
+
if data.eof
|
133
|
+
BufferManager.flash "'#{command}' done!"
|
134
|
+
return nil, true
|
135
|
+
else
|
136
|
+
return data.read, true
|
137
|
+
end
|
135
138
|
end
|
139
|
+
rescue Errno::ENOENT
|
140
|
+
# If the command is invalid
|
141
|
+
return nil, false
|
136
142
|
end
|
137
143
|
end
|
138
144
|
end
|
@@ -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
|
@@ -699,7 +699,7 @@ private
|
|
699
699
|
sigfn = (AccountManager.account_for(from_email) ||
|
700
700
|
AccountManager.default_account).signature
|
701
701
|
|
702
|
-
if sigfn && File.
|
702
|
+
if sigfn && File.exist?(sigfn)
|
703
703
|
["", "-- "] + File.readlines(sigfn).map { |l| l.chomp }
|
704
704
|
else
|
705
705
|
[]
|
@@ -1,6 +1,17 @@
|
|
1
1
|
module Redwood
|
2
2
|
|
3
3
|
class ForwardMode < EditMessageMode
|
4
|
+
|
5
|
+
HookManager.register "forward-attribution", <<EOS
|
6
|
+
Generates the attribution for the forwarded message
|
7
|
+
(["--- Begin forwarded message from John Doe ---",
|
8
|
+
"--- End forwarded message ---"])
|
9
|
+
Variables:
|
10
|
+
message: a message object representing the message being replied to
|
11
|
+
(useful values include message.from.mediumname and message.date)
|
12
|
+
Return value:
|
13
|
+
A list containing two strings: the text of the begin line and the text of the end line
|
14
|
+
EOS
|
4
15
|
## TODO: share some of this with reply-mode
|
5
16
|
def initialize opts={}
|
6
17
|
header = {
|
@@ -65,9 +76,17 @@ class ForwardMode < EditMessageMode
|
|
65
76
|
protected
|
66
77
|
|
67
78
|
def forward_body_lines m
|
68
|
-
|
69
|
-
|
70
|
-
|
79
|
+
attribution = HookManager.run("forward-attribution", :message => m) || default_attribution(m)
|
80
|
+
attribution[0,1] +
|
81
|
+
m.quotable_header_lines +
|
82
|
+
[""] +
|
83
|
+
m.quotable_body_lines +
|
84
|
+
attribution[1,1]
|
85
|
+
end
|
86
|
+
|
87
|
+
def default_attribution m
|
88
|
+
["--- Begin forwarded message from #{m.from.mediumname} ---",
|
89
|
+
"--- End forwarded message ---"]
|
71
90
|
end
|
72
91
|
|
73
92
|
def send_message
|
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
|
data/lib/sup/modes/text_mode.rb
CHANGED
@@ -24,10 +24,15 @@ class TextMode < ScrollMode
|
|
24
24
|
command = BufferManager.ask(:shell, "pipe command: ")
|
25
25
|
return if command.nil? || command.empty?
|
26
26
|
|
27
|
-
output = pipe_to_process(command) do |stream|
|
27
|
+
output, success = pipe_to_process(command) do |stream|
|
28
28
|
@text.each { |l| stream.puts l }
|
29
29
|
end
|
30
30
|
|
31
|
+
unless success
|
32
|
+
BufferManager.flash "Invalid command: '#{command}' is not an executable"
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
31
36
|
if output
|
32
37
|
BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
|
33
38
|
else
|
@@ -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
|
@@ -1026,7 +1026,11 @@ private
|
|
1026
1026
|
end
|
1027
1027
|
|
1028
1028
|
def from_width
|
1029
|
-
|
1029
|
+
if buffer
|
1030
|
+
[(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
|
1031
|
+
else
|
1032
|
+
MIN_FROM_WIDTH # not sure why the buffer is gone
|
1033
|
+
end
|
1030
1034
|
end
|
1031
1035
|
|
1032
1036
|
def initialize_threads
|
@@ -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'
|
@@ -89,6 +88,7 @@ EOS
|
|
89
88
|
k.add :toggle_wrap, "Toggle wrapping of text", 'w'
|
90
89
|
|
91
90
|
k.add :goto_uri, "Goto uri under cursor", 'g'
|
91
|
+
k.add :fetch_and_verify, "Fetch the PGP key on poolserver and re-verify message", "v"
|
92
92
|
|
93
93
|
k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read:", '.' do |kk|
|
94
94
|
kk.add :archive_and_kill, "Archive this thread and kill buffer", 'a'
|
@@ -126,6 +126,7 @@ EOS
|
|
126
126
|
## objects. @person_lines is a map from row #s to Person objects.
|
127
127
|
|
128
128
|
def initialize thread, hidden_labels=[], index_mode=nil
|
129
|
+
@indent_spaces = $config[:indent_spaces]
|
129
130
|
super :slip_rows => $config[:slip_rows]
|
130
131
|
@thread = thread
|
131
132
|
@hidden_labels = hidden_labels
|
@@ -204,6 +205,10 @@ EOS
|
|
204
205
|
@layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
|
205
206
|
update
|
206
207
|
end
|
208
|
+
|
209
|
+
def reload
|
210
|
+
update
|
211
|
+
end
|
207
212
|
|
208
213
|
def reply type_arg=nil
|
209
214
|
m = @message_lines[curpos] or return
|
@@ -224,10 +229,24 @@ EOS
|
|
224
229
|
|
225
230
|
def unsubscribe_from_list
|
226
231
|
m = @message_lines[curpos] or return
|
227
|
-
|
232
|
+
BufferManager.flash "Can't find List-Unsubscribe header for this message." unless m.list_unsubscribe
|
233
|
+
|
234
|
+
if m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
|
228
235
|
ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "unsubscribe")
|
229
|
-
|
230
|
-
|
236
|
+
elsif m.list_unsubscribe =~ /<(http.*)?>/
|
237
|
+
unless HookManager.enabled? "goto"
|
238
|
+
BufferManager.flash "You must add a goto.rb hook before you can goto an unsubscribe URI."
|
239
|
+
return
|
240
|
+
end
|
241
|
+
|
242
|
+
begin
|
243
|
+
u = URI.parse($1)
|
244
|
+
rescue URI::InvalidURIError => e
|
245
|
+
BufferManager.flash("Invalid unsubscribe link")
|
246
|
+
return
|
247
|
+
end
|
248
|
+
|
249
|
+
HookManager.run "goto", :uri => Shellwords.escape(u.to_s)
|
231
250
|
end
|
232
251
|
end
|
233
252
|
|
@@ -374,7 +393,7 @@ EOS
|
|
374
393
|
when Chunk::Attachment
|
375
394
|
default_dir = $config[:default_attachment_save_dir]
|
376
395
|
default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
|
377
|
-
default_fn = File.expand_path File.join(default_dir, chunk.
|
396
|
+
default_fn = File.expand_path File.join(default_dir, chunk.filesafe_filename)
|
378
397
|
fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
|
379
398
|
|
380
399
|
# if user selects directory use file name from message
|
@@ -403,7 +422,7 @@ EOS
|
|
403
422
|
num_errors = 0
|
404
423
|
m.chunks.each do |chunk|
|
405
424
|
next unless chunk.is_a?(Chunk::Attachment)
|
406
|
-
fn = File.join(folder, chunk.
|
425
|
+
fn = File.join(folder, chunk.filesafe_filename)
|
407
426
|
num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
|
408
427
|
num += 1
|
409
428
|
end
|
@@ -546,7 +565,7 @@ EOS
|
|
546
565
|
l = @layout[m]
|
547
566
|
|
548
567
|
## boundaries of the message
|
549
|
-
message_left = l.depth *
|
568
|
+
message_left = l.depth * @indent_spaces
|
550
569
|
message_right = message_left + l.width
|
551
570
|
|
552
571
|
## calculate leftmost colum
|
@@ -708,7 +727,7 @@ EOS
|
|
708
727
|
command = BufferManager.ask(:shell, "pipe command: ")
|
709
728
|
return if command.nil? || command.empty?
|
710
729
|
|
711
|
-
output = pipe_to_process(command) do |stream|
|
730
|
+
output, success = pipe_to_process(command) do |stream|
|
712
731
|
if chunk
|
713
732
|
stream.print chunk.raw_content
|
714
733
|
else
|
@@ -716,6 +735,11 @@ EOS
|
|
716
735
|
end
|
717
736
|
end
|
718
737
|
|
738
|
+
unless success
|
739
|
+
BufferManager.flash "Invalid command: '#{command}' is not an executable"
|
740
|
+
return
|
741
|
+
end
|
742
|
+
|
719
743
|
if output
|
720
744
|
BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
|
721
745
|
else
|
@@ -749,14 +773,13 @@ EOS
|
|
749
773
|
# ]
|
750
774
|
|
751
775
|
linetext = @text.slice(curpos, @text.length).flatten(1)
|
752
|
-
.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
|
753
777
|
.map{|d| d[1].strip}.join("").strip
|
754
778
|
|
755
779
|
found = false
|
756
|
-
(linetext || "").
|
780
|
+
URI.extract(linetext || "").each do |match|
|
757
781
|
begin
|
758
|
-
|
759
|
-
u = URI.parse(link)
|
782
|
+
u = URI.parse(match)
|
760
783
|
next unless u.absolute?
|
761
784
|
next unless ["http", "https"].include?(u.scheme)
|
762
785
|
|
@@ -774,6 +797,27 @@ EOS
|
|
774
797
|
BufferManager.flash "No URI found." unless found
|
775
798
|
end
|
776
799
|
|
800
|
+
def fetch_and_verify
|
801
|
+
message = @message_lines[curpos]
|
802
|
+
crypto_chunk = message.chunks.select {|chunk| chunk.is_a?(Chunk::CryptoNotice)}.first
|
803
|
+
return unless crypto_chunk
|
804
|
+
return unless crypto_chunk.unknown_fingerprint
|
805
|
+
|
806
|
+
BufferManager.flash "Retrieving key #{crypto_chunk.unknown_fingerprint} ..."
|
807
|
+
|
808
|
+
error = CryptoManager.retrieve crypto_chunk.unknown_fingerprint
|
809
|
+
|
810
|
+
if error
|
811
|
+
BufferManager.flash "Couldn't retrieve key: #{error.to_s}"
|
812
|
+
else
|
813
|
+
BufferManager.flash "Key #{crypto_chunk.unknown_fingerprint} successfully retrieved !"
|
814
|
+
end
|
815
|
+
|
816
|
+
# Re-trigger gpg verification
|
817
|
+
message.reload_from_source!
|
818
|
+
update
|
819
|
+
end
|
820
|
+
|
777
821
|
private
|
778
822
|
|
779
823
|
def initial_state_for m
|
@@ -845,7 +889,7 @@ private
|
|
845
889
|
(0 ... text.length).each do |i|
|
846
890
|
@chunk_lines[@text.length + i] = c
|
847
891
|
@message_lines[@text.length + i] = m
|
848
|
-
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)
|
849
893
|
l.width = lw if lw > l.width
|
850
894
|
end
|
851
895
|
@text += text
|
@@ -899,9 +943,10 @@ private
|
|
899
943
|
addressee_lines += format_person_list " Bcc: ", m.bcc
|
900
944
|
end
|
901
945
|
|
902
|
-
headers =
|
903
|
-
|
904
|
-
|
946
|
+
headers = {
|
947
|
+
"Date" => "#{m.date.to_message_nice_s} (#{m.date.to_nice_distance_s})",
|
948
|
+
"Subject" => m.subj
|
949
|
+
}
|
905
950
|
|
906
951
|
show_labels = @thread.labels - LabelManager::HIDDEN_RESERVED_LABELS
|
907
952
|
unless show_labels.empty?
|
@@ -949,7 +994,7 @@ private
|
|
949
994
|
|
950
995
|
## todo: check arguments on this overly complex function
|
951
996
|
def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
|
952
|
-
prefix = " " *
|
997
|
+
prefix = " " * @indent_spaces * depth
|
953
998
|
case chunk
|
954
999
|
when :fake_root
|
955
1000
|
[[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]
|