p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[2].gsub(/^\s+/, "")
p.changes = p.paragraphs_of('History.txt', 0..0).join("\n\n")
p.email = "wmorgan-sup@masanjin.net"
- p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline', 'net-ssh', ['trollop', '>= 1.7'], 'lockfile', 'mime-types', 'gettext']
+ p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline', 'net-ssh', ['trollop', '>= 1.7'], 'lockfile', 'mime-types', 'gettext', 'fastthread']
end
rule 'ss?.png' => 'ss?-small.png' do |t|
require 'curses'
require 'fileutils'
require 'trollop'
+require 'fastthread'
require "sup"
BIN_VERSION = "git"
exit(-1)
end
-$exceptions = []
$opts = Trollop::options do
version "sup v#{Redwood::VERSION}"
banner <<EOS
SearchResultsMode.spawn_from_query $opts[:search]
end
- until $exceptions.nonempty? || SuicideManager.die?
+ until Redwood::exceptions.nonempty? || SuicideManager.die?
c = Ncurses.nonblocking_getch
next unless c
bm.erase_flash
bm.kill_all_buffers if SuicideManager.die?
rescue Exception => e
- $exceptions << [e, "main"]
+ Redwood::record_exception e, "main"
ensure
unless $opts[:no_threads]
PollManager.stop if PollManager.instantiated?
Redwood::log "I've been ordered to commit seppuku. I obey!"
end
- if $exceptions.empty?
+ if Redwood::exceptions.empty?
Redwood::log "no fatal errors. good job, william."
Index.save
else
Index.unlock
end
-unless $exceptions.empty?
+unless Redwood::exceptions.empty?
File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
- $exceptions.each do |e, name|
+ Redwood::exceptions.each do |e, name|
f.puts "--- #{e.class.name} from thread: #{name}"
f.puts e.message, e.backtrace
end
William
----------------------------------------------------------------
EOS
- $exceptions.each do |e, name|
+ Redwood::exceptions.each do |e, name|
puts "--- #{e.class.name} from thread: #{name}"
puts e.message, e.backtrace
end
YAML_DOMAIN = "masanjin.net"
YAML_DATE = "2006-10-01"
-## record exceptions thrown in threads nicely
+ ## record exceptions thrown in threads nicely
+ @exceptions = []
+ @exception_mutex = Mutex.new
+
+ attr_reader :exceptions
+ def record_exception e, name
+ @exception_mutex.synchronize do
+ @exceptions ||= []
+ @exceptions << [e, name]
+ end
+ end
+
def reporting_thread name
if $opts[:no_threads]
yield
begin
yield
rescue Exception => e
- $exceptions ||= []
- $exceptions << [e, name]
- raise
+ record_exception e, name
end
end
end
end
- module_function :reporting_thread
+
+ module_function :reporting_thread, :record_exception, :exceptions
## one-stop shop for yamliciousness
def save_yaml_obj object, fn, safe=false
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
SCAN_INTERVAL = 30 # seconds
## remind me never to use inheritance again.
- yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
- def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[]
+ yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels, :mtimes
+ def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[], mtimes={}
super uri, last_date, usual, archived, id
uri = URI(Source.expand_filesystem_uri(uri))
raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
raise ArgumentError, "maildir URI cannot have a host: #{uri.host}" if uri.host
- raise ArgumentError, "mbox URI must have a path component" unless uri.path
+ raise ArgumentError, "maildir URI must have a path component" unless uri.path
@dir = uri.path
@labels = (labels || []).freeze
@ids_to_fns = {}
@last_scan = nil
@mutex = Mutex.new
+ #the mtime from the subdirs in the maildir with the unix epoch as default.
+ #these are used to determine whether scanning the directory for new mail
+ #is a worthwhile effort
+ @mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }.merge(mtimes || {})
+ @dir_ids = { 'cur' => [], 'new' => [] }
end
def file_path; @dir end
return unless @ids.empty? || opts[:rescan]
return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
- Redwood::log "scanning maildir..."
- cdir = File.join(@dir, 'cur')
- ndir = File.join(@dir, 'new')
-
- raise FatalSourceError, "#{cdir} not a directory" unless File.directory? cdir
- raise FatalSourceError, "#{ndir} not a directory" unless File.directory? ndir
+ initial_poll = @ids.empty?
+ Redwood::log "scanning maildir #@dir..."
begin
- @ids, @ids_to_fns = [], {}
- (Dir[File.join(cdir, "*")] + Dir[File.join(ndir, "*")]).map do |fn|
- id = make_id fn
- @ids << id
- @ids_to_fns[id] = fn
+ @mtimes.each_key do |d|
+ subdir = File.join(@dir, d)
+ raise FatalSourceError, "#{subdir} not a directory" unless File.directory? subdir
+
+ mtime = File.mtime subdir
+
+ #only scan the dir if the mtime is more recent (or we haven't polled
+ #since startup)
+ if @mtimes[d] < mtime || initial_poll
+ @mtimes[d] = mtime
+ @dir_ids[d] = []
+ Dir[File.join(subdir, '*')].map do |fn|
+ id = make_id fn
+ @dir_ids[d] << id
+ @ids_to_fns[id] = fn
+ end
+ else
+ Redwood::log "no poll on #{d}. mtime on indicates no new messages."
+ end
end
- @ids.sort!
+ @ids = @dir_ids.values.flatten.uniq.sort!
+ #remove old id to fn mappings...hopefully this doesn't actually change
+ #anything...normally, we'll add to this list but never remove mail.
+ @ids_to_fns.delete_if { |k, v| !@ids.include?(k) }
rescue SystemCallError, IOError => e
raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
end
private
def make_id fn
+ #doing this means 1 syscall instead of 2 (File.mtime, File.size).
+ #makes a noticeable difference on nfs.
+ stat = File.stat(fn)
# use 7 digits for the size. why 7? seems nice.
- sprintf("%d%07d", File.mtime(fn), File.size(fn) % 10000000).to_i
+ sprintf("%d%07d", stat.mtime, stat.size % 10000000).to_i
end
def with_file_for id
## 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
@source.fn_for_offset @source_info
end
- def sanitize_message_id mid; mid.gsub(/\s+/, "")[0..254] end
+ ## sanitize message ids by removing spaces and non-ascii characters.
+ ## also, truncate to 255 characters. all these steps are necessary
+ ## to make ferret happy. of course, we probably fuck up a couple
+ ## valid message ids as well. as long as we're consistent, this
+ ## should be fine, though.
+ ##
+ ## also, mostly the message ids that are changed by this belong to
+ ## spam email.
+ ##
+ ## an alternative would be to SHA1 or MD5 all message ids on a regular basis.
+ ## don't tempt me.
+ def sanitize_message_id mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end
def save index
return unless @dirty
from_email: the email part of the From: line, or nil if empty
Return value:
A string (multi-line ok) containing the text of the signature, or nil to
- use the default signature.
+ use the default signature, or :none for no signature.
EOS
HookManager.register "before-edit", <<EOS
@file = Tempfile.new "sup.#{self.class.name.gsub(/.*::/, '').camel_to_hyphy}"
@file.puts format_headers(@header - NON_EDITABLE_HEADERS).first
@file.puts
- @file.puts @body
+ @file.puts @body.join("\n")
@file.close
editor = $config[:editor] || ENV['EDITOR'] || "/usr/bin/vi"
def parse_file fn
File.open(fn) do |f|
header = MBox::read_header f
- body = f.readlines
+ body = f.readlines.map { |l| l.chomp }
header.delete_if { |k, v| NON_EDITABLE_HEADERS.member? k }
header.each { |k, v| header[k] = parse_header k, v }
def build_message date
m = RMail::Message.new
m.header["Content-Type"] = "text/plain; charset=#{$encoding}"
- m.body = @body.join
+ m.body = @body.join("\n")
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/
end
f.puts
- f.puts sanitize_body(@body.join)
+ f.puts sanitize_body(@body.join("\n"))
f.puts sig_lines if full unless $config[:edit_signature]
end
end
def top_posting?
- @body.join =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
+ @body.join("\n") =~ /(\S+)\s*Excerpts from.*\n(>.*\n)+\s*\Z/
end
def sig_lines
## first run the hook
hook_sig = HookManager.run "signature", :header => @header, :from_email => from_email
+
+ return [] if hook_sig == :none
return ["", "-- "] + hook_sig.split("\n") if hook_sig
## no hook, do default signature generation based on config.yaml
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
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
def defer_all_other_method_calls_to obj
class_eval %{
def method_missing meth, *a, &b; @#{obj}.send meth, *a, &b; end
- def respond_to? meth; @#{obj}.respond_to?(meth); end
+ def respond_to?(m, include_private = false)
+ @#{obj}.respond_to?(m, include_private)
+ end
}
end
end
def to_yaml x; __pass :to_yaml, x; end
def is_a? c; @o.is_a? c; end
- def respond_to? m; @o.respond_to? m end
+ def respond_to?(m, include_private=false)
+ @o.respond_to?(m, include_private)
+ end
def __pass m, *a, &b
begin
--- /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