sup 0.22.1 → 1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/checks.yml +70 -0
- data/.gitignore +1 -3
- data/.rubocop.yml +5 -0
- data/CONTRIBUTORS +14 -5
- data/Gemfile +6 -1
- data/History.txt +76 -0
- data/Manifest.txt +149 -0
- data/README.md +32 -5
- data/Rakefile +40 -1
- data/bin/sup +7 -5
- data/bin/sup-add +16 -20
- data/bin/sup-config +30 -44
- data/bin/sup-dump +2 -2
- data/bin/sup-import-dump +4 -4
- data/bin/sup-sync +3 -3
- data/bin/sup-sync-back-maildir +2 -2
- data/bin/sup-tweak-labels +5 -5
- data/ext/mkrf_conf_xapian.rb +10 -4
- data/lib/sup/colormap.rb +1 -1
- data/lib/sup/crypto.rb +17 -8
- data/lib/sup/hook.rb +9 -9
- data/lib/sup/index.rb +20 -7
- data/lib/sup/keymap.rb +1 -1
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +4 -4
- data/lib/sup/mbox.rb +4 -4
- data/lib/sup/message.rb +26 -15
- data/lib/sup/message_chunks.rb +29 -20
- data/lib/sup/mode.rb +1 -0
- data/lib/sup/modes/completion_mode.rb +0 -1
- data/lib/sup/modes/contact_list_mode.rb +1 -0
- data/lib/sup/modes/file_browser_mode.rb +2 -2
- data/lib/sup/modes/label_list_mode.rb +1 -1
- data/lib/sup/modes/reply_mode.rb +3 -1
- data/lib/sup/modes/search_list_mode.rb +2 -2
- data/lib/sup/modes/thread_index_mode.rb +1 -1
- data/lib/sup/modes/thread_view_mode.rb +15 -13
- data/lib/sup/rfc2047.rb +21 -6
- data/lib/sup/source.rb +9 -3
- data/lib/sup/textfield.rb +0 -1
- data/lib/sup/thread.rb +0 -1
- data/lib/sup/util/axe.rb +17 -0
- data/lib/sup/util/ncurses.rb +3 -3
- data/lib/sup/util.rb +42 -67
- data/lib/sup/version.rb +10 -1
- data/lib/sup.rb +13 -8
- data/man/sup-add.1 +34 -55
- data/man/sup-config.1 +23 -36
- data/man/sup-dump.1 +25 -35
- data/man/sup-import-dump.1 +33 -54
- data/man/sup-psych-ify-config-files.1 +25 -34
- data/man/sup-recover-sources.1 +34 -49
- data/man/sup-sync-back-maildir.1 +39 -60
- data/man/sup-sync.1 +49 -79
- data/man/sup-tweak-labels.1 +35 -58
- data/man/sup.1 +50 -62
- data/sup.gemspec +12 -9
- data/test/dummy_source.rb +21 -15
- data/test/fixtures/embedded-message.eml +34 -0
- data/test/fixtures/mailing-list-header.eml +80 -0
- data/test/fixtures/non-ascii-header-in-nested-message.eml +36 -0
- data/test/fixtures/non-ascii-header.eml +8 -0
- data/test/fixtures/rfc2047-header-encoding.eml +15 -0
- data/test/fixtures/text-attachments-with-charset.eml +60 -0
- data/test/fixtures/utf8-header.eml +17 -0
- data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
- data/test/gnupg_test_home/gpg.conf +2 -1
- data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/regen_keys.sh +69 -18
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -22
- data/test/integration/test_mbox.rb +1 -1
- data/test/integration/test_sup-add.rb +83 -0
- data/test/test_crypto.rb +46 -0
- data/test/test_header_parsing.rb +9 -1
- data/test/test_helper.rb +7 -4
- data/test/test_message.rb +188 -22
- data/test/test_messages_dir.rb +13 -15
- data/test/unit/test_horizontal_selector.rb +4 -4
- data/test/unit/test_locale_fiddler.rb +1 -1
- data/test/unit/util/test_query.rb +10 -4
- data/test/unit/util/test_string.rb +9 -3
- data/test/unit/util/test_uri.rb +2 -2
- metadata +93 -51
- data/.travis.yml +0 -13
- data/bin/sup-psych-ify-config-files +0 -21
- data/test/gnupg_test_home/key1.gen +0 -15
- data/test/gnupg_test_home/key2.gen +0 -15
- data/test/gnupg_test_home/key_ecc.gen +0 -13
- data/test/gnupg_test_home/private-keys-v1.d/719C7455A7169C6EE8819C6E91002E4F9DD00A65.key +0 -1
- data/test/gnupg_test_home/private-keys-v1.d/8A130806A754AA29D59487D76BD355040D9F26C0.key +0 -0
- data/test/gnupg_test_home/private-keys-v1.d/B7AA46B22BD8A6AD1B4F266C19A3B124A32DDD71.key +0 -0
- data/test/gnupg_test_home/private-keys-v1.d/FA64ACD7CC871371BDF57285A6CDF0E618827783.key +0 -0
- data/test/integration/test_label_service.rb +0 -18
- 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
|
-
|
75
|
-
|
76
|
-
Rfc2047.decode_to $encoding,
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/sup/message_chunks.rb
CHANGED
@@ -128,7 +128,16 @@ EOS
|
|
128
128
|
|
129
129
|
text = case @content_type
|
130
130
|
when /^text\/plain\b/
|
131
|
-
|
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.
|
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
|
-
|
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
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
@lines
|
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
|
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
@@ -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
|
-
|
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
|
-
|
57
|
+
_name, f = @files[curpos - RESERVED_ROWS]
|
58
58
|
return unless f
|
59
59
|
|
60
60
|
if f.directory? && f.to_s != "."
|
data/lib/sup/modes/reply_mode.rb
CHANGED
@@ -36,6 +36,8 @@ Variables:
|
|
36
36
|
[:#{REPLY_TYPES * ', :'}]
|
37
37
|
The default behavior is equivalent to
|
38
38
|
([:list, :sender, :recipent] & modes)[0]
|
39
|
+
message: a message object representing the message being replied to
|
40
|
+
(useful values include message.is_list_message? and message.list_address)
|
39
41
|
Return value:
|
40
42
|
The reply mode you desire, or nil to use the default behavior.
|
41
43
|
EOS
|
@@ -130,7 +132,7 @@ EOS
|
|
130
132
|
types = REPLY_TYPES.select { |t| @headers.member?(t) }
|
131
133
|
@type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
|
132
134
|
|
133
|
-
hook_reply = HookManager.run "reply-to", :modes => types
|
135
|
+
hook_reply = HookManager.run "reply-to", :modes => types, :message => @m
|
134
136
|
|
135
137
|
@type_selector.set_to(
|
136
138
|
if types.include? type_arg
|
@@ -131,13 +131,13 @@ protected
|
|
131
131
|
end
|
132
132
|
|
133
133
|
def select_search
|
134
|
-
name,
|
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,
|
140
|
+
name, _num_unread = @searches[curpos]
|
141
141
|
return unless name
|
142
142
|
reload if SearchManager.delete name
|
143
143
|
end
|
@@ -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
|
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.
|
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.
|
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 *
|
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]
|
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 || "").
|
780
|
+
URI.extract(linetext || "").each do |match|
|
777
781
|
begin
|
778
|
-
|
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 *
|
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 = " " *
|
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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?
|
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
|
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
|
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
data/lib/sup/util/axe.rb
ADDED
@@ -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
|
+
|
data/lib/sup/util/ncurses.rb
CHANGED
@@ -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?(
|
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?(
|
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?(
|
263
|
+
#c.is_a?(Integer) ? c : c.ord
|
264
264
|
end
|
265
265
|
|
266
266
|
## Ncurses::Form.form_driver_w wrapper for charcodes.
|