sup 0.19.0 → 0.23

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 (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