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