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