sup 0.20.0 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -1
  3. data/.travis.yml +11 -6
  4. data/CONTRIBUTORS +27 -15
  5. data/Gemfile +2 -1
  6. data/History.txt +84 -0
  7. data/README.md +26 -5
  8. data/Rakefile +0 -1
  9. data/ReleaseNotes +7 -0
  10. data/bin/sup +17 -30
  11. data/bin/sup-add +15 -16
  12. data/bin/sup-config +30 -45
  13. data/bin/sup-dump +2 -3
  14. data/bin/sup-import-dump +5 -6
  15. data/bin/sup-sync +3 -4
  16. data/bin/sup-sync-back-maildir +3 -4
  17. data/bin/sup-tweak-labels +6 -7
  18. data/contrib/colorpicker.rb +0 -2
  19. data/contrib/completion/_sup.bash +102 -0
  20. data/devel/profile.rb +0 -1
  21. data/ext/mkrf_conf_xapian.rb +1 -1
  22. data/lib/sup.rb +8 -8
  23. data/lib/sup/colormap.rb +5 -2
  24. data/lib/sup/contact.rb +4 -2
  25. data/lib/sup/crypto.rb +58 -16
  26. data/lib/sup/draft.rb +8 -8
  27. data/lib/sup/hook.rb +9 -9
  28. data/lib/sup/index.rb +20 -7
  29. data/lib/sup/label.rb +1 -1
  30. data/lib/sup/logger.rb +1 -1
  31. data/lib/sup/maildir.rb +2 -2
  32. data/lib/sup/mbox.rb +2 -2
  33. data/lib/sup/message.rb +26 -10
  34. data/lib/sup/message_chunks.rb +7 -4
  35. data/lib/sup/mode.rb +34 -28
  36. data/lib/sup/modes/contact_list_mode.rb +1 -0
  37. data/lib/sup/modes/edit_message_mode.rb +1 -1
  38. data/lib/sup/modes/forward_mode.rb +22 -3
  39. data/lib/sup/modes/line_cursor_mode.rb +1 -1
  40. data/lib/sup/modes/reply_mode.rb +3 -1
  41. data/lib/sup/modes/text_mode.rb +6 -1
  42. data/lib/sup/modes/thread_index_mode.rb +6 -2
  43. data/lib/sup/modes/thread_view_mode.rb +63 -18
  44. data/lib/sup/person.rb +68 -61
  45. data/lib/sup/search.rb +1 -1
  46. data/lib/sup/sent.rb +1 -1
  47. data/lib/sup/source.rb +1 -1
  48. data/lib/sup/util.rb +15 -94
  49. data/lib/sup/util/axe.rb +17 -0
  50. data/lib/sup/util/locale_fiddler.rb +24 -0
  51. data/lib/sup/util/ncurses.rb +3 -3
  52. data/lib/sup/version.rb +10 -1
  53. data/sup.gemspec +12 -10
  54. data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
  55. data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
  56. data/test/fixtures/blank-header-fields.eml +71 -0
  57. data/test/fixtures/contacts.txt +1 -0
  58. data/test/fixtures/mailing-list-header.eml +80 -0
  59. data/test/fixtures/malicious-attachment-names.eml +55 -0
  60. data/test/fixtures/missing-from-to.eml +18 -0
  61. data/test/{messages → fixtures}/missing-line.eml +0 -0
  62. data/test/fixtures/multi-part-2.eml +72 -0
  63. data/test/fixtures/multi-part.eml +61 -0
  64. data/test/fixtures/no-body.eml +18 -0
  65. data/test/fixtures/simple-message.eml +29 -0
  66. data/test/fixtures/text-attachments-with-charset.eml +46 -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 +70 -16
  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_maildir.rb +1 -1
  77. data/test/integration/test_mbox.rb +1 -1
  78. data/test/test_crypto.rb +14 -2
  79. data/test/test_header_parsing.rb +1 -1
  80. data/test/test_helper.rb +6 -3
  81. data/test/test_message.rb +115 -341
  82. data/test/test_messages_dir.rb +4 -28
  83. data/test/test_yaml_regressions.rb +1 -1
  84. data/test/unit/test_contact.rb +33 -0
  85. data/test/unit/test_locale_fiddler.rb +15 -0
  86. data/test/unit/test_person.rb +37 -0
  87. data/test/unit/util/test_query.rb +10 -4
  88. data/test/unit/util/test_string.rb +6 -0
  89. metadata +107 -43
  90. data/test/gnupg_test_home/key1.gen +0 -15
  91. data/test/gnupg_test_home/key2.gen +0 -15
@@ -18,11 +18,16 @@ class Person
18
18
  @email = email.strip.gsub(/\s+/, " ")
19
19
  end
20
20
 
21
- def to_s; "#@name <#@email>" end
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 Person.full_address name, email
51
- if name && email
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
- ## return "canonical" person using contact manager or create one if
83
- ## not found or contact manager not available
84
- def self.from_name_and_email name, email
85
- ContactManager.instantiated? && ContactManager.person_for(email) || Person.new(name, email)
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
- def self.from_address s
89
- return nil if s.nil?
90
-
91
- ## try and parse an email address and name
92
- name, email = case s
93
- when /(.+?) ((\S+?)@\S+) \3/
94
- ## ok, this first match cause is insane, but bear with me. email
95
- ## addresses are stored in the to/from/etc fields of the index in a
96
- ## weird format: "name address first-part-of-address", i.e. spaces
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
- [nil, s]
90
+ email
115
91
  end
92
+ end
116
93
 
117
- from_name_and_email name, email
118
- end
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
- def self.from_address_list ss
121
- return [] if ss.nil?
122
- ss.dup.split_on_commas.map { |s| self.from_address s }
123
- end
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
@@ -12,7 +12,7 @@ class SearchManager
12
12
  def initialize fn
13
13
  @fn = fn
14
14
  @searches = {}
15
- if File.exists? fn
15
+ if File.exist? fn
16
16
  IO.foreach(fn) do |l|
17
17
  l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
18
18
  @searches[$1] = $2
@@ -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.exists? @filename
43
+ File.open(@filename, "w") { } unless File.exist? @filename
44
44
  super "mbox://" + @filename, true, $config[:archive_sent]
45
45
  end
46
46
 
@@ -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
@@ -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="?(.*?)"?(;|$)/i
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.width(self.fix_encoding!, false)
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.display_length
283
- buffer << c if len >= 0
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
- cut = s.slice_by_display_length(len).rindex(/\s/)
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 << s.slice_by_display_length(len)
377
- s = s[ret.last.length .. -1]
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 + "b"
458
+ to_s + "B"
502
459
  elsif self < (1024 * 1024)
503
- (self / 1024).to_s + "k"
460
+ (self / 1024).to_s + "KiB"
504
461
  elsif self < (1024 * 1024 * 1024)
505
- (self / 1024 / 1024).to_s + "m"
462
+ (self / 1024 / 1024).to_s + "MiB"
506
463
  else
507
- (self / 1024 / 1024 / 1024).to_s + "g"
464
+ (self / 1024 / 1024 / 1024).to_s + "GiB"
508
465
  end
509
466
  end
510
467
  end
511
468
 
512
- class Fixnum
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
@@ -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
@@ -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.