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.
- checksums.yaml +5 -5
- data/.gitignore +4 -1
- data/.gitmodules +3 -0
- data/.travis.yml +12 -6
- data/CONTRIBUTORS +28 -14
- data/Gemfile +5 -0
- data/History.txt +92 -0
- data/README.md +26 -5
- data/Rakefile +41 -1
- data/ReleaseNotes +17 -0
- data/bin/sup +12 -23
- data/bin/sup-add +15 -16
- data/bin/sup-config +30 -45
- data/bin/sup-dump +2 -3
- data/bin/sup-import-dump +5 -6
- data/bin/sup-sync +3 -4
- data/bin/sup-sync-back-maildir +3 -4
- data/bin/sup-tweak-labels +6 -7
- data/contrib/colorpicker.rb +0 -2
- data/contrib/completion/_sup.bash +102 -0
- data/devel/profile.rb +0 -1
- data/ext/mkrf_conf_xapian.rb +47 -0
- data/lib/sup.rb +10 -8
- data/lib/sup/buffer.rb +12 -0
- data/lib/sup/colormap.rb +5 -2
- data/lib/sup/contact.rb +4 -2
- data/lib/sup/crypto.rb +58 -16
- data/lib/sup/draft.rb +8 -8
- data/lib/sup/hook.rb +9 -9
- data/lib/sup/index.rb +20 -7
- data/lib/sup/label.rb +1 -1
- data/lib/sup/logger.rb +1 -1
- data/lib/sup/maildir.rb +16 -5
- data/lib/sup/mbox.rb +13 -5
- data/lib/sup/message.rb +36 -12
- data/lib/sup/message_chunks.rb +13 -4
- data/lib/sup/mode.rb +34 -28
- data/lib/sup/modes/contact_list_mode.rb +1 -0
- data/lib/sup/modes/edit_message_mode.rb +3 -2
- data/lib/sup/modes/forward_mode.rb +22 -3
- data/lib/sup/modes/line_cursor_mode.rb +1 -1
- data/lib/sup/modes/reply_mode.rb +3 -1
- data/lib/sup/modes/text_mode.rb +6 -1
- data/lib/sup/modes/thread_index_mode.rb +12 -2
- data/lib/sup/modes/thread_view_mode.rb +111 -14
- data/lib/sup/person.rb +68 -61
- data/lib/sup/search.rb +1 -1
- data/lib/sup/sent.rb +1 -1
- data/lib/sup/source.rb +1 -1
- data/lib/sup/util.rb +15 -94
- data/lib/sup/util/axe.rb +17 -0
- data/lib/sup/util/locale_fiddler.rb +24 -0
- data/lib/sup/util/ncurses.rb +3 -3
- data/lib/sup/version.rb +10 -1
- data/sup.gemspec +29 -11
- data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
- data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
- data/test/fixtures/blank-header-fields.eml +71 -0
- data/test/fixtures/contacts.txt +1 -0
- data/test/fixtures/mailing-list-header.eml +80 -0
- data/test/fixtures/malicious-attachment-names.eml +55 -0
- data/test/fixtures/missing-from-to.eml +18 -0
- data/test/{messages → fixtures}/missing-line.eml +0 -0
- data/test/fixtures/multi-part-2.eml +72 -0
- data/test/fixtures/multi-part.eml +61 -0
- data/test/fixtures/no-body.eml +18 -0
- data/test/fixtures/simple-message.eml +29 -0
- data/test/fixtures/text-attachments-with-charset.eml +46 -0
- data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
- data/test/gnupg_test_home/gpg.conf +3 -1
- data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
- data/test/gnupg_test_home/pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
- data/test/gnupg_test_home/receiver_secring.gpg +0 -0
- data/test/gnupg_test_home/regen_keys.sh +89 -0
- data/test/gnupg_test_home/secring.gpg +0 -0
- data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -17
- data/test/integration/test_maildir.rb +75 -0
- data/test/integration/test_mbox.rb +69 -0
- data/test/test_crypto.rb +14 -2
- data/test/test_header_parsing.rb +1 -1
- data/test/test_helper.rb +6 -3
- data/test/test_message.rb +115 -341
- data/test/test_messages_dir.rb +4 -28
- data/test/test_yaml_regressions.rb +1 -1
- data/test/unit/test_contact.rb +33 -0
- data/test/unit/test_locale_fiddler.rb +15 -0
- data/test/unit/test_person.rb +37 -0
- data/test/unit/util/test_query.rb +10 -4
- data/test/unit/util/test_string.rb +6 -0
- metadata +137 -53
- data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
- 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.
|
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
|
-
|
69
|
-
|
70
|
-
|
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
|
data/lib/sup/modes/reply_mode.rb
CHANGED
@@ -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
|
data/lib/sup/modes/text_mode.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
220
|
-
|
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.
|
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.
|
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 *
|
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 *
|
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 =
|
851
|
-
|
852
|
-
|
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 = " " *
|
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>"]]]
|
data/lib/sup/person.rb
CHANGED
@@ -18,11 +18,16 @@ class Person
|
|
18
18
|
@email = email.strip.gsub(/\s+/, " ")
|
19
19
|
end
|
20
20
|
|
21
|
-
def to_s
|
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
|
51
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
90
|
+
email
|
115
91
|
end
|
92
|
+
end
|
116
93
|
|
117
|
-
|
118
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|