sup 0.20.0 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|