sup 0.20.0 → 0.21.0

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