--- /dev/null
+William Morgan <wmorgan-sup at the masanjin dot nets>
+Ismo Puustinen <ismo at the iki dot fis>
+Marcus Williams <marcus-sup at the bar-coded dot nets>
+Nicolas Pouillard <nicolas.pouillard at the gmail dot coms>
+Christopher Warrington <chrisw at the rice dot edus>
+Jeff Balogh <its.jeff.balogh at the gmail dot coms>
+CONTRIBUTORS
HACKING
History.txt
LICENSE
doc/Hooks.txt
doc/NewUserGuide.txt
doc/Philosophy.txt
-doc/TODO
lib/sup.rb
lib/sup/account.rb
lib/sup/buffer.rb
require 'trollop'
require "sup"
-BIN_VERSION = "0.5"
+BIN_VERSION = "git"
unless Redwood::VERSION == BIN_VERSION
$stderr.puts <<EOS
module Redwood
global_keymap = Keymap.new do |k|
- k.add :quit, "Quit Redwood", 'q'
+ k.add :quit_ask, "Quit Sup, but ask first", 'q'
+ k.add :quit_now, "Quit Sup immediately", 'Q'
k.add :help, "Show help", 'H', '?'
k.add :roll_buffers, "Switch to next buffer", 'b'
# k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
end
case action
- when :quit
+ when :quit_now
break if bm.kill_all_buffers_safely
+ when :quit_ask
+ if bm.ask_yes_or_no "Really quit?"
+ break if bm.kill_all_buffers_safely
+ end
when :help
curmode = bm.focus_buf.mode
bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
+++ /dev/null
-for 0.5
--------
-_ mark thread as unread should remember the unread messages and mark
- only them as unread, just like gmail
-_ bugfix: time zone parsing broken?
-_ need a better way to force an address to a particular name,
- for things like evite addresses
-_ imap "add all folders on this server" option in sup-add
-_ for new message flashes, add new message counts until keypress
-_ bugfix: missing sources should be handled better
-_ search results: highlight relevant snippets and open to relevant
- portion of thread
-_ have "notes" (treated as emails to oneself, never sent) as
- first-class objects.
-_ make use of the username in URIs, and move account passwords to
- a different file from sources.yaml. heck, allow encryption of that
- file.
-
-for 0.4
--------
-_ bugfix in drafts: the entire thread is currently discarded, rather
- than just the one message. (need to distinguish per-message and
- per-thread deletion in the update messages.)
-_ bugfix in completion: capitalization for contact names
-_ imap: cache headers
-_ imap: share connection to the same server.
-_ dispatch-and-kill version of mark thread as unread in thread-view-mode
-_ forward for one or more tagged messages should attach them
-_ mbox: don't keep filehandles open, and protect all reads with dotlockfile
-_ bugfix: screwing with the headers when editing causes a crash
-
-future
-------
-_ ldbd support
-_ don't use a people.txt; store email addresses directly in the index. too many
- problems with email addresses that occur with multiple names.
-_ infix match instead of prefix match for tab completion
-_ fix killed threads contributing to unread message count problem (prob. need
- to maintain all killed message ids and our own unread message count for
- inbox).
-_ emlx support (some os x thing)
-_ tab completion for mid-text cursors
-_ bugfix: not horizontal scrolling for ncurses text field entry
-_ use trac or something. this file is getting a little silly.
-_ saved searches
-_ bugfix: sometimes, when one new message comes into an imap folder,
- we don't catch it until a reload. but we do see a message
- indicating they're loaded to inbox (imap only? hard to reproduce.)
-_ bugfix: ferret flakiness: just added message but can't find it.
- possibly a message id tokenization issue.
-_ bugfix: read before thread-index has finished loading then hides the
- thread?!? wtf. (on jamie) (? still valid ?)
-_ bugfix: display field width in index-mode needs to be determined
- per-character rather than per-byte
-_ select all, starred, to me, etc
-_ undo
-_ Net::SMTP support
-_ ruby-talk:XXXX detection (via hooks?)
-_ more control character support in buffer line editing
-_ mboxz, mboxbz
-_ swappable keymappings
-_ bugfix: when returning from a shelling out, sometime ncurses is
- crazy and refuses to interpret any keystrokes
-_ configurable colors
-_ better batch deletion (extend to non-mbox sources)
-_ annotations on messages
-_ pop support
-_ toggleble wrapping of text
-_ maybe: de-archived messages auto-added to inbox
-_ prune old entries from people.txt so that it doesn't grow without
- bound
-_ maildir+ssh
-
-maybe
------
-_ split out threading & message chunk parsing to a separate library
-_ rangefilter on the initial inbox to only consider the most recent 1000 messages
-
-denied
-------
-x rss feed reading: use rss2email
-x gmail support: obsoleted by imap
-
-done
-----
-x bugfix: threading broken
-x bugfix: thread ordering in thread-index-mode sometimes jumps around with 'M'
-x forwards optionally include attachments
-x flesh out gpg integration: sign & encrypt outgoing
-x pressing A in thread-view-mode should jump to next message
-x multi-thread dump upon crash
-x hook manager caches values of any proc "variables"
-x bugfix: remove delay on startup if a usual imap source exists
-x bugfix: broken source handling still needs to be improved
-x speed up querying
-x bugfix: sources sometimes aren't added by sup-add
-x more widgets: terminal title, statusbar
-x mailing list auto-subscribe/unsubscribe
-x bugfix: contacts.txt isn't parsed correctly when there are spaces in
- aliases
-x bugfix: @ signs in names make sendmail die silently
-x bugfix: sent.mbox and >From
-x bugfix: tokenized email addresses (amazon.com, etc)
-x bugfix: trailing spaces in buffermanager.ask
-x bugfix: need to URL-unescape maildir folders
-x bugfix: downcasing tab completion
-x warnings: top-posting, missing attachment
-x hookability
-x bugfix: deadlock (on rubyforge) (? still valid ?)
-x bugfix: ferret corrupt index problem at index.c:901. see http://ferret.davebalmain.com/trac/ticket/279
-x tab completion for to: and cc: in compose-mode
-x individual labeling in thread-view-mode
-x translate aliases in queries on to: and from: fields
-x tab completion on labeling
-x bugfix: any interactive prompt after "No new messages." flash has an
- empty line above it.
-x detect other sup instances and do something intelligent (because
- ferret crashes violently with more than one index writer open)
-x refactor all the *-search-results-mode classes
-x decode RFC 2047 ("encoded word") headers
- - see: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/101949, http://dev.rubyonrails.org/ticket/6807
-x create attachments
-x add arbitrary labels to sources
-x improve sup-config
-x autoload more threads when you go down
-x add a sync-back tool that at least works for mboxes
-x thread by subject configurable in config.yaml
-x view as text command if the mime view command fails for an attachment
-x bugfix: attachment filenames sometimes not detected (filename=)
-x bugfix: rmail multipart error
-x bugfix: sup-add not prompting for old accounts, i think? possibly because
- sources no longer respond_to? :username due to Recoverable wrapping
-x wide character support
-x i18n support
-x tab completion on labels
-x nice little startup config program
-x bugfix: triggering a pageup when cursor scrolling up jumps to the
- bottom of the page rather than the next line
-x bugfix: final logging messages to stdout?
-x bugfix: mbox directory shouldn't generate an exception, just an error
-x bugfix: m in thread-view-mode when a person is not selected should open up a
- blank compose-mode rather than do nothing
-x bugfix: stars on messages with blue backgrounds still have green bgs
-x ferret upgrade script (dump & restore)
-x bugfix: mark messages as read immediately when t-v-m is opened
-x compose in thread-view-mode auto-fills in person
-x bugfix: 'N' in thread-view-mode (expand only new messages) crashes
-x bugfix: detect source corruption at startup
-x maildir
-x bugfix: single-line messages come empty upon reply
-x make 'A' archive in thread-view-mode
-x remove stupid percent_done source methods (still useful; made it optional)
-x don't quit while writing thread index state to disk or with unsaved drafts/messages
-x bugfix: deleted threads are showing up (i don't see this any more)
-x bugfix: changing IMAP ids
-x bugfix: STILL new messages, drafts sometimes not showing up in inbox
-x bugfix: killed threads
-x bugfix: resuming a draft asks before discard
-x add a flag to sup-import to force the creation of a new source (see http://rubyforge.org/forum/forum.php?thread_id=10973&forum_id=10340)
-x use trollop to handle sup-devel args
-x clean up import code and share between poll.rb and sup-import
-x on startup, multi-threadedly call #connect on all sources
-x bugfix: first time viewing a message only gets the first to:; subsequent views get them all (wtf)
-x search for other messages from author in thread-view-mode
-x resuming of arbitrary messages
-x alias authors in thread-view-mode
-x fix up contact list mode: should display while loading, and when you add an alias, should move everything else to the right
-x fix bug: envelope-to thing still not working
-x fix snippet repetitions with small snippets
-x fix next and previous in thread-view-mode with <unreceived messages>
-x move sup-import username/password prompts to highline
-x support different remote servers per user account
-x 'R' to quick-resume most recent draft
-x mbox+ssh
-x handle broken sources better
-x imap
-x word wrap
-x background indexing
-x auto-insertion of draft messages
-x drafts
-x sent messages loader
-x search: from
-x contacts
-x tagging for group operations
-x view: starred, to me, etc
-x pull in messages by subject as well in load_thread_for_
-x reply+compose+forward
-x resize
-x buffer respawns
-x readline
-x "loading" message
-x search: body, to/from, tags (requires: readline)
-x highlighting/different color stuff
-x config: your email, sendmail, etc
-x status: to/from_you, cc_you_others
-x status: new/not, important
-x bugfix: miscellaneous weirdnesses in buffer line editing
end
module Redwood
- VERSION = "0.5"
+ VERSION = "git"
BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
CONFIG_FN = File.join(BASE_DIR, "config.yaml")
payload_fn.write format_payload(payload)
payload_fn.close
- recipient_opts = to.map { |r| "--recipient '#{r}'" }.join(" ")
+ recipient_opts = to.map { |r| "--recipient '<#{r}>'" }.join(" ")
sign_opts = sign ? "--sign --local-user '#{from}'" : ""
gpg_output = run_gpg "--output - --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}"
raise Error, (gpg_output || "gpg command failed: #{cmd}") unless $?.success?
["Can't find gpg binary in path."]
end
+ ## here's where we munge rmail output into the format that signed/encrypted
+ ## PGP/GPG messages should be
def format_payload payload
payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
end
when :home: "<home>"
when :end: "<end>"
when :enter, :return: "<enter>"
- when :ctrl_l: "ctrl-l"
- when :ctrl_g: "ctrl-g"
when :tab: "tab"
when " ": "<space>"
else
- if k.is_a?(String) && k.length == 1
- k
- else
- raise ArgumentError, "unknown key name \"#{k}\""
- end
+ Curses::keyname(keysym_to_keycode(k))
end
end
## TODO: move functionality to somewhere better, like message.rb
module MBox
BREAK_RE = /^From \S+/
+ HEADER_RE = /\s*(.*?\S)\s*/
def read_header f
header = {}
## when scanning over large mbox files.
while(line = f.gets)
case line
- when /^(From):\s*(.*?)\s*$/i,
- /^(To):\s*(.*?)\s*$/i,
- /^(Cc):\s*(.*?)\s*$/i,
- /^(Bcc):\s*(.*?)\s*$/i,
- /^(Subject):\s*(.*?)\s*$/i,
- /^(Date):\s*(.*?)\s*$/i,
- /^(References):\s*(.*?)\s*$/i,
- /^(In-Reply-To):\s*(.*?)\s*$/i,
- /^(Reply-To):\s*(.*?)\s*$/i,
- /^(List-Post):\s*(.*?)\s*$/i,
- /^(List-Subscribe):\s*(.*?)\s*$/i,
- /^(List-Unsubscribe):\s*(.*?)\s*$/i,
- /^(Status):\s*(.*?)\s*$/i: header[last = $1] = $2
- when /^(Message-Id):\s*(.*?)\s*$/i: header[mid_field = 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: header[last = $1] = $2
+ when /^(Message-Id):#{HEADER_RE}$/i: header[mid_field = last = $1] = $2
## these next three can occur multiple times, and we want the
## first one
- when /^(Delivered-To):\s*(.*)$/i,
- /^(X-Original-To):\s*(.*)$/i,
- /^(Envelope-To):\s*(.*)$/i: header[last = $1] ||= $2
+ when /^(Delivered-To):#{HEADER_RE}$/i,
+ /^(X-Original-To):#{HEADER_RE}$/i,
+ /^(Envelope-To):#{HEADER_RE}$/i: header[last = $1] ||= $2
when /^\r*$/: break
when /^\S+:/: last = nil # some other header we don't care about
m = RMail::Message.new
m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
m.body = @body.join("\n")
- m.body = m.body
m.body += sig_lines.join("\n") unless $config[:edit_signature]
+ ## body must end in a newline or GPG signatures will be WRONG!
+ m.body += "\n" unless m.body =~ /\n\Z/
## there are attachments, so wrap body in an attachment of its own
unless @attachments.empty?
## do whatever crypto transformation is necessary
if @crypto_selector && @crypto_selector.val != :none
from_email = PersonManager.person_for(@header["From"]).email
- to_email = (@header["To"] + @header["Cc"] + @header["Bcc"]).map { |p| PersonManager.person_for(p).email }
+ to_email = [@header["To"], @header["Cc"], @header["Bcc"]].flatten.compact.map { |p| PersonManager.person_for(p).email }
m = CryptoManager.send @crypto_selector.val, from_email, to_email, m
end
if text
@header[field] = parse_header field, text
update
- field
end
else
- default =
- case field
+ default = case field
when *MULTI_HEADERS
+ @header[field] ||= []
@header[field].join(", ")
else
@header[field]
text = contacts.map { |s| s.longname }.join(", ")
@header[field] = parse_header field, text
update
- field
end
end
end
register_keymap do |k|
## overwrite toggle_archived with archive
k.add :archive, "Archive thread (remove from inbox)", 'a'
+ k.add :read_and_archive, "Archive thread (remove from inbox) and mark read", 'A'
end
def initialize
regen_text
end
+ def read_and_archive
+ return unless cursor_thread
+ cursor_thread.remove_label :unread
+ cursor_thread.remove_label :inbox
+ hide_thread cursor_thread
+ regen_text
+ end
+
+ def multi_read_and_archive threads
+ threads.each do |t|
+ t.remove_label :unread
+ t.remove_label :inbox
+ hide_thread t
+ end
+ regen_text
+ end
+
def handle_unarchived_update sender, m
add_or_unhide m
end
BufferManager.flash "No labels messages with unread messages."
end
end
+
+ def focus
+ reload # make sure unread message counts are up-to-date
+ end
+
protected
def toggle_show_unread_only
COL_JUMP = 2
register_keymap do |k|
- k.add :line_down, "Down one line", :down, 'j', 'J'
- k.add :line_up, "Up one line", :up, 'k', 'K'
+ k.add :line_down, "Down one line", :down, 'j', 'J', "\C-e"
+ k.add :line_up, "Up one line", :up, 'k', 'K', "\C-y"
k.add :col_left, "Left one column", :left, 'h'
k.add :col_right, "Right one column", :right, 'l'
- k.add :page_down, "Down one page", :page_down, ' '
- k.add :page_up, "Up one page", :page_up, 'p', :backspace
+ k.add :page_down, "Down one page", :page_down, ' ', "\C-f"
+ k.add :page_up, "Up one page", :page_up, 'p', :backspace, "\C-b"
+ k.add :half_page_down, "Down one half page", "\C-d"
+ k.add :half_page_up, "Up one half page", "\C-u"
k.add :jump_to_start, "Jump to top", :home, '^', '1'
k.add :jump_to_end, "Jump to bottom", :end, '$', '0'
k.add :jump_to_left, "Jump to the left", '['
continue_search_in_buffer
end
- ## subclasses can override these two!
+ ## subclasses can override these three!
def search_goto_pos line, leftcol, rightcol
- jump_to_line line
+ search_goto_line line
if rightcol > self.rightcol # if it's occluded...
jump_to_col [rightcol - buffer.content_width + 1, 0].max # move right
end
end
def search_start_line; @topline end
+ def search_goto_line line; jump_to_line line end
def col_left
return unless @leftcol > 0
def line_up; jump_to_line @topline - 1; end
def page_down; jump_to_line @topline + buffer.content_height - @slip_rows; end
def page_up; jump_to_line @topline - buffer.content_height + @slip_rows; end
+ def half_page_down; jump_to_line @topline + buffer.content_height / 2; end
+ def half_page_up; jump_to_line @topline - buffer.content_height / 2; end
def jump_to_start; jump_to_line 0; end
def jump_to_end; jump_to_line lines - buffer.content_height; end
end
def multi_edit_labels threads
- answer = BufferManager.ask :add_labels, "add labels: "
- return unless answer
- user_labels = answer.split(/\s+/).map { |l| l.intern }
+ user_labels = BufferManager.ask_for_labels :add_labels, "Add labels: ", [], @hidden_labels
+ return unless user_labels
hl = user_labels.select { |l| @hidden_labels.member? l }
if hl.empty?
def compose
p = @person_lines[curpos]
if p
- ComposeMode.spawn_nicely :to => [p]
+ ComposeMode.spawn_nicely :to_default => p
else
ComposeMode.spawn_nicely
end
mv tmp.txt History.txt
vi History.txt # and cleanup
vi ReleaseNotes # and add whatever's necessary
-vi www/index.html # and bump version number
+vi www/index.html # and bump version number
+git rank-contributors -o > CONTRIBUTORS
+vi CONTRIBUTORS # and merge
+vi www/index.html # and include CONTRIBUTORS
# ... git add, commit, etc
git checkout -b release-<releasename>
vi lib/sup.rb bin/sup # and bump BOTH version numbers
--- /dev/null
+#!/usr/bin/ruby
+
+require 'test/unit'
+require 'sup'
+require 'stringio'
+
+include Redwood
+
+class TestMessage < 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_ignore_empty_lines
+ variants = [
+ "",
+ "Message-Id: \n",
+ "Message-Id:\n",
+ ]
+ variants.each do |s|
+ h = MBox.read_header StringIO.new(s)
+ assert_nil 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
<h2>Credit</h2>
<p>
- Sup is brought to you by <a href="http://cs.stanford.edu/~ruby/">William Morgan</a>.
- </p>
+ Sup is brought to you by <a href="http://cs.stanford.edu/~ruby/">William Morgan</a> and the following honorable contributors:
+ <ul>
+ <li>Ismo Puustinen <ismo at the iki dot fis></li>
+ <li>Marcus Williams <marcus-sup at the bar-coded dot nets></li>
+ <li>Nicolas Pouillard <nicolas.pouillard at the gmail dot coms></li>
+ <li>Christopher Warrington <chrisw at the rice dot edus></li>
+ <li>Jeff Balogh <its.jeff.balogh at the gmail dot coms></li>
+ </ul>
+ </p>
<p>
Sup is made possible by the hard work of <a