sup 0.20.0 → 1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +2 -1
- data/.travis.yml +11 -6
- data/CONTRIBUTORS +27 -15
- data/Gemfile +2 -1
- data/History.txt +84 -0
- data/README.md +26 -5
- data/Rakefile +0 -1
- data/ReleaseNotes +7 -0
- data/bin/sup +17 -30
- data/bin/sup-add +15 -16
- data/bin/sup-config +30 -45
- data/bin/sup-dump +2 -3
- data/bin/sup-import-dump +5 -6
- data/bin/sup-sync +3 -4
- data/bin/sup-sync-back-maildir +3 -4
- data/bin/sup-tweak-labels +6 -7
- data/contrib/colorpicker.rb +0 -2
- data/contrib/completion/_sup.bash +102 -0
- data/devel/profile.rb +0 -1
- data/ext/mkrf_conf_xapian.rb +1 -1
- data/lib/sup.rb +8 -8
- data/lib/sup/colormap.rb +5 -2
- data/lib/sup/contact.rb +4 -2
- data/lib/sup/crypto.rb +58 -16
- data/lib/sup/draft.rb +8 -8
- data/lib/sup/hook.rb +9 -9
- data/lib/sup/index.rb +20 -7
- data/lib/sup/label.rb +1 -1
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +2 -2
- data/lib/sup/mbox.rb +2 -2
- data/lib/sup/message.rb +26 -10
- data/lib/sup/message_chunks.rb +7 -4
- data/lib/sup/mode.rb +34 -28
- data/lib/sup/modes/contact_list_mode.rb +1 -0
- data/lib/sup/modes/edit_message_mode.rb +1 -1
- data/lib/sup/modes/forward_mode.rb +22 -3
- data/lib/sup/modes/line_cursor_mode.rb +1 -1
- data/lib/sup/modes/reply_mode.rb +3 -1
- data/lib/sup/modes/text_mode.rb +6 -1
- data/lib/sup/modes/thread_index_mode.rb +6 -2
- data/lib/sup/modes/thread_view_mode.rb +63 -18
- data/lib/sup/person.rb +68 -61
- data/lib/sup/search.rb +1 -1
- data/lib/sup/sent.rb +1 -1
- data/lib/sup/source.rb +1 -1
- data/lib/sup/util.rb +15 -94
- data/lib/sup/util/axe.rb +17 -0
- data/lib/sup/util/locale_fiddler.rb +24 -0
- data/lib/sup/util/ncurses.rb +3 -3
- data/lib/sup/version.rb +10 -1
- data/sup.gemspec +12 -10
- data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
- data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
- data/test/fixtures/blank-header-fields.eml +71 -0
- data/test/fixtures/contacts.txt +1 -0
- data/test/fixtures/mailing-list-header.eml +80 -0
- data/test/fixtures/malicious-attachment-names.eml +55 -0
- data/test/fixtures/missing-from-to.eml +18 -0
- data/test/{messages → fixtures}/missing-line.eml +0 -0
- data/test/fixtures/multi-part-2.eml +72 -0
- data/test/fixtures/multi-part.eml +61 -0
- data/test/fixtures/no-body.eml +18 -0
- data/test/fixtures/simple-message.eml +29 -0
- data/test/fixtures/text-attachments-with-charset.eml +46 -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 +70 -16
- 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_maildir.rb +1 -1
- data/test/integration/test_mbox.rb +1 -1
- data/test/test_crypto.rb +14 -2
- data/test/test_header_parsing.rb +1 -1
- data/test/test_helper.rb +6 -3
- data/test/test_message.rb +115 -341
- data/test/test_messages_dir.rb +4 -28
- data/test/test_yaml_regressions.rb +1 -1
- data/test/unit/test_contact.rb +33 -0
- data/test/unit/test_locale_fiddler.rb +15 -0
- data/test/unit/test_person.rb +37 -0
- data/test/unit/util/test_query.rb +10 -4
- data/test/unit/util/test_string.rb +6 -0
- metadata +107 -43
- data/test/gnupg_test_home/key1.gen +0 -15
- data/test/gnupg_test_home/key2.gen +0 -15
data/lib/sup/person.rb
CHANGED
@@ -18,11 +18,16 @@ class Person
|
|
18
18
|
@email = email.strip.gsub(/\s+/, " ")
|
19
19
|
end
|
20
20
|
|
21
|
-
def to_s
|
21
|
+
def to_s
|
22
|
+
if @name
|
23
|
+
"#@name <#@email>"
|
24
|
+
else
|
25
|
+
@email
|
26
|
+
end
|
27
|
+
end
|
22
28
|
|
23
29
|
# def == o; o && o.email == email; end
|
24
30
|
# alias :eql? :==
|
25
|
-
# def hash; [name, email].hash; end
|
26
31
|
|
27
32
|
def shortname
|
28
33
|
case @name
|
@@ -37,26 +42,10 @@ class Person
|
|
37
42
|
end
|
38
43
|
end
|
39
44
|
|
40
|
-
def longname
|
41
|
-
if @name && @email
|
42
|
-
"#@name <#@email>"
|
43
|
-
else
|
44
|
-
@email
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
45
|
def mediumname; @name || @email; end
|
49
46
|
|
50
|
-
def
|
51
|
-
|
52
|
-
if name =~ /[",@]/
|
53
|
-
"#{name.inspect} <#{email}>" # escape quotes
|
54
|
-
else
|
55
|
-
"#{name} <#{email}>"
|
56
|
-
end
|
57
|
-
else
|
58
|
-
email
|
59
|
-
end
|
47
|
+
def longname
|
48
|
+
to_s
|
60
49
|
end
|
61
50
|
|
62
51
|
def full_address
|
@@ -79,56 +68,74 @@ class Person
|
|
79
68
|
end.downcase
|
80
69
|
end
|
81
70
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
71
|
+
def eql? o; email.eql? o.email end
|
72
|
+
def hash; email.hash end
|
73
|
+
|
74
|
+
|
75
|
+
## see comments in self.from_address
|
76
|
+
def indexable_content
|
77
|
+
[name, email, email.split(/@/).first].join(" ")
|
86
78
|
end
|
87
79
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
## separating those three bits, and no <>'s. this is the output of
|
98
|
-
## #indexable_content. here, we reverse-engineer that format to extract
|
99
|
-
## a valid address.
|
100
|
-
##
|
101
|
-
## we store things this way to allow searches on a to/from/etc field to
|
102
|
-
## match any of those parts. a more robust solution would be to store a
|
103
|
-
## separate, non-indexed field with the proper headers. but this way we
|
104
|
-
## save precious bits, and it's backwards-compatible with older indexes.
|
105
|
-
[$1, $2]
|
106
|
-
when /["'](.*?)["'] <(.*?)>/, /([^,]+) <(.*?)>/
|
107
|
-
a, b = $1, $2
|
108
|
-
[a.gsub('\"', '"'), b]
|
109
|
-
when /<((\S+?)@\S+?)>/
|
110
|
-
[$2, $1]
|
111
|
-
when /((\S+?)@\S+)/
|
112
|
-
[$2, $1]
|
80
|
+
class << self
|
81
|
+
|
82
|
+
def full_address name, email
|
83
|
+
if name && email
|
84
|
+
if name =~ /[",@]/
|
85
|
+
"#{name.inspect} <#{email}>" # escape quotes
|
86
|
+
else
|
87
|
+
"#{name} <#{email}>"
|
88
|
+
end
|
113
89
|
else
|
114
|
-
|
90
|
+
email
|
115
91
|
end
|
92
|
+
end
|
116
93
|
|
117
|
-
|
118
|
-
|
94
|
+
## return "canonical" person using contact manager or create one if
|
95
|
+
## not found or contact manager not available
|
96
|
+
def from_name_and_email name, email
|
97
|
+
ContactManager.instantiated? && ContactManager.person_for(email) || Person.new(name, email)
|
98
|
+
end
|
119
99
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
100
|
+
def from_address s
|
101
|
+
return nil if s.nil?
|
102
|
+
|
103
|
+
## try and parse an email address and name
|
104
|
+
name, email = case s
|
105
|
+
when /(.+?) ((\S+?)@\S+) \3/
|
106
|
+
## ok, this first match cause is insane, but bear with me. email
|
107
|
+
## addresses are stored in the to/from/etc fields of the index in a
|
108
|
+
## weird format: "name address first-part-of-address", i.e. spaces
|
109
|
+
## separating those three bits, and no <>'s. this is the output of
|
110
|
+
## #indexable_content. here, we reverse-engineer that format to extract
|
111
|
+
## a valid address.
|
112
|
+
##
|
113
|
+
## we store things this way to allow searches on a to/from/etc field to
|
114
|
+
## match any of those parts. a more robust solution would be to store a
|
115
|
+
## separate, non-indexed field with the proper headers. but this way we
|
116
|
+
## save precious bits, and it's backwards-compatible with older indexes.
|
117
|
+
[$1, $2]
|
118
|
+
when /["'](.*?)["'] <(.*?)>/, /([^,]+) <(.*?)>/
|
119
|
+
a, b = $1, $2
|
120
|
+
[a.gsub('\"', '"'), b]
|
121
|
+
when /<((\S+?)@\S+?)>/
|
122
|
+
[$2, $1]
|
123
|
+
when /((\S+?)@\S+)/
|
124
|
+
[$2, $1]
|
125
|
+
else
|
126
|
+
[nil, s]
|
127
|
+
end
|
128
|
+
|
129
|
+
from_name_and_email name, email
|
130
|
+
end
|
131
|
+
|
132
|
+
def from_address_list ss
|
133
|
+
return [] if ss.nil?
|
134
|
+
ss.dup.split_on_commas.map { |s| self.from_address s }
|
135
|
+
end
|
124
136
|
|
125
|
-
## see comments in self.from_address
|
126
|
-
def indexable_content
|
127
|
-
[name, email, email.split(/@/).first].join(" ")
|
128
137
|
end
|
129
138
|
|
130
|
-
def eql? o; email.eql? o.email end
|
131
|
-
def hash; email.hash end
|
132
139
|
end
|
133
140
|
|
134
141
|
end
|
data/lib/sup/search.rb
CHANGED
data/lib/sup/sent.rb
CHANGED
@@ -40,7 +40,7 @@ class SentLoader < MBox
|
|
40
40
|
|
41
41
|
def initialize
|
42
42
|
@filename = Redwood::SENT_FN
|
43
|
-
File.open(@filename, "w") { } unless File.
|
43
|
+
File.open(@filename, "w") { } unless File.exist? @filename
|
44
44
|
super "mbox://" + @filename, true, $config[:archive_sent]
|
45
45
|
end
|
46
46
|
|
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
|
data/lib/sup/util.rb
CHANGED
@@ -8,17 +8,9 @@ require 'set'
|
|
8
8
|
require 'enumerator'
|
9
9
|
require 'benchmark'
|
10
10
|
require 'unicode'
|
11
|
+
require 'unicode/display_width'
|
11
12
|
require 'fileutils'
|
12
13
|
|
13
|
-
## time for some monkeypatching!
|
14
|
-
class Symbol
|
15
|
-
unless method_defined? :to_proc
|
16
|
-
def to_proc
|
17
|
-
proc { |obj, *args| obj.send(self, *args) }
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
14
|
class Lockfile
|
23
15
|
def gen_lock_id
|
24
16
|
Hash[
|
@@ -89,7 +81,7 @@ module RMail
|
|
89
81
|
end
|
90
82
|
|
91
83
|
def charset
|
92
|
-
if header.field?("content-type") && header.fetch("content-type") =~ /charset
|
84
|
+
if header.field?("content-type") && header.fetch("content-type") =~ /charset\s*=\s*"?(.*?)"?(;|$)/i
|
93
85
|
$1
|
94
86
|
end
|
95
87
|
end
|
@@ -170,13 +162,6 @@ module RMail
|
|
170
162
|
end
|
171
163
|
end
|
172
164
|
|
173
|
-
class Range
|
174
|
-
## only valid for integer ranges (unless I guess it's exclusive)
|
175
|
-
def size
|
176
|
-
last - first + (exclude_end? ? 0 : 1)
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
165
|
class Module
|
181
166
|
def bool_reader *args
|
182
167
|
args.each { |sym| class_eval %{ def #{sym}?; @#{sym}; end } }
|
@@ -198,17 +183,6 @@ class Module
|
|
198
183
|
end
|
199
184
|
|
200
185
|
class Object
|
201
|
-
def ancestors
|
202
|
-
ret = []
|
203
|
-
klass = self.class
|
204
|
-
|
205
|
-
until klass == Object
|
206
|
-
ret << klass
|
207
|
-
klass = klass.superclass
|
208
|
-
end
|
209
|
-
ret
|
210
|
-
end
|
211
|
-
|
212
186
|
## "k combinator"
|
213
187
|
def returning x; yield x; x; end
|
214
188
|
|
@@ -266,21 +240,14 @@ end
|
|
266
240
|
|
267
241
|
class String
|
268
242
|
def display_length
|
269
|
-
@display_length ||= Unicode.
|
270
|
-
|
271
|
-
# if Unicode.width fails and returns -1, fall back to
|
272
|
-
# regular String#length, see pull-request: #256.
|
273
|
-
if @display_length < 0
|
274
|
-
@display_length = self.length
|
275
|
-
end
|
276
|
-
|
277
|
-
@display_length
|
243
|
+
@display_length ||= Unicode::DisplayWidth.of(self)
|
278
244
|
end
|
279
245
|
|
280
246
|
def slice_by_display_length len
|
281
247
|
each_char.each_with_object "" do |c, buffer|
|
282
|
-
len -= c
|
283
|
-
buffer
|
248
|
+
len -= Unicode::DisplayWidth.of(c)
|
249
|
+
return buffer if len < 0
|
250
|
+
buffer << c
|
284
251
|
end
|
285
252
|
end
|
286
253
|
|
@@ -368,13 +335,14 @@ class String
|
|
368
335
|
ret = []
|
369
336
|
s = self
|
370
337
|
while s.display_length > len
|
371
|
-
|
338
|
+
slice = s.slice_by_display_length(len)
|
339
|
+
cut = slice.rindex(/\s/)
|
372
340
|
if cut
|
373
341
|
ret << s[0 ... cut]
|
374
342
|
s = s[(cut + 1) .. -1]
|
375
343
|
else
|
376
|
-
ret <<
|
377
|
-
s = s[
|
344
|
+
ret << slice
|
345
|
+
s = s[slice.length .. -1]
|
378
346
|
end
|
379
347
|
end
|
380
348
|
ret << s
|
@@ -382,8 +350,6 @@ class String
|
|
382
350
|
|
383
351
|
# Fix the damn string! make sure it is valid utf-8, then convert to
|
384
352
|
# user encoding.
|
385
|
-
#
|
386
|
-
# Not Ruby 1.8 compatible
|
387
353
|
def fix_encoding!
|
388
354
|
# first try to encode to utf-8 from whatever current encoding
|
389
355
|
encode!('UTF-8', :invalid => :replace, :undef => :replace)
|
@@ -406,8 +372,6 @@ class String
|
|
406
372
|
|
407
373
|
# transcode the string if original encoding is know
|
408
374
|
# fix if broken.
|
409
|
-
#
|
410
|
-
# Not Ruby 1.8 compatible
|
411
375
|
def transcode to_encoding, from_encoding
|
412
376
|
begin
|
413
377
|
encode!(to_encoding, from_encoding, :invalid => :replace, :undef => :replace)
|
@@ -474,13 +438,6 @@ class String
|
|
474
438
|
out = out.fix_encoding! # this should now be an utf-8 string of ascii
|
475
439
|
# compat chars.
|
476
440
|
end
|
477
|
-
|
478
|
-
unless method_defined? :ascii_only?
|
479
|
-
def ascii_only?
|
480
|
-
size.times { |i| return false if self[i] & 128 != 0 }
|
481
|
-
return true
|
482
|
-
end
|
483
|
-
end
|
484
441
|
end
|
485
442
|
|
486
443
|
class Numeric
|
@@ -498,18 +455,18 @@ class Numeric
|
|
498
455
|
|
499
456
|
def to_human_size
|
500
457
|
if self < 1024
|
501
|
-
to_s + "
|
458
|
+
to_s + "B"
|
502
459
|
elsif self < (1024 * 1024)
|
503
|
-
(self / 1024).to_s + "
|
460
|
+
(self / 1024).to_s + "KiB"
|
504
461
|
elsif self < (1024 * 1024 * 1024)
|
505
|
-
(self / 1024 / 1024).to_s + "
|
462
|
+
(self / 1024 / 1024).to_s + "MiB"
|
506
463
|
else
|
507
|
-
(self / 1024 / 1024 / 1024).to_s + "
|
464
|
+
(self / 1024 / 1024 / 1024).to_s + "GiB"
|
508
465
|
end
|
509
466
|
end
|
510
467
|
end
|
511
468
|
|
512
|
-
class
|
469
|
+
class Integer
|
513
470
|
def to_character
|
514
471
|
if self < 128 && self >= 0
|
515
472
|
chr
|
@@ -518,12 +475,6 @@ class Fixnum
|
|
518
475
|
end
|
519
476
|
end
|
520
477
|
|
521
|
-
unless method_defined?(:ord)
|
522
|
-
def ord
|
523
|
-
self
|
524
|
-
end
|
525
|
-
end
|
526
|
-
|
527
478
|
## hacking the english language
|
528
479
|
def pluralize s
|
529
480
|
to_s + " " +
|
@@ -607,10 +558,6 @@ module Enumerable
|
|
607
558
|
end
|
608
559
|
end
|
609
560
|
|
610
|
-
unless Object.const_defined? :Enumerator
|
611
|
-
Enumerator = Enumerable::Enumerator
|
612
|
-
end
|
613
|
-
|
614
561
|
class Array
|
615
562
|
def flatten_one_level
|
616
563
|
inject([]) { |a, e| a + e }
|
@@ -706,32 +653,6 @@ class SavingHash
|
|
706
653
|
defer_all_other_method_calls_to :hash
|
707
654
|
end
|
708
655
|
|
709
|
-
class OrderedHash < Hash
|
710
|
-
alias_method :store, :[]=
|
711
|
-
alias_method :each_pair, :each
|
712
|
-
attr_reader :keys
|
713
|
-
|
714
|
-
def initialize *a
|
715
|
-
@keys = []
|
716
|
-
a.each { |k, v| self[k] = v }
|
717
|
-
end
|
718
|
-
|
719
|
-
def []= key, val
|
720
|
-
@keys << key unless member?(key)
|
721
|
-
super
|
722
|
-
end
|
723
|
-
|
724
|
-
def values; keys.map { |k| self[k] } end
|
725
|
-
def index key; @keys.index key end
|
726
|
-
|
727
|
-
def delete key
|
728
|
-
@keys.delete key
|
729
|
-
super
|
730
|
-
end
|
731
|
-
|
732
|
-
def each; @keys.each { |k| yield k, self[k] } end
|
733
|
-
end
|
734
|
-
|
735
656
|
## easy thread-safe class for determining who's the "winner" in a race (i.e.
|
736
657
|
## first person to hit the finish line
|
737
658
|
class FinishLine
|
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
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
## the following magic enables wide characters when used with a ruby
|
2
|
+
## ncurses.so that's been compiled against libncursesw. (note the w.) why
|
3
|
+
## this works, i have no idea. much like pretty much every aspect of
|
4
|
+
## dealing with curses. cargo cult programming at its best.
|
5
|
+
require 'fiddle'
|
6
|
+
require 'fiddle/import'
|
7
|
+
|
8
|
+
module LocaleFiddler
|
9
|
+
extend Fiddle::Importer
|
10
|
+
|
11
|
+
SETLOCALE_LIB = case RbConfig::CONFIG['arch']
|
12
|
+
when /darwin/; "libc.dylib"
|
13
|
+
when /cygwin/; "cygwin1.dll"
|
14
|
+
when /freebsd/; "libc.so.7"
|
15
|
+
else; "libc.so.6"
|
16
|
+
end
|
17
|
+
|
18
|
+
dlload SETLOCALE_LIB
|
19
|
+
extern "char *setlocale(int, char const *)"
|
20
|
+
|
21
|
+
def setlocale(type, string)
|
22
|
+
LocaleFiddler.setlocale(type, string)
|
23
|
+
end
|
24
|
+
end
|
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.
|