def initialize fn
     @fn = fn
-    @people = {}
+    @p2a = {} # person to alias map
+    @a2p = {} # alias to person map
 
     if File.exists? fn
       IO.foreach(fn) do |l|
         l =~ /^(\S+): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
         aalias, addr = $1, $2
-        @people[aalias] = Person.for addr
+        p = Person.for addr
+        @p2a[p] = aalias
+        @a2p[aalias] = p
       end
     end
 
     self.class.i_am_the_instance self
   end
 
-  def contacts; @people; end
+  def contacts; @p2a.keys; end
   def set_contact person, aalias
-    oldentry = @people.find { |a, p| p == person }
-    @people.delete oldentry.first if oldentry
-    @people[aalias] = person
+    if(pold = @a2p[aalias]) && (pold != person)
+      drop_contact pold
+    end
+    @p2a[person] = aalias
+    @a2p[aalias] = person
   end
-  def drop_contact person; @people.find { |a, p| @people.delete a if p == person }; end
-  def delete t; @people.delete t; end
-  def resolve aalias; @people[aalias]; end
-
+  def drop_contact person
+    if(aalias = @p2a[person])
+      @p2a.delete person
+      @a2p.delete aalias
+    end
+  end    
+  def person_with aalias; @a2p[aalias]; end
+  def alias_for person; @p2a[person]; end
+  def is_contact? person; @p2a.member? person; end
   def save
     File.open(@fn, "w") do |f|
-      @people.keys.sort.each do |aalias|
-        f.puts "#{aalias}: #{@people[aalias].full_address}"
+      @p2a.each do |p, a|
+        f.puts "#{a}: #{p.full_address}"
       end
     end
   end
 
 module Redwood
 
+module CanAliasContacts
+  def alias_contact p
+    a = BufferManager.ask(:alias, "Nickname for #{p.longname}: ", ContactManager.alias_for(p)) or return
+    if a.empty?
+      ContactManager.drop_contact p
+    else
+      ContactManager.set_contact p, a
+    end
+  end
+end
+
 class ContactListMode < LineCursorMode
   LOAD_MORE_CONTACTS_NUM = 10
 
   register_keymap do |k|
     k.add :load_more, "Load #{LOAD_MORE_CONTACTS_NUM} more contacts", 'M'
     k.add :reload, "Drop contact list and reload", 'D'
-    k.add :alias, "Edit alias for contact", 'a'
+    k.add :alias, "Edit nickname/alias for contact", 'a'
     k.add :toggle_tagged, "Tag/untag current line", 't'
     k.add :apply_to_tagged, "Apply next command to all tagged items", ';'
     k.add :search, "Search for messages from particular people", 'S'
     super()
   end
 
+  include CanAliasContacts
+  def alias
+    p = @contacts[curpos] or return
+    alias_contact p
+    regen_text
+  end
+
   def lines; @text.length; end
   def [] i; @text[i]; end
 
     load
   end
 
-  def alias
-    p = @contacts[curpos] or return
-    a = BufferManager.ask(:alias, "alias for #{p.longname}: ", @user_contacts[p]) or return
-    if a.empty?
-      ContactManager.drop_contact p
-      @user_contacts.delete p
-    else
-      ContactManager.set_contact p, a
-      @user_contacts[p] = a
-    end
-    regen_text # in case columns need to be shifted
-  end
-
   def load_in_background
     Redwood::reporting_thread do
       load
 
   def load
     @num ||= buffer.content_height
-    @user_contacts = ContactManager.contacts.invert
+    @user_contacts = ContactManager.contacts
     num = [@num - @user_contacts.length, 0].max
     BufferManager.say("Loading #{num} contacts from index...") do
       recentc = Index.load_contacts AccountManager.user_emails, :num => num
-      @contacts = (@user_contacts.keys + recentc).sort_by { |p| p.sort_by_me }.uniq
+      @contacts = (@user_contacts + recentc).sort_by { |p| p.sort_by_me }.uniq
     end
   end
   
   end
 
   def text_for_contact p
-    aalias = @user_contacts[p] || ""
+    aalias = ContactManager.alias_for(p) || ""
     [[:tagged_color, @tags.tagged?(p) ? ">" : " "],
      [:none, sprintf("%-#{@awidth}s %-#{@nwidth}s %s", aalias, p.name, p.email)]]
   end
   def regen_text
     @awidth, @nwidth = 0, 0
     @contacts.each do |p|
-      aalias = @user_contacts[p]
+      aalias = ContactManager.alias_for_person(p)
       @awidth = aalias.length if aalias && aalias.length > @awidth
       @nwidth = p.name.length if p.name && p.name.length > @nwidth
     end
 
     k.add :collapse_non_new_messages, "Collapse all but new messages", 'N'
     k.add :reply, "Reply to a message", 'r'
     k.add :forward, "Forward a message", 'f'
+    k.add :alias, "Edit alias/nickname for a person", 'a'
     k.add :save_to_disk, "Save message/attachment to disk", 's'
   end
 
-  ## there are three important instance variables we hold to lay out
-  ## the thread. @layout is a map from Message and Chunk objects to
-  ## Layout objects. (for chunks, we only use the state field right
-  ## now.) @message_lines is a map from row #s to Message objects.
-  ## @chunk_lines is a map from row #s to Chunk objects.
+  ## there are a couple important instance variables we hold to lay
+  ## out the thread and to provide line-based functionality. @layout
+  ## is a map from Message and Chunk objects to Layout objects. (for
+  ## chunks, we only use the state field right now.) @message_lines is
+  ## a map from row #s to Message objects. @chunk_lines is a map from
+  ## row #s to Chunk objects. @person_lines is a map from row #s to
+  ## Person objects.
 
   def initialize thread, hidden_labels=[]
     super()
     mode.edit
   end
 
+  include CanAliasContacts
+  def alias
+    p = @person_lines[curpos] or return
+    alias_contact p
+    regen_text
+  end
+
   def toggle_starred
     return unless(m = @message_lines[curpos])
     if m.has_label? :starred
     @text = []
     @chunk_lines = []
     @message_lines = []
+    @person_lines = []
 
     prevm = nil
     @thread.each do |m, depth, parent|
     end
   end
 
-  def message_patina_lines m, state, parent, prefix
+  def message_patina_lines m, state, start, parent, prefix
     prefix_widget = [:message_patina_color, prefix]
     widget = 
       case state
 
     case state
     when :open
+      @person_lines[start] = m.from
       [[prefix_widget, widget, imp_widget,
         [:message_patina_color, 
             "#{m.from ? m.from.mediumname : '?'} to #{m.recipients.map { |l| l.shortname }.join(', ')} #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})"]]]
+
     when :closed
+      @person_lines[start] = m.from
       [[prefix_widget, widget, imp_widget,
         [:message_patina_color, 
         "#{m.from ? m.from.mediumname : '?'}, #{m.date.to_nice_s} (#{m.date.to_nice_distance_s})  #{m.snippet}"]]]
+
     when :detailed
-      labels = m.labels# - @hidden_labels
-      x = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? m.from.longname : '?'}"]]] +
-        ((m.to.empty? ? [] : break_into_lines("  To: ", m.to.map { |x| x.longname })) +
-           (m.cc.empty? ? [] : break_into_lines("  Cc: ", m.cc.map { |x| x.longname })) +
-           (m.bcc.empty? ? [] : break_into_lines("  Bcc: ", m.bcc.map { |x| x.longname })) +
-           ["  Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})"] +
-           ["  Subject: #{m.subj}"] +
-           [(parent ? "  In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil)] +
-           [labels.empty? ? nil : "  Labels: #{labels.join(', ')}"]
-        ).flatten.compact.map { |l| [[:message_patina_color, prefix + "  " + l]] }
-      #raise x.inspect
-      x
+      @person_lines[start] = m.from
+      from = [[prefix_widget, widget, imp_widget, [:message_patina_color, "From: #{m.from ? format_person(m.from) : '?'}"]]]
+
+      rest = []
+      unless m.to.empty?
+        m.to.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
+        rest += format_person_list "  To: ", m.to
+      end
+      unless m.cc.empty?
+        m.cc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
+        rest += format_person_list "  Cc: ", m.cc
+      end
+      unless m.bcc.empty?
+        m.bcc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
+        rest += format_person_list "  Bcc: ", m.bcc
+      end
+
+      rest += [
+        "  Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})",
+        "  Subject: #{m.subj}",
+        (parent ? "  In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil),
+        m.labels.empty? ? nil : "  Labels: #{m.labels.join(', ')}",
+      ].compact
+      
+      from + rest.map { |l| [[:message_patina_color, prefix + "  " + l]] }
     end
   end
 
-  def break_into_lines prefix, list
+  def format_person_list prefix, people
+    ptext = people.map { |p| format_person p }
     pad = " " * prefix.length
-    [prefix + list.first + (list.length > 1 ? "," : "")] + 
-      list[1 .. -1].map_with_index do |e, i|
-        pad + e + (i == list.length - 1 ? "" : ",")
+    [prefix + ptext.first + (ptext.length > 1 ? "," : "")] + 
+      ptext[1 .. -1].map_with_index do |e, i|
+        pad + e + (i == ptext.length - 1 ? "" : ",")
       end
   end
 
+  def format_person p
+    p.longname + (ContactManager.is_contact?(p) ? " (#{ContactManager.alias_for p})" : "")
+  end
 
   def chunk_to_lines chunk, state, start, depth, parent=nil
     prefix = " " * INDENT_SPACES * depth
     when nil
       [[[:message_patina_color, "#{prefix}<an unreceived message>"]]]
     when Message
-      message_patina_lines(chunk, state, parent, prefix) +
+      message_patina_lines(chunk, state, start, parent, prefix) +
         (chunk.is_draft? ? [[[:draft_notification_color, prefix + " >>> This message is a draft. To edit, hit 'e'. <<<"]]] : [])
     when Message::Attachment
       [[[:mime_color, "#{prefix}+ MIME attachment #{chunk.content_type}#{chunk.desc ? ' (' + chunk.desc + ')': ''}"]]]