sup 0.22.1 → 1.1

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 (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.