sup 0.20.0 → 1.0
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.
- 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>"]]]
|