sup 0.22.1 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/checks.yml +70 -0
  3. data/.gitignore +1 -3
  4. data/.rubocop.yml +5 -0
  5. data/CONTRIBUTORS +14 -5
  6. data/Gemfile +6 -1
  7. data/History.txt +76 -0
  8. data/Manifest.txt +149 -0
  9. data/README.md +32 -5
  10. data/Rakefile +40 -1
  11. data/bin/sup +7 -5
  12. data/bin/sup-add +16 -20
  13. data/bin/sup-config +30 -44
  14. data/bin/sup-dump +2 -2
  15. data/bin/sup-import-dump +4 -4
  16. data/bin/sup-sync +3 -3
  17. data/bin/sup-sync-back-maildir +2 -2
  18. data/bin/sup-tweak-labels +5 -5
  19. data/ext/mkrf_conf_xapian.rb +10 -4
  20. data/lib/sup/colormap.rb +1 -1
  21. data/lib/sup/crypto.rb +17 -8
  22. data/lib/sup/hook.rb +9 -9
  23. data/lib/sup/index.rb +20 -7
  24. data/lib/sup/keymap.rb +1 -1
  25. data/lib/sup/logger.rb +1 -1
  26. data/lib/sup/maildir.rb +4 -4
  27. data/lib/sup/mbox.rb +4 -4
  28. data/lib/sup/message.rb +26 -15
  29. data/lib/sup/message_chunks.rb +29 -20
  30. data/lib/sup/mode.rb +1 -0
  31. data/lib/sup/modes/completion_mode.rb +0 -1
  32. data/lib/sup/modes/contact_list_mode.rb +1 -0
  33. data/lib/sup/modes/file_browser_mode.rb +2 -2
  34. data/lib/sup/modes/label_list_mode.rb +1 -1
  35. data/lib/sup/modes/reply_mode.rb +3 -1
  36. data/lib/sup/modes/search_list_mode.rb +2 -2
  37. data/lib/sup/modes/thread_index_mode.rb +1 -1
  38. data/lib/sup/modes/thread_view_mode.rb +15 -13
  39. data/lib/sup/rfc2047.rb +21 -6
  40. data/lib/sup/source.rb +9 -3
  41. data/lib/sup/textfield.rb +0 -1
  42. data/lib/sup/thread.rb +0 -1
  43. data/lib/sup/util/axe.rb +17 -0
  44. data/lib/sup/util/ncurses.rb +3 -3
  45. data/lib/sup/util.rb +42 -67
  46. data/lib/sup/version.rb +10 -1
  47. data/lib/sup.rb +13 -8
  48. data/man/sup-add.1 +34 -55
  49. data/man/sup-config.1 +23 -36
  50. data/man/sup-dump.1 +25 -35
  51. data/man/sup-import-dump.1 +33 -54
  52. data/man/sup-psych-ify-config-files.1 +25 -34
  53. data/man/sup-recover-sources.1 +34 -49
  54. data/man/sup-sync-back-maildir.1 +39 -60
  55. data/man/sup-sync.1 +49 -79
  56. data/man/sup-tweak-labels.1 +35 -58
  57. data/man/sup.1 +50 -62
  58. data/sup.gemspec +12 -9
  59. data/test/dummy_source.rb +21 -15
  60. data/test/fixtures/embedded-message.eml +34 -0
  61. data/test/fixtures/mailing-list-header.eml +80 -0
  62. data/test/fixtures/non-ascii-header-in-nested-message.eml +36 -0
  63. data/test/fixtures/non-ascii-header.eml +8 -0
  64. data/test/fixtures/rfc2047-header-encoding.eml +15 -0
  65. data/test/fixtures/text-attachments-with-charset.eml +60 -0
  66. data/test/fixtures/utf8-header.eml +17 -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 +69 -18
  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_mbox.rb +1 -1
  77. data/test/integration/test_sup-add.rb +83 -0
  78. data/test/test_crypto.rb +46 -0
  79. data/test/test_header_parsing.rb +9 -1
  80. data/test/test_helper.rb +7 -4
  81. data/test/test_message.rb +188 -22
  82. data/test/test_messages_dir.rb +13 -15
  83. data/test/unit/test_horizontal_selector.rb +4 -4
  84. data/test/unit/test_locale_fiddler.rb +1 -1
  85. data/test/unit/util/test_query.rb +10 -4
  86. data/test/unit/util/test_string.rb +9 -3
  87. data/test/unit/util/test_uri.rb +2 -2
  88. metadata +93 -51
  89. data/.travis.yml +0 -13
  90. data/bin/sup-psych-ify-config-files +0 -21
  91. data/test/gnupg_test_home/key1.gen +0 -15
  92. data/test/gnupg_test_home/key2.gen +0 -15
  93. data/test/gnupg_test_home/key_ecc.gen +0 -13
  94. data/test/gnupg_test_home/private-keys-v1.d/719C7455A7169C6EE8819C6E91002E4F9DD00A65.key +0 -1
  95. data/test/gnupg_test_home/private-keys-v1.d/8A130806A754AA29D59487D76BD355040D9F26C0.key +0 -0
  96. data/test/gnupg_test_home/private-keys-v1.d/B7AA46B22BD8A6AD1B4F266C19A3B124A32DDD71.key +0 -0
  97. data/test/gnupg_test_home/private-keys-v1.d/FA64ACD7CC871371BDF57285A6CDF0E618827783.key +0 -0
  98. data/test/integration/test_label_service.rb +0 -18
  99. data/test/test_yaml_migration.rb +0 -85
data/lib/sup/message.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  require 'time'
4
+ require 'string-scrub' if /^2\.0\./ =~ RUBY_VERSION
4
5
 
5
6
  module Redwood
6
7
 
@@ -71,9 +72,9 @@ class Message
71
72
  return unless v
72
73
  return v unless v.is_a? String
73
74
  return unless v.size < MAX_HEADER_VALUE_SIZE # avoid regex blowup on spam
74
- d = v.dup
75
- d = d.transcode($encoding, 'ASCII')
76
- Rfc2047.decode_to $encoding, d
75
+ ## Header values should be either 7-bit with RFC2047-encoded words
76
+ ## or UTF-8 as per RFC6532. Replace any invalid high bytes with U+FFFD.
77
+ Rfc2047.decode_to $encoding, v.dup.force_encoding(Encoding::UTF_8).scrub
77
78
  end
78
79
 
79
80
  def parse_header encoded_header
@@ -104,7 +105,7 @@ class Message
104
105
  when String
105
106
  begin
106
107
  Time.parse date
107
- rescue ArgumentError => e
108
+ rescue ArgumentError
108
109
  #debug "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
109
110
  Time.now
110
111
  end
@@ -136,6 +137,11 @@ class Message
136
137
  header["list-post"] # just try the whole fucking thing
137
138
  end
138
139
  address && Person.from_address(address)
140
+ elsif header["mailing-list"]
141
+ address = if header["mailing-list"] =~ /list (.*?);/
142
+ $1
143
+ end
144
+ address && Person.from_address(address)
139
145
  elsif header["x-mailing-list"]
140
146
  Person.from_address header["x-mailing-list"]
141
147
  end
@@ -264,14 +270,14 @@ class Message
264
270
  parse_header rmsg.header
265
271
  message_to_chunks rmsg
266
272
  rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e
267
- warn "problem reading message #{id}"
273
+ warn_with_location "problem reading message #{id}"
268
274
  debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
269
275
 
270
276
  [Chunk::Text.new(error_message.split("\n"))]
271
277
 
272
278
  rescue Exception => e
273
279
 
274
- warn "problem reading message #{id}"
280
+ warn_with_location "problem reading message #{id}"
275
281
  debug "could not load message: #{location.inspect}, exception: #{e.inspect}"
276
282
 
277
283
  raise e
@@ -304,7 +310,7 @@ EOS
304
310
  end
305
311
 
306
312
  def each_raw_message_line &b
307
- location.each_raw_message_line &b
313
+ location.each_raw_message_line(&b)
308
314
  end
309
315
 
310
316
  def sync_back
@@ -404,19 +410,19 @@ private
404
410
 
405
411
  def multipart_signed_to_chunks m
406
412
  if m.body.size != 2
407
- warn "multipart/signed with #{m.body.size} parts (expecting 2)"
413
+ warn_with_location "multipart/signed with #{m.body.size} parts (expecting 2)"
408
414
  return
409
415
  end
410
416
 
411
417
  payload, signature = m.body
412
418
  if signature.multipart?
413
- warn "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
419
+ warn_with_location "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
414
420
  return
415
421
  end
416
422
 
417
423
  ## this probably will never happen
418
424
  if payload.header.content_type && payload.header.content_type.downcase == "application/pgp-signature"
419
- warn "multipart/signed with payload content type #{payload.header.content_type}"
425
+ warn_with_location "multipart/signed with payload content type #{payload.header.content_type}"
420
426
  return
421
427
  end
422
428
 
@@ -431,23 +437,23 @@ private
431
437
 
432
438
  def multipart_encrypted_to_chunks m
433
439
  if m.body.size != 2
434
- warn "multipart/encrypted with #{m.body.size} parts (expecting 2)"
440
+ warn_with_location "multipart/encrypted with #{m.body.size} parts (expecting 2)"
435
441
  return
436
442
  end
437
443
 
438
444
  control, payload = m.body
439
445
  if control.multipart?
440
- warn "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
446
+ warn_with_location "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
441
447
  return
442
448
  end
443
449
 
444
450
  if payload.header.content_type && payload.header.content_type.downcase != "application/octet-stream"
445
- warn "multipart/encrypted with payload content type #{payload.header.content_type}"
451
+ warn_with_location "multipart/encrypted with payload content type #{payload.header.content_type}"
446
452
  return
447
453
  end
448
454
 
449
455
  if control.header.content_type && control.header.content_type.downcase != "application/pgp-encrypted"
450
- warn "multipart/encrypted with control content type #{signature.header.content_type}"
456
+ warn_with_location "multipart/encrypted with control content type #{signature.header.content_type}"
451
457
  return
452
458
  end
453
459
 
@@ -691,7 +697,7 @@ private
691
697
  newstate = :quote
692
698
  elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE && !lines[(i+1)..-1].index { |l| l =~ /^-- $/ }
693
699
  newstate = :sig
694
- elsif line =~ BLOCK_QUOTE_PATTERN
700
+ elsif line =~ BLOCK_QUOTE_PATTERN && nextline !~ QUOTE_PATTERN
695
701
  newstate = :block_quote
696
702
  end
697
703
 
@@ -751,6 +757,11 @@ private
751
757
  end
752
758
  chunks
753
759
  end
760
+
761
+ def warn_with_location msg
762
+ warn msg
763
+ warn "Message is in #{location.source.uri} at #{location.info}"
764
+ end
754
765
  end
755
766
 
756
767
  class Location
@@ -128,7 +128,16 @@ EOS
128
128
 
129
129
  text = case @content_type
130
130
  when /^text\/plain\b/
131
- @raw_content
131
+ if /^UTF-7$/i =~ encoded_content.charset
132
+ @raw_content.decode_utf7
133
+ else
134
+ begin
135
+ charset = Encoding.find(encoded_content.charset || 'US-ASCII')
136
+ rescue ArgumentError
137
+ charset = 'US-ASCII'
138
+ end
139
+ @raw_content.force_encoding(charset)
140
+ end
132
141
  else
133
142
  HookManager.run "mime-decode", :content_type => @content_type,
134
143
  :filename => lambda { write_to_disk },
@@ -138,7 +147,7 @@ EOS
138
147
 
139
148
  @lines = nil
140
149
  if text
141
- text = text.transcode(encoded_content.charset || $encoding, text.encoding)
150
+ text = text.encode($encoding, :invalid => :replace, :undef => :replace)
142
151
  begin
143
152
  @lines = text.gsub("\r\n", "\n").gsub(/\t/, " ").gsub(/\r/, "").split("\n")
144
153
  rescue Encoding::CompatibilityError
@@ -160,6 +169,7 @@ EOS
160
169
  end
161
170
  end
162
171
  def safe_filename; Shellwords.escape(@filename).gsub("/", "_") end
172
+ def filesafe_filename; @filename.gsub("/", "_") end
163
173
 
164
174
  ## an attachment is exapndable if we've managed to decode it into
165
175
  ## something we can display inline. otherwise, it's viewable.
@@ -273,24 +283,19 @@ EOS
273
283
  class EnclosedMessage
274
284
  attr_reader :lines
275
285
  def initialize from, to, cc, date, subj
276
- @from = from ? "unknown sender" : from.full_address
277
- @to = to ? "" : to.map { |p| p.full_address }.join(", ")
278
- @cc = cc ? "" : cc.map { |p| p.full_address }.join(", ")
279
- if date
280
- @date = date.rfc822
281
- else
282
- @date = ""
283
- end
284
-
286
+ @from = !from ? "unknown sender" : from.full_address
287
+ @to = !to ? "" : to.map { |p| p.full_address }.join(", ")
288
+ @cc = !cc ? "" : cc.map { |p| p.full_address }.join(", ")
289
+ @date = !date ? "" : date.rfc822
285
290
  @subj = subj
286
-
287
- @lines = "\nFrom: #{from}\n"
288
- @lines += "To: #{to}\n"
289
- if !cc.empty?
290
- @lines += "Cc: #{cc}\n"
291
- end
292
- @lines += "Date: #{date}\n"
293
- @lines += "Subject: #{subj}\n\n"
291
+ @lines = [
292
+ "From: #{@from}",
293
+ "To: #{@to}",
294
+ "Cc: #{@cc}",
295
+ "Date: #{@date}",
296
+ "Subject: #{@subj}"
297
+ ]
298
+ @lines.delete_if{ |line| line == 'Cc: ' }
294
299
  end
295
300
 
296
301
  def inlineable?; false end
@@ -301,7 +306,11 @@ EOS
301
306
  def viewable?; false end
302
307
 
303
308
  def patina_color; :generic_notice_patina_color end
304
- def patina_text; "Begin enclosed message sent on #{@date}" end
309
+ def patina_text
310
+ "Begin enclosed message" + (
311
+ @date == "" ? "" : " sent on #{@date}"
312
+ )
313
+ end
305
314
 
306
315
  def color; :quote_color end
307
316
  end
data/lib/sup/mode.rb CHANGED
@@ -83,6 +83,7 @@ EOS
83
83
  ### helper functions
84
84
 
85
85
  def save_to_file fn, talk=true
86
+ FileUtils.mkdir_p File.dirname(fn)
86
87
  if File.exist? fn
87
88
  unless BufferManager.ask_yes_or_no "File \"#{fn}\" exists. Overwrite?"
88
89
  info "Not overwriting #{fn}"
@@ -26,7 +26,6 @@ class CompletionMode < ScrollMode
26
26
  private
27
27
 
28
28
  def update_lines
29
- width = buffer.content_width
30
29
  max_length = @list.max_of { |s| s.length }
31
30
  num_per = [1, buffer.content_width / (max_length + INTERSTITIAL.length)].max
32
31
  @lines = [@header].compact
@@ -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
@@ -43,7 +43,7 @@ protected
43
43
  end
44
44
 
45
45
  def view
46
- name, f = @files[curpos - RESERVED_ROWS]
46
+ _name, f = @files[curpos - RESERVED_ROWS]
47
47
  return unless f && f.file?
48
48
 
49
49
  begin
@@ -54,7 +54,7 @@ protected
54
54
  end
55
55
 
56
56
  def select_file_or_follow_directory
57
- name, f = @files[curpos - RESERVED_ROWS]
57
+ _name, f = @files[curpos - RESERVED_ROWS]
58
58
  return unless f
59
59
 
60
60
  if f.directory? && f.to_s != "."
@@ -129,7 +129,7 @@ protected
129
129
  end
130
130
 
131
131
  def select_label
132
- label, num_unread = @labels[curpos]
132
+ label, _num_unread = @labels[curpos]
133
133
  return unless label
134
134
  LabelSearchResultsMode.spawn_nicely label
135
135
  end
@@ -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
@@ -131,13 +131,13 @@ protected
131
131
  end
132
132
 
133
133
  def select_search
134
- name, num_unread = @searches[curpos]
134
+ name, _num_unread = @searches[curpos]
135
135
  return unless name
136
136
  SearchResultsMode.spawn_from_query SearchManager.search_string_for(name)
137
137
  end
138
138
 
139
139
  def delete_selected_search
140
- name, num_unread = @searches[curpos]
140
+ name, _num_unread = @searches[curpos]
141
141
  return unless name
142
142
  reload if SearchManager.delete name
143
143
  end
@@ -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
@@ -10,8 +10,6 @@ class ThreadViewMode < LineCursorMode
10
10
  attr_accessor :state
11
11
  end
12
12
 
13
- INDENT_SPACES = 2 # how many spaces to indent child messages
14
-
15
13
  HookManager.register "detailed-headers", <<EOS
16
14
  Add or remove headers from the detailed header display of a message.
17
15
  Variables:
@@ -54,6 +52,7 @@ EOS
54
52
  k.add :toggle_detailed_header, "Toggle detailed header", 'h'
55
53
  k.add :show_header, "Show full message header", 'H'
56
54
  k.add :show_message, "Show full message (raw form)", 'V'
55
+ k.add :reload, "Update message in thread", '@'
57
56
  k.add :activate_chunk, "Expand/collapse or activate item", :enter
58
57
  k.add :expand_all_messages, "Expand/collapse all messages", 'E'
59
58
  k.add :edit_draft, "Edit draft", 'e'
@@ -127,6 +126,7 @@ EOS
127
126
  ## objects. @person_lines is a map from row #s to Person objects.
128
127
 
129
128
  def initialize thread, hidden_labels=[], index_mode=nil
129
+ @indent_spaces = $config[:indent_spaces]
130
130
  super :slip_rows => $config[:slip_rows]
131
131
  @thread = thread
132
132
  @hidden_labels = hidden_labels
@@ -205,6 +205,10 @@ EOS
205
205
  @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
206
206
  update
207
207
  end
208
+
209
+ def reload
210
+ update
211
+ end
208
212
 
209
213
  def reply type_arg=nil
210
214
  m = @message_lines[curpos] or return
@@ -237,7 +241,7 @@ EOS
237
241
 
238
242
  begin
239
243
  u = URI.parse($1)
240
- rescue URI::InvalidURIError => e
244
+ rescue URI::InvalidURIError
241
245
  BufferManager.flash("Invalid unsubscribe link")
242
246
  return
243
247
  end
@@ -389,7 +393,7 @@ EOS
389
393
  when Chunk::Attachment
390
394
  default_dir = $config[:default_attachment_save_dir]
391
395
  default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
392
- default_fn = File.expand_path File.join(default_dir, chunk.safe_filename)
396
+ default_fn = File.expand_path File.join(default_dir, chunk.filesafe_filename)
393
397
  fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
394
398
 
395
399
  # if user selects directory use file name from message
@@ -418,7 +422,7 @@ EOS
418
422
  num_errors = 0
419
423
  m.chunks.each do |chunk|
420
424
  next unless chunk.is_a?(Chunk::Attachment)
421
- fn = File.join(folder, chunk.safe_filename)
425
+ fn = File.join(folder, chunk.filesafe_filename)
422
426
  num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
423
427
  num += 1
424
428
  end
@@ -561,7 +565,7 @@ EOS
561
565
  l = @layout[m]
562
566
 
563
567
  ## boundaries of the message
564
- message_left = l.depth * INDENT_SPACES
568
+ message_left = l.depth * @indent_spaces
565
569
  message_right = message_left + l.width
566
570
 
567
571
  ## calculate leftmost colum
@@ -769,14 +773,13 @@ EOS
769
773
  # ]
770
774
 
771
775
  linetext = @text.slice(curpos, @text.length).flatten(1)
772
- .take_while{|d| d[0] == :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
773
777
  .map{|d| d[1].strip}.join("").strip
774
778
 
775
779
  found = false
776
- (linetext || "").scan(URI::regexp).each do |matches|
780
+ URI.extract(linetext || "").each do |match|
777
781
  begin
778
- link = $& # ruby magic: $& is the whole regexp match
779
- u = URI.parse(link)
782
+ u = URI.parse(match)
780
783
  next unless u.absolute?
781
784
  next unless ["http", "https"].include?(u.scheme)
782
785
 
@@ -865,7 +868,6 @@ private
865
868
  (0 ... text.length).each do |i|
866
869
  @chunk_lines[@text.length + i] = m
867
870
  @message_lines[@text.length + i] = m
868
- lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum
869
871
  end
870
872
 
871
873
  @text += text
@@ -886,7 +888,7 @@ private
886
888
  (0 ... text.length).each do |i|
887
889
  @chunk_lines[@text.length + i] = c
888
890
  @message_lines[@text.length + i] = m
889
- lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * INDENT_SPACES)
891
+ lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * @indent_spaces)
890
892
  l.width = lw if lw > l.width
891
893
  end
892
894
  @text += text
@@ -991,7 +993,7 @@ private
991
993
 
992
994
  ## todo: check arguments on this overly complex function
993
995
  def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
994
- prefix = " " * INDENT_SPACES * depth
996
+ prefix = " " * @indent_spaces * depth
995
997
  case chunk
996
998
  when :fake_root
997
999
  [[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]
data/lib/sup/rfc2047.rb CHANGED
@@ -17,7 +17,7 @@
17
17
  # This file is distributed under the same terms as Ruby.
18
18
 
19
19
  module Rfc2047
20
- WORD = %r{=\?([!\#$%&'*+-/0-9A-Z\\^\`a-z{|}~]+)\?([BbQq])\?([!->@-~]+)\?=} # :nodoc: 'stupid ruby-mode
20
+ WORD = %r{=\?([!\#$%&'*+-/0-9A-Z\\^\`a-z{|}~]+)\?([BbQq])\?([!->@-~ ]+)\?=} # :nodoc: 'stupid ruby-mode
21
21
  WORDSEQ = %r{(#{WORD.source})\s+(?=#{WORD.source})}
22
22
 
23
23
  def Rfc2047.is_encoded? s; s =~ WORD end
@@ -28,16 +28,17 @@ module Rfc2047
28
28
  # converted to the target encoding, it is left in its encoded form.
29
29
  def Rfc2047.decode_to(target, from)
30
30
  from = from.gsub(WORDSEQ, '\1')
31
- out = from.gsub(WORD) do
31
+ from.gsub(WORD) do
32
32
  |word|
33
33
  charset, encoding, text = $1, $2, $3
34
34
 
35
35
  # B64 or QP decode, as necessary:
36
36
  case encoding
37
37
  when 'b', 'B'
38
- #puts text
39
- text = text.unpack('m*')[0]
40
- #puts text.dump
38
+ ## Padding is optional in RFC 2047 words. Add some extra padding
39
+ ## before decoding the base64, otherwise on Ruby 2.0 the final byte
40
+ ## might be discarded.
41
+ text = (text + '===').unpack('m*')[0]
41
42
 
42
43
  when 'q', 'Q'
43
44
  # RFC 2047 has a variant of quoted printable where a ' ' character
@@ -50,7 +51,21 @@ module Rfc2047
50
51
  # WORD.
51
52
  end
52
53
 
53
- text.transcode(target, charset)
54
+ # Handle UTF-7 specially because Ruby doesn't actually support it as
55
+ # a normal character encoding.
56
+ if charset == 'UTF-7'
57
+ begin
58
+ next text.decode_utf7.encode(target)
59
+ rescue ArgumentError, EncodingError
60
+ next word
61
+ end
62
+ end
63
+
64
+ begin
65
+ text.force_encoding(charset).encode(target)
66
+ rescue ArgumentError, EncodingError
67
+ word
68
+ end
54
69
  end
55
70
  end
56
71
  end
data/lib/sup/source.rb CHANGED
@@ -58,7 +58,7 @@ class Source
58
58
  attr_accessor :id
59
59
 
60
60
  def initialize uri, usual=true, archived=false, id=nil
61
- raise ArgumentError, "id must be an integer: #{id.inspect}" unless id.is_a? Fixnum if id
61
+ raise ArgumentError, "id must be an integer: #{id.inspect}" unless id.is_a? Integer if id
62
62
 
63
63
  @uri = uri
64
64
  @usual = usual
@@ -102,7 +102,7 @@ class Source
102
102
  end
103
103
 
104
104
  def synchronize &block
105
- @poll_lock.synchronize &block
105
+ @poll_lock.synchronize(&block)
106
106
  end
107
107
 
108
108
  def try_lock
@@ -153,7 +153,7 @@ class Source
153
153
  next unless Rfc2047.is_encoded? v
154
154
  header[k] = begin
155
155
  Rfc2047.decode_to $encoding, v
156
- rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
156
+ rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence
157
157
  #debug "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
158
158
  v
159
159
  end
@@ -169,6 +169,12 @@ protected
169
169
  def Source.expand_filesystem_uri uri
170
170
  uri.gsub "~", File.expand_path("~")
171
171
  end
172
+
173
+ def Source.encode_path_for_uri path
174
+ path.gsub(Regexp.new("[#{Regexp.quote(URI_ENCODE_CHARS)}]")) { |c|
175
+ c.each_byte.map { |x| sprintf("%%%02X", x) }.join
176
+ }
177
+ end
172
178
  end
173
179
 
174
180
  ## if you have a @labels instance variable, include this
data/lib/sup/textfield.rb CHANGED
@@ -119,7 +119,6 @@ class TextField
119
119
  Ncurses::Form::REQ_END_FIELD
120
120
  when Ncurses::KEY_UP, Ncurses::KEY_DOWN
121
121
  unless !@i || @history.empty?
122
- value = get_cursed_value
123
122
  #debug "history before #{@history.inspect}"
124
123
  @i = @i + (c.is_keycode?(Ncurses::KEY_UP) ? -1 : 1)
125
124
  @i = 0 if @i < 0
data/lib/sup/thread.rb CHANGED
@@ -407,7 +407,6 @@ class ThreadSet
407
407
  #puts "adding: #{message.id}, refs #{message.refs.inspect}"
408
408
 
409
409
  el.message = message
410
- oldroot = el.root
411
410
 
412
411
  ## link via references:
413
412
  (message.refs + [el.id]).inject(nil) do |prev, ref_id|
@@ -0,0 +1,17 @@
1
+ require 'highline'
2
+ @cli = HighLine.new
3
+
4
+ def axe q, default=nil
5
+ question = if default && !default.empty?
6
+ "#{q} (enter for \"#{default}\"): "
7
+ else
8
+ "#{q}: "
9
+ end
10
+ ans = @cli.ask question
11
+ ans.empty? ? default : ans.to_s
12
+ end
13
+
14
+ def axe_yes q, default="n"
15
+ axe(q, default) =~ /^y|yes$/i
16
+ end
17
+
@@ -76,7 +76,7 @@ module Ncurses
76
76
  @status = status
77
77
  c = "" if c.nil?
78
78
  return super("") if status == Ncurses::ERR
79
- c = enc_char(c) if c.is_a?(Fixnum)
79
+ c = enc_char(c) if c.is_a?(Integer)
80
80
  super c.length > 1 ? c[0,1] : c
81
81
  end
82
82
 
@@ -89,7 +89,7 @@ module Ncurses
89
89
  else
90
90
  @status = Ncurses::OK
91
91
  c = "" if c.nil?
92
- c = enc_char(c) if c.is_a?(Fixnum)
92
+ c = enc_char(c) if c.is_a?(Integer)
93
93
  super c.length > 1 ? c[0,1] : c
94
94
  end
95
95
  end
@@ -260,7 +260,7 @@ module Ncurses
260
260
  ## Ncurses::Form.form_driver_w wrapper for printable characters.
261
261
  def form_driver_char c
262
262
  form_driver CharCode.character(c)
263
- #c.is_a?(Fixnum) ? c : c.ord
263
+ #c.is_a?(Integer) ? c : c.ord
264
264
  end
265
265
 
266
266
  ## Ncurses::Form.form_driver_w wrapper for charcodes.