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