component: sup
release:
reporter: William Morgan <wmorgan-sup@masanjin.net>
-status: :unstarted
-disposition:
+status: :closed
+disposition: :fixed
creation_time: 2008-05-19 23:42:25.910550 Z
references: []
- William Morgan <wmorgan-sup@masanjin.net>
- unassigned from release 0.6
- ""
+- - 2008-11-22 16:31:27.450146 Z
+ - Nicolas Pouillard <nicolas.pouillard@gmail.com>
+ - closed with disposition fixed
+ - This mapping and the PersonManager are now removed.
git_branch:
COLOR_FN = File.join(BASE_DIR, "colors.yaml")
SOURCE_FN = File.join(BASE_DIR, "sources.yaml")
LABEL_FN = File.join(BASE_DIR, "labels.txt")
- PERSON_FN = File.join(BASE_DIR, "people.txt")
CONTACT_FN = File.join(BASE_DIR, "contacts.txt")
DRAFT_DIR = File.join(BASE_DIR, "drafts")
SENT_FN = File.join(BASE_DIR, "sent.mbox")
end
def start
- Redwood::PersonManager.new Redwood::PERSON_FN
Redwood::SentManager.new Redwood::SENT_FN
Redwood::ContactManager.new Redwood::CONTACT_FN
Redwood::LabelManager.new Redwood::LABEL_FN
def finish
Redwood::LabelManager.save if Redwood::LabelManager.instantiated?
Redwood::ContactManager.save if Redwood::ContactManager.instantiated?
- Redwood::PersonManager.save if Redwood::PersonManager.instantiated?
Redwood::BufferManager.deinstantiate! if Redwood::BufferManager.instantiated?
end
def initialize h
raise ArgumentError, "no name for account" unless h[:name]
- raise ArgumentError, "no name for email" unless h[:name]
- super h[:name], h[:email], 0, true
+ raise ArgumentError, "no email for account" unless h[:email]
+ super h[:name], h[:email]
@sendmail = h[:sendmail]
@signature = h[:signature]
end
hash[:alternates] ||= []
a = Account.new hash
- PersonManager.register a
@accounts[a] = true
if default
answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
if answer
- answer.split_on_commas.map { |x| ContactManager.contact_for(x) || PersonManager.person_for(x) }
+ answer.split_on_commas.map { |x| ContactManager.contact_for(x) || Person.from_address(x) }
end
end
IO.foreach(fn) do |l|
l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
aalias, addr = $1, $2
- p = PersonManager.person_for addr, :definitive => true
+ p = Person.from_address addr
@p2a[p] = aalias
@a2p[aalias] = p unless aalias.nil? || aalias.empty?
end
:snippet => snippet, # always override
:label => labels.uniq.join(" "),
:attachments => (entry[:attachments] || m.attachments.uniq.join(" ")),
- :from => (entry[:from] || (m.from ? m.from.indexable_content : "")),
- :to => (entry[:to] || (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" ")),
+
+ ## always override :from and :to.
+ ## older versions of Sup would often store the wrong thing in the index
+ ## (because they were canonicalizing email addresses, resulting in the
+ ## wrong name associated with each.) the correct address is read from
+ ## the original header when these messages are opened in thread-view-mode,
+ ## so this allows people to forcibly update the address in the index by
+ ## marking those threads for saving.
+ :from => (m.from ? m.from.indexable_content : ""),
+ :to => (m.to + m.cc + m.bcc).map { |x| x.indexable_content }.join(" "),
+
:subject => (entry[:subject] || wrap_subj(Message.normalize_subj(m.subj))),
:refs => (entry[:refs] || (m.refs + m.replytos).uniq.join(" ")),
}
t = @index[docid][:to]
if AccountManager.is_account_email? f
- t.split(" ").each { |e| contacts[PersonManager.person_for(e)] = true }
+ t.split(" ").each { |e| contacts[Person.from_address(e)] = true }
else
- contacts[PersonManager.person_for(f)] = true
+ contacts[Person.from_address(f)] = true
end
end
end
@from =
if header["from"]
- PersonManager.person_for header["from"]
+ Person.from_address header["from"]
else
fakename = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
- PersonManager.person_for fakename
+ Person.from_address fakename
end
Redwood::log "faking message-id for message from #@from: #{id}" if fakeid
end
@subj = header.member?("subject") ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
- @to = PersonManager.people_for header["to"]
- @cc = PersonManager.people_for header["cc"]
- @bcc = PersonManager.people_for header["bcc"]
+ @to = Person.from_address_list header["to"]
+ @cc = Person.from_address_list header["cc"]
+ @bcc = Person.from_address_list header["bcc"]
## before loading our full header from the source, we can actually
## have some extra refs set by the UI. (this happens when the user
@refs = (@refs + refs).uniq
@replytos = (header["in-reply-to"] || "").scan(/<(.+?)>/).map { |x| sanitize_message_id x.first }
- @replyto = PersonManager.person_for header["reply-to"]
+ @replyto = Person.from_address header["reply-to"]
@list_address =
if header["list-post"]
- @list_address = PersonManager.person_for header["list-post"].gsub(/^<mailto:|>$/, "")
+ @list_address = Person.from_address header["list-post"].gsub(/^<mailto:|>$/, "")
else
nil
end
elsif m.header.content_type == "message/rfc822"
payload = RMail::Parser.read(m.body)
from = payload.header.from.first
- from_person = from ? PersonManager.person_for(from.format) : nil
+ from_person = from ? Person.from_address(from.format) : nil
[Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
message_to_chunks(payload, encrypted)
else
## do whatever crypto transformation is necessary
if @crypto_selector && @crypto_selector.val != :none
- from_email = PersonManager.person_for(@header["From"]).email
- to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| PersonManager.person_for(p).email }
+ from_email = Person.from_address(@header["From"]).email
+ to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| Person.from_address(p).email }
m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
end
end
def sig_lines
- p = PersonManager.person_for(@header["From"])
+ p = Person.from_address(@header["From"])
from_email = p && p.email
## first run the hook
if hook_reply_from
hook_reply_from
elsif @m.recipient_email && AccountManager.is_account_email?(@m.recipient_email)
- PersonManager.person_for(@m.recipient_email)
+ Person.from_address(@m.recipient_email)
elsif(b = (@m.to + @m.cc).find { |p| AccountManager.is_account? p })
b
else
def subscribe_to_list
m = @message_lines[curpos] or return
if m.list_subscribe && m.list_subscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
- ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
+ ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => $3
else
BufferManager.flash "Can't find List-Subscribe header for this message."
end
def unsubscribe_from_list
m = @message_lines[curpos] or return
if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
- ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
+ ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [Person.from_address($1)], :subj => $3
else
BufferManager.flash "Can't find List-Unsubscribe header for this message."
end
module Redwood
-class PersonManager
- include Singleton
-
- def initialize fn
- @fn = fn
- @@people = {}
-
- ## read in stored people
- IO.readlines(fn).map do |l|
- l =~ /^(.*)?:\s+(\d+)\s+(.*)$/ or next
- email, time, name = $1, $2, $3
- @@people[email] = Person.new name, email, time, false
- end if File.exists? fn
-
- self.class.i_am_the_instance self
- end
-
- def save
- File.open(@fn, "w") do |f|
- @@people.each do |email, p|
- next if p.email == p.name
- next if p.name =~ /=/ # drop rfc2047-encoded, and lots of other useless emails. definitely a heuristic.
- f.puts "#{p.email}: #{p.timestamp} #{p.name}"
- end
- end
- end
-
- def self.people_for s, opts={}
- return [] if s.nil?
- s.split_on_commas.map { |ss| self.person_for ss, opts }
- end
-
- def self.person_for s, opts={}
- p = Person.from_address(s) or return nil
- p.definitive = true if opts[:definitive]
- register p
- end
-
- def self.register p
- oldp = @@people[p.email]
-
- if oldp.nil? || p.better_than?(oldp)
- @@people[p.email] = p
- end
-
- @@people[p.email].touch!
- @@people[p.email]
- end
-end
-
-## don't create these by hand. rather, go through personmanager, to
-## ensure uniqueness and overriding.
class Person
- attr_accessor :name, :email, :timestamp
- bool_accessor :definitive
+ attr_accessor :name, :email
- def initialize name, email, timestamp=0, definitive=false
+ def initialize name, email
raise ArgumentError, "email can't be nil" unless email
if name
end
@email = email.gsub(/^\s+|\s+$/, "").gsub(/\s+/, " ").downcase
- @definitive = definitive
- @timestamp = timestamp
- end
-
- ## heuristic: whether the name attached to this email is "real", i.e.
- ## we should bother to store it.
- def generic?
- @email =~ /no\-?reply/
- end
-
- def better_than? o
- return false if o.definitive? || generic?
- return true if definitive?
- o.name.nil? || (name && name.length > o.name.length && name =~ /[a-z]/)
end
def to_s; "#@name <#@email>" end
- def touch!; @timestamp = Time.now.to_i end
-
# def == o; o && o.email == email; end
# alias :eql? :==
# def hash; [name, email].hash; end
return nil if s.nil?
## try and parse an email address and name
- name, email =
- case s
+ name, email = case s
+ when /(.+?) ((\S+?)@\S+) \3/
+ ## ok, this first match cause is insane, but bear with me. email
+ ## addresses are stored in the to/from/etc fields of the index in a
+ ## weird format: "name address first-part-of-address", i.e. spaces
+ ## separating those three bits, and no <>'s. this is the output of
+ ## #indexable_content. here, we reverse-engineer that format to extract
+ ## a valid address.
+ ##
+ ## we store things this way to allow searches on a to/from/etc field to
+ ## match any of those parts. a more robust solution would be to store a
+ ## separate, non-indexed field with the proper headers. but this way we
+ ## save precious bits, and it's backwards-compatible with older indexes.
+ [$1, $2]
when /["'](.*?)["'] <(.*?)>/, /([^,]+) <(.*?)>/
a, b = $1, $2
[a.gsub('\"', '"'), b]
Person.new name, email
end
+ def self.from_address_list ss
+ return [] if ss.nil?
+ ss.split_on_commas.map { |s| self.from_address s }
+ end
+
+ ## see comments in self.from_address
def indexable_content
[name, email, email.split(/@/).first].join(" ")
end
class TestMessage < Test::Unit::TestCase
def setup
- person_file = StringIO.new("")
- # this is a singleton
- if not PersonManager.instantiated?
- @person_manager = PersonManager.new(person_file)
- end
end
def teardown