]> git.notmuchmail.org Git - sup/commitdiff
Merge branch 'labels-before-subj'
authorWilliam Morgan <wmorgan-sup@masanjin.net>
Wed, 24 Jun 2009 13:35:15 +0000 (09:35 -0400)
committerWilliam Morgan <wmorgan-sup@masanjin.net>
Wed, 24 Jun 2009 13:35:15 +0000 (09:35 -0400)
43 files changed:
.gitignore
CONTRIBUTORS
History.txt
Manifest.txt
README.txt
Rakefile
ReleaseNotes
bin/sup
bin/sup-recover-sources
bin/sup-sync
bin/sup-tweak-labels
doc/FAQ.txt
doc/NewUserGuide.txt
lib/sup.rb
lib/sup/buffer.rb
lib/sup/draft.rb
lib/sup/imap.rb
lib/sup/index.rb
lib/sup/keymap.rb
lib/sup/maildir.rb
lib/sup/mbox.rb
lib/sup/mbox/loader.rb
lib/sup/message-chunks.rb
lib/sup/message.rb
lib/sup/mode.rb
lib/sup/modes/edit-message-mode.rb
lib/sup/modes/inbox-mode.rb
lib/sup/modes/reply-mode.rb
lib/sup/modes/scroll-mode.rb
lib/sup/modes/search-results-mode.rb
lib/sup/modes/thread-index-mode.rb
lib/sup/modes/thread-view-mode.rb
lib/sup/poll.rb
lib/sup/sent.rb
lib/sup/source.rb
lib/sup/undo.rb [new file with mode: 0644]
lib/sup/util.rb
sup.gemspec [deleted file]
test/dummy_source.rb
test/test_header_parsing.rb [new file with mode: 0644]
test/test_mbox_parsing.rb [deleted file]
test/test_message.rb
www/index.html

index b8d3bcd9db2d341bdbfd2ac740281e33f5efa77c..082016036f6d6be2ad5a79435e657c7f1fa24388 100644 (file)
@@ -3,3 +3,5 @@
 .ditz-config
 # i use emacs
 *~
+# i use rake package task
+pkg/
index 56f553fe333252beb2cc07a8646344838db72542..f75015304c5bcf0fc3e256c5aba070a519bb64c9 100644 (file)
@@ -1,17 +1,22 @@
-William Morgan <wmorgan-sup at the masanjin dot nets>
-Ismo Puustinen <ismo at the iki dot fis>
 Nicolas Pouillard <nicolas.pouillard at the gmail dot coms>
+Mike Stipicevic <stipim at the rpi dot edus>
 Marcus Williams <marcus-sup at the bar-coded dot nets>
 Lionel Ott <white.magic at the gmx dot des>
+Ingmar Vanhassel <ingmar at the exherbo dot orgs>
 Mark Alexander <marka at the pobox dot coms>
 Christopher Warrington <chrisw at the rice dot edus>
-Marc Hartstein <marc.hartstein at the alum.vassar dot edus>
+Richard Brown <rbrown at the exherbo dot orgs>
 Ben Walton <bwalton at the artsci.utoronto dot cas>
+Marc Hartstein <marc.hartstein at the alum.vassar dot edus>
 Grant Hollingworth <grant at the antiflux dot orgs>
 Steve Goldman <sgoldman at the tower-research dot coms>
 Decklin Foster <decklin at the red-bean dot coms>
+Ismo Puustinen <ismo at the iki dot fis>
 Jeff Balogh <its.jeff.balogh at the gmail dot coms>
+Alex Vandiver <alexmv at the mit dot edus>
 Giorgio Lando <patroclo7 at the gmail dot coms>
 Israel Herraiz <israel.herraiz at the gmail dot coms>
 Ian Taylor <ian at the lorf dot orgs>
+Stefan Lundström <lundst at the snabb.(none)>
 Rich Lane <rlane at the club.cc.cmu dot edus>
+Kirill Smelkov <kirr at the landau.phys.spbu dot rus>
index 2813c6448459e7e76938a63d043e6c8e509e6031..5cdd5a626320b9206f5335c7a7550ee6dc3d8061 100644 (file)
@@ -1,3 +1,27 @@
+== 0.8.1 / 2009-06-15
+* make multibyte display "work" for non-utf8 locales
+* fix reply-mode always selecting "Customized"
+* reduce email quote parsing worst-case behavior
+
+== 0.8 / 2009-06-05
+* Undo support on many operations. Yay!
+* Mbox splitting fixes. No more "From "-line problems.
+* Mail parsing speedups.
+* Many utf8 and widechar fixes. Display of crazy characters should be pretty
+  close.
+* Outgoing email with non-ASCII headers is now properly encoded.
+* Email addresses are no longer permanently attached to names. This was
+  causing problems with automated email systems that used different names
+  with the same address.
+* Curses background now retains the terminal default color. This also makes
+  Sup work better on transparent terminals.
+* Improve dynamic loading of setlocale for Cygwin and BSD systems.
+* Labels can now be removed from multiple tagged threads.
+* Applying operations to tagged threads is now invoked with '='.
+* Buffer list is betterified and is now invoked with ';'.
+* Zsh autocompletion support.
+* As always, many bugfixes and tweaks.
+
 == 0.7 / 2009-03-16
 * Ferret index corruption issues fixed (hopefully!)
 * Text entry now scrolls to the right on overflow, i.e. is actually usable
index 6ecfe670d07649cf288cfadcfe78f3ce71cd4731..be633d776e2b1e5165edeac4d7631cf714471207 100644 (file)
@@ -71,5 +71,6 @@ lib/sup/suicide.rb
 lib/sup/tagger.rb
 lib/sup/textfield.rb
 lib/sup/thread.rb
+lib/sup/undo.rb
 lib/sup/update.rb
 lib/sup/util.rb
index 9a4490acabe3da4e986d470e46bea0b0da3c49e4..4204270a3091ef9510be29bf6886e04f7981a900 100644 (file)
@@ -13,22 +13,20 @@ Sup makes it easy to:
 - Handle massive amounts of email.
 
 - Mix email from different sources: mbox files (even across different
-  machines), Maildir directories, IMAP folders, POP accounts, and
-  GMail accounts.
+  machines), Maildir directories, IMAP folders, and GMail accounts.
 
-- Instantaneously search over your entire email collection. Search
-  over body text, or use a query language to combine search
-  predicates in any way.
+- Instantaneously search over your entire email collection. Search over
+  body text, or use a query language to combine search predicates in any
+  way.
 
 - Handle multiple accounts. Replying to email sent to a particular
-  account will use the correct SMTP server, signature, and from
-  address.
+  account will use the correct SMTP server, signature, and from address.
 
-- Add custom code to handle certain types of messages or to handle
-  certain types of text within messages.
+- Add custom code to customize Sup to whatever particular and bizarre
+  needs you may have.
 
-- Organize email with user-defined labels, automatically track
-  recent contacts, and much more!
+- Organize email with user-defined labels, automatically track recent
+  contacts, and much more!
 
 The goal of Sup is to become the email client of choice for nerds
 everywhere.
@@ -48,40 +46,41 @@ Features:
   message level. Entire threads are manipulated and viewed (with
   redundancies removed) at a time.
 
-- Labels instead of folders. Drop that tired old metaphor and you'll
-  see how much easier it is to organize email.
+- Labels instead of folders. Drop that tired old metaphor and you'll see
+  how much easier it is to organize email.
 
-- GMail-style thread management (but better!). Archive a thread, and
-  it will disappear from your inbox until someone replies. Kill a
-  thread, and it will never come back to your inbox (but will still
-  show up in searches.) Mark a thread as spam and you'll never again
-  see it unless explicitly searching for spam.
+- GMail-style thread management. Archive a thread, and it will disappear
+  from your inbox until someone replies. Kill a thread, and it will
+  never come back to your inbox (but will still show up in searches.)
+  Mark a thread as spam and you'll never again see it unless explicitly
+  searching for spam.
 
 - Console based interface. No mouse clicking required!
 
-- Programmability. It's in Ruby. The code is good. It's easy to
-  extend.
+- Programmability. It's in Ruby. The code is good. It has an extensive
+  hook system that makes it easy to extend and customize.
 
-- Multiple buffer support. Why be limited to viewing one thread at a
+- Multiple buffer support. Why be limited to viewing one thing at a
   time?
 
-- Tons of other little features, like automatic context-sensitive
-  help, multi-message operations, MIME attachment viewing, recent
-  contact list generation, etc.
+- Tons of other little features, like automatic context-sensitive help,
+  multi-message operations, MIME attachment viewing, recent contact list
+  generation, etc.
 
 Current limitations which will be fixed:
 
-- Support for mbox, remote mbox, and IMAP only at this point. No
-  support for POP, mh, or GMail mailstores.
+- Sup doesn't play nicely with other mail clients. If you alter a mail
+  source (read, move, delete, etc) with another client Sup will punish
+  you with a lengthy reindexing process.
 
-- No internationalization support. No wide characters, no subject
-  demangling. 
+- Support for mbox, Maildir, and IMAP only at this point. No support for
+  POP or mh.
 
-- Unix-centrism in MIME attachment handling and in sendmail
-  invocation.
+- IMAP support is very slow due mostly to Ruby's IMAP library.  You may
+  consider something like offlineimap to mirror your IMAP folders with
+  local Maildir ones.
 
-- Several obvious missing features, like undo, filters / saved
-  searches, message annotations, etc.
+- Unix-centrism in MIME attachment handling and in sendmail invocation.
 
 == SYNOPSYS:
 
@@ -90,21 +89,23 @@ Current limitations which will be fixed:
 
   Note that Sup never changes the contents of any mailboxes; it only
   indexes in to them. So it shouldn't ever corrupt your mail. The flip
-  side is that if you change a mailbox (e.g. delete messages, or, in
-  the case of mbox files, read an unread message) then Sup will be
-  unable to load messages from that source and will ask you to run
-  sup-sync --changed.
+  side is that if you change a mailbox (e.g. delete messages, or, in the
+  case of mbox files, read an unread message) then Sup will be unable to
+  load messages from that source and will ask you to run sup-sync
+  --changed.
 
 == REQUIREMENTS:
 
-* ferret >= 0.10.13
-* ncurses
-* rmail
-* highline
-* net-ssh
-* trollop >= 1.7
-* lockfile
-* mime-types
+ - ferret >= 0.11.6
+ - ncurses >= 0.9.1
+ - rmail >= 0.17
+ - highline
+ - net-ssh
+ - trollop >= 1.12
+ - lockfile
+ - mime-types
+ - gettext
+ - fastthread
 
 == INSTALL:
 
index 3b1d9f4fa82953e74a5d8af946591f6468749003..67cd0d21df9ca2e6b1ed14e6a873cc1fdabf742b 100644 (file)
--- a/Rakefile
+++ b/Rakefile
@@ -29,12 +29,38 @@ task :upload_report do |t|
   sh "rsync -essh -cavz ditz wmorgan@rubyforge.org:/var/www/gforge-projects/sup/"
 end
 
-task :gem do |t|
-  sh "gem1.8 build sup.gemspec"
+$:.push "lib"
+require 'rubygems'
+require "sup-files"
+require "sup-version"
+require 'rake/gempackagetask.rb'
+
+spec = Gem::Specification.new do |s|
+  s.name = %q{sup}
+  s.version = SUP_VERSION
+  s.date = Time.now.to_s
+  s.authors = ["William Morgan"]
+  s.email = %q{wmorgan-sup@masanjin.net}
+  s.summary = %q{A console-based email client with the best features of GMail, mutt, and emacs. Features full text search, labels, tagged operations, multiple buffers, recent contacts, and more.}
+  s.homepage = %q{http://sup.rubyforge.org/}
+  s.description = %q{Sup is a console-based email client for people with a lot of email. It supports tagging, very fast full-text search, automatic contact-list management, and more. If you're the type of person who treats email as an extension of your long-term memory, Sup is for you.  Sup makes it easy to: - Handle massive amounts of email.  - Mix email from different sources: mbox files (even across different machines), Maildir directories, IMAP folders, POP accounts, and GMail accounts.  - Instantaneously search over your entire email collection. Search over body text, or use a query language to combine search predicates in any way.  - Handle multiple accounts. Replying to email sent to a particular account will use the correct SMTP server, signature, and from address.  - Add custom code to handle certain types of messages or to handle certain types of text within messages.  - Organize email with user-defined labels, automatically track recent contacts, and much more!  The goal of Sup is to become the email client of choice for nerds everywhere.}
+  s.files = SUP_FILES
+  s.executables = SUP_EXECUTABLES
+
+  s.add_dependency "ferret", ">= 0.11.6"
+  s.add_dependency "ncurses", ">= 0.9.1"
+  s.add_dependency "rmail", ">= 0.17"
+  s.add_dependency "highline"
+  s.add_dependency "net-ssh"
+  s.add_dependency "trollop", ">= 1.12"
+  s.add_dependency "lockfile"
+  s.add_dependency "mime-types", "~> 1"
+  s.add_dependency "gettext"
+  s.add_dependency "fastthread"
 end
 
-task :tarball do |t|
-  require "sup-files"
-  require "sup-version"
-  sh "tar cfvz sup-#{SUP_VERSION}.tgz #{SUP_FILES.join(' ')}"
+Rake::GemPackageTask.new(spec) do |pkg|
+    pkg.need_tar = true
 end
+
+task :tarball => ["pkg/sup-#{SUP_VERSION}.tgz"]
index a965fa0ec099b1ab7cfa3afa21ee5c64d97ad07a..1923a6ef6c8f08b5b2dfcebd9cd83e3c9df95bc1 100644 (file)
@@ -1,3 +1,14 @@
+Release 0.8.1:
+
+A bugfix release with fixes for quote parsing (bad behavior in certain long
+emails), multibyte display for non-utf8 locales, and reply-mode mode selection.
+
+Release 0.8:
+
+The big wins are undo support, mbox splitting fixes, and the various UI
+speedups and bugfixes. Parsing new email should also be faster, although
+IMAP remains tragically slow, as usual.
+
 Release 0.7:
 
 The big win in this release is that Ferret index corruption issues should now
diff --git a/bin/sup b/bin/sup
index 0af3d11ecfea671bc299342f6ca410d6cb83bf12..2ea39c6d07499a2c06e59c35827d5a6ce5ae04b9 100755 (executable)
--- a/bin/sup
+++ b/bin/sup
@@ -5,7 +5,6 @@ require 'ncurses'
 require 'curses'
 require 'fileutils'
 require 'trollop'
-require 'fastthread'
 require "sup"
 
 BIN_VERSION = "git"
@@ -89,7 +88,7 @@ end
 ## BSD users: if libc.so.6 is not found, try installing compat6x.
 require 'dl/import'
 module LibC
-  extend DL::Importable
+  extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable
   setlocale_lib = case Config::CONFIG['arch']
     when /darwin/; "libc.dylib"
     when /cygwin/; "cygwin1.dll"
@@ -203,7 +202,7 @@ begin
     end
   end unless $opts[:no_initial_poll]
   
-  imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }
+  imode.load_threads :num => ibuf.content_height, :when_done => lambda { |num| reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }
 
   if $opts[:compose]
     ComposeMode.spawn_nicely :to_default => $opts[:compose]
index af39b7df4ed466afa3907cc93128a83ef0eb08dc..d3b1424de0778bb6350bc4fd53dbbd3bff4602f9 100755 (executable)
@@ -72,18 +72,14 @@ ARGV.each do |fn|
   source_ids = {}
   count = 0
   source.each do |offset, labels|
-    begin
-      m = Redwood::Message.new :source => source, :source_info => offset
-      docid, entry = index.load_entry_for_id m.id
-      next unless entry
-      #puts "# #{source} #{offset} #{entry[:source_id]}"
-
-      source_ids[entry[:source_id]] = (source_ids[entry[:source_id]] || 0) + 1
-      count += 1
-      break if count == $opts[:scan_num]
-    rescue Redwood::MessageFormatError => e
-      puts "# #{e.message}"
-    end
+    m = Redwood::Message.new :source => source, :source_info => offset
+    docid, entry = index.load_entry_for_id m.id
+    next unless entry
+    #puts "# #{source} #{offset} #{entry[:source_id]}"
+
+    source_ids[entry[:source_id]] = (source_ids[entry[:source_id]] || 0) + 1
+    count += 1
+    break if count == $opts[:scan_num]
   end
 
   if source_ids.size == 1
index 4a0482a9951e6967999ecf5b56e262edf3f77304..9c342d25693f908e26825c3d7c4348e0b5ba84ee 100755 (executable)
@@ -184,6 +184,14 @@ begin
         ## nothin! use default source labels
       end
 
+      if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
+        last_info_time = Time.now
+        elapsed = last_info_time - start_time
+        pctdone = source.respond_to?(:pct_done) ? source.pct_done : 100.0 * (source.cur_offset.to_f - source.start_offset).to_f / (source.end_offset - source.start_offset).to_f
+        remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
+        $stderr.printf "## read %dm (about %.0f%%) @ %.1fm/s. %s elapsed, about %s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
+      end
+
       if index_state.nil?
         puts "Adding message #{source}##{offset} from #{m.from} with state {#{m.labels * ', '}}" if opts[:verbose]
         num_added += 1
index 8a8152d60b18db9c0a0dcfd45637e9070ece1293..538db8b38ac402604c6ef5b1f24475895ee7635d 100755 (executable)
@@ -81,19 +81,15 @@ begin
   end
   query += ' ' + opts[:query] if opts[:query]
 
-  qobj, opts = Redwood::Index.parse_user_query_string query
-  query = Redwood::Index.build_query opts.merge(:qobj => qobj)
-
-  results = index.ferret.search query, :limit => :all
-  num_total = results.total_hits
+  docs = Redwood::Index.run_query query
+  num_total = docs.size
 
   $stderr.puts "Found #{num_total} documents across #{source_ids.length} sources. Scanning..."
 
   num_changed = num_scanned = 0
   last_info_time = start_time = Time.now
-  results.hits.each do |hit|
+  docs.each do |id|
     num_scanned += 1
-    id = hit.doc
 
     m = index.build_message id
     old_labels = m.labels.clone
index 5c85d9c55346f37221dd7ad47ccecf0f483d0946..997a35735b1be65f111150452ef055f7789df717 100644 (file)
@@ -2,21 +2,17 @@ Sup FAQ
 -------
 
 Q: What is Sup?
-A: Sup is a console-based email client for people with a lot of email.
-   It supports tagging, very fast full-text search, automatic contact-
-   list management, custom code insertion via a hook system, and more.
-   If you're the type of person who treats email as an extension of your
-   long-term memory, Sup is for you.
+A: A console-based email client for people with a lot of email.
 
 Q: What does Sup stand for?
 A: "What's up?"
 
 Q: Sup looks like a text-based Gmail.
-A: I stole their ideas. And improved them!
+A: First I stole their ideas. Then I improved them.
 
 Q: Why not just use Gmail?
-A: I wrote Sup because I hate ads, I hate using a mouse, and I hate
-   non-programmability and non-extensibility.
+A: I hate ads, I hate using a mouse, and I hate non-programmability and
+   non-extensibility.
 
    Also, Gmail doesn't let you use a monospace font, which is just
    lame.
@@ -30,8 +26,9 @@ A: Because a keystroke is worth a hundred mouse clicks, as any Unix
 
 Q: How does Sup deal with spam?
 A: You can manually mark messages as spam, which prevents them from
-   showing up in future searches, but that's as far as Sup goes. Spam
-   filtering should be done by a dedicated tool like SpamAssassin.
+   showing up in future searches. Later, you can run a batch process to
+   remove such messages from your sources. That's as far as Sup goes.
+   Spam filtering should be done by a dedicated tool like SpamAssassin.
 
 Q: How do I delete a message?
 A: Why delete? Unless it's spam, you might as well just archive it.
@@ -90,21 +87,13 @@ A: Move the messages from the source to the target using whatever tool
 
 Q: What are all these "Redwood" references I see in the code?
 A: That was Sup's original name. (Think pine, elm. Although I was a
-   Mutt user, I couldn't think of a good progression there.) But it
-   was taken by another project on RubyForge, and wasn't that
-   original, and was too long to type anyways.
-
-   Maybe one day I'll do a huge search-and-replace on the code, but it
-   doesn't seem that important at this point.
+   Mutt user, I couldn't think of a good progression there.) But it was
+   taken by another project on RubyForge, and wasn't that original, and
+   was too long to type anyways.
 
 Common Problems
 ---------------
 
-P: I see this message from Ferret:
-     Error occured in index.c:825 - sis_find_segments_file
-S: Yikes! You've upgraded Ferret and the index format changed beneath
-   you. Follow the index rebuild instructions above.
-
 P: I get some error message from Rubymail about frozen strings when
    importing messages with attachments.
 S: The current solution is to directly modify RubyMail. Change line 159 of
index 6ec8be9457ab6fd006b3c50672d3377a43769151..646b9bfc03a55327e27a8ee86a09a9a332eb9ac8 100644 (file)
@@ -7,11 +7,12 @@ anything into its index yet, and has no idea where to look for them
 anyways.
 
 If you want to play around a little at this point, you can press 'b'
-to cycle between buffers and 'x' to kill a buffer. There's probably
-not too much interesting there, but there's a log buffer with some
-cryptic messages. You can also press '?' at any point to get a list of
-keyboard commands, but in the absence of any email, these will be
-mostly useless. When you get bored, press 'q' to quit.
+to cycle between buffers, ';' to get a list of the open buffers, and
+'x' to kill a buffer. There's probably not too much interesting there,
+but there's a log buffer with some cryptic messages. You can also
+press '?' at any point to get a list of keyboard commands, but in the
+absence of any email, these will be mostly useless. When you get
+bored, press 'q' to quit.
 
 To use Sup for email, we need to load messages into the index. The
 index is where Sup stores all message state (e.g. read or unread, any
@@ -20,18 +21,17 @@ threading messages. Sup only knows about messages in its index.
 
 We can add messages to the index by telling Sup about the "source"
 where the messages reside. Sources are things like IMAP folders, mbox
-folders, maildir directories, and Gmail accounts (in the future). Sup
-doesn't duplicate the actual message content in the index; it only
-stores whatever information is necessary for searching, threading and
-labelling. So when you search for messages or view your inbox, Sup
-talks only to the index (stored locally on disk). When you view a
-thread, Sup requests the full content of all the messages from the
-source.
+folders, and maildir directories. Sup doesn't duplicate the actual
+message content in the index; it only stores whatever information is
+necessary for searching, threading and labelling. So when you search
+for messages or view your inbox, Sup talks only to the index (stored
+locally on disk). When you view a thread, Sup requests the full
+content of all the messages from the source.
 
 The easiest way to set up all your sources is to run `sup-config`.
 This will interactively walk you through some basic configuration,
 prompt you for all the sources you need, and optionally import
-messages from them. Sup-config uses two other tools, sup-add and
+messages from them.  Sup-config uses two other tools, sup-add and
 sup-sync, to load messages into the index. In the future you may make
 use of these tools directly (see below).
 
@@ -49,7 +49,8 @@ results of a search. I mentioned above that your inbox is, by
 definition, the set of all messages that aren't archived. This means
 that your inbox is nothing more than the result of the search for all
 messages with the label "inbox". (It's actually slightly more
-complicated---we omit messages marked as killed, deleted or spam.)
+complicated---we also omit messages marked as killed, deleted or
+spam.)
 
 You could replicate the folder paradigm easily under this scheme, by
 giving each message exactly one label and only viewing the results of
@@ -59,8 +60,7 @@ labels judiciously for things that are too hard to find with search.
 The idea is that a labeling system that allows arbitrary, user-defined
 labels, supplemented by a quick and easy-to-access search mechanism
 provides all the functionality that folders does, plus much more, at a
-far lower cost to the user. (The Sup philosophical treatise has a
-little more on this.)
+far lower cost to the user.
 
 Now let's take a look at your inbox. You'll see that Sup groups
 messages together into threads: each line in the inbox is a thread,
@@ -88,19 +88,25 @@ expand or collapse all messages or 'N' to expand only the new
 messages. You'll also notice that Sup hides quoted text and
 signatures. If you highlight a particular hidden chunk, you can press
 enter to expand it, or you can press 'o' to toggle every hidden chunk
-in a particular message. (Remember, you can hit '?' to see the full
-list of keyboard commands at any point.)
+in a particular message.
 
-A few other useful commands while viewing a thread. Press 'd' to
-toggle a detailed header for a message. If you've scrolled too far to
-the right, press '[' to jump all the way to the left. Finally, you can
-press 'n' and 'p' to jump forward and backward between open messages,
-aligning the display as necessary.
+Other useful keyboard commands when viewing a thread are: 'n' and 'p'
+to jump to the next and previous open messages, 'h' to toggle the
+detailed headers for the current message, and enter to expand or
+collapse the current message (when it's on a text region). Enter and
+'n' in combination are useful for scanning through a thread---press
+enter to close the current message and jump to the next open one, and
+'n' to keep it open and jump. If the buffer is misaligned with a
+message, you can press 'z' to highlight it.
+
+This is a lot to remember, but you can always hit '?' to see the full
+list of keyboard commands at any point. There's a lot of useful stuff
+in there---once you learn some, try out some of the others!
 
 Now press 'x' to kill the thread view buffer. You should see the inbox
-again. If you don't, you can cycle through the buffers by pressing 'b'
-and 'B' (forwards and backwards, respectively), or you can press ';' to
-see a list of all buffers and simply select the inbox.
+again. If you don't, you can cycle through the buffers by pressing
+'b', or you can press ';' to see a list of all buffers and simply
+select the inbox.
 
 There are many operations you can perform on threads beyond viewing
 them. To archive a thread, press 'a'. The thread will disappear from
@@ -108,8 +114,8 @@ your inbox, but will still appear in search results. If someone
 replies an archived thread, it will reappear in your inbox. To kill a
 thread, press '&'. Killed threads will never come back to your inbox,
 even if people reply, but will still be searchable. (This is useful
-for those interminable threads that you really have no interest in,
-but which seem to pop up on every mailing list.)
+for those interminable threads that you really have no immediate
+interest in, but which seem to pop up on every mailing list.)
 
 If a thread is spam, press 'S'. It will disappear and won't come back.
 It won't even appear in search results, unless you explicitly search
@@ -125,8 +131,8 @@ in the labels as a sequence of space-separated words. To cancel the
 input, press Ctrl-G.
 
 Many of these operations can be applied to a group of threads. Press
-'t' to tag a thread. Tag a couple, then press '+' to apply the next
-command to the set of threads. '+t', of course, will untag all tagged
+'t' to tag a thread. Tag a couple, then press '=' to apply the next
+command to the set of threads. '=t', of course, will untag all tagged
 messages.
 
 Ok, let's try using labels and search. Press 'L' to do a quick label
@@ -188,7 +194,7 @@ how to invoke sup-sync when it detects a problem. This is a
 complication you will almost certainly run in to if you use both Sup
 and another MUA on the same source, so it's good to be aware of it.
 
-Have fun, and let me know if you have any problems!
+Have fun, and email sup-talk@rubyforge.org if you have any problems!
 
 Appendix A: sup-add and sup-sync
 ---------------------------------
@@ -233,17 +239,17 @@ Sup-sync will now load all the messages from the source into the
 index. Depending on the size of the source, this may take a while.
 Don't panic! It's a one-time process.
 
-Appendix B: Handling high-volume mailing lists
-----------------------------------------------
-
-Here's what I recommend:
-1. Use procmail to filter messages from the list into a distinct source.
-2. Add that source to Sup as a usual source with auto-archive turned
-   on, and with a label corresponding to the mailing list name.
-   (E.g.: sup-add mbox:/home/me/Mail/ruby-talk -a -l ruby-talk)
-3. Voila! Sup will load new messages into the index but not into the
-   inbox, and you can browse the mailing list traffic at any point by
-   searching for that label.
+Appendix B: Automatically labeling incoming email
+-------------------------------------------------
+
+One option is to filter incoming email into different sources with
+something like procmail, and have each of these sources auto-apply
+labels by using sup-add --labels.
+
+But the better option is to learn Ruby and write a before-add hook.
+This will allow you to apply labels based on whatever crazy logic you
+can come up with. See http://sup.rubyforge.org/wiki/wiki.pl?Hooks for
+examples.
 
 Appendix C: Reading blogs with Sup
 ----------------------------------
index 88eae7fc5037da797616b0301d8005d36eb1b6b4..991bd2dc6f0b35f756f38a1154b04de789d83c0a 100644 (file)
@@ -5,6 +5,10 @@ require 'thread'
 require 'fileutils'
 require 'gettext'
 require 'curses'
+begin
+  require 'fastthread'
+rescue LoadError
+end
 
 class Object
   ## this is for debugging purposes because i keep calling #id on the
@@ -110,6 +114,7 @@ module Redwood
     Redwood::PollManager.new
     Redwood::SuicideManager.new Redwood::SUICIDE_FN
     Redwood::CryptoManager.new
+    Redwood::UndoManager.new
   end
 
   def finish
@@ -242,7 +247,7 @@ end
     Redwood::log "using character set encoding #{$encoding.inspect}"
   else
     Redwood::log "warning: can't find character set by using locale, defaulting to utf-8"
-    $encoding = "utf-8"
+    $encoding = "UTF-8"
   end
 
 ## now everything else (which can feel free to call Redwood::log at load time)
@@ -266,6 +271,7 @@ require "sup/tagger"
 require "sup/draft"
 require "sup/poll"
 require "sup/crypto"
+require "sup/undo"
 require "sup/horizontal-selector"
 require "sup/modes/line-cursor-mode"
 require "sup/modes/help-mode"
index 6f0acf9852ea51d4b45125d035476051ed42d2f4..8eedf9697c26f1aa94c8a6769aa4f7c9e5752406 100644 (file)
@@ -108,10 +108,16 @@ class Buffer
 
     @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
     s ||= ""
-    maxl = @width - x
-    @w.mvaddstr y, x, s[0 ... maxl]
-    unless s.length >= maxl || opts[:no_fill]
-      @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
+    maxl = @width - x # maximum display width width
+    stringl = maxl    # string "length"
+    ## the next horribleness is thanks to ruby's lack of widechar support
+    stringl += 1 while stringl < s.length && s[0 ... stringl].display_length < maxl
+    @w.mvaddstr y, x, s[0 ... stringl]
+    unless opts[:no_fill]
+      l = s.display_length
+      unless l >= maxl
+        @w.mvaddstr(y, x + l, " " * (maxl - l))
+      end
     end
   end
 
index 35fac30ee8cc0915632958abb6579c6ede54ebfa..32266b5374eb3d67b336926a28e8fe75eecf30ce 100644 (file)
@@ -79,9 +79,7 @@ class DraftLoader < Source
   def fn_for_offset o; File.join(@dir, o.to_s); end
 
   def load_header offset
-    File.open fn_for_offset(offset) do |f|
-      return MBox::read_header(f)
-    end
+    File.open(fn_for_offset(offset)) { |f| parse_raw_email_header f }
   end
   
   def load_message offset
index 4eb13f4ed619d3259df4894eec4a8b66c11c326e..7508c2c7b3e64d4fd39f7a8467a380d68128f6d0 100644 (file)
@@ -93,7 +93,7 @@ class IMAP < Source
   def == o; o.is_a?(IMAP) && o.uri == self.uri && o.username == self.username; end
 
   def load_header id
-    MBox::read_header StringIO.new(raw_header(id))
+    parse_raw_email_header StringIO.new(raw_header(id))
   end
 
   def load_message id
index 0c62d310d3286499e64361f8c7d14c58932d3e03..ca01ee76cc89c118933c5122b616b4b6f45a59bb 100644 (file)
@@ -2,7 +2,6 @@
 
 require 'fileutils'
 require 'ferret'
-require 'fastthread'
 
 begin
   require 'chronic'
@@ -177,31 +176,31 @@ EOS
     end
   end
 
-  ## Syncs the message to the index: deleting if it's already there,
-  ## and adding either way. Index state will be determined by m.labels.
+  ## Syncs the message to the index, replacing any previous version.  adding
+  ## either way. Index state will be determined by the message's #labels
+  ## accessor.
   ##
-  ## docid and entry can be specified if they're already known.
-  def sync_message m, docid=nil, entry=nil, opts={}
-    docid, entry = load_entry_for_id m.id unless docid && entry
+  ## if need_load is false, docid and entry are assumed to be set to the
+  ## result of load_entry_for_id (which can be nil).
+  def sync_message m, need_load=true, docid=nil, entry=nil, opts={}
+    docid, entry = load_entry_for_id m.id if need_load
 
     raise "no source info for message #{m.id}" unless m.source && m.source_info
     @index_mutex.synchronize do
       raise "trying to delete non-corresponding entry #{docid} with index message-id #{@index[docid][:message_id].inspect} and parameter message id #{m.id.inspect}" if docid && @index[docid][:message_id] != m.id
     end
 
-    source_id = 
-      if m.source.is_a? Integer
-        m.source
-      else
-        m.source.id or raise "unregistered source #{m.source} (id #{m.source.id.inspect})"
-      end
+    source_id = if m.source.is_a? Integer
+      m.source
+    else
+      m.source.id or raise "unregistered source #{m.source} (id #{m.source.id.inspect})"
+    end
 
-    snippet = 
-      if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
-        ""
-      else
-        m.snippet
-      end
+    snippet = if m.snippet_contains_encrypted_content? && $config[:discard_snippets_from_encrypted_messages]
+      ""
+    else
+      m.snippet
+    end
 
     ## write the new document to the index. if the entry already exists in the
     ## index, reuse it (which avoids having to reload the entry from the source,
@@ -261,15 +260,14 @@ EOS
       :refs => (entry[:refs] || (m.refs + m.replytos).uniq.join(" ")),
     }
 
-    @index_mutex.synchronize  do
+    @index_mutex.synchronize do
       @index.delete docid if docid
       @index.add_document d
     end
 
-    docid, entry = load_entry_for_id m.id
-    ## this hasn't been triggered in a long time. TODO: decide whether it's still a problem.
-    raise "just added message #{m.id.inspect} but couldn't find it in a search" unless docid
-    true
+    ## this hasn't been triggered in a long time.
+    ## docid, entry = load_entry_for_id m.id
+    ## raise "just added message #{m.id.inspect} but couldn't find it in a search" unless docid
   end
 
   def save_index fn=File.join(@dir, "ferret")
@@ -414,9 +412,11 @@ EOS
         "references" => doc[:refs].split.map { |x| "<#{x}>" }.join(" "),
       }
 
-      Message.new :source => source, :source_info => doc[:source_info].to_i,
+      m = Message.new :source => source, :source_info => doc[:source_info].to_i,
                   :labels => doc[:label].symbolistize,
-                  :snippet => doc[:snippet], :header => fake_header
+                  :snippet => doc[:snippet]
+      m.parse_header fake_header
+      m
     end
   end
 
@@ -483,10 +483,27 @@ EOS
     @index_mutex.synchronize { @index.search(q, :limit => 1).total_hits > 0 }
   end
 
+  ## takes a user query string and returns the list of docids for messages
+  ## that match the query.
+  ##
+  ## messages can then be loaded from the index with #build_message.
+  ##
+  ## raises a ParseError if the parsing failed.
+  def run_query query
+    qobj, opts = Redwood::Index.parse_user_query_string query
+    query = Redwood::Index.build_query opts.merge(:qobj => qobj)
+    results = @index.search query, :limit => (opts[:limit] || :all)
+    results.hits.map { |hit| hit.doc }
+  end
+
 protected
 
-  ## do any specialized parsing
-  ## returns nil and flashes error message if parsing failed
+  class ParseError < StandardError; end
+
+  ## parse a query string from the user. returns a query object and a set of
+  ## extra flags; both of these are meant to be passed to #build_query.
+  ##
+  ## raises a ParseError if something went wrong.
   def parse_user_query_string s
     extraopts = {}
 
@@ -548,11 +565,9 @@ protected
     end
 
     if $have_chronic
-      chronic_failure = false
       subs = subs.gsub(/\b(before|on|in|during|after):(\((.+?)\)\B|(\S+)\b)/) do
-        break if chronic_failure
         field, datestr = $1, ($3 || $4)
-        realdate = Chronic.parse(datestr, :guess => false, :context => :past)
+        realdate = Chronic.parse datestr, :guess => false, :context => :past
         if realdate
           case field
           when "after"
@@ -566,11 +581,9 @@ protected
             "date:(<= #{sprintf "%012d", realdate.end.to_i}) date:(>= #{sprintf "%012d", realdate.begin.to_i})"
           end
         else
-          BufferManager.flash "Can't understand date #{datestr.inspect}!"
-          chronic_failure = true
+          raise ParseError, "can't understand date #{datestr.inspect}"
         end
       end
-      subs = nil if chronic_failure
     end
 
     ## limit:42 restrict the search to 42 results
@@ -580,15 +593,14 @@ protected
         extraopts[:limit] = lim.to_i
         ''
       else
-        BufferManager.flash "Can't understand limit #{lim.inspect}!"
-        subs = nil
+        raise ParseError, "non-numeric limit #{lim.inspect}"
       end
     end
     
-    if subs
+    begin
       [@qparser.parse(subs), extraopts]
-    else
-      nil
+    rescue Ferret::QueryParser::QueryParseException => e
+      raise ParseError, e.message
     end
   end
 
index 76c7139f45eb9478a627e49ac7c87eea81d0ee5d..cb039e44788f164e30f8851ce1fe10803a65f566 100644 (file)
@@ -9,22 +9,22 @@ class Keymap
 
   def self.keysym_to_keycode k
     case k
-    when :down: Curses::KEY_DOWN
-    when :up: Curses::KEY_UP
-    when :left: Curses::KEY_LEFT
-    when :right: Curses::KEY_RIGHT
-    when :page_down: Curses::KEY_NPAGE
-    when :page_up: Curses::KEY_PPAGE
-    when :backspace: Curses::KEY_BACKSPACE
-    when :home: Curses::KEY_HOME
-    when :end: Curses::KEY_END
-    when :ctrl_l: "\f"[0]
-    when :ctrl_g: "\a"[0]
-    when :tab: "\t"[0]
-    when :enter, :return: 10 #Curses::KEY_ENTER
+    when :down then Curses::KEY_DOWN
+    when :up then Curses::KEY_UP
+    when :left then Curses::KEY_LEFT
+    when :right then Curses::KEY_RIGHT
+    when :page_down then Curses::KEY_NPAGE
+    when :page_up then Curses::KEY_PPAGE
+    when :backspace then Curses::KEY_BACKSPACE
+    when :home then Curses::KEY_HOME
+    when :end then Curses::KEY_END
+    when :ctrl_l then "\f".ord
+    when :ctrl_g then "\a".ord
+    when :tab then "\t".ord
+    when :enter, :return then 10 #Curses::KEY_ENTER
     else
       if k.is_a?(String) && k.length == 1
-        k[0]
+        k.ord
       else
         raise ArgumentError, "unknown key name '#{k}'"
       end
@@ -33,18 +33,18 @@ class Keymap
 
   def self.keysym_to_string k
     case k
-    when :down: "<down arrow>"
-    when :up: "<up arrow>"
-    when :left: "<left arrow>"
-    when :right: "<right arrow>"
-    when :page_down: "<page down>"
-    when :page_up: "<page up>"
-    when :backspace: "<backspace>"
-    when :home: "<home>"
-    when :end: "<end>"
-    when :enter, :return: "<enter>"
-    when :tab: "tab"
-    when " ": "<space>"
+    when :down then "<down arrow>"
+    when :up then "<up arrow>"
+    when :left then "<left arrow>"
+    when :right then "<right arrow>"
+    when :page_down then "<page down>"
+    when :page_up then "<page up>"
+    when :backspace then "<backspace>"
+    when :home then "<home>"
+    when :end then "<end>"
+    when :enter, :return then "<enter>"
+    when :tab then "tab"
+    when " " then "<space>"
     else
       Curses::keyname(keysym_to_keycode(k))
     end
index 3d584f76d6054d2c5ffaa10954ea752ed98c808c..a9ae05c71ec61befa861001fd5aca7dd7c3c0a41 100644 (file)
@@ -56,7 +56,7 @@ class Maildir < Source
 
   def load_header id
     scan_mailbox
-    with_file_for(id) { |f| MBox::read_header f }
+    with_file_for(id) { |f| parse_raw_email_header f }
   end
 
   def load_message id
index 8497a37f11a6f6a00bce35953e4a43d5c96103e7..e1e3a4d1aed75c0a3d190cdc9ce8257ff46a56e4 100644 (file)
@@ -1,81 +1,24 @@
 require "sup/mbox/loader"
 require "sup/mbox/ssh-file"
 require "sup/mbox/ssh-loader"
-require "sup/rfc2047"
 
 module Redwood
 
-## some utility functions. actually these are not mbox-specific at all
-## and should be moved somewhere else.
-##
-## TODO: move functionality to somewhere better, like message.rb
 module MBox
-  BREAK_RE = /^From \S+/
-  HEADER_RE = /\s*(.*?)\s*/
-
-  def read_header f
-    header = {}
-    last = nil
-
-    ## i do it in this weird way because i am trying to speed things up
-    ## when scanning over large mbox files.
-    while(line = f.gets)
-      case line
-      ## these three can occur multiple times, and we want the first one
-      when /^(Delivered-To):#{HEADER_RE}$/i,
-        /^(X-Original-To):#{HEADER_RE}$/i,
-        /^(Envelope-To):#{HEADER_RE}$/i: header[last = $1] ||= $2
-
-      when /^(From):#{HEADER_RE}$/i,
-        /^(To):#{HEADER_RE}$/i,
-        /^(Cc):#{HEADER_RE}$/i,
-        /^(Bcc):#{HEADER_RE}$/i,
-        /^(Subject):#{HEADER_RE}$/i,
-        /^(Date):#{HEADER_RE}$/i,
-        /^(References):#{HEADER_RE}$/i,
-        /^(In-Reply-To):#{HEADER_RE}$/i,
-        /^(Reply-To):#{HEADER_RE}$/i,
-        /^(List-Post):#{HEADER_RE}$/i,
-        /^(List-Subscribe):#{HEADER_RE}$/i,
-        /^(List-Unsubscribe):#{HEADER_RE}$/i,
-        /^(Status):#{HEADER_RE}$/i,
-        /^(X-\S+):#{HEADER_RE}$/: header[last = $1] = $2
-      when /^(Message-Id):#{HEADER_RE}$/i: header[mid_field = last = $1] = $2
-
-      when /^\r*$/: break
-      when /^\S+:/: last = nil # some other header we don't care about
-      else
-        header[last] += " " + line.chomp.gsub(/^\s+/, "") if last
-      end
-    end
-
-    if mid_field && header[mid_field] && header[mid_field] =~ /<(.*?)>/
-      header[mid_field] = $1
+  BREAK_RE = /^From \S+ (.+)$/
+
+  def is_break_line? l
+    l =~ BREAK_RE or return false
+    time = $1
+    begin
+      ## hack -- make Time.parse fail when trying to substitute values from Time.now
+      Time.parse time, 0
+      true
+    rescue NoMethodError
+      Redwood::log "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
+      false
     end
-
-    header.each do |k, v|
-      next unless Rfc2047.is_encoded? v
-      header[k] =
-        begin
-          Rfc2047.decode_to $encoding, v
-        rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
-          Redwood::log "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
-          v
-        end
-    end
-    header
   end
-  
-  ## never actually called
-  def read_body f
-    body = []
-    f.each_line do |l|
-      break if l =~ BREAK_RE
-      body << l.chomp
-    end
-    body
-  end
-
-  module_function :read_header, :read_body
+  module_function :is_break_line?
 end
 end
index 65d0bd102a2612339770945965fc15d71050901d..ebb2aed675e171397b99e877023e24611cf119c7 100644 (file)
@@ -9,7 +9,7 @@ class Loader < Source
   attr_accessor :labels
 
   ## uri_or_fp is horrific. need to refactor.
-  def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=[]
+  def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=[]
     @mutex = Mutex.new
     @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
 
@@ -56,10 +56,10 @@ class Loader < Source
     @mutex.synchronize do
       @f.seek offset
       l = @f.gets
-      unless l =~ BREAK_RE
+      unless MBox::is_break_line? l
         raise OutOfSyncSourceError, "mismatch in mbox file offset #{offset.inspect}: #{l.inspect}." 
       end
-      header = MBox::read_header @f
+      header = parse_raw_email_header @f
     end
     header
   end
@@ -68,13 +68,12 @@ class Loader < Source
     @mutex.synchronize do
       @f.seek offset
       begin
-        RMail::Mailbox::MBoxReader.new(@f).each_message do |input|
-          m = RMail::Parser.read(input)
-          if m.body && m.body.is_a?(String)
-            m.body.gsub!(/^>From /, "From ")
-          end
-          return m
-        end
+        ## don't use RMail::Mailbox::MBoxReader because it doesn't properly ignore
+        ## "From" at the start of a message body line.
+        string = ""
+        l = @f.gets
+        string << l until @f.eof? || MBox::is_break_line?(l = @f.gets)
+        RMail::Parser.read string
       rescue RMail::Parser::Error => e
         raise FatalSourceError, "error parsing mbox file: #{e.message}"
       end
@@ -98,7 +97,7 @@ class Loader < Source
     @mutex.synchronize do
       @f.seek offset
       until @f.eof? || (l = @f.gets) =~ /^\r*$/
-        ret += l
+        ret << l
       end
     end
     ret
@@ -106,7 +105,7 @@ class Loader < Source
 
   def raw_message offset
     ret = ""
-    each_raw_message_line(offset) { |l| ret += l }
+    each_raw_message_line(offset) { |l| ret << l }
     ret
   end
 
@@ -120,7 +119,7 @@ class Loader < Source
     @mutex.synchronize do
       @f.seek offset
       yield @f.gets
-      until @f.eof? || (l = @f.gets) =~ BREAK_RE
+      until @f.eof? || MBox::is_break_line?(l = @f.gets)
         yield l
       end
     end
@@ -141,7 +140,7 @@ class Loader < Source
         ## 2. at the beginning of an mbox separator (in all other
         ##    cases).
 
-        l = @f.gets or raise "next while at EOF"
+        l = @f.gets or return nil
         if l =~ /^\s*$/ # case 1
           returned_offset = @f.tell
           @f.gets # now we're at a BREAK_RE, so skip past it
@@ -151,7 +150,7 @@ class Loader < Source
         end
 
         while(line = @f.gets)
-          break if line =~ BREAK_RE
+          break if MBox::is_break_line? line
           next_offset = @f.tell
         end
       end
index 1bf779612f4c135d00b8343efb4ee8971efe32d6..0d742d99e746a9ce96a4c11eddcd57506fcae22f 100644 (file)
@@ -99,7 +99,7 @@ EOS
       text =
         case @content_type
         when /^text\/plain\b/
-          Message.convert_from @raw_content, encoded_content.charset
+          Iconv.easy_decode $encoding, encoded_content.charset || $encoding, @raw_content
         else
           HookManager.run "mime-decode", :content_type => content_type,
                           :filename => lambda { write_to_disk },
@@ -240,8 +240,8 @@ EOS
 
     def patina_color
       case status
-      when :valid: :cryptosig_valid_color
-      when :invalid: :cryptosig_invalid_color
+      when :valid then :cryptosig_valid_color
+      when :invalid then :cryptosig_invalid_color
       else :cryptosig_unknown_color
       end
     end
index bedc03dfebafba33749cc641759852e812e2f66d..ded577aa4f9d953a8e7cacd44708b626f51d00ec 100644 (file)
@@ -1,10 +1,7 @@
 require 'time'
-require 'iconv'
 
 module Redwood
 
-class MessageFormatError < StandardError; end
-
 ## a Message is what's threaded.
 ##
 ## it is also where the parsing for quotes and signatures is done, but
@@ -29,7 +26,6 @@ class Message
 
   QUOTE_PATTERN = /^\s{0,4}[>|\}]/
   BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
-  QUOTE_START_PATTERN = /\w.*:$/
   SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)|(^\s*--~--~-)|(^\s*--\+\+\*\*==)/
 
   MAX_SIG_DISTANCE = 15 # lines from the end
@@ -60,49 +56,49 @@ class Message
     ## why.
     @refs = []
 
-    parse_header(opts[:header] || @source.load_header(@source_info))
+    #parse_header(opts[:header] || @source.load_header(@source_info))
   end
 
   def parse_header header
-    header.keys.each { |k| header[k.downcase] = header[k] } # canonicalize
-
-    fakeid = nil
-    fakename = nil
-
-    @id =
-      if header["message-id"]
-        sanitize_message_id header["message-id"]
-      else
-        fakeid = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
-      end
-    
-    @from =
-      if header["from"]
-        Person.from_address header["from"]
-      else
-        fakename = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
-        Person.from_address fakename
-      end
+    ## forcibly decode these headers from and to the current encoding,
+    ## which serves to strip out characters that aren't displayable
+    ## (and which would otherwise be screwing up the display)
+    %w(from to subject cc bcc).each do |f|
+      header[f] = Iconv.easy_decode($encoding, $encoding, header[f]) if header[f]
+    end
 
-    Redwood::log "faking message-id for message from #@from: #{id}" if fakeid
-    Redwood::log "faking from for message #@id: #{fakename}" if fakename
+    @id = if header["message-id"]
+      mid = header["message-id"] =~ /<(.+?)>/ ? $1 : header["message-id"]
+      sanitize_message_id mid
+    else
+      id = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
+      from = header["from"]
+      #Redwood::log "faking non-existent message-id for message from #{from}: #{id}"
+      id
+    end
 
-    date = header["date"]
-    @date =
-      case date
-      when Time
-        date
-      when String
-        begin
-          Time.parse date
-        rescue ArgumentError => e
-          Redwood::log "faking date header for #{@id} due to error parsing date #{header['date'].inspect}: #{e.message}"
-          Time.now
-        end
-      else
-        Redwood::log "faking date header for #{@id}"
+    @from = Person.from_address(if header["from"]
+      header["from"]
+    else
+      name = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
+      #Redwood::log "faking non-existent sender for message #@id: #{name}"
+      name
+    end)
+
+    @date = case(date = header["date"])
+    when Time
+      date
+    when String
+      begin
+        Time.parse date
+      rescue ArgumentError => e
+        #Redwood::log "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
         Time.now
       end
+    else
+      #Redwood::log "faking non-existent date header for #{@id}"
+      Time.now
+    end
 
     @subj = header.member?("subject") ? header["subject"].gsub(/\s+/, " ").gsub(/\s+$/, "") : DEFAULT_SUBJECT
     @to = Person.from_address_list header["to"]
@@ -130,7 +126,6 @@ class Message
     @list_subscribe = header["list-subscribe"]
     @list_unsubscribe = header["list-unsubscribe"]
   end
-  private :parse_header
 
   def add_ref ref
     @refs << ref
@@ -198,7 +193,7 @@ class Message
   ## this is called when the message body needs to actually be loaded.
   def load_from_source!
     @chunks ||=
-      if @source.has_errors?
+      if @source.respond_to?(:has_errors?) && @source.has_errors?
         [Chunk::Text.new(error_message(@source.error.message).split("\n"))]
       else
         begin
@@ -212,7 +207,7 @@ class Message
           ## so i will keep this.
           parse_header @source.load_header(@source_info)
           message_to_chunks @source.load_message(@source_info)
-        rescue SourceError, SocketError, MessageFormatError => e
+        rescue SourceError, SocketError => e
           Redwood::log "problem getting messages from #{@source}: #{e.message}"
           ## we need force_to_top here otherwise this window will cover
           ## up the error message one
@@ -372,6 +367,7 @@ private
     [notice, sig, children].flatten.compact
   end
 
+  ## takes a RMail::Message, breaks it into Chunk:: classes.
   def message_to_chunks m, encrypted=false, sibling_types=[]
     if m.multipart?
       chunks =
@@ -409,8 +405,8 @@ private
         elsif m.header["Content-Type"] && m.header["Content-Type"] !~ /^text\/plain/
           extension =
             case m.header["Content-Type"]
-            when /text\/html/: "html"
-            when /image\/(.*)/: $1
+            when /text\/html/ then "html"
+            when /image\/(.*)/ then $1
             end
 
           ["sup-attachment-#{Time.now.to_i}-#{rand 10000}", extension].join(".")
@@ -428,24 +424,15 @@ private
 
       ## otherwise, it's body text
       else
-        body = Message.convert_from m.decode, m.charset if m.body
+        ## if there's no charset, use the current encoding as the charset.
+        ## this ensures that the body is normalized to avoid non-displayable
+        ## characters
+        body = Iconv.easy_decode($encoding, m.charset || $encoding, m.decode) if m.body
         text_to_chunks((body || "").normalize_whitespace.split("\n"), encrypted)
       end
     end
   end
 
-  def self.convert_from body, charset
-    begin
-      raise MessageFormatError, "RubyMail decode returned a null body" unless body
-      return body unless charset
-      Iconv.easy_decode($encoding, charset, body)
-    rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence, MessageFormatError => e
-      Redwood::log "warning: error (#{e.class.name}) decoding message body from #{charset}: #{e.message}"
-      File.open(File.join(BASE_DIR,"unable-to-decode.txt"), "w") { |f| f.write body }
-      body
-    end
-  end
-
   ## parse the lines of text into chunk objects.  the heuristics here
   ## need tweaking in some nice manner. TODO: move these heuristics
   ## into the classes themselves.
@@ -461,7 +448,11 @@ private
       when :text
         newstate = nil
 
-        if line =~ QUOTE_PATTERN || (line =~ QUOTE_START_PATTERN && nextline =~ QUOTE_PATTERN)
+        ## the following /:$/ followed by /\w/ is an attempt to detect the
+        ## start of a quote. this is split into two regexen because the
+        ## original regex /\w.*:$/ had very poor behavior on long lines
+        ## like ":a:a:a:a:a" that occurred in certain emails.
+        if line =~ QUOTE_PATTERN || (line =~ /:$/ && line =~ /\w/ && nextline =~ QUOTE_PATTERN)
           newstate = :quote
         elsif line =~ SIG_PATTERN && (lines.length - i) < MAX_SIG_DISTANCE
           newstate = :sig
index bea46d92d425835142f2ae3b94eca035b0a64148..6433492c4d50acb187c1ae426620fde6a13b36be 100644 (file)
@@ -58,7 +58,7 @@ class Mode
       title = "Keybindings from #{Mode.make_name klass.name}"
       s = <<EOS
 #{title}
-#{'-' * title.length}
+#{'-' * title.display_length}
 
 #{km.help_text used_keys}
 EOS
index bbf956bcb1d340e8863f145cf38527bb63faa749..f956d6587948cb3cc01f0579faa98f548850c1d6 100644 (file)
@@ -3,6 +3,10 @@ require 'socket' # just for gethostname!
 require 'pathname'
 require 'rmail'
 
+# from jcode.rb, not included in ruby 1.9
+PATTERN_UTF8 = '[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf]'
+RE_UTF8 = Regexp.new(PATTERN_UTF8, 0, 'n')
+
 module Redwood
 
 class SendmailCommandFailed < StandardError; end
@@ -12,7 +16,7 @@ class EditMessageMode < LineCursorMode
 
   FORCE_HEADERS = %w(From To Cc Bcc Subject)
   MULTI_HEADERS = %w(To Cc Bcc)
-  NON_EDITABLE_HEADERS = %w(Message-Id Date)
+  NON_EDITABLE_HEADERS = %w(Message-id Date)
 
   HookManager.register "signature", <<EOS
 Generates a message signature.
@@ -170,6 +174,29 @@ EOS
 
 protected
 
+  def mime_encode string
+    string = [string].pack('M') # basic quoted-printable
+    string.gsub!(/=\n/,'')      # .. remove trailing newline
+    string.gsub!(/_/,'=96')     # .. encode underscores
+    string.gsub!(/\?/,'=3F')    # .. encode question marks
+    string.gsub!(/ /,'_')       # .. translate space to underscores
+    "=?utf-8?q?#{string}?="
+  end
+
+  def mime_encode_subject string
+    return string unless string.match(RE_UTF8)
+    mime_encode string
+  end
+
+  RE_ADDRESS = /(.+)( <.*@.*>)/
+
+  # Encode "bælammet mitt <user@example.com>" into
+  # "=?utf-8?q?b=C3=A6lammet_mitt?= <user@example.com>
+  def mime_encode_address string
+    return string unless string.match(RE_UTF8)
+    string.sub(RE_ADDRESS) { |match| mime_encode($1) + $2 }
+  end
+
   def move_cursor_left
     if curpos < @selectors.length
       @selectors[curpos].roll_left
@@ -214,7 +241,7 @@ protected
 
   def parse_file fn
     File.open(fn) do |f|
-      header = MBox::read_header f
+      header = Source.parse_raw_email_header(f).inject({}) { |h, (k, v)| h[k.capitalize] = v; h } # lousy HACK
       body = f.readlines.map { |l| l.chomp }
 
       header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
@@ -259,7 +286,7 @@ protected
           if i == 0
             header + " " + name
           else
-            (" " * (header.length + 1)) + name
+            (" " * (header.display_length + 1)) + name
           end + (i == things.length - 1 ? "" : ",")
         end
       end
@@ -335,14 +362,16 @@ protected
       m.header[k] = 
         case v
         when String
-          v
+          k.match(/subject/i) ? mime_encode_subject(v) : mime_encode_address(v)
         when Array
-          v.join ", "
+          v.map { |v| mime_encode_address v }.join ", "
         end
     end
+
     m.header["Date"] = date.rfc2822
     m.header["Message-Id"] = @message_id
     m.header["User-Agent"] = "Sup/#{Redwood::VERSION}"
+    m.header["Content-Transfer-Encoding"] = '8bit'
     m
   end
 
index 559892d5655f07b80a1186e114950156220a13c5..d8daeb9c4cbe19ad1fc2644aa1d5c84a4e382f23 100644 (file)
@@ -26,12 +26,27 @@ class InboxMode < ThreadIndexMode
 
   def archive
     return unless cursor_thread
+    thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
+
+    UndoManager.register "archiving thread" do
+      thread.apply_label :inbox
+      add_or_unhide thread.first
+    end
+
     cursor_thread.remove_label :inbox
     hide_thread cursor_thread
     regen_text
   end
 
   def multi_archive threads
+    UndoManager.register "archiving #{threads.size.pluralize 'thread'}" do
+      threads.map do |t|
+        t.apply_label :inbox
+        add_or_unhide t.first
+      end
+      regen_text
+    end
+
     threads.each do |t|
       t.remove_label :inbox
       hide_thread t
@@ -41,6 +56,14 @@ class InboxMode < ThreadIndexMode
 
   def read_and_archive
     return unless cursor_thread
+    thread = cursor_thread # to make sure lambda only knows about 'old' cursor_thread
+
+    UndoManager.register "reading and archiving thread" do
+      thread.apply_label :inbox
+      thread.apply_label :unread
+      add_or_unhide thread.first
+    end
+
     cursor_thread.remove_label :unread
     cursor_thread.remove_label :inbox
     hide_thread cursor_thread
@@ -48,12 +71,23 @@ class InboxMode < ThreadIndexMode
   end
 
   def multi_read_and_archive threads
+    old_labels = threads.map { |t| t.labels.dup }
+
     threads.each do |t|
       t.remove_label :unread
       t.remove_label :inbox
       hide_thread t
     end
     regen_text
+
+    UndoManager.register "reading and archiving #{threads.size.pluralize 'thread'}" do
+      threads.zip(old_labels).each do |t, l|
+        t.labels = l
+        add_or_unhide t.first
+      end
+      regen_text
+    end
+
   end
 
   def handle_unarchived_update sender, m
index 6b9f55933a6ef967fc221f8b31a6f209e48bbe84..c79c5dbe51dd59df0426572c523a2b79492f524f 100644 (file)
@@ -126,7 +126,7 @@ EOS
                "To" => [],
                "Cc" => [],
                "Bcc" => [],
-               "In-Reply-To" => "<#{@m.id}>",
+               "In-reply-to" => "<#{@m.id}>",
                "Subject" => Message.reify_subj(@m.subj),
                "References" => refs,
              }.merge v
index 66c098be06b43d343ac7b0e120589d71b3ea20f7..63b48ec00edd6e7b7cc50bd82b51d8b00c7c7ed9 100644 (file)
@@ -73,7 +73,7 @@ class ScrollMode < Mode
     end
     if line
       @search_line = line + 1
-      search_goto_pos line, col, col + @search_query.length
+      search_goto_pos line, col, col + @search_query.display_length
       buffer.mark_dirty
     else
       BufferManager.flash "Not found!"
@@ -164,7 +164,7 @@ protected
           if match
             return [i, offset + match]
           else
-            offset += string.length
+            offset += string.display_length
           end
         end
       end
@@ -219,24 +219,25 @@ protected
 
   def draw_line_from_array ln, a, opts
     xpos = 0
-    a.each do |color, text|
+    a.each_with_index do |(color, text), i|
       raise "nil text for color '#{color}'" if text.nil? # good for debugging
+      l = text.display_length
+      no_fill = i != a.size - 1
       
-      if xpos + text.length < @leftcol
+      if xpos + l < @leftcol
         buffer.write ln - @topline, 0, "", :color => color,
                      :highlight => opts[:highlight]
-        xpos += text.length
       elsif xpos < @leftcol
         ## partial
         buffer.write ln - @topline, 0, text[(@leftcol - xpos) .. -1],
                      :color => color,
-                     :highlight => opts[:highlight]
-        xpos += text.length
+                     :highlight => opts[:highlight], :no_fill => no_fill
       else
         buffer.write ln - @topline, xpos - @leftcol, text,
-                     :color => color, :highlight => opts[:highlight]
-        xpos += text.length
+                     :color => color, :highlight => opts[:highlight],
+                     :no_fill => no_fill
       end
+      xpos += l
     end
   end
 
index 6fdc58a0c9da1f0df907ce8e29d786a36db9b4d7..227ee9ba7f7a469ed751a45a9bcc52a767c697b9 100644 (file)
@@ -32,8 +32,8 @@ class SearchResultsMode < ThreadIndexMode
       mode = SearchResultsMode.new qobj, extraopts
       BufferManager.spawn "search: \"#{short_text}\"", mode
       mode.load_threads :num => mode.buffer.content_height
-    rescue Ferret::QueryParser::QueryParseException => e
-      BufferManager.flash "Couldn't parse query."
+    rescue Index::ParseError => e
+      BufferManager.flash "Problem: #{e.message}!"
     end
   end
 end
index e298ad225debb28c3287ca5cad54de7d8c75b5cd..145c6eb7cf605290a4a7a3f7f09d111b5235d5f0 100644 (file)
@@ -44,6 +44,7 @@ EOS
     k.add :tag_matching, "Tag matching threads", 'g'
     k.add :apply_to_tagged, "Apply next command to all tagged threads", '+', '='
     k.add :join_threads, "Force tagged threads to be joined into the same thread", '#'
+    k.add :undo, "Undo the previous action", 'u'
   end
 
   def initialize hidden_labels=[], load_thread_opts={}
@@ -86,6 +87,7 @@ EOS
 
   def reload
     drop_all_threads
+    UndoManager.clear
     BufferManager.draw_screen
     load_threads :num => buffer.content_height
   end
@@ -211,12 +213,16 @@ EOS
     add_or_unhide m
   end
 
+  def undo
+    UndoManager.undo
+  end
+
   def update
     @mutex.synchronize do
       ## let's see you do THIS in python
       @threads = @ts.threads.select { |t| !@hidden_threads[t] }.sort_by { |t| [t.date, t.first.id] }.reverse
       @size_widgets = @threads.map { |t| size_widget_for_thread t }
-      @size_widget_width = @size_widgets.max_of { |w| w.length }
+      @size_widget_width = @size_widgets.max_of { |w| w.display_length }
     end
 
     regen_text
@@ -233,66 +239,122 @@ EOS
     end
   end
 
+  ## returns an undo lambda
   def actually_toggle_starred t
+    pos = curpos
     if t.has_label? :starred # if ANY message has a star
       t.remove_label :starred # remove from all
       UpdateManager.relay self, :unstarred, t.first
+      lambda do
+        t.first.add_label :starred
+        UpdateManager.relay self, :starred, t.first
+        regen_text
+      end
     else
       t.first.add_label :starred # add only to first
       UpdateManager.relay self, :starred, t.first
+      lambda do
+        t.remove_label :starred
+        UpdateManager.relay self, :unstarred, t.first
+        regen_text
+      end
     end
   end  
 
   def toggle_starred 
     t = cursor_thread or return
-    actually_toggle_starred t
+    undo = actually_toggle_starred t
+    UndoManager.register "toggling thread starred status", undo
     update_text_for_line curpos
     cursor_down
   end
 
   def multi_toggle_starred threads
-    threads.each { |t| actually_toggle_starred t }
+    UndoManager.register "toggling #{threads.size.pluralize 'thread'} starred status",
+      threads.map { |t| actually_toggle_starred t }
     regen_text
   end
 
+  ## returns an undo lambda
   def actually_toggle_archived t
+    thread = t
+    pos = curpos
     if t.has_label? :inbox
       t.remove_label :inbox
       UpdateManager.relay self, :archived, t.first
+      lambda do
+        thread.apply_label :inbox
+        update_text_for_line pos
+        UpdateManager.relay self,:unarchived, thread.first
+      end
     else
       t.apply_label :inbox
       UpdateManager.relay self, :unarchived, t.first
+      lambda do
+        thread.remove_label :inbox
+        update_text_for_line pos
+        UpdateManager.relay self, :unarchived, thread.first
+      end
     end
   end
 
+  ## returns an undo lambda
   def actually_toggle_spammed t
+    thread = t
     if t.has_label? :spam
       t.remove_label :spam
+      add_or_unhide t.first
       UpdateManager.relay self, :unspammed, t.first
+      lambda do
+        thread.apply_label :spam
+        self.hide_thread thread
+        UpdateManager.relay self,:spammed, thread.first
+      end
     else
       t.apply_label :spam
+      hide_thread t
       UpdateManager.relay self, :spammed, t.first
+      lambda do
+        thread.remove_label :spam
+        add_or_unhide thread.first
+        UpdateManager.relay self,:unspammed, thread.first
+      end
     end
   end
 
+  ## returns an undo lambda
   def actually_toggle_deleted t
     if t.has_label? :deleted
       t.remove_label :deleted
+      add_or_unhide t.first
       UpdateManager.relay self, :undeleted, t.first
+      lambda do
+        t.apply_label :deleted
+        hide_thread t
+        UpdateManager.relay self, :deleted, t.first
+      end
     else
       t.apply_label :deleted
+      hide_thread t
       UpdateManager.relay self, :deleted, t.first
+      lambda do
+        t.remove_label :deleted
+        add_or_unhide t.first
+        UpdateManager.relay self, :undeleted, t.first
+      end
     end
   end
 
   def toggle_archived 
     t = cursor_thread or return
-    actually_toggle_archived t
+    undo = actually_toggle_archived t
+    UndoManager.register "deleting/undeleting thread #{t.first.id}", undo, lambda { update_text_for_line curpos }
     update_text_for_line curpos
   end
 
   def multi_toggle_archived threads
-    threads.each { |t| actually_toggle_archived t }
+    undos = threads.map { |t| actually_toggle_archived t }
+    UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}", undos, lambda { regen_text }
     regen_text
   end
 
@@ -353,10 +415,9 @@ EOS
   ## see deleted or spam emails, and when you undelete or unspam them
   ## you also want them to disappear immediately.
   def multi_toggle_spam threads
-    threads.each do |t|
-      actually_toggle_spammed t
-      hide_thread t 
-    end
+    undos = threads.map { |t| actually_toggle_spammed t }
+    UndoManager.register "marking/unmarking  #{threads.size.pluralize 'thread'} as spam",
+                         undos, lambda { regen_text }
     regen_text
   end
 
@@ -367,10 +428,9 @@ EOS
 
   ## see comment for multi_toggle_spam
   def multi_toggle_deleted threads
-    threads.each do |t|
-      actually_toggle_deleted t
-      hide_thread t 
-    end
+    undos = threads.map { |t| actually_toggle_deleted t }
+    UndoManager.register "deleting/undeleting #{threads.size.pluralize 'thread'}",
+                         undos, lambda { regen_text }
     regen_text
   end
 
@@ -379,13 +439,23 @@ EOS
     multi_kill [t]
   end
 
+  ## m-m-m-m-MULTI-KILL
   def multi_kill threads
+    UndoManager.register "killing #{threads.size.pluralize 'thread'}" do
+      threads.each do |t|
+        t.remove_label :killed
+        add_or_unhide t.first
+      end
+      regen_text
+    end
+
     threads.each do |t|
       t.apply_label :killed
       hide_thread t
     end
+
     regen_text
-    BufferManager.flash "#{threads.size.pluralize 'Thread'} killed."
+    BufferManager.flash "#{threads.size.pluralize 'thread'} killed."
   end
 
   def save background=true
@@ -454,6 +524,10 @@ EOS
   def edit_labels
     thread = cursor_thread or return
     speciall = (@hidden_labels + LabelManager::RESERVED_LABELS).uniq
+
+    old_labels = thread.labels
+    pos = curpos
+
     keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
 
     user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl, @hidden_labels
@@ -462,6 +536,13 @@ EOS
     thread.labels = keepl + user_labels
     user_labels.each { |l| LabelManager << l }
     update_text_for_line curpos
+
+    UndoManager.register "labeling thread" do
+      thread.labels = old_labels
+      update_text_for_line pos
+      UpdateManager.relay self, :labeled, thread.first
+    end
+
     UpdateManager.relay self, :labeled, thread.first
   end
 
@@ -471,21 +552,33 @@ EOS
 
     user_labels.map! { |l| (l.to_s =~ /^-/)? [l.to_s.gsub(/^-?/, '').to_sym, true] : [l, false] }
     hl = user_labels.select { |(l,_)| @hidden_labels.member? l }
-    if hl.empty?
-      threads.each do |t|
-        user_labels.each do |(l, to_remove)|
-          if to_remove
-            t.remove_label l
-          else
-            t.apply_label l
-          end
+    unless hl.empty?
+      BufferManager.flash "'#{hl}' is a reserved label!"
+      return
+    end
+
+    old_labels = threads.map { |t| t.labels.dup }
+
+    threads.each do |t|
+      user_labels.each do |(l, to_remove)|
+        if to_remove
+          t.remove_label l
+        else
+          t.apply_label l
+          LabelManager << l
         end
       end
-      user_labels.each { |(l,_)| LabelManager << l }
-    else
-      BufferManager.flash "'#{hl}' is a reserved label!"
     end
+
     regen_text
+
+    UndoManager.register "labeling #{threads.size.pluralize 'thread'}" do
+      threads.zip(old_labels).map do |t, old_labels|
+        t.labels = old_labels
+        UpdateManager.relay self, :labeled, t.first
+      end
+      regen_text
+    end
   end
 
   def reply
@@ -689,7 +782,7 @@ protected
 
     date = t.date.to_nice_s
 
-    starred = t.has_label?(:starred)
+    starred = t.has_label? :starred
 
     ## format the from column
     cur_width = 0
@@ -700,9 +793,9 @@ protected
       last = i == ann.length - 1
 
       abbrev =
-        if cur_width + name.length > from_width
+        if cur_width + name.display_length > from_width
           name[0 ... (from_width - cur_width - 1)] + "."
-        elsif cur_width + name.length == from_width
+        elsif cur_width + name.display_length == from_width
           name[0 ... (from_width - cur_width)]
         else
           if last
@@ -712,7 +805,7 @@ protected
           end
         end
 
-      cur_width += abbrev.length
+      cur_width += abbrev.display_length
 
       if last && from_width > cur_width
         abbrev += " " * (from_width - cur_width)
index f27f00d81ec28bb7563dac53e2ccfd406ae93e2f..487c05b8a3b43e749a934ca72fa171943447ac2e 100644 (file)
@@ -27,6 +27,7 @@ EOS
   register_keymap do |k|
     k.add :toggle_detailed_header, "Toggle detailed header", 'h'
     k.add :show_header, "Show full message header", 'H'
+    k.add :show_message, "Show full message (raw form)", 'V'
     k.add :activate_chunk, "Expand/collapse or activate item", :enter
     k.add :expand_all_messages, "Expand/collapse all messages", 'E'
     k.add :edit_draft, "Edit draft", 'e'
@@ -134,6 +135,13 @@ EOS
     end
   end
 
+  def show_message
+    m = @message_lines[curpos] or return
+    BufferManager.spawn_unless_exists("Raw message for #{m.id}") do
+      TextMode.new m.raw_message
+    end
+  end
+
   def toggle_detailed_header
     m = @message_lines[curpos] or return
     @layout[m].state = (@layout[m].state == :detailed ? :open : :detailed)
@@ -234,12 +242,16 @@ EOS
   ## view.
   def activate_chunk
     chunk = @chunk_lines[curpos] or return
-    layout = 
-      if chunk.is_a?(Message)
-        @layout[chunk]
-      elsif chunk.expandable?
-        @chunk_layout[chunk]
-      end
+    if chunk.is_a? Chunk::Text
+      ## if the cursor is over a text region, expand/collapse the
+      ## entire message
+      chunk = @message_lines[curpos]
+    end
+    layout = if chunk.is_a?(Message)
+      @layout[chunk]
+    elsif chunk.expandable?
+      @chunk_layout[chunk]
+    end
     if layout
       layout.state = (layout.state != :closed ? :closed : :open)
       #cursor_down if layout.state == :closed # too annoying
@@ -247,6 +259,10 @@ EOS
     elsif chunk.viewable?
       view chunk
     end
+    if chunk.is_a?(Message)
+      jump_to_message chunk
+      jump_to_next_open if layout.state == :closed
+    end
   end
 
   def edit_as_new
@@ -540,7 +556,7 @@ private
       (0 ... text.length).each do |i|
         @chunk_lines[@text.length + i] = m
         @message_lines[@text.length + i] = m
-        lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum
+        lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum
       end
 
       @text += text
@@ -561,7 +577,7 @@ private
           (0 ... text.length).each do |i|
             @chunk_lines[@text.length + i] = c
             @message_lines[@text.length + i] = m
-            lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.length }.sum - (depth * INDENT_SPACES)
+            lw = text[i].flatten.select { |x| x.is_a? String }.map { |x| x.display_length }.sum - (depth * INDENT_SPACES)
             l.width = lw if lw > l.width
           end
           @text += text
@@ -635,7 +651,7 @@ private
 
   def format_person_list prefix, people
     ptext = people.map { |p| format_person p }
-    pad = " " * prefix.length
+    pad = " " * prefix.display_length
     [prefix + ptext.first + (ptext.length > 1 ? "," : "")] + 
       ptext[1 .. -1].map_with_index do |e, i|
         pad + e + (i == ptext.length - 1 ? "" : ",")
index 3a8b57fcda94dd78edb864e3ba7c7d0a2024ffe9..354bd211f695bacf11865815fe3591e979bc0779 100644 (file)
@@ -86,7 +86,7 @@ EOS
       Index.usual_sources.each do |source|
 #        yield "source #{source} is done? #{source.done?} (cur_offset #{source.cur_offset} >= #{source.end_offset})"
         begin
-          yield "Loading from #{source}... " unless source.done? || source.has_errors?
+          yield "Loading from #{source}... " unless source.done? || (source.respond_to?(:has_errors?) && source.has_errors?)
         rescue SourceError => e
           Redwood::log "problem getting messages from #{source}: #{e.message}"
           Redwood::report_broken_sources :force_to_top => true
@@ -137,31 +137,29 @@ EOS
   def add_messages_from source, opts={}
     begin
       return if source.done? || source.has_errors?
-      
+
       source.each do |offset, labels|
         if source.has_errors?
           Redwood::log "error loading messages from #{source}: #{source.error.message}"
           return
         end
-      
+
         labels.each { |l| LabelManager << l }
         labels = labels + (source.archived? ? [] : [:inbox])
 
-        begin
-          m = Message.new :source => source, :source_info => offset, :labels => labels
-          if m.source_marked_read?
-            m.remove_label :unread
-            labels.delete :unread
-          end
+        m = Message.new :source => source, :source_info => offset, :labels => labels
+        m.load_from_source!
 
-          docid, entry = Index.load_entry_for_id m.id
-          HookManager.run "before-add-message", :message => m
-          m = yield(m, offset, entry) or next if block_given?
-          Index.sync_message m, docid, entry, opts
-          UpdateManager.relay self, :added, m unless entry
-        rescue MessageFormatError => e
-          Redwood::log "ignoring erroneous message at #{source}##{offset}: #{e.message}"
+        if m.source_marked_read?
+          m.remove_label :unread
+          labels.delete :unread
         end
+
+        docid, entry = Index.load_entry_for_id m.id
+        HookManager.run "before-add-message", :message => m
+        m = yield(m, offset, entry) or next if block_given?
+        times = Index.sync_message m, false, docid, entry, opts
+        UpdateManager.relay self, :added, m unless entry
       end
     rescue SourceError => e
       Redwood::log "problem getting messages from #{source}: #{e.message}"
index ee843c7dc54837acbcc39daab851509037692bb0..d4fcc2bc37dafdcd6e2b37beb3d6ec71cbbff716 100644 (file)
@@ -14,11 +14,11 @@ class SentManager
   def self.source_id; 9998; end
   def new_source; @source = Recoverable.new SentLoader.new; end
 
-  def write_sent_message date, from_email
+  def write_sent_message time, from_email
     need_blank = File.exists?(@fn) && !File.zero?(@fn)
     File.open(@fn, "a") do |f|
       f.puts if need_blank
-      f.puts "From #{from_email} #{date}"
+      f.puts "From #{from_email} #{time.asctime}"
       yield f
     end
 
index 6510aae8a738dba0b3d81acb617312a1e1334bec..fb98dbc71a1bdfee155225a74ab0f860706893bf 100644 (file)
@@ -1,3 +1,5 @@
+require "sup/rfc2047"
+
 module Redwood
 
 class SourceError < StandardError
@@ -99,7 +101,49 @@ class Source
     end
   end
 
+  ## read a raw email header from a filehandle (or anything that responds to
+  ## #gets), and turn it into a hash of key-value pairs.
+  ##
+  ## WARNING! THIS IS A SPEED-CRITICAL SECTION. Everything you do here will have
+  ## a significant effect on Sup's processing speed of email from ALL sources.
+  ## Little things like string interpolation, regexp interpolation, += vs <<,
+  ## all have DRAMATIC effects. BE CAREFUL WHAT YOU DO!
+  def self.parse_raw_email_header f
+    header = {}
+    last = nil
+
+    while(line = f.gets)
+      case line
+      ## these three can occur multiple times, and we want the first one
+      when /^(Delivered-To|X-Original-To|Envelope-To):\s*(.*?)\s*$/i; header[last = $1.downcase] ||= $2
+      ## mark this guy specially. not sure why i care.
+      when /^([^:\s]+):\s*(.*?)\s*$/i; header[last = $1.downcase] = $2
+      when /^\r*$/; break
+      else
+        if last
+          header[last] << " " unless header[last].empty?
+          header[last] << line.strip
+        end
+      end
+    end
+
+    %w(subject from to cc bcc).each do |k|
+      v = header[k] or next
+      next unless Rfc2047.is_encoded? v
+      header[k] = begin
+        Rfc2047.decode_to $encoding, v
+      rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
+        #Redwood::log "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
+        v
+      end
+    end
+    header
+  end
+
 protected
+
+  ## convenience function
+  def parse_raw_email_header f; self.class.parse_raw_email_header f end
   
   def Source.expand_filesystem_uri uri
     uri.gsub "~", File.expand_path("~")
diff --git a/lib/sup/undo.rb b/lib/sup/undo.rb
new file mode 100644 (file)
index 0000000..5a93c31
--- /dev/null
@@ -0,0 +1,39 @@
+module Redwood
+
+## Implements a single undo list for the Sup instance
+##
+## The basic idea is to keep a list of lambdas to undo
+## things. When an action is called (such as 'archive'),
+## a lambda is registered with UndoManager that will
+## undo the archival action
+
+class UndoManager
+  include Singleton
+
+  def initialize
+    @@actionlist = []
+    self.class.i_am_the_instance self
+  end
+
+  def register desc, *actions, &b
+    actions = [*actions.flatten]
+    actions << b if b
+    raise ArgumentError, "need at least one action" unless actions.length > 0
+    @@actionlist.push :desc => desc, :actions => actions
+  end
+
+  def undo
+    unless @@actionlist.empty?
+      actionset = @@actionlist.pop
+      actionset[:actions].each { |action| action.call }
+      BufferManager.flash "undid #{actionset[:desc]}"
+    else
+      BufferManager.flash "nothing more to undo!"
+    end
+  end
+
+  def clear
+    @@actionlist = []
+  end
+end
+end
index c26b4dbe9a3fe5207d34be9067394d571c36c979..8f60cc43216979aaa207696324f117ba7025cbdd 100644 (file)
@@ -133,8 +133,8 @@ class Object
   ## clone of java-style whole-method synchronization
   ## assumes a @mutex variable
   ## TODO: clean up, try harder to avoid namespace collisions
-  def synchronized *meth
-    meth.each do
+  def synchronized *methods
+    methods.each do |meth|
       class_eval <<-EOF
         alias unsynchronized_#{meth} #{meth}
         def #{meth}(*a, &b)
@@ -144,8 +144,8 @@ class Object
     end
   end
 
-  def ignore_concurrent_calls *meth
-    meth.each do
+  def ignore_concurrent_calls *methods
+    methods.each do |meth|
       mutex = "@__concurrent_protector_#{meth}"
       flag = "@__concurrent_flag_#{meth}"
       oldmeth = "__unprotected_#{meth}"
@@ -172,6 +172,16 @@ class Object
 end
 
 class String
+  ## nasty multibyte hack for ruby 1.8. if it's utf-8, split into chars using
+  ## the utf8 regex and count those. otherwise, use the byte length.
+  def display_length
+    if $encoding == "UTF-8"
+      scan(/./u).size
+    else
+      size
+    end
+  end
+
   def camel_to_hyphy
     self.gsub(/([a-z])([A-Z0-9])/, '\1-\2').downcase
   end
@@ -203,7 +213,7 @@ class String
     region_start = 0
     while pos <= length
       newpos = case state
-        when :escaped_instring, :escaped_outstring: pos
+        when :escaped_instring, :escaped_outstring then pos
         else index(/[,"\\]/, pos)
       end 
       
@@ -217,26 +227,26 @@ class String
       case char
       when ?"
         state = case state
-          when :outstring: :instring
-          when :instring: :outstring
-          when :escaped_instring: :instring
-          when :escaped_outstring: :outstring
+          when :outstring then :instring
+          when :instring then :outstring
+          when :escaped_instring then :instring
+          when :escaped_outstring then :outstring
         end
       when ?,, nil
         state = case state
-          when :outstring, :escaped_outstring:
+          when :outstring, :escaped_outstring then
             ret << self[region_start ... newpos].gsub(/^\s+|\s+$/, "")
             region_start = newpos + 1
             :outstring
-          when :instring: :instring
-          when :escaped_instring: :instring
+          when :instring then :instring
+          when :escaped_instring then :instring
         end
       when ?\\
         state = case state
-          when :instring: :escaped_instring
-          when :outstring: :escaped_outstring
-          when :escaped_instring: :instring
-          when :escaped_outstring: :outstring
+          when :instring then :escaped_instring
+          when :outstring then :escaped_outstring
+          when :escaped_instring then :instring
+          when :escaped_outstring then :outstring
         end
       end
       pos = newpos + 1
@@ -272,6 +282,12 @@ class String
     gsub(/\t/, "    ").gsub(/\r/, "")
   end
 
+  if not defined? ord
+    def ord
+      self[0]
+    end
+  end
+
   ## takes a space-separated list of words, and returns an array of symbols.
   ## typically used in Sup for translating Ferret's representation of a list
   ## of labels (a string) to an array of label symbols.
@@ -626,16 +642,18 @@ class Iconv
   def self.easy_decode target, charset, text
     return text if charset =~ /^(x-unknown|unknown[-_ ]?8bit|ascii[-_ ]?7[-_ ]?bit)$/i
     charset = case charset
-                when /UTF[-_ ]?8/i: "utf-8"
-                when /(iso[-_ ])?latin[-_ ]?1$/i: "ISO-8859-1"
-                when /iso[-_ ]?8859[-_ ]?15/i: 'ISO-8859-15'
-                when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i: "utf-7"
-                else charset
-              end
-
-    # Convert:
-    #
-    # Remember - Iconv.open(to, from)!
-    Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]
+      when /UTF[-_ ]?8/i then "utf-8"
+      when /(iso[-_ ])?latin[-_ ]?1$/i then "ISO-8859-1"
+      when /iso[-_ ]?8859[-_ ]?15/i then 'ISO-8859-15'
+      when /unicode[-_ ]1[-_ ]1[-_ ]utf[-_]7/i then "utf-7"
+      else charset
+    end
+
+    begin
+      Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]
+    rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
+      Redwood::log "warning: error (#{e.class.name}) decoding text from #{charset} to #{target}: #{text[0 ... 20]}"
+      text
+    end
   end
 end
diff --git a/sup.gemspec b/sup.gemspec
deleted file mode 100644 (file)
index 506d8ad..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-$:.push "lib"
-
-require "sup-files"
-require "sup-version"
-
-Gem::Specification.new do |s|
-  s.name = %q{sup}
-  s.version = SUP_VERSION
-  s.date = Time.now.to_s
-  s.authors = ["William Morgan"]
-  s.email = %q{wmorgan-sup@masanjin.net}
-  s.summary = %q{A console-based email client with the best features of GMail, mutt, and emacs. Features full text search, labels, tagged operations, multiple buffers, recent contacts, and more.}
-  s.homepage = %q{http://sup.rubyforge.org/}
-  s.description = %q{Sup is a console-based email client for people with a lot of email. It supports tagging, very fast full-text search, automatic contact-list management, and more. If you're the type of person who treats email as an extension of your long-term memory, Sup is for you.  Sup makes it easy to: - Handle massive amounts of email.  - Mix email from different sources: mbox files (even across different machines), Maildir directories, IMAP folders, POP accounts, and GMail accounts.  - Instantaneously search over your entire email collection. Search over body text, or use a query language to combine search predicates in any way.  - Handle multiple accounts. Replying to email sent to a particular account will use the correct SMTP server, signature, and from address.  - Add custom code to handle certain types of messages or to handle certain types of text within messages.  - Organize email with user-defined labels, automatically track recent contacts, and much more!  The goal of Sup is to become the email client of choice for nerds everywhere.}
-  s.files = SUP_FILES
-  s.executables = SUP_EXECUTABLES
-
-  s.add_dependency "ferret", ">= 0.11.6"
-  s.add_dependency "ncurses", ">= 0.9.1"
-  s.add_dependency "rmail", ">= 0.17"
-  s.add_dependency "highline"
-  s.add_dependency "net-ssh"
-  s.add_dependency "trollop", ">= 1.12"
-  s.add_dependency "lockfile"
-  s.add_dependency "mime-types", "~> 1"
-  s.add_dependency "gettext"
-  s.add_dependency "fastthread"
-
-  puts s.files
-end 
index f3afa31e51a648da767c56e18cfb6fcaca635fe8..83790c5dcaeb26ff716bb7b87537373eb8a79def 100644 (file)
@@ -26,7 +26,7 @@ class DummySource < Source
   end
 
   def load_header offset
-    MBox::read_header StringIO.new(raw_header(offset))
+    Source.parse_raw_email_header StringIO.new(raw_header(offset))
   end
   
   def load_message offset
@@ -53,13 +53,6 @@ class DummySource < Source
       yield f.gets
     end
   end
-
-  # FIXME: this one was not mentioned in the source documentation, but
-  # it's still required
-  def has_errors?
-
-  end
-
 end
 
 end
diff --git a/test/test_header_parsing.rb b/test/test_header_parsing.rb
new file mode 100644 (file)
index 0000000..91cf7c7
--- /dev/null
@@ -0,0 +1,157 @@
+#!/usr/bin/ruby
+
+require 'test/unit'
+require 'sup'
+require 'stringio'
+
+include Redwood
+
+class TestMBoxParsing < Test::Unit::TestCase
+  def setup
+  end
+
+  def teardown
+  end
+
+  def test_normal_headers
+    h = Source.parse_raw_email_header StringIO.new(<<EOS)
+From: Bob <bob@bob.com>
+To: Sally <sally@sally.com>
+EOS
+
+    assert_equal "Bob <bob@bob.com>", h["from"]
+    assert_equal "Sally <sally@sally.com>", h["to"]
+    assert_nil h["message-id"]
+  end
+
+  def test_multiline
+    h = Source.parse_raw_email_header StringIO.new(<<EOS)
+From: Bob <bob@bob.com>
+Subject: one two three
+  four five six
+To: Sally <sally@sally.com>
+References: <seven>
+  <eight>
+Seven: Eight
+EOS
+
+    assert_equal "one two three four five six", h["subject"]
+    assert_equal "Sally <sally@sally.com>", h["to"]
+    assert_equal "<seven> <eight>", h["references"]
+  end
+
+  def test_ignore_spacing
+    variants = [
+      "Subject:one two  three   end\n",
+      "Subject:    one two  three   end\n",
+      "Subject:   one two  three   end    \n",
+    ]
+    variants.each do |s|
+      h = Source.parse_raw_email_header StringIO.new(s)
+      assert_equal "one two  three   end", h["subject"]
+    end
+  end
+
+  def test_message_id_ignore_spacing
+    variants = [
+      "Message-Id:     <one@bob.com>       \n",
+      "Message-Id:<one@bob.com>       \n",
+    ]
+    variants.each do |s|
+      h = Source.parse_raw_email_header StringIO.new(s)
+      assert_equal "<one@bob.com>", h["message-id"]
+    end
+  end
+
+  def test_blank_lines
+    h = Source.parse_raw_email_header StringIO.new("")
+    assert_equal nil, h["message-id"]
+  end
+
+  def test_empty_headers
+    variants = [
+      "Message-Id:       \n",
+      "Message-Id:\n",
+    ]
+    variants.each do |s|
+      h = Source.parse_raw_email_header StringIO.new(s)
+      assert_equal "", h["message-id"]
+    end
+  end
+
+  def test_detect_end_of_headers
+    h = Source.parse_raw_email_header StringIO.new(<<EOS)
+From: Bob <bob@bob.com>
+
+To: a dear friend
+EOS
+  assert_equal "Bob <bob@bob.com>", h["from"]
+  assert_nil h["to"]
+
+  h = Source.parse_raw_email_header StringIO.new(<<EOS)
+From: Bob <bob@bob.com>
+\r
+To: a dear friend
+EOS
+  assert_equal "Bob <bob@bob.com>", h["from"]
+  assert_nil h["to"]
+
+  h = Source.parse_raw_email_header StringIO.new(<<EOS)
+From: Bob <bob@bob.com>
+\r\n\r
+To: a dear friend
+EOS
+  assert_equal "Bob <bob@bob.com>", h["from"]
+  assert_nil h["to"]
+  end
+
+  def test_from_line_splitting
+    l = MBox::Loader.new StringIO.new(<<EOS)
+From sup-talk-bounces@rubyforge.org Mon Apr 27 12:56:18 2009
+From: Bob <bob@bob.com>
+To: a dear friend
+
+Hello there friend. How are you?
+
+From sea to shining sea
+
+From bob@bob.com I get only spam.
+
+From bob@bob.com   
+
+From bob@bob.com
+
+(that second one has spaces at the endj
+
+This is the end of the email.
+EOS
+    offset, labels = l.next
+    assert_equal 0, offset
+    offset, labels = l.next
+    assert_nil offset
+  end
+
+  def test_more_from_line_splitting
+    l = MBox::Loader.new StringIO.new(<<EOS)
+From sup-talk-bounces@rubyforge.org Mon Apr 27 12:56:18 2009
+From: Bob <bob@bob.com>
+To: a dear friend
+
+Hello there friend. How are you?
+
+From bob@bob.com Mon Apr 27 12:56:19 2009
+From: Bob <bob@bob.com>
+To: a dear friend
+
+Hello again! Would you like to buy my products?
+EOS
+    offset, labels = l.next
+    assert_not_nil offset
+
+    offset, labels = l.next
+    assert_not_nil offset
+
+    offset, labels = l.next
+    assert_nil offset
+  end
+end
diff --git a/test/test_mbox_parsing.rb b/test/test_mbox_parsing.rb
deleted file mode 100644 (file)
index 32687e5..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/usr/bin/ruby
-
-require 'test/unit'
-require 'sup'
-require 'stringio'
-
-include Redwood
-
-class TestMBoxParsing < Test::Unit::TestCase
-  def setup
-  end
-
-  def teardown
-  end
-
-  def test_normal_headers
-    h = MBox.read_header StringIO.new(<<EOS)
-From: Bob <bob@bob.com>
-To: Sally <sally@sally.com>
-EOS
-
-    assert_equal "Bob <bob@bob.com>", h["From"]
-    assert_equal "Sally <sally@sally.com>", h["To"]
-    assert_nil h["Message-Id"]
-  end
-
-  ## this is shitty behavior in retrospect, but it's built in now.
-  def test_message_id_stripping
-    h = MBox.read_header StringIO.new("Message-Id: <one@bob.com>\n")
-    assert_equal "one@bob.com", h["Message-Id"]
-
-    h = MBox.read_header StringIO.new("Message-Id: one@bob.com\n")
-    assert_equal "one@bob.com", h["Message-Id"]
-  end
-
-  def test_multiline
-    h = MBox.read_header StringIO.new(<<EOS)
-From: Bob <bob@bob.com>
-Subject: one two three
-  four five six
-To: Sally <sally@sally.com>
-References: seven
-  eight
-Seven: Eight
-EOS
-
-    assert_equal "one two three four five six", h["Subject"]
-    assert_equal "Sally <sally@sally.com>", h["To"]
-    assert_equal "seven eight", h["References"]
-  end
-
-  def test_ignore_spacing
-    variants = [
-      "Subject:one two  three   end\n",
-      "Subject:    one two  three   end\n",
-      "Subject:   one two  three   end    \n",
-    ]
-    variants.each do |s|
-      h = MBox.read_header StringIO.new(s)
-      assert_equal "one two  three   end", h["Subject"]
-    end
-  end
-
-  def test_message_id_ignore_spacing
-    variants = [
-      "Message-Id:     <one@bob.com>       \n",
-      "Message-Id:      one@bob.com        \n",
-      "Message-Id:<one@bob.com>       \n",
-      "Message-Id:one@bob.com       \n",
-    ]
-    variants.each do |s|
-      h = MBox.read_header StringIO.new(s)
-      assert_equal "one@bob.com", h["Message-Id"]
-    end
-  end
-
-  def test_blank_lines
-    h = MBox.read_header StringIO.new("")
-    assert_equal nil, h["Message-Id"]
-  end
-
-  def test_empty_headers
-    variants = [
-      "Message-Id:       \n",
-      "Message-Id:\n",
-    ]
-    variants.each do |s|
-      h = MBox.read_header StringIO.new(s)
-      assert_equal "", h["Message-Id"]
-    end
-  end
-
-  def test_detect_end_of_headers
-    h = MBox.read_header StringIO.new(<<EOS)
-From: Bob <bob@bob.com>
-
-To: a dear friend
-EOS
-  assert_equal "Bob <bob@bob.com>", h["From"]
-  assert_nil h["To"]
-
-  h = MBox.read_header StringIO.new(<<EOS)
-From: Bob <bob@bob.com>
-\r
-To: a dear friend
-EOS
-  assert_equal "Bob <bob@bob.com>", h["From"]
-  assert_nil h["To"]
-
-  h = MBox.read_header StringIO.new(<<EOS)
-From: Bob <bob@bob.com>
-\r\n\r
-To: a dear friend
-EOS
-  assert_equal "Bob <bob@bob.com>", h["From"]
-  assert_nil h["To"]
-  end
-end
index e38ac5064ca999f8b95b6d70c93f9b05ed8796f4..0a7db454febd8c93238dacaa89590f6110222cc6 100644 (file)
@@ -511,7 +511,7 @@ EOS
 
     # Look at another header field whose first line was blank.
     list_unsubscribe = sup_message.list_unsubscribe
-    assert_equal(" <http://mailman2.widget.com/mailman/listinfo/monitor-list>, " +
+    assert_equal("<http://mailman2.widget.com/mailman/listinfo/monitor-list>, " +
                  "<mailto:monitor-list-request@widget.com?subject=unsubscribe>",
                  list_unsubscribe)
 
index bdf40e97822db4102aefc836d8505dd0044570b5..272abc02536c045a4b2218ecc7537a54fb4a4a16 100644 (file)
@@ -3,7 +3,7 @@
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
        <head>
                <title>Sup</title>
-               <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
+               <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
                <link rel="stylesheet" href="main.css" type="text/css" />
        </head>
 
                <h2>Status</h2>
 
                <p>
-               The current version of Sup is 0.7, released 2009-03-25. This is a
-               beta release. It supports mbox, mbox over ssh, IMAP, IMAPS, and Maildir mailstores.
+               The current version of Sup is 0.8.1, released 2009-06-15. This is a
+               beta release. It supports mbox, IMAP, IMAPS, and Maildir mailstores.
                </p>
 
+        <p>To be notified by email of Sup releases, subscribe to the
+           <a href="http://rubyforge.org/mailman/listinfo/sup-announce">sup-announce mailing list</a>. One email per release.
+        </p>
+
         <!-- <p>Issue and release status is available on the <a href="ditz/">Sup ditz page</a>.</p> -->
         <p>Sup news can often be see on <a href="http://all-thing.net/label/sup/">William's blog</a>.</p>
 
                <p>
                Sup is brought to you by <a href="http://cs.stanford.edu/~ruby/">William Morgan</a> and the following honorable contributors:
         <ul>
-<li>William Morgan &lt;wmorgan-sup at the masanjin dot nets&gt;</li>
-<li>Ismo Puustinen &lt;ismo at the iki dot fis&gt;</li>
-<li>Nicolas Pouillard &lt;nicolas.pouillard at the gmail dot coms&gt;</li>
-<li>Marcus Williams &lt;marcus-sup at the bar-coded dot nets&gt;</li>
-<li>Lionel Ott &lt;white.magic at the gmx dot des&gt;</li>
-<li>Christopher Warrington &lt;chrisw at the rice dot edus&gt;</li>
-<li>Marc Hartstein &lt;marc.hartstein at the alum.vassar dot edus&gt;</li>
-<li>Ben Walton &lt;bwalton at the artsci.utoronto dot cas&gt;</li>
-<li>Grant Hollingworth &lt;grant at the antiflux dot orgs&gt;</li>
-<li>Steve Goldman &lt;sgoldman at the tower-research dot coms&gt;</li>
-<li>Decklin Foster &lt;decklin at the red-bean dot coms&gt;</li>
-<li>Jeff Balogh &lt;its.jeff.balogh at the gmail dot coms&gt;</li>
-<li>Giorgio Lando &lt;patroclo7 at the gmail dot coms&gt;</li>
-<li>Israel Herraiz &lt;israel.herraiz at the gmail dot coms&gt;</li>
-<li>Ian Taylor &lt;ian at the lorf dot orgs&gt;</li>
-<li>Rich Lane &lt;rlane at the club.cc.cmu dot edus&gt;</li>
+<li>Nicolas Pouillard </li>
+<li>Mike Stipicevic </li>
+<li>Marcus Williams </li>
+<li>Lionel Ott </li>
+<li>Ingmar Vanhassel </li>
+<li>Mark Alexander </li>
+<li>Christopher Warrington </li>
+<li>Richard Brown </li>
+<li>Ben Walton </li>
+<li>Marc Hartstein </li>
+<li>Grant Hollingworth </li>
+<li>Steve Goldman </li>
+<li>Decklin Foster </li>
+<li>Ismo Puustinen </li>
+<li>Jeff Balogh </li>
+<li>Alex Vandiver </li>
+<li>Giorgio Lando </li>
+<li>Israel Herraiz </li>
+<li>Ian Taylor </li>
+<li>Stefan Lundström </li>
+<li>Rich Lane </li>
+<li>Kirill Smelkov </li>
         </ul>
         </p>
 
                <p>
                Sup is made possible by the hard work of <a
                        href="http://www.davebalmain.com/">Dave Balmain</a> and his
-               fantastic IR engine <a
-                       href="http://ferret.davebalmain.com/trac/">Ferret</a>, and by <a
-                       href="http://www.lickey.com/">Matt Armstrong</a>'s tragically
-               abandoned <a href="http://www.rfc20.org/rubymail/">RubyMail</a>
-               package (note: possibly no longer abandoned).
+               tragically abandoned IR engine <a
+                       href="http://ferret.davebalmain.com/trac/">Ferret</a>, and by that of <a
+                       href="http://www.lickey.com/">Matt Armstrong</a> and his
+               tagically abandoned <a href="http://www.rfc20.org/rubymail/">RubyMail</a>
+               package.
                </p>
        </body>
 </html>