sup 0.20.0 → 0.21.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +1 -1
  4. data/CONTRIBUTORS +15 -12
  5. data/History.txt +16 -0
  6. data/ReleaseNotes +7 -0
  7. data/bin/sup +10 -24
  8. data/bin/sup-sync-back-maildir +1 -1
  9. data/contrib/completion/_sup.bash +102 -0
  10. data/lib/sup.rb +7 -7
  11. data/lib/sup/colormap.rb +5 -2
  12. data/lib/sup/contact.rb +4 -2
  13. data/lib/sup/crypto.rb +34 -2
  14. data/lib/sup/draft.rb +7 -7
  15. data/lib/sup/hook.rb +1 -1
  16. data/lib/sup/index.rb +2 -2
  17. data/lib/sup/label.rb +1 -1
  18. data/lib/sup/maildir.rb +2 -2
  19. data/lib/sup/mbox.rb +2 -2
  20. data/lib/sup/message.rb +6 -0
  21. data/lib/sup/message_chunks.rb +4 -2
  22. data/lib/sup/mode.rb +31 -26
  23. data/lib/sup/modes/edit_message_mode.rb +1 -1
  24. data/lib/sup/modes/forward_mode.rb +22 -3
  25. data/lib/sup/modes/line_cursor_mode.rb +1 -1
  26. data/lib/sup/modes/text_mode.rb +6 -1
  27. data/lib/sup/modes/thread_index_mode.rb +1 -1
  28. data/lib/sup/modes/thread_view_mode.rb +47 -6
  29. data/lib/sup/person.rb +68 -61
  30. data/lib/sup/search.rb +1 -1
  31. data/lib/sup/sent.rb +1 -1
  32. data/lib/sup/util/locale_fiddler.rb +24 -0
  33. data/lib/sup/version.rb +1 -1
  34. data/sup.gemspec +4 -3
  35. data/test/integration/test_maildir.rb +1 -1
  36. data/test/integration/test_mbox.rb +1 -1
  37. data/test/test_crypto.rb +1 -1
  38. data/test/test_header_parsing.rb +1 -1
  39. data/test/test_message.rb +77 -19
  40. data/test/test_messages_dir.rb +1 -19
  41. data/test/test_yaml_regressions.rb +1 -1
  42. data/test/unit/fixtures/contacts.txt +1 -0
  43. data/test/unit/test_contact.rb +33 -0
  44. data/test/unit/test_locale_fiddler.rb +15 -0
  45. data/test/unit/test_person.rb +37 -0
  46. metadata +31 -7
@@ -16,7 +16,7 @@ class DraftManager
16
16
  def write_draft
17
17
  offset = @source.gen_offset
18
18
  fn = @source.fn_for_offset offset
19
- File.open(fn, "w") { |f| yield f }
19
+ File.open(fn, "w:UTF-8") { |f| yield f }
20
20
  PollManager.poll_from @source
21
21
  end
22
22
 
@@ -33,7 +33,7 @@ class DraftLoader < Source
33
33
  yaml_properties
34
34
 
35
35
  def initialize dir=Redwood::DRAFT_DIR
36
- Dir.mkdir dir unless File.exists? dir
36
+ Dir.mkdir dir unless File.exist? dir
37
37
  super DraftManager.source_name, true, false
38
38
  @dir = dir
39
39
  @cur_offset = 0
@@ -62,7 +62,7 @@ class DraftLoader < Source
62
62
 
63
63
  def gen_offset
64
64
  i = 0
65
- while File.exists? fn_for_offset(i)
65
+ while File.exist? fn_for_offset(i)
66
66
  i += 1
67
67
  end
68
68
  i
@@ -75,7 +75,7 @@ class DraftLoader < Source
75
75
  end
76
76
 
77
77
  def load_message offset
78
- raise SourceError, "Draft not found" unless File.exists? fn_for_offset(offset)
78
+ raise SourceError, "Draft not found" unless File.exist? fn_for_offset(offset)
79
79
  File.open fn_for_offset(offset) do |f|
80
80
  RMail::Mailbox::MBoxReader.new(f).each_message do |input|
81
81
  return RMail::Parser.read(input)
@@ -85,7 +85,7 @@ class DraftLoader < Source
85
85
 
86
86
  def raw_header offset
87
87
  ret = ""
88
- File.open fn_for_offset(offset) do |f|
88
+ File.open(fn_for_offset(offset), "r:UTF-8") do |f|
89
89
  until f.eof? || (l = f.gets) =~ /^$/
90
90
  ret += l
91
91
  end
@@ -94,13 +94,13 @@ class DraftLoader < Source
94
94
  end
95
95
 
96
96
  def each_raw_message_line offset
97
- File.open(fn_for_offset(offset)) do |f|
97
+ File.open(fn_for_offset(offset), "r:UTF-8") do |f|
98
98
  yield f.gets until f.eof?
99
99
  end
100
100
  end
101
101
 
102
102
  def raw_message offset
103
- IO.read(fn_for_offset(offset))
103
+ IO.read(fn_for_offset(offset), :encoding => "UTF-8")
104
104
  end
105
105
 
106
106
  def start_offset; 0; end
@@ -83,7 +83,7 @@ class HookManager
83
83
  @contexts = {}
84
84
  @tags = {}
85
85
 
86
- Dir.mkdir dir unless File.exists? dir
86
+ Dir.mkdir dir unless File.exist? dir
87
87
  end
88
88
 
89
89
  attr_reader :tags
@@ -105,7 +105,7 @@ EOS
105
105
 
106
106
  def save
107
107
  debug "saving index and sources..."
108
- FileUtils.mkdir_p @dir unless File.exists? @dir
108
+ FileUtils.mkdir_p @dir unless File.exist? @dir
109
109
  SourceManager.save_sources
110
110
  save_index
111
111
  end
@@ -116,7 +116,7 @@ EOS
116
116
 
117
117
  def load_index failsafe=false
118
118
  path = File.join(@dir, 'xapian')
119
- if File.exists? path
119
+ if File.exist? path
120
120
  @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
121
121
  db_version = @xapian.get_metadata 'version'
122
122
  db_version = '0' if db_version.empty?
@@ -15,7 +15,7 @@ class LabelManager
15
15
  def initialize fn
16
16
  @fn = fn
17
17
  labels =
18
- if File.exists? fn
18
+ if File.exist? fn
19
19
  IO.readlines(fn).map { |x| x.chomp.intern }
20
20
  else
21
21
  []
@@ -68,7 +68,7 @@ class Maildir < Source
68
68
  File.safe_link tmp_path, new_path
69
69
  stored = true
70
70
  ensure
71
- File.unlink tmp_path if File.exists? tmp_path
71
+ File.unlink tmp_path if File.exist? tmp_path
72
72
  end
73
73
  end #rescue Errno...
74
74
  end #Dir.chdir
@@ -201,7 +201,7 @@ class Maildir < Source
201
201
  def trashed? id; maildir_data(id)[2].include? "T"; end
202
202
 
203
203
  def valid? id
204
- File.exists? File.join(@dir, id)
204
+ File.exist? File.join(@dir, id)
205
205
  end
206
206
 
207
207
  private
@@ -115,7 +115,7 @@ class MBox < Source
115
115
  end
116
116
 
117
117
  def store_message date, from_email, &block
118
- need_blank = File.exists?(@path) && !File.zero?(@path)
118
+ need_blank = File.exist?(@path) && !File.zero?(@path)
119
119
  File.open(@path, "ab") do |f|
120
120
  f.puts if need_blank
121
121
  f.puts "From #{from_email} #{date.asctime}"
@@ -180,7 +180,7 @@ class MBox < Source
180
180
  time = $1
181
181
  begin
182
182
  ## hack -- make Time.parse fail when trying to substitute values from Time.now
183
- Time.parse time, 0
183
+ Time.parse time, Time.at(0)
184
184
  true
185
185
  rescue NoMethodError, ArgumentError
186
186
  warn "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
@@ -279,6 +279,12 @@ class Message
279
279
  end
280
280
  end
281
281
 
282
+ def reload_from_source!
283
+ @chunks = nil
284
+ load_from_source!
285
+ end
286
+
287
+
282
288
  def error_message
283
289
  <<EOS
284
290
  #@snippet...
@@ -159,6 +159,7 @@ EOS
159
159
  "Attachment: #{filename} (#{content_type}; #{@raw_content.size.to_human_size})"
160
160
  end
161
161
  end
162
+ def safe_filename; Shellwords.escape(@filename).gsub("/", "_") end
162
163
 
163
164
  ## an attachment is exapndable if we've managed to decode it into
164
165
  ## something we can display inline. otherwise, it's viewable.
@@ -306,12 +307,13 @@ EOS
306
307
  end
307
308
 
308
309
  class CryptoNotice
309
- attr_reader :lines, :status, :patina_text
310
+ attr_reader :lines, :status, :patina_text, :unknown_fingerprint
310
311
 
311
- def initialize status, description, lines=[]
312
+ def initialize status, description, lines=[], unknown_fingerprint=nil
312
313
  @status = status
313
314
  @patina_text = description
314
315
  @lines = lines
316
+ @unknown_fingerprint = unknown_fingerprint
315
317
  end
316
318
 
317
319
  def patina_color
@@ -83,7 +83,7 @@ EOS
83
83
  ### helper functions
84
84
 
85
85
  def save_to_file fn, talk=true
86
- if File.exists? fn
86
+ if File.exist? fn
87
87
  unless BufferManager.ask_yes_or_no "File \"#{fn}\" exists. Overwrite?"
88
88
  info "Not overwriting #{fn}"
89
89
  return
@@ -102,37 +102,42 @@ EOS
102
102
  end
103
103
 
104
104
  def pipe_to_process command
105
- Open3.popen3(command) do |input, output, error|
106
- err, data, * = IO.select [error], [input], nil
107
-
108
- unless err.empty?
109
- message = err.first.read
110
- if message =~ /^\s*$/
111
- warn "error running #{command} (but no error message)"
112
- BufferManager.flash "Error running #{command}!"
113
- else
114
- warn "error running #{command}: #{message}"
115
- BufferManager.flash "Error: #{message}"
105
+ begin
106
+ Open3.popen3(command) do |input, output, error|
107
+ err, data, * = IO.select [error], [input], nil
108
+
109
+ unless err.empty?
110
+ message = err.first.read
111
+ if message =~ /^\s*$/
112
+ warn "error running #{command} (but no error message)"
113
+ BufferManager.flash "Error running #{command}!"
114
+ else
115
+ warn "error running #{command}: #{message}"
116
+ BufferManager.flash "Error: #{message}"
117
+ end
118
+ return nil, false
116
119
  end
117
- return
118
- end
119
120
 
120
- data = data.first
121
- data.sync = false # buffer input
121
+ data = data.first
122
+ data.sync = false # buffer input
122
123
 
123
- yield data
124
- data.close # output will block unless input is closed
124
+ yield data
125
+ data.close # output will block unless input is closed
125
126
 
126
- ## BUG?: shows errors or output but not both....
127
- data, * = IO.select [output, error], nil, nil
128
- data = data.first
127
+ ## BUG?: shows errors or output but not both....
128
+ data, * = IO.select [output, error], nil, nil
129
+ data = data.first
129
130
 
130
- if data.eof
131
- BufferManager.flash "'#{command}' done!"
132
- nil
133
- else
134
- data.read
131
+ if data.eof
132
+ BufferManager.flash "'#{command}' done!"
133
+ return nil, true
134
+ else
135
+ return data.read, true
136
+ end
135
137
  end
138
+ rescue Errno::ENOENT
139
+ # If the command is invalid
140
+ return nil, false
136
141
  end
137
142
  end
138
143
  end
@@ -699,7 +699,7 @@ private
699
699
  sigfn = (AccountManager.account_for(from_email) ||
700
700
  AccountManager.default_account).signature
701
701
 
702
- if sigfn && File.exists?(sigfn)
702
+ if sigfn && File.exist?(sigfn)
703
703
  ["", "-- "] + File.readlines(sigfn).map { |l| l.chomp }
704
704
  else
705
705
  []
@@ -1,6 +1,17 @@
1
1
  module Redwood
2
2
 
3
3
  class ForwardMode < EditMessageMode
4
+
5
+ HookManager.register "forward-attribution", <<EOS
6
+ Generates the attribution for the forwarded message
7
+ (["--- Begin forwarded message from John Doe ---",
8
+ "--- End forwarded message ---"])
9
+ Variables:
10
+ message: a message object representing the message being replied to
11
+ (useful values include message.from.mediumname and message.date)
12
+ Return value:
13
+ A list containing two strings: the text of the begin line and the text of the end line
14
+ EOS
4
15
  ## TODO: share some of this with reply-mode
5
16
  def initialize opts={}
6
17
  header = {
@@ -65,9 +76,17 @@ class ForwardMode < EditMessageMode
65
76
  protected
66
77
 
67
78
  def forward_body_lines m
68
- ["--- Begin forwarded message from #{m.from.mediumname} ---"] +
69
- m.quotable_header_lines + [""] + m.quotable_body_lines +
70
- ["--- End forwarded message ---"]
79
+ attribution = HookManager.run("forward-attribution", :message => m) || default_attribution(m)
80
+ attribution[0,1] +
81
+ m.quotable_header_lines +
82
+ [""] +
83
+ m.quotable_body_lines +
84
+ attribution[1,1]
85
+ end
86
+
87
+ def default_attribution m
88
+ ["--- Begin forwarded message from #{m.from.mediumname} ---",
89
+ "--- End forwarded message ---"]
71
90
  end
72
91
 
73
92
  def send_message
@@ -65,7 +65,7 @@ protected
65
65
  def set_cursor_pos p
66
66
  return if @curpos == p
67
67
  @curpos = p.clamp @cursor_top, lines
68
- buffer.mark_dirty
68
+ buffer.mark_dirty if buffer # not sure why the buffer is gone
69
69
  set_status
70
70
  end
71
71
 
@@ -24,10 +24,15 @@ class TextMode < ScrollMode
24
24
  command = BufferManager.ask(:shell, "pipe command: ")
25
25
  return if command.nil? || command.empty?
26
26
 
27
- output = pipe_to_process(command) do |stream|
27
+ output, success = pipe_to_process(command) do |stream|
28
28
  @text.each { |l| stream.puts l }
29
29
  end
30
30
 
31
+ unless success
32
+ BufferManager.flash "Invalid command: '#{command}' is not an executable"
33
+ return
34
+ end
35
+
31
36
  if output
32
37
  BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
33
38
  else
@@ -1026,7 +1026,7 @@ private
1026
1026
  end
1027
1027
 
1028
1028
  def from_width
1029
- [(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
1029
+ [(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max if buffer else MIN_FROM_WIDTH # not sure why the buffer is gone
1030
1030
  end
1031
1031
 
1032
1032
  def initialize_threads
@@ -89,6 +89,7 @@ EOS
89
89
  k.add :toggle_wrap, "Toggle wrapping of text", 'w'
90
90
 
91
91
  k.add :goto_uri, "Goto uri under cursor", 'g'
92
+ k.add :fetch_and_verify, "Fetch the PGP key on poolserver and re-verify message", "v"
92
93
 
93
94
  k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read:", '.' do |kk|
94
95
  kk.add :archive_and_kill, "Archive this thread and kill buffer", 'a'
@@ -224,10 +225,24 @@ EOS
224
225
 
225
226
  def unsubscribe_from_list
226
227
  m = @message_lines[curpos] or return
227
- if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
228
+ BufferManager.flash "Can't find List-Unsubscribe header for this message." unless m.list_unsubscribe
229
+
230
+ if m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
228
231
  ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "unsubscribe")
229
- else
230
- BufferManager.flash "Can't find List-Unsubscribe header for this message."
232
+ elsif m.list_unsubscribe =~ /<(http.*)?>/
233
+ unless HookManager.enabled? "goto"
234
+ BufferManager.flash "You must add a goto.rb hook before you can goto an unsubscribe URI."
235
+ return
236
+ end
237
+
238
+ begin
239
+ u = URI.parse($1)
240
+ rescue URI::InvalidURIError => e
241
+ BufferManager.flash("Invalid unsubscribe link")
242
+ return
243
+ end
244
+
245
+ HookManager.run "goto", :uri => Shellwords.escape(u.to_s)
231
246
  end
232
247
  end
233
248
 
@@ -374,7 +389,7 @@ EOS
374
389
  when Chunk::Attachment
375
390
  default_dir = $config[:default_attachment_save_dir]
376
391
  default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
377
- default_fn = File.expand_path File.join(default_dir, chunk.filename)
392
+ default_fn = File.expand_path File.join(default_dir, chunk.safe_filename)
378
393
  fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
379
394
 
380
395
  # if user selects directory use file name from message
@@ -403,7 +418,7 @@ EOS
403
418
  num_errors = 0
404
419
  m.chunks.each do |chunk|
405
420
  next unless chunk.is_a?(Chunk::Attachment)
406
- fn = File.join(folder, chunk.filename)
421
+ fn = File.join(folder, chunk.safe_filename)
407
422
  num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
408
423
  num += 1
409
424
  end
@@ -708,7 +723,7 @@ EOS
708
723
  command = BufferManager.ask(:shell, "pipe command: ")
709
724
  return if command.nil? || command.empty?
710
725
 
711
- output = pipe_to_process(command) do |stream|
726
+ output, success = pipe_to_process(command) do |stream|
712
727
  if chunk
713
728
  stream.print chunk.raw_content
714
729
  else
@@ -716,6 +731,11 @@ EOS
716
731
  end
717
732
  end
718
733
 
734
+ unless success
735
+ BufferManager.flash "Invalid command: '#{command}' is not an executable"
736
+ return
737
+ end
738
+
719
739
  if output
720
740
  BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
721
741
  else
@@ -774,6 +794,27 @@ EOS
774
794
  BufferManager.flash "No URI found." unless found
775
795
  end
776
796
 
797
+ def fetch_and_verify
798
+ message = @message_lines[curpos]
799
+ crypto_chunk = message.chunks.select {|chunk| chunk.is_a?(Chunk::CryptoNotice)}.first
800
+ return unless crypto_chunk
801
+ return unless crypto_chunk.unknown_fingerprint
802
+
803
+ BufferManager.flash "Retrieving key #{crypto_chunk.unknown_fingerprint} ..."
804
+
805
+ error = CryptoManager.retrieve crypto_chunk.unknown_fingerprint
806
+
807
+ if error
808
+ BufferManager.flash "Couldn't retrieve key: #{error.to_s}"
809
+ else
810
+ BufferManager.flash "Key #{crypto_chunk.unknown_fingerprint} successfully retrieved !"
811
+ end
812
+
813
+ # Re-trigger gpg verification
814
+ message.reload_from_source!
815
+ update
816
+ end
817
+
777
818
  private
778
819
 
779
820
  def initial_state_for m
@@ -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