I'm very sorry, but it seems that an error occurred in Sup. 
 Please accept my sincere apologies. If you don't mind, please
 send the backtrace below and a brief report of the circumstances
-to user wmorgan-sup at site masanjin dot net so that I might
-address this problem. Thank you!
+to wmorgan-sup at masanjin dot nets so that I might address this
+problem. Thank you!
 
 Sincerely,
 William
 
 #!/usr/bin/env ruby
 
+require 'uri'
 require 'rubygems'
-require 'highline'
+require 'highline/import'
 require "sup"
 
+
 Thread.abort_on_exception = true # make debugging possible
 
 class Float
   exit
 end
 
+## for sources that require login information, prompt the user for
+## that. also provide a list of previously-defined login info to
+## choose from, if any.
+def get_login_info uri, sources
+  uri = URI(uri)
+  accounts = sources.map do |s|
+    next unless s.respond_to?(:username)
+    suri = URI(s.uri)
+    [suri.host, s.username, s.password]
+  end.compact.uniq.sort_by { |h, u, p| h == uri.host ? 0 : 1 }
+
+  username, password = nil, nil
+  unless accounts.empty?
+    say "Would you like to use the same account as for a previous source?"
+    choose do |menu|
+      accounts.each do |host, olduser, oldpw|
+        menu.choice("Use the account info for #{olduser}@#{host}") { username, password = olduser, oldpw }
+      end
+      menu.choice("Use a new account") { }
+    end
+  end
+
+  unless username && password
+    username = ask("Username for #{uri.host}: ");
+    password = ask("Password for #{uri.host}: ") { |q| q.echo = false }
+  end
+
+  [username, password]
+end
+
+
 educate_user if ARGV.member? '--help'
 
 archive = ARGV.delete "--archive"
   educate_user
 end
 
+$terminal.wrap_at = :auto
 Redwood::start
-
 index = Redwood::Index.new
 index.load
 
-h = HighLine.new
-
-sources = ARGV.map do |fn|
-  fn = "mbox://#{fn}" unless fn =~ %r!://!
-  source = index.source_for fn
+sources = ARGV.map do |uri|
+  uri = "mbox://#{uri}" unless uri =~ %r!://!
+  source = index.source_for uri
   unless source
     source = 
-      case fn
+      case uri
       when %r!^mbox\+ssh://!
-        username = h.ask("Username for #{fn}: ");
-        password = h.ask("Password for #{fn}: ") { |q| q.echo = false }
-        puts # why?
-        Redwood::MBox::SSHLoader.new(fn, username, password, nil, !unusual, !!archive)
+        say "For SSH connections, if you will use public key authentication, you may leave the username and password blank."
+        say "\n"
+        username, password = get_login_info uri, index.sources
+        Redwood::MBox::SSHLoader.new(uri, username, password, nil, !unusual, !!archive)
       when %r!^imaps?://!
-        username = h.ask("Username for #{fn}: ");
-        password = h.ask("Password for #{fn}: ") { |q| q.echo = false }
-        puts # why?
-        Redwood::IMAP.new(fn, username, password, nil, !unusual, !!archive)
+        username, password = get_login_info uri, sources
+        Redwood::IMAP.new(uri, username, password, nil, !unusual, !!archive)
       else
-        Redwood::MBox::Loader.new(fn, nil, !unusual, !!archive)
+        Redwood::MBox::Loader.new(uri, nil, !unusual, !!archive)
       end
     index.add_source source
   end
 begin
   sources.each do |source|
     if source.broken?
-      puts "error loading messages from #{source}: #{source.broken_msg}"
+      $stderr.puts "error loading messages from #{source}: #{source.broken_msg}"
       next
     end
     next if source.done?
 
 
 module Redwood
 
+## could be a bottleneck, but doesn't seem to significantly slow
+## things down.
+
+class SafeNcurses
+  def self.method_missing meth, *a, &b
+    @mutex ||= Mutex.new
+    @mutex.synchronize { Ncurses.send meth, *a, &b }
+  end
+end
+
 class Buffer
   attr_reader :mode, :x, :y, :width, :height, :title
   bool_reader :dirty
   def content_height; @height - 1; end
   def content_width; @width; end
 
-  def resize rows, cols
+  def resize rows, cols 
+    return if rows == @width && cols == @height
     @width = cols
     @height = rows
     mode.resize rows, cols
 
   def completely_redraw_screen
     return if @freeze
-    Ncurses.clear
+    SafeNcurses.clear
     @dirty = true
     draw_screen
   end
 
   def handle_resize
     return if @freeze
-    rows, cols = Ncurses.rows, Ncurses.cols
-    @buffers.each { |b| b.resize rows - 1, cols }
+    rows, cols = SafeNcurses.rows, SafeNcurses.cols
+    @buffers.each { |b| b.resize rows - minibuf_lines, cols }
     completely_redraw_screen
     flash "resized to #{rows}x#{cols}"
   end
     ## (currently we only have one buffer visible at a time).
     ## TODO: reenable this if we allow multiple buffers
     false && @buffers.inject(@dirty) do |dirty, buf|
-      dirty ? buf.draw : buf.redraw
-      dirty || buf.dirty?
+      buf.resize SafeNcurses.rows - minibuf_lines, SafeNcurses.cols
+      @dirty ? buf.draw : buf.redraw
     end
     ## quick hack
-    true && (@dirty ? @buffers.last.draw : @buffers.last.redraw)
-    
+    if true
+      buf = @buffers.last
+      buf.resize SafeNcurses.rows - minibuf_lines, SafeNcurses.cols
+      @dirty ? buf.draw : buf.redraw
+    end
+
     draw_minibuf unless skip_minibuf
     @dirty = false
-    Ncurses.doupdate
+    SafeNcurses.doupdate
   end
 
   ## gets the mode from the block, which is only called if the buffer
     realtitle = title
     num = 2
     while @name_map.member? realtitle
-      realtitle = "#{title} #{num}"
+      realtitle = "#{title} <#{num}>"
       num += 1
     end
 
     Redwood::log "spawning buffer \"#{realtitle}\""
-    width = opts[:width] || Ncurses.cols
-    height = opts[:height] || Ncurses.rows - 1
+    width = opts[:width] || SafeNcurses.cols
+    height = opts[:height] || SafeNcurses.rows - 1
 
     ## since we are currently only doing multiple full-screen modes,
     ## use stdscr for each window. once we become more sophisticated,
     ##
     ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
     ## (opts[:left] || 0))
-    w = Ncurses.stdscr
-    raise "nil window" unless w
-    
+    w = SafeNcurses.stdscr
     b = Buffer.new w, mode, width, height, :title => realtitle
     mode.buffer = b
     @name_map[realtitle] = b
   end
 
   def ask domain, question, default=nil
-    @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0,
-                            Ncurses.cols
+    @textfields[domain] ||= TextField.new SafeNcurses.stdscr, SafeNcurses.rows - 1, 0,
+                            SafeNcurses.cols
     tf = @textfields[domain]
 
     ## this goddamn ncurses form shit is a fucking 1970's
     ret = nil
     @freeze = true
     tf.position_cursor
-    Ncurses.refresh
-    while tf.handle_input(Ncurses.nonblocking_getch); end
+    SafeNcurses.refresh
+    while tf.handle_input(SafeNcurses.nonblocking_getch); end
     @freeze = false
 
     ret = tf.value
     accept = accept.split(//).map { |x| x[0] } if accept
 
     flash question
-    Ncurses.curs_set 1
-    Ncurses.move Ncurses.rows - 1, question.length + 1
-    Ncurses.refresh
+    SafeNcurses.curs_set 1
+    SafeNcurses.move SafeNcurses.rows - 1, question.length + 1
+    SafeNcurses.refresh
 
     ret = nil
     done = false
     @freeze = true
     until done
-      key = Ncurses.nonblocking_getch
+      key = SafeNcurses.nonblocking_getch
       if key == Ncurses::KEY_CANCEL
         done = true
       elsif (accept && accept.member?(key)) || !accept
       end
     end
     @freeze = false
-    Ncurses.curs_set 0
+    SafeNcurses.curs_set 0
     erase_flash
     draw_screen
-    Ncurses.curs_set 0
+    SafeNcurses.curs_set 0
 
     ret
   end
     end
   end
 
+  def minibuf_lines; [(@flash ? 1 : 0) + @minibuf_stack.compact.size, 1].max; end
+  
   def draw_minibuf
-    s = @flash || @minibuf_stack.reverse.find { |x| x } || ""
-
-    Ncurses.attrset Colormap.color_for(:none)
-    Ncurses.mvaddstr Ncurses.rows - 1, 0, s + (" " * [Ncurses.cols - s.length,
-                                                      0].max)
+    SafeNcurses.attrset Colormap.color_for(:none)
+    m = @minibuf_stack.compact
+    m << @flash if @flash
+    m << "" if m.empty?
+    m.each_with_index do |s, i|
+      SafeNcurses.mvaddstr SafeNcurses.rows - i - 1, 0, s + (" " * [SafeNcurses.cols - s.length, 0].max)
+    end
   end
 
   def say s, id=nil
     @minibuf_stack[id] = s
     unless @freeze
       draw_screen
-      Ncurses.refresh
+      SafeNcurses.refresh
     end
     if block_given?
-      yield
-      clear id
-      return
+      begin
+        yield
+      ensure
+        clear id
+      end
     end
     id
   end
     @flash = s
     unless @freeze
       draw_screen
-      Ncurses.refresh
+      SafeNcurses.refresh
     end
   end
 
+  ## a little tricky because we can't just delete_at id because ids
+  ## are relative (they're positions into the array).
   def clear id
     @minibuf_stack[id] = nil
     if id == @minibuf_stack.length - 1
         @minibuf_stack.delete_at i
       end
     end
+
     unless @freeze
       draw_screen
-      Ncurses.refresh
+      SafeNcurses.refresh
     end
   end
 
   def shell_out command
     @freeze = true
-    Ncurses.endwin
+    SafeNcurses.endwin
     system command
-    Ncurses.refresh
-    Ncurses.curs_set 0
+    SafeNcurses.refresh
+    SafeNcurses.curs_set 0
     @freeze = false
   end
 end
 
 require 'uri'
 require 'net/imap'
 require 'stringio'
+require 'time'
 
 ## fucking imap fucking sucks. what the FUCK kind of committee of
 ## dunces designed this shit.
 
-## you see, imap touts 'unique ids' for messages, which are to be used
-## for cross-session identification. great, just what sup needs! only,
-## it turns out the uids can be invalidated every time some arbitrary
-## 'uidvalidity' value changes on the server, and 'uidvalidity' has no
-## restrictions. it can change any time you log in. it can change
-## EVERY time you log in. of course the imap spec "strongly
+## imap talks about 'unique ids' for messages, to be used for
+## cross-session identification. great---just what sup needs! except
+## it turns out the uids can be invalidated every time the
+## 'uidvalidity' value changes on the server, and 'uidvalidity' can
+## change without restriction. it can change any time you log in. it
+## can change EVERY time you log in. of course the imap spec "strongly
 ## recommends" that it never change, but there's nothing to stop
-## people from just setting it to the current time, and in fact that's
-## exactly what the one imap server i have at my disposal does. thus
-## the so-called uids are absolutely useless and imap provides no
-## cross-session way of uniquely identifying a message. but thanks for
-## the "strong recommendation", guys!
-
-## right now i'm using the 'internal date' and the size of each
-## message to uniquely identify it, and i have to scan over the entire
-## mailbox each time i open it to map those things to message ids, and
-## we'll just hope that there are no collisions. ho ho! that's a
-## perfectly reasonable solution!
-
-## fuck you imap committee. you managed to design something as shitty
+## people from just setting it to the current timestamp, and in fact
+## that's exactly what the one imap server i have at my disposal
+## does. thus the so-called uids are absolutely useless and imap
+## provides no cross-session way of uniquely identifying a
+## message. but thanks for the "strong recommendation", guys!
+
+## so right now i'm using the 'internal date' and the size of each
+## message to uniquely identify it, and i scan over the entire mailbox
+## each time i open it to map those things to message ids. that can be
+## slow for large mailboxes, and we'll just have to hope that there
+## are no collisions. ho ho! a perfectly reasonable solution!
+
+## fuck you, imap committee. you managed to design something as shitty
 ## as mbox but goddamn THIRTY YEARS LATER.
 
 module Redwood
 
 class IMAP < Source
   attr_reader_cloned :labels
-  
+  attr_accessor :username, :password
+
   def initialize uri, username, password, last_idate=nil, usual=true, archived=false, id=nil
     raise ArgumentError, "username and password must be specified" unless username && password
     raise ArgumentError, "not an imap uri" unless uri =~ %r!imaps?://!
     @labels = [:unread]
     @labels << :inbox unless archived?
     @labels << mailbox.intern unless mailbox =~ /inbox/i || mailbox.nil?
+    @mutex = Mutex.new
   end
 
   def connect
     Redwood::log "connecting to #{@parsed_uri.host} port #{ssl? ? 993 : 143}, ssl=#{ssl?} ..."
     sid = BufferManager.say "Connecting to IMAP server #{host}..." if BufferManager.instantiated?
 
-    ::Thread.new do
+    Redwood::reporting_thread do
       begin
         #raise Net::IMAP::ByeResponseError, "simulated imap failure"
-        @imap = Net::IMAP.new host, ssl? ? 993 : 143, ssl?
+        # @imap = Net::IMAP.new host, ssl? ? 993 : 143, ssl?
+        sleep 3
         BufferManager.say "Logging in...", sid if BufferManager.instantiated?
-        @imap.authenticate 'LOGIN', @username, @password
+        # @imap.authenticate 'LOGIN', @username, @password
+        sleep 3
         BufferManager.say "Sizing mailbox...", sid if BufferManager.instantiated?
-        @imap.examine mailbox
-        last_id = @imap.responses["EXISTS"][-1]
-
+        # @imap.examine mailbox
+        # last_id = @imap.responses["EXISTS"][-1]
+        sleep 1
+        
         BufferManager.say "Reading headers (because IMAP sucks)...", sid if BufferManager.instantiated?
-        values = @imap.fetch(1 .. last_id, ['RFC822.SIZE', 'INTERNALDATE'])
-
+        # values = @imap.fetch(1 .. last_id, ['RFC822.SIZE', 'INTERNALDATE'])
+        sleep 3
+        
+        raise Net::IMAP::ByeResponseError, "simulated imap failure"
         Redwood::log "successfully connected to #{@parsed_uri}"
-
+        
         values.each do |v|
-          msize, mdate = v.attr['RFC822.SIZE'], Time.parse(v.attr["INTERNALDATE"])
-          id = sprintf("%d.%07d", mdate.to_i, msize).to_i
+          id = make_id v
           @ids << id
           @imap_ids[id] = v.seqno
         end
       end
     end.join
 
+    @mutex.unlock
     !!@imap
   end
   private :connect
 
+  def make_id imap_stuff
+    msize, mdate = imap_stuff.attr['RFC822.SIZE'], Time.parse(imap_stuff.attr["INTERNALDATE"])
+    sprintf("%d.%07d", mdate.to_i, msize).to_i
+  end
+  private :make_id
+
   def host; @parsed_uri.host; end
   def mailbox; @parsed_uri.path[1..-1] end ##XXXX TODO handle nil
   def ssl?; @parsed_uri.scheme == 'imaps' end
 
   ## load the full header text
   def raw_header id
-    connect or raise SourceError, broken_msg
-    get_imap_field(id, 'RFC822.HEADER').gsub(/\r\n/, "\n")
+    @mutex.synchronize do
+      connect or raise SourceError, broken_msg
+      get_imap_field(id, 'RFC822.HEADER').gsub(/\r\n/, "\n")
+    end
   end
 
   def raw_full_message id
-    connect or raise SourceError, broken_msg
-    get_imap_field(id, 'RFC822').gsub(/\r\n/, "\n")
+    @mutex.synchronize do
+      connect or raise SourceError, broken_msg
+      get_imap_field(id, 'RFC822').gsub(/\r\n/, "\n")
+    end
   end
 
   def get_imap_field id, field
-    imap_id = @imap_ids[id] or raise SourceError, "Unknown message id #{id}. It is likely that messages have been deleted from this IMAP mailbox. Please run sup-import --rebuild #{to_s} in order to correct this problem."
-
-    f = 
+    f = nil
+    @mutex.synchronize do
+      imap_id = @imap_ids[id] or raise SourceError, "Unknown message id #{id}. It is likely that messages have been deleted from this IMAP mailbox."
       begin
-        @imap.fetch imap_id, field
+        f = @imap.fetch imap_id, [field, 'RFC822.SIZE', 'INTERNALDATE']
+        got_id = make_id f
+        raise SourceError, "IMAP message mismatch: requested #{id}, got #{got_id}. It is likely the IMAP mailbox has been modified." unless got_id == id
       rescue Net::IMAP::Error => e
         raise SourceError, e.message
       end
-    raise SourceError, "null IMAP field '#{field}' for message with id #{id} imap id #{imap_id}" if f.nil?
+      raise SourceError, "null IMAP field '#{field}' for message with id #{id} imap id #{imap_id}" if f.nil?
+    end
     f[0].attr[field]
   end
   private :get_imap_field
   
   def each
-    connect or raise SourceError, broken_msg
+    @mutex.synchronize { connect or raise SourceError, broken_msg }
 
     start = @ids.index(cur_offset || start_offset)
     start.upto(@ids.length - 1) do |i|
   end
 
   def start_offset
-    connect or raise SourceError, broken_msg
+    @mutex.synchronize { connect or raise SourceError, broken_msg }
     @ids.first
   end
   def end_offset
-    connect or raise SourceError, broken_msg
+    @mutex.synchronize { connect or raise SourceError, broken_msg }
     @ids.last
   end
 end
 
 class Index
   include Singleton
 
-  attr_reader :index # debugging only
-  
+  attr_reader :index
   def initialize dir=BASE_DIR
     @dir = dir
     @sources = {}
 
   def source_for name; @sources.values.find { |s| s.is_source_for? name }; end
   def usual_sources; @sources.values.find_all { |s| s.usual? }; end
+  def sources; @sources.values; end
 
   def load_index dir=File.join(@dir, "ferret")
     if File.exists? dir
 
   end
 
   def user_labels; @labels.keys; end
-
   def << t; @labels[t] = true unless @labels.member?(t) || RESERVED_LABELS.member?(t); end
-
   def delete t; @labels.delete t; end
-
   def save
     File.open(@fn, "w") { |f| f.puts @labels.keys }
   end
 
 ## straight through the mbox (an import) or we're reading a few
 ## messages at a time (viewing messages) so the latency is not a problem.
 
-## all of the methods here catch SSHFileErrors, SocketErrors, and
-## Net::SSH::Exceptions and reraise them as SourceErrors. due to this
-## and to the logging, this class is somewhat tied to Sup, but it
-## wouldn't be too difficult to remove those bits and make it more
-## general-purpose.
+## all of the methods here can throw SSHFileErrors, SocketErrors,
+## Net::SSH::Exceptions and Errno::ENOENTs.
 
 ## debugging TODO: remove me
 def debug s
     @ssh_opts = ssh_opts
     @file_size = nil
     @offset = 0
+    @say_id = nil
+    @broken_msg = nil
+  end
+
+  def broken?; !@broken_msg.nil?; end
+
+  def say s
+    @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
+    Redwood::log s
+  end
+  private :say
+
+  def shutup
+    BufferManager.clear @say_id if BufferManager.instantiated?
+    @say_id = nil
   end
 
   def connect
     return if @session
+    raise SSHFileError, @broken_msg if broken?
 
-    Redwood::log "starting SSH session to #@host for #@fn..."
-    sid = BufferManager.say "Connecting to SSH host #{@host}..." if BufferManager.instantiated?
+    say "Opening SSH connection to #{@host}..."
 
     begin
-      @session = Net::SSH.start @host, @ssh_opts
-      MBox::debug "starting SSH shell..."
-      BufferManager.say "Starting SSH shell...", sid if BufferManager.instantiated?
-      @shell = @session.shell.sync
-      MBox::debug "checking for file existence..."
+      #raise SSHFileError, "simulated SSH file error"
+      #@session = Net::SSH.start @host, @ssh_opts
+      sleep 3
+      say "Starting SSH shell..."
+      # @shell = @session.shell.sync
+      sleep 3
+      say "Checking for #@fn..."
+      sleep 1
+      raise Errno::ENOENT, @fn
       raise Errno::ENOENT, @fn unless @shell.test("-e #@fn").status == 0
-      MBox::debug "SSH is ready"
-    ensure 
-      BufferManager.clear sid if BufferManager.instantiated?
+    ensure
+      shutup
     end
   end
 
-  def eof?; raise "offset #@offset size #{size}" unless @offset && size; @offset >= size; end
-  def eof; eof?; end # lame but IO does this and rmail depends on it
-  def seek loc; raise "nil" unless loc; @offset = loc; end
+  def eof?; @offset >= size; end
+  def eof; eof?; end # lame but IO's method is named this and rmail calls that
+  def seek loc; @offset = loc; end
   def tell; @offset; end
   def total; size; end
 
 
   def gets
     return nil if eof?
-
     make_buf_include @offset
     expand_buf_forward while @buf.index("\n", @offset).nil? && @buf.endd < size
-
-    with(@buf[@offset .. (@buf.index("\n", @offset) || -1)]) { |line| @offset += line.length }
+    returning(@buf[@offset .. (@buf.index("\n", @offset) || -1)]) { |line| @offset += line.length }
   end
 
   def read n
       begin
         result = @shell.send_command cmd
         raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{result.stderr[0 .. 100]}" unless result.status == 0
-
       rescue Net::SSH::Exception # these happen occasionally for no apparent reason. gotta love that nondeterminism!
         retry if (retries += 1) < 3
         raise
       end
-      result.stdout
-    rescue Net::SSH::Exception, SocketError, Errno::ENOENT => e
-      @session = nil
-      Redwood::log "error connecting to SSH server: #{e.message}"
-      raise SourceError, "error connecting to SSH server: #{e.message}"
+    rescue Net::SSH::Exception, SSHFileError, Errno::ENOENT => e
+      @broken_msg = e.message
+      raise
     end
+    result.stdout
   end
 
   def get_bytes offset, size
-    #MBox::debug "! request for [#{offset}, #{offset + size}); buf is #@buf"
-    raise "wtf: offset #{offset} size #{size}" if size == 0 || offset < 0
     do_remote "tail -c +#{offset + 1} #@fn | head -c #{size}", size
   end
 
 
 module Redwood
 module MBox
 
+## this is slightly complicated because SSHFile (and thus @f or
+## @loader) can throw a variety of exceptions, and we need to catch
+## those, reraise them as SourceErrors, and set ourselves as broken.
+
 class SSHLoader < Source
   attr_reader_cloned :labels
+  attr_accessor :username, :password
 
   def initialize uri, username=nil, password=nil, start_offset=nil, usual=true, archived=false, id=nil
     raise ArgumentError, "not an mbox+ssh uri: #{uri.inspect}" unless uri =~ %r!^mbox\+ssh://!
   end
 
   def host; @parsed_uri.host; end
-  def filename; @parsed_uri.path[1..-1] end ##XXXX TODO handle nil
+  def filename; @parsed_uri.path[1..-1] end
 
   def next
-    offset, labels = @loader.next
-    self.cur_offset = @loader.cur_offset  # only necessary because YAML is a PITA
-    [offset, (labels + @labels).uniq]
+    return if broken?
+    begin
+      offset, labels = @loader.next
+      self.cur_offset = @loader.cur_offset # superclass keeps @cur_offset which is used by yaml
+      [offset, (labels + @labels).uniq] # add our labels
+    rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
+      recover_from e
+    end
+  end
+
+  def end_offset
+    begin
+      @f.size
+    rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
+      recover_from e
+    end
   end
 
-  def end_offset; @f.size; end
   def cur_offset= o; @cur_offset = @loader.cur_offset = o; @dirty = true; end
   def id; @loader.id; end
   def id= o; @id = @loader.id = o; end
-  def cur_offset; @loader.cur_offset; end
+  # def cur_offset; @loader.cur_offset; end # think we'll be ok without this
   def to_s; @parsed_uri.to_s; end
 
-  defer_all_other_method_calls_to :loader
+  def recover_from e
+    m = "error communicating with SSH server #{host} (#{e.class.name}): #{e.message}"
+    Redwood::log m
+    self.broken_msg = @loader.broken_msg = m
+    raise SourceError, m
+  end
+
+  [:start_offset, :load_header, :load_message, :raw_header, :raw_full_message].each do |meth|
+    define_method meth do |*a|
+      begin
+        @loader.send meth, *a
+      rescue Net::SSH::Exception, SocketError, SSHFileError, Errno::ENOENT => e
+        recover_from e
+      end
+    end
+  end
 end
 
 Redwood::register_yaml(SSHLoader, %w(uri username password cur_offset usual archived id))
 
     <<EOS
 #@snippet...
 
-***********
-** ERROR **
-***********
-
-An error occurred while loading this message. It is possible that the source
-has changed, or (in the case of remote sources) is down.
+***********************************************************************
+* An error occurred while loading this message. It is possible that   *
+* the source has changed, or (in the case of remote sources) is down. *
+***********************************************************************
 
 The error message was:
   #{msg}
 
     @botline = [@topline + buffer.content_height, lines].min
   end
 
+  def resize *a
+    super *a
+    ensure_mode_validity
+  end
+
 protected
 
   def draw_line ln, opts={}
 
 
     ## TODO: don't regen text completely
     Redwood::reporting_thread do
-      Redwood::log "loading messages for thread"
       mode = ThreadViewMode.new t, @hidden_labels
       BufferManager.spawn t.subj, mode
       BufferManager.draw_screen
 
         UpdateManager.relay :read, m
       end
     end
-
-    Redwood::log "releasing chunks and text from \"#{buffer.title}\""
     @messages = @chunks = @text = nil
   end
 
 
   ## reraise them as source errors.
 
   bool_reader :usual, :archived, :dirty
-  attr_reader :cur_offset, :broken_msg
+  attr_reader :uri, :cur_offset, :broken_msg
   attr_accessor :id
 
   def initialize uri, initial_offset=nil, usual=true, archived=false, id=nil
   def is_source_for? s; to_s == s; end
 
   def each
+    return if broken?
     begin
       self.cur_offset ||= start_offset
       until done? || broken? # just like life!
         raise "no message" unless n
         yield n, labels
       end
-    rescue SourceError
-      # just die
+    rescue SourceError => e
+      self.broken_msg = e.message
     end
   end
 
 
   def broken_msg= m
     @broken_msg = m
-    Redwood::log "#{to_s}: #{m}"
+#    Redwood::log "#{to_s}: #{m}"
   end
 end
 
 
   ##
   ## i'm sure there's pithy comment i could make here about the
   ## superiority of lisp, but fuck lisp.
-  def with x; yield x; x; end
+  def returning x; yield x; x; end
 end
 
 class String