sup 0.20.0 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +1 -1
- data/CONTRIBUTORS +15 -12
- data/History.txt +16 -0
- data/ReleaseNotes +7 -0
- data/bin/sup +10 -24
- data/bin/sup-sync-back-maildir +1 -1
- data/contrib/completion/_sup.bash +102 -0
- data/lib/sup.rb +7 -7
- data/lib/sup/colormap.rb +5 -2
- data/lib/sup/contact.rb +4 -2
- data/lib/sup/crypto.rb +34 -2
- data/lib/sup/draft.rb +7 -7
- data/lib/sup/hook.rb +1 -1
- data/lib/sup/index.rb +2 -2
- data/lib/sup/label.rb +1 -1
- data/lib/sup/maildir.rb +2 -2
- data/lib/sup/mbox.rb +2 -2
- data/lib/sup/message.rb +6 -0
- data/lib/sup/message_chunks.rb +4 -2
- data/lib/sup/mode.rb +31 -26
- data/lib/sup/modes/edit_message_mode.rb +1 -1
- data/lib/sup/modes/forward_mode.rb +22 -3
- data/lib/sup/modes/line_cursor_mode.rb +1 -1
- data/lib/sup/modes/text_mode.rb +6 -1
- data/lib/sup/modes/thread_index_mode.rb +1 -1
- data/lib/sup/modes/thread_view_mode.rb +47 -6
- data/lib/sup/person.rb +68 -61
- data/lib/sup/search.rb +1 -1
- data/lib/sup/sent.rb +1 -1
- data/lib/sup/util/locale_fiddler.rb +24 -0
- data/lib/sup/version.rb +1 -1
- data/sup.gemspec +4 -3
- data/test/integration/test_maildir.rb +1 -1
- data/test/integration/test_mbox.rb +1 -1
- data/test/test_crypto.rb +1 -1
- data/test/test_header_parsing.rb +1 -1
- data/test/test_message.rb +77 -19
- data/test/test_messages_dir.rb +1 -19
- data/test/test_yaml_regressions.rb +1 -1
- data/test/unit/fixtures/contacts.txt +1 -0
- 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
- metadata +31 -7
data/lib/sup/draft.rb
CHANGED
@@ -16,7 +16,7 @@ class DraftManager
|
|
16
16
|
def write_draft
|
17
17
|
offset = @source.gen_offset
|
18
18
|
fn = @source.fn_for_offset offset
|
19
|
-
File.open(fn, "w") { |f| yield f }
|
19
|
+
File.open(fn, "w:UTF-8") { |f| yield f }
|
20
20
|
PollManager.poll_from @source
|
21
21
|
end
|
22
22
|
|
@@ -33,7 +33,7 @@ class DraftLoader < Source
|
|
33
33
|
yaml_properties
|
34
34
|
|
35
35
|
def initialize dir=Redwood::DRAFT_DIR
|
36
|
-
Dir.mkdir dir unless File.
|
36
|
+
Dir.mkdir dir unless File.exist? dir
|
37
37
|
super DraftManager.source_name, true, false
|
38
38
|
@dir = dir
|
39
39
|
@cur_offset = 0
|
@@ -62,7 +62,7 @@ class DraftLoader < Source
|
|
62
62
|
|
63
63
|
def gen_offset
|
64
64
|
i = 0
|
65
|
-
while File.
|
65
|
+
while File.exist? fn_for_offset(i)
|
66
66
|
i += 1
|
67
67
|
end
|
68
68
|
i
|
@@ -75,7 +75,7 @@ class DraftLoader < Source
|
|
75
75
|
end
|
76
76
|
|
77
77
|
def load_message offset
|
78
|
-
raise SourceError, "Draft not found" unless File.
|
78
|
+
raise SourceError, "Draft not found" unless File.exist? fn_for_offset(offset)
|
79
79
|
File.open fn_for_offset(offset) do |f|
|
80
80
|
RMail::Mailbox::MBoxReader.new(f).each_message do |input|
|
81
81
|
return RMail::Parser.read(input)
|
@@ -85,7 +85,7 @@ class DraftLoader < Source
|
|
85
85
|
|
86
86
|
def raw_header offset
|
87
87
|
ret = ""
|
88
|
-
File.open
|
88
|
+
File.open(fn_for_offset(offset), "r:UTF-8") do |f|
|
89
89
|
until f.eof? || (l = f.gets) =~ /^$/
|
90
90
|
ret += l
|
91
91
|
end
|
@@ -94,13 +94,13 @@ class DraftLoader < Source
|
|
94
94
|
end
|
95
95
|
|
96
96
|
def each_raw_message_line offset
|
97
|
-
File.open(fn_for_offset(offset)) do |f|
|
97
|
+
File.open(fn_for_offset(offset), "r:UTF-8") do |f|
|
98
98
|
yield f.gets until f.eof?
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
102
|
def raw_message offset
|
103
|
-
IO.read(fn_for_offset(offset))
|
103
|
+
IO.read(fn_for_offset(offset), :encoding => "UTF-8")
|
104
104
|
end
|
105
105
|
|
106
106
|
def start_offset; 0; end
|
data/lib/sup/hook.rb
CHANGED
data/lib/sup/index.rb
CHANGED
@@ -105,7 +105,7 @@ EOS
|
|
105
105
|
|
106
106
|
def save
|
107
107
|
debug "saving index and sources..."
|
108
|
-
FileUtils.mkdir_p @dir unless File.
|
108
|
+
FileUtils.mkdir_p @dir unless File.exist? @dir
|
109
109
|
SourceManager.save_sources
|
110
110
|
save_index
|
111
111
|
end
|
@@ -116,7 +116,7 @@ EOS
|
|
116
116
|
|
117
117
|
def load_index failsafe=false
|
118
118
|
path = File.join(@dir, 'xapian')
|
119
|
-
if File.
|
119
|
+
if File.exist? path
|
120
120
|
@xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
|
121
121
|
db_version = @xapian.get_metadata 'version'
|
122
122
|
db_version = '0' if db_version.empty?
|
data/lib/sup/label.rb
CHANGED
data/lib/sup/maildir.rb
CHANGED
@@ -68,7 +68,7 @@ class Maildir < Source
|
|
68
68
|
File.safe_link tmp_path, new_path
|
69
69
|
stored = true
|
70
70
|
ensure
|
71
|
-
File.unlink tmp_path if File.
|
71
|
+
File.unlink tmp_path if File.exist? tmp_path
|
72
72
|
end
|
73
73
|
end #rescue Errno...
|
74
74
|
end #Dir.chdir
|
@@ -201,7 +201,7 @@ class Maildir < Source
|
|
201
201
|
def trashed? id; maildir_data(id)[2].include? "T"; end
|
202
202
|
|
203
203
|
def valid? id
|
204
|
-
File.
|
204
|
+
File.exist? File.join(@dir, id)
|
205
205
|
end
|
206
206
|
|
207
207
|
private
|
data/lib/sup/mbox.rb
CHANGED
@@ -115,7 +115,7 @@ class MBox < Source
|
|
115
115
|
end
|
116
116
|
|
117
117
|
def store_message date, from_email, &block
|
118
|
-
need_blank = File.
|
118
|
+
need_blank = File.exist?(@path) && !File.zero?(@path)
|
119
119
|
File.open(@path, "ab") do |f|
|
120
120
|
f.puts if need_blank
|
121
121
|
f.puts "From #{from_email} #{date.asctime}"
|
@@ -180,7 +180,7 @@ class MBox < Source
|
|
180
180
|
time = $1
|
181
181
|
begin
|
182
182
|
## hack -- make Time.parse fail when trying to substitute values from Time.now
|
183
|
-
Time.parse time, 0
|
183
|
+
Time.parse time, Time.at(0)
|
184
184
|
true
|
185
185
|
rescue NoMethodError, ArgumentError
|
186
186
|
warn "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
|
data/lib/sup/message.rb
CHANGED
data/lib/sup/message_chunks.rb
CHANGED
@@ -159,6 +159,7 @@ EOS
|
|
159
159
|
"Attachment: #{filename} (#{content_type}; #{@raw_content.size.to_human_size})"
|
160
160
|
end
|
161
161
|
end
|
162
|
+
def safe_filename; Shellwords.escape(@filename).gsub("/", "_") end
|
162
163
|
|
163
164
|
## an attachment is exapndable if we've managed to decode it into
|
164
165
|
## something we can display inline. otherwise, it's viewable.
|
@@ -306,12 +307,13 @@ EOS
|
|
306
307
|
end
|
307
308
|
|
308
309
|
class CryptoNotice
|
309
|
-
attr_reader :lines, :status, :patina_text
|
310
|
+
attr_reader :lines, :status, :patina_text, :unknown_fingerprint
|
310
311
|
|
311
|
-
def initialize status, description, lines=[]
|
312
|
+
def initialize status, description, lines=[], unknown_fingerprint=nil
|
312
313
|
@status = status
|
313
314
|
@patina_text = description
|
314
315
|
@lines = lines
|
316
|
+
@unknown_fingerprint = unknown_fingerprint
|
315
317
|
end
|
316
318
|
|
317
319
|
def patina_color
|
data/lib/sup/mode.rb
CHANGED
@@ -83,7 +83,7 @@ EOS
|
|
83
83
|
### helper functions
|
84
84
|
|
85
85
|
def save_to_file fn, talk=true
|
86
|
-
if File.
|
86
|
+
if File.exist? fn
|
87
87
|
unless BufferManager.ask_yes_or_no "File \"#{fn}\" exists. Overwrite?"
|
88
88
|
info "Not overwriting #{fn}"
|
89
89
|
return
|
@@ -102,37 +102,42 @@ EOS
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def pipe_to_process command
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
105
|
+
begin
|
106
|
+
Open3.popen3(command) do |input, output, error|
|
107
|
+
err, data, * = IO.select [error], [input], nil
|
108
|
+
|
109
|
+
unless err.empty?
|
110
|
+
message = err.first.read
|
111
|
+
if message =~ /^\s*$/
|
112
|
+
warn "error running #{command} (but no error message)"
|
113
|
+
BufferManager.flash "Error running #{command}!"
|
114
|
+
else
|
115
|
+
warn "error running #{command}: #{message}"
|
116
|
+
BufferManager.flash "Error: #{message}"
|
117
|
+
end
|
118
|
+
return nil, false
|
116
119
|
end
|
117
|
-
return
|
118
|
-
end
|
119
120
|
|
120
|
-
|
121
|
-
|
121
|
+
data = data.first
|
122
|
+
data.sync = false # buffer input
|
122
123
|
|
123
|
-
|
124
|
-
|
124
|
+
yield data
|
125
|
+
data.close # output will block unless input is closed
|
125
126
|
|
126
|
-
|
127
|
-
|
128
|
-
|
127
|
+
## BUG?: shows errors or output but not both....
|
128
|
+
data, * = IO.select [output, error], nil, nil
|
129
|
+
data = data.first
|
129
130
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
131
|
+
if data.eof
|
132
|
+
BufferManager.flash "'#{command}' done!"
|
133
|
+
return nil, true
|
134
|
+
else
|
135
|
+
return data.read, true
|
136
|
+
end
|
135
137
|
end
|
138
|
+
rescue Errno::ENOENT
|
139
|
+
# If the command is invalid
|
140
|
+
return nil, false
|
136
141
|
end
|
137
142
|
end
|
138
143
|
end
|
@@ -699,7 +699,7 @@ private
|
|
699
699
|
sigfn = (AccountManager.account_for(from_email) ||
|
700
700
|
AccountManager.default_account).signature
|
701
701
|
|
702
|
-
if sigfn && File.
|
702
|
+
if sigfn && File.exist?(sigfn)
|
703
703
|
["", "-- "] + File.readlines(sigfn).map { |l| l.chomp }
|
704
704
|
else
|
705
705
|
[]
|
@@ -1,6 +1,17 @@
|
|
1
1
|
module Redwood
|
2
2
|
|
3
3
|
class ForwardMode < EditMessageMode
|
4
|
+
|
5
|
+
HookManager.register "forward-attribution", <<EOS
|
6
|
+
Generates the attribution for the forwarded message
|
7
|
+
(["--- Begin forwarded message from John Doe ---",
|
8
|
+
"--- End forwarded message ---"])
|
9
|
+
Variables:
|
10
|
+
message: a message object representing the message being replied to
|
11
|
+
(useful values include message.from.mediumname and message.date)
|
12
|
+
Return value:
|
13
|
+
A list containing two strings: the text of the begin line and the text of the end line
|
14
|
+
EOS
|
4
15
|
## TODO: share some of this with reply-mode
|
5
16
|
def initialize opts={}
|
6
17
|
header = {
|
@@ -65,9 +76,17 @@ class ForwardMode < EditMessageMode
|
|
65
76
|
protected
|
66
77
|
|
67
78
|
def forward_body_lines m
|
68
|
-
|
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/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
|
@@ -1026,7 +1026,7 @@ private
|
|
1026
1026
|
end
|
1027
1027
|
|
1028
1028
|
def from_width
|
1029
|
-
[(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max
|
1029
|
+
[(buffer.content_width.to_f * 0.2).to_i, MIN_FROM_WIDTH].max if buffer else MIN_FROM_WIDTH # not sure why the buffer is gone
|
1030
1030
|
end
|
1031
1031
|
|
1032
1032
|
def initialize_threads
|
@@ -89,6 +89,7 @@ EOS
|
|
89
89
|
k.add :toggle_wrap, "Toggle wrapping of text", 'w'
|
90
90
|
|
91
91
|
k.add :goto_uri, "Goto uri under cursor", 'g'
|
92
|
+
k.add :fetch_and_verify, "Fetch the PGP key on poolserver and re-verify message", "v"
|
92
93
|
|
93
94
|
k.add_multi "(a)rchive/(d)elete/mark as (s)pam/mark as u(N)read:", '.' do |kk|
|
94
95
|
kk.add :archive_and_kill, "Archive this thread and kill buffer", 'a'
|
@@ -224,10 +225,24 @@ EOS
|
|
224
225
|
|
225
226
|
def unsubscribe_from_list
|
226
227
|
m = @message_lines[curpos] or return
|
227
|
-
|
228
|
+
BufferManager.flash "Can't find List-Unsubscribe header for this message." unless m.list_unsubscribe
|
229
|
+
|
230
|
+
if m.list_unsubscribe =~ /<mailto:(.*?)(\?subject=(.*?))?>/
|
228
231
|
ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => ($3 || "unsubscribe")
|
229
|
-
|
230
|
-
|
232
|
+
elsif m.list_unsubscribe =~ /<(http.*)?>/
|
233
|
+
unless HookManager.enabled? "goto"
|
234
|
+
BufferManager.flash "You must add a goto.rb hook before you can goto an unsubscribe URI."
|
235
|
+
return
|
236
|
+
end
|
237
|
+
|
238
|
+
begin
|
239
|
+
u = URI.parse($1)
|
240
|
+
rescue URI::InvalidURIError => e
|
241
|
+
BufferManager.flash("Invalid unsubscribe link")
|
242
|
+
return
|
243
|
+
end
|
244
|
+
|
245
|
+
HookManager.run "goto", :uri => Shellwords.escape(u.to_s)
|
231
246
|
end
|
232
247
|
end
|
233
248
|
|
@@ -374,7 +389,7 @@ EOS
|
|
374
389
|
when Chunk::Attachment
|
375
390
|
default_dir = $config[:default_attachment_save_dir]
|
376
391
|
default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
|
377
|
-
default_fn = File.expand_path File.join(default_dir, chunk.
|
392
|
+
default_fn = File.expand_path File.join(default_dir, chunk.safe_filename)
|
378
393
|
fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
|
379
394
|
|
380
395
|
# if user selects directory use file name from message
|
@@ -403,7 +418,7 @@ EOS
|
|
403
418
|
num_errors = 0
|
404
419
|
m.chunks.each do |chunk|
|
405
420
|
next unless chunk.is_a?(Chunk::Attachment)
|
406
|
-
fn = File.join(folder, chunk.
|
421
|
+
fn = File.join(folder, chunk.safe_filename)
|
407
422
|
num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
|
408
423
|
num += 1
|
409
424
|
end
|
@@ -708,7 +723,7 @@ EOS
|
|
708
723
|
command = BufferManager.ask(:shell, "pipe command: ")
|
709
724
|
return if command.nil? || command.empty?
|
710
725
|
|
711
|
-
output = pipe_to_process(command) do |stream|
|
726
|
+
output, success = pipe_to_process(command) do |stream|
|
712
727
|
if chunk
|
713
728
|
stream.print chunk.raw_content
|
714
729
|
else
|
@@ -716,6 +731,11 @@ EOS
|
|
716
731
|
end
|
717
732
|
end
|
718
733
|
|
734
|
+
unless success
|
735
|
+
BufferManager.flash "Invalid command: '#{command}' is not an executable"
|
736
|
+
return
|
737
|
+
end
|
738
|
+
|
719
739
|
if output
|
720
740
|
BufferManager.spawn "Output of '#{command}'", TextMode.new(output.ascii)
|
721
741
|
else
|
@@ -774,6 +794,27 @@ EOS
|
|
774
794
|
BufferManager.flash "No URI found." unless found
|
775
795
|
end
|
776
796
|
|
797
|
+
def fetch_and_verify
|
798
|
+
message = @message_lines[curpos]
|
799
|
+
crypto_chunk = message.chunks.select {|chunk| chunk.is_a?(Chunk::CryptoNotice)}.first
|
800
|
+
return unless crypto_chunk
|
801
|
+
return unless crypto_chunk.unknown_fingerprint
|
802
|
+
|
803
|
+
BufferManager.flash "Retrieving key #{crypto_chunk.unknown_fingerprint} ..."
|
804
|
+
|
805
|
+
error = CryptoManager.retrieve crypto_chunk.unknown_fingerprint
|
806
|
+
|
807
|
+
if error
|
808
|
+
BufferManager.flash "Couldn't retrieve key: #{error.to_s}"
|
809
|
+
else
|
810
|
+
BufferManager.flash "Key #{crypto_chunk.unknown_fingerprint} successfully retrieved !"
|
811
|
+
end
|
812
|
+
|
813
|
+
# Re-trigger gpg verification
|
814
|
+
message.reload_from_source!
|
815
|
+
update
|
816
|
+
end
|
817
|
+
|
777
818
|
private
|
778
819
|
|
779
820
|
def initial_state_for m
|
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
|