sup 0.19.0 → 0.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -1
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +12 -6
  5. data/CONTRIBUTORS +28 -14
  6. data/Gemfile +5 -0
  7. data/History.txt +92 -0
  8. data/README.md +26 -5
  9. data/Rakefile +41 -1
  10. data/ReleaseNotes +17 -0
  11. data/bin/sup +12 -23
  12. data/bin/sup-add +15 -16
  13. data/bin/sup-config +30 -45
  14. data/bin/sup-dump +2 -3
  15. data/bin/sup-import-dump +5 -6
  16. data/bin/sup-sync +3 -4
  17. data/bin/sup-sync-back-maildir +3 -4
  18. data/bin/sup-tweak-labels +6 -7
  19. data/contrib/colorpicker.rb +0 -2
  20. data/contrib/completion/_sup.bash +102 -0
  21. data/devel/profile.rb +0 -1
  22. data/ext/mkrf_conf_xapian.rb +47 -0
  23. data/lib/sup.rb +10 -8
  24. data/lib/sup/buffer.rb +12 -0
  25. data/lib/sup/colormap.rb +5 -2
  26. data/lib/sup/contact.rb +4 -2
  27. data/lib/sup/crypto.rb +58 -16
  28. data/lib/sup/draft.rb +8 -8
  29. data/lib/sup/hook.rb +9 -9
  30. data/lib/sup/index.rb +20 -7
  31. data/lib/sup/label.rb +1 -1
  32. data/lib/sup/logger.rb +1 -1
  33. data/lib/sup/maildir.rb +16 -5
  34. data/lib/sup/mbox.rb +13 -5
  35. data/lib/sup/message.rb +36 -12
  36. data/lib/sup/message_chunks.rb +13 -4
  37. data/lib/sup/mode.rb +34 -28
  38. data/lib/sup/modes/contact_list_mode.rb +1 -0
  39. data/lib/sup/modes/edit_message_mode.rb +3 -2
  40. data/lib/sup/modes/forward_mode.rb +22 -3
  41. data/lib/sup/modes/line_cursor_mode.rb +1 -1
  42. data/lib/sup/modes/reply_mode.rb +3 -1
  43. data/lib/sup/modes/text_mode.rb +6 -1
  44. data/lib/sup/modes/thread_index_mode.rb +12 -2
  45. data/lib/sup/modes/thread_view_mode.rb +111 -14
  46. data/lib/sup/person.rb +68 -61
  47. data/lib/sup/search.rb +1 -1
  48. data/lib/sup/sent.rb +1 -1
  49. data/lib/sup/source.rb +1 -1
  50. data/lib/sup/util.rb +15 -94
  51. data/lib/sup/util/axe.rb +17 -0
  52. data/lib/sup/util/locale_fiddler.rb +24 -0
  53. data/lib/sup/util/ncurses.rb +3 -3
  54. data/lib/sup/version.rb +10 -1
  55. data/sup.gemspec +29 -11
  56. data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
  57. data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
  58. data/test/fixtures/blank-header-fields.eml +71 -0
  59. data/test/fixtures/contacts.txt +1 -0
  60. data/test/fixtures/mailing-list-header.eml +80 -0
  61. data/test/fixtures/malicious-attachment-names.eml +55 -0
  62. data/test/fixtures/missing-from-to.eml +18 -0
  63. data/test/{messages → fixtures}/missing-line.eml +0 -0
  64. data/test/fixtures/multi-part-2.eml +72 -0
  65. data/test/fixtures/multi-part.eml +61 -0
  66. data/test/fixtures/no-body.eml +18 -0
  67. data/test/fixtures/simple-message.eml +29 -0
  68. data/test/fixtures/text-attachments-with-charset.eml +46 -0
  69. data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
  70. data/test/gnupg_test_home/gpg.conf +3 -1
  71. data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
  72. data/test/gnupg_test_home/pubring.gpg +0 -0
  73. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  74. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  75. data/test/gnupg_test_home/regen_keys.sh +89 -0
  76. data/test/gnupg_test_home/secring.gpg +0 -0
  77. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -17
  78. data/test/integration/test_maildir.rb +75 -0
  79. data/test/integration/test_mbox.rb +69 -0
  80. data/test/test_crypto.rb +14 -2
  81. data/test/test_header_parsing.rb +1 -1
  82. data/test/test_helper.rb +6 -3
  83. data/test/test_message.rb +115 -341
  84. data/test/test_messages_dir.rb +4 -28
  85. data/test/test_yaml_regressions.rb +1 -1
  86. data/test/unit/test_contact.rb +33 -0
  87. data/test/unit/test_locale_fiddler.rb +15 -0
  88. data/test/unit/test_person.rb +37 -0
  89. data/test/unit/util/test_query.rb +10 -4
  90. data/test/unit/util/test_string.rb +6 -0
  91. metadata +137 -53
  92. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  93. data/test/gnupg_test_home/trustdb.gpg +0 -0
@@ -108,6 +108,7 @@ class ContactListMode < LineCursorMode
108
108
  def load
109
109
  @num ||= (buffer.content_height * 2)
110
110
  @user_contacts = ContactManager.contacts_with_aliases
111
+ @user_contacts += (HookManager.run("extra-contact-addresses") || []).map { |addr| Person.from_address addr }
111
112
  num = [@num - @user_contacts.length, 0].max
112
113
  BufferManager.say("Loading #{num} contacts from index...") do
113
114
  recentc = Index.load_contacts AccountManager.user_emails, :num => num
@@ -20,6 +20,7 @@ Variables:
20
20
  to the raw headers for the message. E.g., header["From"],
21
21
  header["To"], etc.
22
22
  from_email: the email part of the From: line, or nil if empty
23
+ message_id: the unique message id of the message
23
24
  Return value:
24
25
  A string (multi-line ok) containing the text of the signature, or nil to
25
26
  use the default signature, or :none for no signature.
@@ -688,7 +689,7 @@ private
688
689
  from_email = p && p.email
689
690
 
690
691
  ## first run the hook
691
- hook_sig = HookManager.run "signature", :header => @header, :from_email => from_email
692
+ hook_sig = HookManager.run "signature", :header => @header, :from_email => from_email, :message_id => @message_id
692
693
 
693
694
  return [] if hook_sig == :none
694
695
  return ["", "-- "] + hook_sig.split("\n") if hook_sig
@@ -698,7 +699,7 @@ private
698
699
  sigfn = (AccountManager.account_for(from_email) ||
699
700
  AccountManager.default_account).signature
700
701
 
701
- if sigfn && File.exists?(sigfn)
702
+ if sigfn && File.exist?(sigfn)
702
703
  ["", "-- "] + File.readlines(sigfn).map { |l| l.chomp }
703
704
  else
704
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
 
@@ -36,6 +36,8 @@ Variables:
36
36
  [:#{REPLY_TYPES * ', :'}]
37
37
  The default behavior is equivalent to
38
38
  ([:list, :sender, :recipent] & modes)[0]
39
+ message: a message object representing the message being replied to
40
+ (useful values include message.is_list_message? and message.list_address)
39
41
  Return value:
40
42
  The reply mode you desire, or nil to use the default behavior.
41
43
  EOS
@@ -130,7 +132,7 @@ EOS
130
132
  types = REPLY_TYPES.select { |t| @headers.member?(t) }
131
133
  @type_selector = HorizontalSelector.new "Reply to:", types, types.map { |x| TYPE_DESCRIPTIONS[x] }
132
134
 
133
- hook_reply = HookManager.run "reply-to", :modes => types
135
+ hook_reply = HookManager.run "reply-to", :modes => types, :message => @m
134
136
 
135
137
  @type_selector.set_to(
136
138
  if types.include? type_arg
@@ -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
@@ -696,7 +696,7 @@ EOS
696
696
  @ts.threads.each { |th| th.labels.each { |l| LabelManager << l } }
697
697
 
698
698
  update
699
- BufferManager.clear @mbid
699
+ BufferManager.clear @mbid if @mbid
700
700
  @mbid = nil
701
701
  BufferManager.draw_screen
702
702
  @ts.size - orig_size
@@ -856,6 +856,12 @@ protected
856
856
  need_update = false
857
857
 
858
858
  @mutex.synchronize do
859
+ # and certainly not sure why this happens..
860
+ #
861
+ # probably a race condition between thread modification and updating
862
+ # going on.
863
+ return if @threads[l].empty?
864
+
859
865
  @size_widgets[l] = size_widget_for_thread @threads[l]
860
866
  @date_widgets[l] = date_widget_for_thread @threads[l]
861
867
 
@@ -1020,7 +1026,11 @@ private
1020
1026
  end
1021
1027
 
1022
1028
  def from_width
1023
- [(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
1029
+ if buffer
1030
+ [(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
1031
+ else
1032
+ MIN_FROM_WIDTH # not sure why the buffer is gone
1033
+ end
1024
1034
  end
1025
1035
 
1026
1036
  def initialize_threads
@@ -10,8 +10,6 @@ class ThreadViewMode < LineCursorMode
10
10
  attr_accessor :state
11
11
  end
12
12
 
13
- INDENT_SPACES = 2 # how many spaces to indent child messages
14
-
15
13
  HookManager.register "detailed-headers", <<EOS
16
14
  Add or remove headers from the detailed header display of a message.
17
15
  Variables:
@@ -42,10 +40,19 @@ Return value:
42
40
  None.
43
41
  EOS
44
42
 
43
+ HookManager.register "goto", <<EOS
44
+ Open the uri given as a parameter.
45
+ Variables:
46
+ uri: The uri
47
+ Return value:
48
+ None.
49
+ EOS
50
+
45
51
  register_keymap do |k|
46
52
  k.add :toggle_detailed_header, "Toggle detailed header", 'h'
47
53
  k.add :show_header, "Show full message header", 'H'
48
54
  k.add :show_message, "Show full message (raw form)", 'V'
55
+ k.add :reload, "Update message in thread", '@'
49
56
  k.add :activate_chunk, "Expand/collapse or activate item", :enter
50
57
  k.add :expand_all_messages, "Expand/collapse all messages", 'E'
51
58
  k.add :edit_draft, "Edit draft", 'e'
@@ -80,6 +87,9 @@ EOS
80
87
  k.add :kill_and_next, "Kill this thread, kill buffer, and view next", '&'
81
88
  k.add :toggle_wrap, "Toggle wrapping of text", 'w'
82
89
 
90
+ k.add :goto_uri, "Goto uri under cursor", 'g'
91
+ k.add :fetch_and_verify, "Fetch the PGP key on poolserver and re-verify message", "v"
92
+
83
93
  k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read:", '.' do |kk|
84
94
  kk.add :archive_and_kill, "Archive this thread and kill buffer", 'a'
85
95
  kk.add :delete_and_kill, "Delete this thread and kill buffer", 'd'
@@ -116,6 +126,7 @@ EOS
116
126
  ## objects. @person_lines is a map from row #s to Person objects.
117
127
 
118
128
  def initialize thread, hidden_labels=[], index_mode=nil
129
+ @indent_spaces = $config[:indent_spaces]
119
130
  super :slip_rows => $config[:slip_rows]
120
131
  @thread = thread
121
132
  @hidden_labels = hidden_labels
@@ -194,6 +205,10 @@ EOS
194
205
  @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
195
206
  update
196
207
  end
208
+
209
+ def reload
210
+ update
211
+ end
197
212
 
198
213
  def reply type_arg=nil
199
214
  m = @message_lines[curpos] or return
@@ -214,10 +229,24 @@ EOS
214
229
 
215
230
  def unsubscribe_from_list
216
231
  m = @message_lines[curpos] or return
217
- if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
232
+ BufferManager.flash "Can't find List-Unsubscribe header for this message." unless m.list_unsubscribe
233
+
234
+ if m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
218
235
  ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "unsubscribe")
219
- else
220
- BufferManager.flash "Can't find List-Unsubscribe header for this message."
236
+ elsif m.list_unsubscribe =~ /<(http.*)?>/
237
+ unless HookManager.enabled? "goto"
238
+ BufferManager.flash "You must add a goto.rb hook before you can goto an unsubscribe URI."
239
+ return
240
+ end
241
+
242
+ begin
243
+ u = URI.parse($1)
244
+ rescue URI::InvalidURIError => e
245
+ BufferManager.flash("Invalid unsubscribe link")
246
+ return
247
+ end
248
+
249
+ HookManager.run "goto", :uri => Shellwords.escape(u.to_s)
221
250
  end
222
251
  end
223
252
 
@@ -364,7 +393,7 @@ EOS
364
393
  when Chunk::Attachment
365
394
  default_dir = $config[:default_attachment_save_dir]
366
395
  default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
367
- default_fn = File.expand_path File.join(default_dir, chunk.filename)
396
+ default_fn = File.expand_path File.join(default_dir, chunk.filesafe_filename)
368
397
  fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
369
398
 
370
399
  # if user selects directory use file name from message
@@ -393,7 +422,7 @@ EOS
393
422
  num_errors = 0
394
423
  m.chunks.each do |chunk|
395
424
  next unless chunk.is_a?(Chunk::Attachment)
396
- fn = File.join(folder, chunk.filename)
425
+ fn = File.join(folder, chunk.filesafe_filename)
397
426
  num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
398
427
  num += 1
399
428
  end
@@ -536,7 +565,7 @@ EOS
536
565
  l = @layout[m]
537
566
 
538
567
  ## boundaries of the message
539
- message_left = l.depth * INDENT_SPACES
568
+ message_left = l.depth * @indent_spaces
540
569
  message_right = message_left + l.width
541
570
 
542
571
  ## calculate leftmost colum
@@ -698,7 +727,7 @@ EOS
698
727
  command = BufferManager.ask(:shell, "pipe command: ")
699
728
  return if command.nil? || command.empty?
700
729
 
701
- output = pipe_to_process(command) do |stream|
730
+ output, success = pipe_to_process(command) do |stream|
702
731
  if chunk
703
732
  stream.print chunk.raw_content
704
733
  else
@@ -706,6 +735,11 @@ EOS
706
735
  end
707
736
  end
708
737
 
738
+ unless success
739
+ BufferManager.flash "Invalid command: '#{command}' is not an executable"
740
+ return
741
+ end
742
+
709
743
  if output
710
744
  BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
711
745
  else
@@ -722,6 +756,68 @@ EOS
722
756
  [user_labels, super].join(" -- ")
723
757
  end
724
758
 
759
+ def goto_uri
760
+ unless (chunk = @chunk_lines[curpos])
761
+ BufferManager.flash "No URI found."
762
+ return
763
+ end
764
+ unless HookManager.enabled? "goto"
765
+ BufferManager.flash "You must add a goto.rb hook before you can goto a URI."
766
+ return
767
+ end
768
+
769
+ # @text is a list of lines with this format:
770
+ # [
771
+ # [[:text_color, "Some text"]]
772
+ # [[:text_color, " continued here"]]
773
+ # ]
774
+
775
+ linetext = @text.slice(curpos, @text.length).flatten(1)
776
+ .take_while{|d| [:text_color, :sig_color].include?(d[0]) and d[1].strip != ""} # Only take up to the first "" alone on its line
777
+ .map{|d| d[1].strip}.join("").strip
778
+
779
+ found = false
780
+ URI.extract(linetext || "").each do |match|
781
+ begin
782
+ u = URI.parse(match)
783
+ next unless u.absolute?
784
+ next unless ["http", "https"].include?(u.scheme)
785
+
786
+ reallink = Shellwords.escape(u.to_s)
787
+ BufferManager.flash "Going to #{reallink} ..."
788
+ HookManager.run "goto", :uri => reallink
789
+ BufferManager.completely_redraw_screen
790
+ found = true
791
+
792
+ rescue URI::InvalidURIError => e
793
+ debug "not a uri: #{e}"
794
+ # Do nothing, this is an ok flow
795
+ end
796
+ end
797
+ BufferManager.flash "No URI found." unless found
798
+ end
799
+
800
+ def fetch_and_verify
801
+ message = @message_lines[curpos]
802
+ crypto_chunk = message.chunks.select {|chunk| chunk.is_a?(Chunk::CryptoNotice)}.first
803
+ return unless crypto_chunk
804
+ return unless crypto_chunk.unknown_fingerprint
805
+
806
+ BufferManager.flash "Retrieving key #{crypto_chunk.unknown_fingerprint} ..."
807
+
808
+ error = CryptoManager.retrieve crypto_chunk.unknown_fingerprint
809
+
810
+ if error
811
+ BufferManager.flash "Couldn't retrieve key: #{error.to_s}"
812
+ else
813
+ BufferManager.flash "Key #{crypto_chunk.unknown_fingerprint} successfully retrieved !"
814
+ end
815
+
816
+ # Re-trigger gpg verification
817
+ message.reload_from_source!
818
+ update
819
+ end
820
+
725
821
  private
726
822
 
727
823
  def initial_state_for m
@@ -793,7 +889,7 @@ private
793
889
  (0 ... text.length).each do |i|
794
890
  @chunk_lines[@text.length + i] = c
795
891
  @message_lines[@text.length + i] = m
796
- lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * INDENT_SPACES)
892
+ lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * @indent_spaces)
797
893
  l.width = lw if lw > l.width
798
894
  end
799
895
  @text += text
@@ -847,9 +943,10 @@ private
847
943
  addressee_lines += format_person_list " Bcc: ", m.bcc
848
944
  end
849
945
 
850
- headers = OrderedHash.new
851
- headers["Date"] = "#{m.date.to_message_nice_s} (#{m.date.to_nice_distance_s})"
852
- headers["Subject"] = m.subj
946
+ headers = {
947
+ "Date" => "#{m.date.to_message_nice_s} (#{m.date.to_nice_distance_s})",
948
+ "Subject" => m.subj
949
+ }
853
950
 
854
951
  show_labels = @thread.labels - LabelManager::HIDDEN_RESERVED_LABELS
855
952
  unless show_labels.empty?
@@ -897,7 +994,7 @@ private
897
994
 
898
995
  ## todo: check arguments on this overly complex function
899
996
  def chunk_to_lines chunk, state, start, depth, parent=nil, color=nil, star_color=nil
900
- prefix = " " * INDENT_SPACES * depth
997
+ prefix = " " * @indent_spaces * depth
901
998
  case chunk
902
999
  when :fake_root
903
1000
  [[[:missing_message_color, "#{prefix}<one or more unreceived messages>"]]]
@@ -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