sup 0.20.0 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -1
  3. data/.travis.yml +11 -6
  4. data/CONTRIBUTORS +27 -15
  5. data/Gemfile +2 -1
  6. data/History.txt +84 -0
  7. data/README.md +26 -5
  8. data/Rakefile +0 -1
  9. data/ReleaseNotes +7 -0
  10. data/bin/sup +17 -30
  11. data/bin/sup-add +15 -16
  12. data/bin/sup-config +30 -45
  13. data/bin/sup-dump +2 -3
  14. data/bin/sup-import-dump +5 -6
  15. data/bin/sup-sync +3 -4
  16. data/bin/sup-sync-back-maildir +3 -4
  17. data/bin/sup-tweak-labels +6 -7
  18. data/contrib/colorpicker.rb +0 -2
  19. data/contrib/completion/_sup.bash +102 -0
  20. data/devel/profile.rb +0 -1
  21. data/ext/mkrf_conf_xapian.rb +1 -1
  22. data/lib/sup.rb +8 -8
  23. data/lib/sup/colormap.rb +5 -2
  24. data/lib/sup/contact.rb +4 -2
  25. data/lib/sup/crypto.rb +58 -16
  26. data/lib/sup/draft.rb +8 -8
  27. data/lib/sup/hook.rb +9 -9
  28. data/lib/sup/index.rb +20 -7
  29. data/lib/sup/label.rb +1 -1
  30. data/lib/sup/logger.rb +1 -1
  31. data/lib/sup/maildir.rb +2 -2
  32. data/lib/sup/mbox.rb +2 -2
  33. data/lib/sup/message.rb +26 -10
  34. data/lib/sup/message_chunks.rb +7 -4
  35. data/lib/sup/mode.rb +34 -28
  36. data/lib/sup/modes/contact_list_mode.rb +1 -0
  37. data/lib/sup/modes/edit_message_mode.rb +1 -1
  38. data/lib/sup/modes/forward_mode.rb +22 -3
  39. data/lib/sup/modes/line_cursor_mode.rb +1 -1
  40. data/lib/sup/modes/reply_mode.rb +3 -1
  41. data/lib/sup/modes/text_mode.rb +6 -1
  42. data/lib/sup/modes/thread_index_mode.rb +6 -2
  43. data/lib/sup/modes/thread_view_mode.rb +63 -18
  44. data/lib/sup/person.rb +68 -61
  45. data/lib/sup/search.rb +1 -1
  46. data/lib/sup/sent.rb +1 -1
  47. data/lib/sup/source.rb +1 -1
  48. data/lib/sup/util.rb +15 -94
  49. data/lib/sup/util/axe.rb +17 -0
  50. data/lib/sup/util/locale_fiddler.rb +24 -0
  51. data/lib/sup/util/ncurses.rb +3 -3
  52. data/lib/sup/version.rb +10 -1
  53. data/sup.gemspec +12 -10
  54. data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
  55. data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
  56. data/test/fixtures/blank-header-fields.eml +71 -0
  57. data/test/fixtures/contacts.txt +1 -0
  58. data/test/fixtures/mailing-list-header.eml +80 -0
  59. data/test/fixtures/malicious-attachment-names.eml +55 -0
  60. data/test/fixtures/missing-from-to.eml +18 -0
  61. data/test/{messages → fixtures}/missing-line.eml +0 -0
  62. data/test/fixtures/multi-part-2.eml +72 -0
  63. data/test/fixtures/multi-part.eml +61 -0
  64. data/test/fixtures/no-body.eml +18 -0
  65. data/test/fixtures/simple-message.eml +29 -0
  66. data/test/fixtures/text-attachments-with-charset.eml +46 -0
  67. data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
  68. data/test/gnupg_test_home/gpg.conf +2 -1
  69. data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
  70. data/test/gnupg_test_home/pubring.gpg +0 -0
  71. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  72. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  73. data/test/gnupg_test_home/regen_keys.sh +70 -16
  74. data/test/gnupg_test_home/secring.gpg +0 -0
  75. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -22
  76. data/test/integration/test_maildir.rb +1 -1
  77. data/test/integration/test_mbox.rb +1 -1
  78. data/test/test_crypto.rb +14 -2
  79. data/test/test_header_parsing.rb +1 -1
  80. data/test/test_helper.rb +6 -3
  81. data/test/test_message.rb +115 -341
  82. data/test/test_messages_dir.rb +4 -28
  83. data/test/test_yaml_regressions.rb +1 -1
  84. data/test/unit/test_contact.rb +33 -0
  85. data/test/unit/test_locale_fiddler.rb +15 -0
  86. data/test/unit/test_person.rb +37 -0
  87. data/test/unit/util/test_query.rb +10 -4
  88. data/test/unit/util/test_string.rb +6 -0
  89. metadata +107 -43
  90. data/test/gnupg_test_home/key1.gen +0 -15
  91. data/test/gnupg_test_home/key2.gen +0 -15
@@ -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
@@ -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.exists? tmp_path
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.exists? File.join(@dir, id)
204
+ File.exist? File.join(@dir, id)
205
205
  end
206
206
 
207
207
  private
@@ -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.exists?(@path) && !File.zero?(@path)
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}"
@@ -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
@@ -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
- warn "multipart/signed with #{m.body.size} parts (expecting 2)"
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
- 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?}"
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
- 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}"
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
- warn "multipart/encrypted with #{m.body.size} parts (expecting 2)"
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
- 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?}"
435
446
  return
436
447
  end
437
448
 
438
449
  if payload.header.content_type && payload.header.content_type.downcase != "application/octet-stream"
439
- 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}"
440
451
  return
441
452
  end
442
453
 
443
454
  if control.header.content_type && control.header.content_type.downcase != "application/pgp-encrypted"
444
- 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}"
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
@@ -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
@@ -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
@@ -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
- if File.exists? fn
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
- Open3.popen3(command) do |input, output, error|
106
- err, data, * = IO.select [error], [input], nil
107
-
108
- unless err.empty?
109
- message = err.first.read
110
- if message =~ /^\s*$/
111
- warn "error running #{command} (but no error message)"
112
- BufferManager.flash "Error running #{command}!"
113
- else
114
- warn "error running #{command}: #{message}"
115
- BufferManager.flash "Error: #{message}"
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
- data = data.first
121
- data.sync = false # buffer input
122
+ data = data.first
123
+ data.sync = false # buffer input
122
124
 
123
- yield data
124
- data.close # output will block unless input is closed
125
+ yield data
126
+ data.close # output will block unless input is closed
125
127
 
126
- ## BUG?: shows errors or output but not both....
127
- data, * = IO.select [output, error], nil, nil
128
- data = data.first
128
+ ## BUG?: shows errors or output but not both....
129
+ data, * = IO.select [output, error], nil, nil
130
+ data = data.first
129
131
 
130
- if data.eof
131
- BufferManager.flash "'#{command}' done!"
132
- nil
133
- else
134
- data.read
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.exists?(sigfn)
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
- ["--- Begin forwarded message from #{m.from.mediumname} ---"] +
69
- m.quotable_header_lines + [""] + m.quotable_body_lines +
70
- ["--- End forwarded message ---"]
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
@@ -65,7 +65,7 @@ protected
65
65
  def set_cursor_pos p
66
66
  return if @curpos == p
67
67
  @curpos = p.clamp @cursor_top, lines
68
- buffer.mark_dirty
68
+ buffer.mark_dirty if buffer # not sure why the buffer is gone
69
69
  set_status
70
70
  end
71
71
 
@@ -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
@@ -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
- [(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
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
- if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
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
- else
230
- BufferManager.flash "Can't find List-Unsubscribe header for this message."
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.filename)
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.filename)
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 * INDENT_SPACES
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] == :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
753
777
  .map{|d| d[1].strip}.join("").strip
754
778
 
755
779
  found = false
756
- (linetext || "").scan(URI::regexp).each do |matches|
780
+ URI.extract(linetext || "").each do |match|
757
781
  begin
758
- link = $& # ruby magic: $& is the whole regexp match
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 * INDENT_SPACES)
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 = OrderedHash.new
903
- headers["Date"] = "#{m.date.to_message_nice_s} (#{m.date.to_nice_distance_s})"
904
- headers["Subject"] = m.subj
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 = " " * INDENT_SPACES * depth
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>"]]]