sup 0.20.0 → 0.21.0
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 +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
|