commit 45c3433c036446455e63142d2d2db4e37557a260
parent 9be79c3141bd56bf8af0c6c877d51b9ae0911b83
Author: Rich Lane <rlane@club.cc.cmu.edu>
Date: Thu, 15 Jul 2010 19:22:17 -0700
Merge remote branch 'origin/maildir'
Conflicts:
bin/sup-dump
lib/sup/message.rb
lib/sup/modes/thread-index-mode.rb
lib/sup/server.rb
lib/sup/source.rb
Diffstat:
19 files changed, 374 insertions(+), 561 deletions(-)
diff --git a/bin/sup-add b/bin/sup-add
@@ -97,9 +97,9 @@ begin
source =
case parsed_uri.scheme
when "maildir"
- Redwood::Maildir.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
+ Redwood::Maildir.new uri, !$opts[:unusual], $opts[:archive], nil, labels
when "mbox"
- Redwood::MBox::Loader.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
+ Redwood::MBox.new uri, !$opts[:unusual], $opts[:archive], nil, labels
when nil
Trollop::die "Sources must be specified with an URI"
else
diff --git a/bin/sup-config b/bin/sup-config
@@ -57,7 +57,7 @@ def add_source
return if fn.nil? || fn.empty?
$last_fn = fn
- [Redwood::MBox::Loader.suggest_labels_for(fn),
+ [Redwood::MBox.suggest_labels_for(fn),
{ :scheme => "mbox", :path => fn }]
when :maildir
$last_fn ||= ENV["MAIL"]
diff --git a/bin/sup-dump b/bin/sup-dump
@@ -1,17 +1,19 @@
#!/usr/bin/env ruby
require 'rubygems'
+require 'xapian'
require 'trollop'
-require "sup"; Redwood::check_library_version_against "git"
+
+BASE_DIR = ENV["SUP_BASE"] || File.join(ENV["HOME"], ".sup")
$opts = Trollop::options do
- version "sup-dump (sup #{Redwood::VERSION})"
+ version "sup-dump"
banner <<EOS
Dumps all message state from the sup index to standard out. You can
later use sup-sync --restored --restore <filename> to recover the index.
-This tool is primarily useful in the event that a Xapian upgrade breaks
-the index format.
+This tool is primarily useful in the event that a Sup upgrade breaks index
+format compatibility.
Usage:
sup-dump > <filename>
@@ -19,17 +21,16 @@ Usage:
EOS
end
-$config = Redwood::load_config Redwood::CONFIG_FN
-index = Redwood::Index.init
-Redwood::SourceManager.init
-index.load
-Redwood::SentManager.init $config[:sent_source] || 'sup://sent'
-if(s = Redwood::SourceManager.source_for Redwood::SentManager.source_uri)
- Redwood::SentManager.source = s
-else
- Redwood::SourceManager.add_source Redwood::SentManager.default_source
-end
+xapian = Xapian::Database.new File.join(BASE_DIR, 'xapian')
+version = xapian.get_metadata 'rescue-version'
+version = '0' if version.empty?
-index.each_message :load_spam => true, :load_deleted => true, :load_killed => true do |m|
- puts "#{m.id} (#{m.labels.to_a.sort_by { |l| l.to_s } * ' '})"
+case version
+when '0'
+ xapian.postlist('Kmail').each do |x|
+ entry = Marshal.load(xapian.document(x.docid).data)
+ puts "#{entry[:message_id]} (#{entry[:labels].sort_by { |l| l.to_s } * ' '})"
+ end
+else
+ abort "this sup-dump version doesn't understand your index"
end
diff --git a/bin/sup-recover-sources b/bin/sup-recover-sources
@@ -58,7 +58,7 @@ ARGV.each do |fn|
next if Redwood::SourceManager.source_for fn
## TODO: merge this code with the same snippet in import
- source = Redwood::MBox::Loader.new(fn, nil, !$opts[:unusual], $opts[:archive])
+ source = Redwood::MBox.new(fn, nil, !$opts[:unusual], $opts[:archive])
source_ids = Hash.new 0
count = 0
diff --git a/bin/sup-sync b/bin/sup-sync
@@ -53,16 +53,6 @@ where <source>* is zero or more source URIs. If no sources are given,
sync from all usual sources. Supported source URI schemes can be seen
by running "sup-add --help".
-Options controlling WHICH messages sup-sync operates on:
-EOS
- opt :new, "Operate on new messages only. Don't scan over the entire source. (Default.)", :short => :none
- opt :changed, "Scan over the entire source for messages that have been deleted, altered, or moved from another source."
- opt :restored, "Operate only on those messages included in a dump file as specified by --restore which have changed state."
- opt :all, "Operate on all messages in the source, regardless of newness or changedness."
- opt :start_at, "For --changed, --restored and --all, start at a particular offset.", :type => :int
-
-text <<EOS
-
Options controlling HOW message state is altered:
EOS
opt :asis, "If the message is already in the index, preserve its state. Otherwise, use default source state. (Default.)", :short => :none
@@ -82,16 +72,9 @@ EOS
opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
opt :version, "Show version information", :short => :none
- conflicts :changed, :all, :new, :restored
conflicts :asis, :restore, :discard
end
-Trollop::die :restored, "requires --restore" if opts[:restored] unless opts[:restore]
-if opts[:start_at]
- Trollop::die :start_at, "must be non-negative" if opts[:start_at] < 0
- Trollop::die :start_at, "requires either --changed, --restored or --all" unless opts[:changed] || opts[:restored] || opts[:all]
-end
-target = [:new, :changed, :all, :restored].find { |x| opts[x] } || :new
op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis
Redwood::start
@@ -132,136 +115,80 @@ begin
end
end
- ## for all target specifications except for only-new messages, reset the
- ## source to the beginning (or to the user-specified starting point.)
- unless target == :new
- if opts[:start_at]
- Trollop::die :start_at, "can only be used on one source" unless sources.size == 1
- sources.first.seek_to! opts[:start_at]
- sources.first.correct_offset! if sources.first.respond_to?(:correct_offset!)
- else
- sources.each { |s| s.reset! }
- end
- end
-
sources.each do |source|
puts "Scanning #{source}..."
num_added = num_updated = num_scanned = num_restored = 0
last_info_time = start_time = Time.now
- Redwood::PollManager.each_message_from source do |m|
- num_scanned += 1
- seen[m.id] = true
- old_m = index.build_message m.id
-
- case target
- when :changed
- ## skip this message if we're operating only on changed messages, the
- ## message is in the index, and it's unchanged from what the source is
- ## reporting.
- next if old_m && old_m.locations.member?([m.source, m.source_info])
- when :restored
- ## skip if we're operating on restored messages, and this one
- ## ain't (or we wouldn't be making a change)
- next unless old_m && restored_state[m.id] && restored_state[m.id] != old_m.labels
- when :new
- ## nothing to do; we'll consider all messages starting at the start offset, which
- ## hasn't been changed.
- when :all
- ## nothing to do; we'll consider all messages starting at the start offset, which
- ## was reset to the beginning above.
- end
-
- ## tweak source labels according to commandline arguments if necessary
- m.labels.delete :inbox if opts[:archive]
- m.labels.delete :unread if opts[:read]
- m.labels += opts[:extra_labels].to_set_of_symbols(",")
-
- ## decide what to do based on message labels and the operation we're performing
- dothis, new_labels = case
- when (op == :restore) && restored_state[m.id]
- if old_m && (old_m.labels != restored_state[m.id])
- num_restored += 1
- [:update_message_state, restored_state[m.id]]
- elsif old_m.nil?
- num_restored += 1
- m.labels = restored_state[m.id]
- :add_message
- else
- # labels are the same; don't do anything
- end
- when op == :discard
- if old_m && (old_m.labels != m.labels)
- [:update_message_state, m.labels]
- else
- # labels are the same; don't do anything
- end
- else
- ## duplicate behavior of poll mode: if index_state is non-nil, this is a newer
- ## version of an older message, so merge in any new labels except :unread and
- ## :inbox.
- ##
- ## TODO: refactor such that this isn't duplicated
- if old_m
- m.labels = old_m.labels + (m.labels - [:unread, :inbox])
- :update_message
+ Redwood::PollManager.poll_from source do |action,m,old_m|
+ if action == :delete
+ puts "Deleting #{m.id}"
+ elsif action == :add
+ num_scanned += 1
+ seen[m.id] = true
+
+ ## tweak source labels according to commandline arguments if necessary
+ m.labels.delete :inbox if opts[:archive]
+ m.labels.delete :unread if opts[:read]
+ m.labels += opts[:extra_labels].to_set_of_symbols(",")
+
+ ## decide what to do based on message labels and the operation we're performing
+ dothis = case
+ when (op == :restore) && restored_state[m.id]
+ if old_m && (old_m.labels != restored_state[m.id])
+ num_restored += 1
+ m.labels = restored_state[m.id]
+ :update_message_state
+ elsif old_m.nil?
+ num_restored += 1
+ m.labels = restored_state[m.id]
+ :add_message
+ else
+ # labels are the same; don't do anything
+ end
+ when op == :discard
+ if old_m && (old_m.labels != m.labels)
+ :update_message_state
+ else
+ # labels are the same; don't do anything
+ end
else
- :add_message
+ if old_m
+ :update_message
+ else
+ :add_message
+ end
end
- end
- m.locations = old_m.locations + m.locations if old_m
-
- ## now, actually do the operation
- case dothis
- when :add_message
- puts "Adding new message #{source}##{m.source_info} with labels #{m.labels}" if opts[:verbose]
- index.add_message m unless opts[:dry_run]
- num_added += 1
- when :update_message
- puts "Updating message #{source}##{m.source_info}; labels #{old_m.labels} => #{m.labels}; offset #{old_m.source_info} => #{m.source_info}" if opts[:verbose]
- index.update_message m unless opts[:dry_run]
- num_updated += 1
- when :update_message_state
- puts "Changing flags for #{source}##{m.source_info} from #{m.labels} to #{new_labels}" if opts[:verbose]
- m.labels = new_labels
- index.update_message_state m unless opts[:dry_run]
- num_updated += 1
- end
+ ## now, actually do the operation
+ case dothis
+ when :add_message
+ puts "Adding new message #{source}##{m.source_info} with labels #{m.labels}" if opts[:verbose]
+ num_added += 1
+ when :update_message
+ puts "Updating message #{source}##{m.source_info}; labels #{old_m.labels} => #{m.labels}; offset #{old_m.source_info} => #{m.source_info}" if opts[:verbose]
+ num_updated += 1
+ when :update_message_state
+ puts "Changing flags for #{source}##{m.source_info} from #{old_m.labels} to #{m.labels}" if opts[:verbose]
+ num_updated += 1
+ 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)
- printf "## read %dm (~%.0f%%) @ %.1fm/s. %s elapsed, ~%s remaining, offset #{source.cur_offset}\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
+ if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
+ last_info_time = Time.now
+ elapsed = last_info_time - start_time
+ pctdone = 0.0 * 100.0
+ remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
+ printf "## read %dm (~%.0f%%) @ %.1fm/s. %s elapsed, ~%s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
+ end
+ else fail
end
+ next if opts[:dry_run]
end
puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated} messages from #{source}."
puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
end
- ## delete any messages in the index that claim they're from one of
- ## these sources, but that we didn't see.
- if (target == :all || target == :changed)
- puts "Deleting missing messages from the index..."
- num_del, num_scanned = 0, 0
- sources.each do |source|
- raise "no source id for #{source}" unless source.id
- index.each_message :source_id => source.id, :load_spam => true, :load_deleted => true, :load_killed => true do |m|
- num_scanned += 1
- unless seen[m.id]
- next unless m.source_info >= opts[:start_at] if opts[:start_at]
- puts "Deleting #{m.id}" if opts[:verbose]
- index.delete m.id unless opts[:dry_run]
- num_del += 1
- end
- end
- end
- puts "Deleted #{num_del} / #{num_scanned} messages"
- end
-
index.save
if opts[:optimize]
diff --git a/bin/sup-sync-back b/bin/sup-sync-back
@@ -6,6 +6,8 @@ require 'tempfile'
require 'trollop'
require "sup"; Redwood::check_library_version_against "git"
+fail "not working yet"
+
## save a message 'm' to an open file pointer 'fp'
def save m, fp
m.source.each_raw_message_line(m.source_info) { |l| fp.print l }
@@ -80,12 +82,12 @@ begin
sources = ARGV.map do |uri|
s = Redwood::SourceManager.source_for(uri) or die "unknown source: #{uri}. Did you add it with sup-add first?"
- s.is_a?(Redwood::MBox::Loader) or die "#{uri} is not an mbox source."
+ s.is_a?(Redwood::MBox) or die "#{uri} is not an mbox source."
s
end
if sources.empty?
- sources = Redwood::SourceManager.usual_sources.select { |s| s.is_a? Redwood::MBox::Loader }
+ sources = Redwood::SourceManager.usual_sources.select { |s| s.is_a? Redwood::MBox }
end
unless sources.all? { |s| s.file_path.nil? } || File.executable?(dotlockfile) || opts[:dont_use_dotlockfile]
diff --git a/lib/sup/draft.rb b/lib/sup/draft.rb
@@ -11,20 +11,13 @@ class DraftManager
def self.source_name; "sup://drafts"; end
def self.source_id; 9999; end
- def new_source; @source = Recoverable.new DraftLoader.new; end
+ def new_source; @source = DraftLoader.new; end
def write_draft
offset = @source.gen_offset
fn = @source.fn_for_offset offset
File.open(fn, "w") { |f| yield f }
-
- my_message = nil
- PollManager.each_message_from(@source) do |m|
- PollManager.add_new_message m
- my_message = m
- end
-
- my_message
+ PollManager.poll_from @source
end
def discard m
@@ -37,31 +30,35 @@ end
class DraftLoader < Source
attr_accessor :dir
- yaml_properties :cur_offset
+ yaml_properties
- def initialize cur_offset=0
+ def initialize
dir = Redwood::DRAFT_DIR
Dir.mkdir dir unless File.exists? dir
- super DraftManager.source_name, cur_offset, true, false
+ super DraftManager.source_name, true, false
@dir = dir
+ @cur_offset = 0
end
def id; DraftManager.source_id; end
def to_s; DraftManager.source_name; end
def uri; DraftManager.source_name; end
- def each
+ def poll
ids = get_ids
ids.each do |id|
- if id >= cur_offset
- self.cur_offset = id + 1
- yield [id, [:draft, :inbox]]
+ if id >= @cur_offset
+ @cur_offset = id + 1
+ yield :add,
+ :info => id,
+ :labels => [:draft, :inbox],
+ :progress => 0.0
end
end
end
def gen_offset
- i = cur_offset
+ i = 0
while File.exists? fn_for_offset(i)
i += 1
end
diff --git a/lib/sup/index.rb b/lib/sup/index.rb
@@ -22,7 +22,7 @@ class Index
include InteractiveLock
STEM_LANGUAGE = "english"
- INDEX_VERSION = '3'
+ INDEX_VERSION = '4'
## dates are converted to integers for xapian, and are used for document ids,
## so we must ensure they're reasonably valid. this typically only affect
@@ -105,15 +105,16 @@ EOS
@xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
db_version = @xapian.get_metadata 'version'
db_version = '0' if db_version.empty?
- if db_version == '1' || db_version == '2'
+ if false
info "Upgrading index format #{db_version} to #{INDEX_VERSION}"
@xapian.set_metadata 'version', INDEX_VERSION
elsif db_version != INDEX_VERSION
- fail "This Sup version expects a v#{INDEX_VERSION} index, but you have an existing v#{db_version} index. Please downgrade to your previous version and dump your labels before upgrading to this version (then run sup-sync --restore)."
+ fail "This Sup version expects a v#{INDEX_VERSION} index, but you have an existing v#{db_version} index. Please run sup-dump to save your labels, move #{path} out of the way, and run sup-sync --restore."
end
else
@xapian = Xapian::WritableDatabase.new(path, Xapian::DB_CREATE)
@xapian.set_metadata 'version', INDEX_VERSION
+ @xapian.set_metadata 'rescue-version', '0'
end
@enquire = Xapian::Enquire.new @xapian
@enquire.weighting_scheme = Xapian::BoolWeight.new
@@ -197,7 +198,7 @@ EOS
locations = entry[:locations].map do |source_id,source_info|
source = SourceManager[source_id]
raise "invalid source #{source_id}" unless source
- [source, source_info]
+ Location.new source, source_info
end
m = Message.new :locations => locations,
@@ -265,6 +266,26 @@ EOS
synchronize { get_entry(id)[:source_id] }
end
+ ## Yields each tearm in the index that starts with prefix
+ def each_prefixed_term prefix
+ term = @xapian._dangerous_allterms_begin prefix
+ lastTerm = @xapian._dangerous_allterms_end prefix
+ until term.equals lastTerm
+ yield term.term
+ term.next
+ end
+ nil
+ end
+
+ ## Yields (in lexicographical order) the source infos of all locations from
+ ## the given source with the given source_info prefix
+ def each_source_info source_id, prefix='', &b
+ prefix = mkterm :location, source_id, prefix
+ each_prefixed_term prefix do |x|
+ yield x[prefix.length..-1]
+ end
+ end
+
class ParseError < StandardError; end
## parse a query string from the user. returns a query object
@@ -583,7 +604,7 @@ EOS
entry = {
:message_id => m.id,
- :locations => m.locations.map { |source,source_info| [source.id, source_info] },
+ :locations => m.locations.map { |x| [x.source.id, x.info] },
:date => truncate_date(m.date),
:snippet => snippet,
:labels => m.labels.to_a,
@@ -746,13 +767,7 @@ end
class Xapian::Document
def entry
- entry = Marshal.load data
- if entry[:source_id]
- entry[:locations] = [[entry[:source_id], entry[:source_info]]]
- entry.delete :source_id
- entry.delete :source_info
- end
- entry
+ Marshal.load data
end
def entry=(x)
diff --git a/lib/sup/logger.rb b/lib/sup/logger.rb
@@ -54,7 +54,7 @@ private
when "error"; "ERROR: "
else ""
end
- "[#{time.to_s}] #{prefix}#{msg}\n"
+ "[#{time.to_s}] #{prefix}#{msg.rstrip}\n"
end
## actually distribute the message
diff --git a/lib/sup/maildir.rb b/lib/sup/maildir.rb
@@ -3,20 +3,14 @@ require 'uri'
module Redwood
-## Maildir doesn't provide an ordered unique id, which is what Sup
-## requires to be really useful. So we must maintain, in memory, a
-## mapping between Sup "ids" (timestamps, essentially) and the
-## pathnames on disk.
-
class Maildir < Source
include SerializeLabelsNicely
- SCAN_INTERVAL = 30 # seconds
MYHOSTNAME = Socket.gethostname
## remind me never to use inheritance again.
- 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
+ yaml_properties :uri, :usual, :archived, :id, :labels
+ def initialize uri, usual=true, archived=false, id=nil, labels=[]
+ super uri, usual, archived, id
uri = URI(Source.expand_filesystem_uri(uri))
raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
@@ -25,28 +19,14 @@ class Maildir < Source
@dir = uri.path
@labels = Set.new(labels || [])
- @ids = []
- @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' => [] }
+ @mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
end
def file_path; @dir end
def self.suggest_labels_for path; [] end
def is_source_for? uri; super || (URI(Source.expand_filesystem_uri(uri)) == URI(self.uri)); end
- def check
- scan_mailbox
- return unless start_offset
-
- start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
- end
-
def store_message date, from_email, &block
stored = false
new_fn = new_maildir_basefn + ':2,S'
@@ -76,7 +56,6 @@ class Maildir < Source
end
def each_raw_message_line id
- scan_mailbox
with_file_for(id) do |f|
until f.eof?
yield f.gets
@@ -85,17 +64,14 @@ class Maildir < Source
end
def load_header id
- scan_mailbox
with_file_for(id) { |f| parse_raw_email_header f }
end
def load_message id
- scan_mailbox
with_file_for(id) { |f| RMail::Parser.read f }
end
def raw_header id
- scan_mailbox
ret = ""
with_file_for(id) do |f|
until f.eof? || (l = f.gets) =~ /^$/
@@ -106,106 +82,75 @@ class Maildir < Source
end
def raw_message id
- scan_mailbox
with_file_for(id) { |f| f.read }
end
- def scan_mailbox opts={}
- return unless @ids.empty? || opts[:rescan]
- return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
-
- initial_poll = @ids.empty?
-
- debug "scanning maildir #@dir..."
- begin
- @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
- debug "no poll on #{d}. mtime on indicates no new messages."
- end
+ ## XXX use less memory
+ def poll
+ @mtimes.each do |d,prev_mtime|
+ subdir = File.join @dir, d
+ debug "polling maildir #{subdir}"
+ raise FatalSourceError, "#{subdir} not a directory" unless File.directory? subdir
+ mtime = File.mtime subdir
+ next if prev_mtime >= mtime
+ @mtimes[d] = mtime
+
+ old_ids = benchmark(:maildir_read_index) { Enumerator.new(Index.instance, :each_source_info, self.id, "#{d}/").to_a }
+ new_ids = benchmark(:maildir_read_dir) { Dir.glob("#{subdir}/*").map { |x| File.basename x }.sort }
+ added = new_ids - old_ids
+ deleted = old_ids - new_ids
+ debug "#{old_ids.size} in index, #{new_ids.size} in filesystem"
+ debug "#{added.size} added, #{deleted.size} deleted"
+
+ added.each do |id|
+ yield :add,
+ :info => File.join(d,id),
+ :labels => @labels + maildir_labels(id) + [:inbox],
+ :progress => 0.0
end
- @ids = @dir_ids.values.flatten.uniq.sort!
- rescue SystemCallError, IOError => e
- raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
- end
-
- debug "done scanning maildir"
- @last_scan = Time.now
- end
- synchronized :scan_mailbox
- def each
- scan_mailbox
- return unless start_offset
-
- start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
-
- start.upto(@ids.length - 1) do |i|
- id = @ids[i]
- self.cur_offset = id
- yield id, @labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : []) + (flagged?(id) ? [:starred] : [])
+ deleted.each do |id|
+ yield :delete,
+ :info => File.join(d,id),
+ :progress => 0.0
+ end
end
+ nil
end
- def start_offset
- scan_mailbox
- @ids.first
- end
-
- def end_offset
- scan_mailbox :rescan => true
- @ids.last + 1
+ def maildir_labels id
+ (seen?(id) ? [] : [:unread]) +
+ (trashed?(id) ? [:deleted] : []) +
+ (flagged?(id) ? [:starred] : [])
end
- def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
+ def draft? id; maildir_data(id)[2].include? "D"; end
+ def flagged? id; maildir_data(id)[2].include? "F"; end
+ def passed? id; maildir_data(id)[2].include? "P"; end
+ def replied? id; maildir_data(id)[2].include? "R"; end
+ def seen? id; maildir_data(id)[2].include? "S"; end
+ def trashed? id; maildir_data(id)[2].include? "T"; end
- def draft? msg; maildir_data(msg)[2].include? "D"; end
- def flagged? msg; maildir_data(msg)[2].include? "F"; end
- def passed? msg; maildir_data(msg)[2].include? "P"; end
- def replied? msg; maildir_data(msg)[2].include? "R"; end
- def seen? msg; maildir_data(msg)[2].include? "S"; end
- def trashed? msg; maildir_data(msg)[2].include? "T"; end
+ def mark_draft id; maildir_mark_file id, "D" unless draft? id; end
+ def mark_flagged id; maildir_mark_file id, "F" unless flagged? id; end
+ def mark_passed id; maildir_mark_file id, "P" unless passed? id; end
+ def mark_replied id; maildir_mark_file id, "R" unless replied? id; end
+ def mark_seen id; maildir_mark_file id, "S" unless seen? id; end
+ def mark_trashed id; maildir_mark_file id, "T" unless trashed? id; end
- def mark_draft msg; maildir_mark_file msg, "D" unless draft? msg; end
- def mark_flagged msg; maildir_mark_file msg, "F" unless flagged? msg; end
- def mark_passed msg; maildir_mark_file msg, "P" unless passed? msg; end
- def mark_replied msg; maildir_mark_file msg, "R" unless replied? msg; end
- def mark_seen msg; maildir_mark_file msg, "S" unless seen? msg; end
- def mark_trashed msg; maildir_mark_file msg, "T" unless trashed? msg; end
-
- def filename_for_id id; @ids_to_fns[id] end
+ def valid? id
+ File.exists? File.join(@dir, id)
+ 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", stat.mtime, stat.size % 10000000).to_i
- end
-
def new_maildir_basefn
Kernel::srand()
"#{Time.now.to_i.to_s}.#{$$}#{Kernel.rand(1000000)}.#{MYHOSTNAME}"
end
def with_file_for id
- fn = @ids_to_fns[id] or raise OutOfSyncSourceError, "No such id: #{id.inspect}."
+ fn = File.join(@dir, id)
begin
File.open(fn, 'rb') { |f| yield f }
rescue SystemCallError, IOError => e
@@ -213,10 +158,9 @@ private
end
end
- def maildir_data msg
- fn = File.basename @ids_to_fns[msg]
- fn =~ %r{^([^:]+):([12]),([DFPRST]*)$}
- [($1 || fn), ($2 || "2"), ($3 || "")]
+ def maildir_data id
+ id =~ %r{^([^:]+):([12]),([DFPRST]*)$}
+ [($1 || id), ($2 || "2"), ($3 || "")]
end
## not thread-safe on msg
diff --git a/lib/sup/mbox.rb b/lib/sup/mbox.rb
@@ -8,12 +8,12 @@ class MBox < Source
BREAK_RE = /^From \S+ (.+)$/
include SerializeLabelsNicely
- yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
+ yaml_properties :uri, :usual, :archived, :id, :labels
attr_reader :labels
## uri_or_fp is horrific. need to refactor.
- def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=nil
+ def initialize uri_or_fp, usual=true, archived=false, id=nil, labels=nil
@mutex = Mutex.new
@labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
@@ -30,8 +30,7 @@ class MBox < Source
@path = uri_or_fp.path
end
- start_offset ||= 0
- super uri_or_fp, start_offset, usual, archived, id
+ super uri_or_fp, usual, archived, id
end
def file_path; @path end
@@ -47,23 +46,10 @@ class MBox < Source
end
end
- def check
- if (cur_offset ||= start_offset) > end_offset
- raise OutOfSyncSourceError, "mbox file is smaller than last recorded message offset. Messages have probably been deleted by another client."
- end
- end
-
- def start_offset; 0; end
- def end_offset; File.size @f; end
-
def load_header offset
header = nil
@mutex.synchronize do
@f.seek offset
- l = @f.gets
- unless MBox::is_break_line? l
- raise OutOfSyncSourceError, "mismatch in mbox file offset #{offset.inspect}: #{l.inspect}."
- end
header = parse_raw_email_header @f
end
header
@@ -76,8 +62,9 @@ class MBox < Source
## 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)
+ until @f.eof? || MBox::is_break_line?(l = @f.gets)
+ string << l
+ end
RMail::Parser.read string
rescue RMail::Parser::Error => e
raise FatalSourceError, "error parsing mbox file: #{e.message}"
@@ -85,18 +72,6 @@ class MBox < Source
end
end
- ## scan forward until we're at the valid start of a message
- def correct_offset!
- @mutex.synchronize do
- @f.seek cur_offset
- string = ""
- until @f.eof? || MBox::is_break_line?(l = @f.gets)
- string << l
- end
- self.cur_offset += string.length
- end
- end
-
def raw_header offset
ret = ""
@mutex.synchronize do
@@ -132,48 +107,50 @@ class MBox < Source
def each_raw_message_line offset
@mutex.synchronize do
@f.seek offset
- yield @f.gets
until @f.eof? || MBox::is_break_line?(l = @f.gets)
yield l
end
end
end
- def next
- returned_offset = nil
- next_offset = cur_offset
+ def pct_done
+ 0.0
+ end
+
+ def default_labels
+ [:inbox, :unread]
+ end
- begin
- @mutex.synchronize do
- @f.seek cur_offset
-
- ## cur_offset could be at one of two places here:
-
- ## 1. before a \n and a mbox separator, if it was previously at
- ## EOF and a new message was added; or,
- ## 2. at the beginning of an mbox separator (in all other
- ## cases).
-
- 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
- else # case 2
- returned_offset = cur_offset
- ## we've already skipped past the BREAK_RE, so just go
- end
+ def poll
+ offset = first_new_message
+ end_offset = File.size @f
+ while offset and offset < end_offset
+ yield :add,
+ :info => offset,
+ :labels => (labels + default_labels),
+ :progress => 0.0
+ offset = next_offset offset
+ end
+ end
- while(line = @f.gets)
- break if MBox::is_break_line? line
- next_offset = @f.tell
- end
- end
- rescue SystemCallError, IOError => e
- raise FatalSourceError, "Error reading #{@f.path}: #{e.message}"
+ def next_offset offset
+ @mutex.synchronize do
+ @f.seek offset
+ nil while line = @f.gets and not MBox::is_break_line? line
+ offset = @f.tell
+ offset != File.size(@f) ? offset : nil
end
+ end
+
+ ## TODO optimize this by iterating over allterms list backwards or
+ ## storing source_info negated
+ def last_indexed_message
+ benchmark(:mbox_read_index) { Enumerator.new(Index.instance, :each_source_info, self.id).map(&:to_i).max }
+ end
- self.cur_offset = next_offset
- [returned_offset, (labels + [:unread])]
+ ## offset of first new message or nil
+ def first_new_message
+ next_offset(last_indexed_message || 0)
end
def self.is_break_line? l
@@ -190,7 +167,7 @@ class MBox < Source
end
class Loader < self
- yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
+ yaml_properties :uri, :usual, :archived, :id, :labels
end
end
end
diff --git a/lib/sup/message.rb b/lib/sup/message.rb
@@ -9,9 +9,6 @@ module Redwood
## i would like, for example, to be able to add in a ruby-talk
## specific module that would detect and link to /ruby-talk:\d+/
## sequences in the text of an email. (how sweet would that be?)
-##
-## this class catches all source exceptions. if the underlying source
-## throws an error, it is caught and handled.
class Message
SNIPPET_LEN = 80
@@ -179,7 +176,7 @@ class Message
attr_reader :snippet
def is_list_message?; !@list_address.nil?; end
- def is_draft?; source.is_a? DraftLoader; end
+ def is_draft?; @labels.member? :draft; end
def draft_filename
raise "not a draft" unless is_draft?
source.fn_for_offset source_info
@@ -233,87 +230,59 @@ class Message
@chunks
end
+ def location
+ @locations.find { |x| x.valid? } || raise(OutOfSyncSourceError.new)
+ end
+
def source
- fail if @locations.empty?
- @locations.last[0]
+ location.source
end
def source_info
- fail if @locations.empty?
- @locations.last[1]
+ location.info
end
## this is called when the message body needs to actually be loaded.
def load_from_source!
@chunks ||=
- if source.respond_to?(:has_errors?) && source.has_errors?
- [Chunk::Text.new(error_message(source.error.message).split("\n"))]
- else
- begin
- ## we need to re-read the header because it contains information
- ## that we don't store in the index. actually i think it's just
- ## the mailing list address (if any), so this is kinda overkill.
- ## i could just store that in the index, but i think there might
- ## be other things like that in the future, and i'd rather not
- ## bloat the index.
- ## actually, it's also the differentiation between to/cc/bcc,
- ## so i will keep this.
- rmsg = source.load_message(source_info)
- parse_header rmsg.header
- message_to_chunks rmsg
- rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e
- warn "problem getting messages from #{source}: #{e.message}"
- ## we need force_to_top here otherwise this window will cover
- ## up the error message one
- source.error ||= e
- Redwood::report_broken_sources :force_to_top => true
- [Chunk::Text.new(error_message(e.message).split("\n"))]
- end
+ begin
+ ## we need to re-read the header because it contains information
+ ## that we don't store in the index. actually i think it's just
+ ## the mailing list address (if any), so this is kinda overkill.
+ ## i could just store that in the index, but i think there might
+ ## be other things like that in the future, and i'd rather not
+ ## bloat the index.
+ ## actually, it's also the differentiation between to/cc/bcc,
+ ## so i will keep this.
+ rmsg = location.parsed_message
+ parse_header rmsg.header
+ message_to_chunks rmsg
+ rescue SourceError, SocketError, RMail::EncodingUnsupportedError => e
+ warn "problem reading message #{id}"
+ [Chunk::Text.new(error_message.split("\n"))]
end
end
- def error_message msg
+ def error_message
<<EOS
#@snippet...
***********************************************************************
- An error occurred while loading this message. It is possible that
- the source has changed, or (in the case of remote sources) is down.
- You can check the log for errors, though hopefully an error window
- should have popped up at some point.
-
- The message location was:
- #{source}##{source_info}
+ An error occurred while loading this message.
***********************************************************************
-
-The error message was:
- #{msg}
EOS
end
- ## wrap any source methods that might throw sourceerrors
- def with_source_errors_handled
- begin
- yield
- rescue SourceError => e
- warn "problem getting messages from #{source}: #{e.message}"
- source.error ||= e
- Redwood::report_broken_sources :force_to_top => true
- error_message e.message
- end
- end
-
def raw_header
- with_source_errors_handled { source.raw_header source_info }
+ location.raw_header
end
def raw_message
- with_source_errors_handled { source.raw_message source_info }
+ location.raw_message
end
- ## much faster than raw_message
def each_raw_message_line &b
- with_source_errors_handled { source.each_raw_message_line(source_info, &b) }
+ location.each_raw_message_line &b
end
## returns all the content from a message that will be indexed
@@ -355,7 +324,7 @@ EOS
end
def self.build_from_source source, source_info
- m = Message.new :locations => [[source, source_info]]
+ m = Message.new :locations => [Location.new(source, source_info)]
m.load_from_source!
m
end
@@ -696,4 +665,43 @@ private
end
end
+class Location
+ attr_reader :source
+ attr_reader :info
+
+ def initialize source, info
+ @source = source
+ @info = info
+ end
+
+ def raw_header
+ source.raw_header info
+ end
+
+ def raw_message
+ source.raw_message info
+ end
+
+ ## much faster than raw_message
+ def each_raw_message_line &b
+ source.each_raw_message_line info, &b
+ end
+
+ def parsed_message
+ source.load_message info
+ end
+
+ def valid?
+ source.valid? info
+ end
+
+ def == o
+ o.source.id == source.id and o.info == info
+ end
+
+ def hash
+ [source.id, info].hash
+ end
+end
+
end
diff --git a/lib/sup/modes/console-mode.rb b/lib/sup/modes/console-mode.rb
@@ -8,7 +8,7 @@ class Console
end
def query(query)
- Enumerator.new(Index, :each_message, Index.parse_query(query))
+ Enumerator.new(Index.instance, :each_message, Index.parse_query(query))
end
def add_labels(query, *labels)
@@ -26,6 +26,9 @@ class Console
def special_methods; methods - Object.methods end
+ def puts x; @mode << "#{x.to_s.rstrip}\n" end
+ def p x; puts x.inspect end
+
## files that won't cause problems when reloaded
## TODO expand this list / convert to blacklist
RELOAD_WHITELIST = %w(sup/index.rb sup/modes/console-mode.rb)
diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb
@@ -101,44 +101,37 @@ EOS
@mutex.synchronize do
@poll_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.respond_to?(:has_errors?) && source.has_errors?)
+ yield "Loading from #{source}... "
rescue SourceError => e
warn "problem getting messages from #{source}: #{e.message}"
- Redwood::report_broken_sources :force_to_top => true
next
end
num = 0
numi = 0
- each_message_from source do |m|
- old_m = Index.build_message m.id
- if old_m
- if not old_m.locations.member? [source, m.source_info]
- ## here we merge labels between new and old versions, but we don't let the new
- ## message add :unread or :inbox labels. (they can exist in the old version,
- ## just not be added.)
- new_labels = old_m.labels + (m.labels - [:unread, :inbox])
- yield "Message at #{m.source_info} is an updated of an old message. Updating labels from #{m.labels.to_a * ','} => #{new_labels.to_a * ','}"
- m.labels = new_labels
- m.locations = old_m.locations + m.locations
- Index.update_message m
+ poll_from source do |action,m,old_m|
+ if action == :delete
+ yield "Deleting #{m.id}"
+ elsif action == :add
+ if old_m
+ if not old_m.locations.member? m.location
+ yield "Message at #{m.source_info} is an updated of an old message. Updating labels from #{old_m.labels.to_a * ','} => #{m.labels.to_a * ','}"
+ else
+ yield "Skipping already-imported message at #{m.source_info}"
+ end
else
- yield "Skipping already-imported message at #{m.source_info}"
- end
- else
- yield "Found new message at #{m.source_info} with labels #{m.labels.to_a * ','}"
- add_new_message m
- loaded_labels.merge m.labels
- num += 1
- from_and_subj << [m.from && m.from.longname, m.subj]
- if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
- from_and_subj_inbox << [m.from && m.from.longname, m.subj]
- numi += 1
+ yield "Found new message at #{m.source_info} with labels #{m.labels.to_a * ','}"
+ loaded_labels.merge m.labels
+ num += 1
+ from_and_subj << [m.from && m.from.longname, m.subj]
+ if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
+ from_and_subj_inbox << [m.from && m.from.longname, m.subj]
+ numi += 1
+ end
end
+ else fail
end
- m
end
yield "Found #{num} messages, #{numi} to inbox." unless num == 0
total_num += num
@@ -153,44 +146,41 @@ EOS
[total_num, total_numi, from_and_subj, from_and_subj_inbox, loaded_labels]
end
- ## like Source#each, but yields successive Message objects, which have their
- ## labels and offsets set correctly.
- ##
- ## this is the primary mechanism for iterating over messages from a source.
- def each_message_from source, opts={}
+ ## like Source#poll, but yields successive Message objects, which have their
+ ## labels and locations set correctly. The Messages are saved to or removed
+ ## from the index after being yielded.
+ def poll_from source, opts={}
begin
- return if source.done? || source.has_errors?
-
- source.each do |offset, source_labels|
- if source.has_errors?
- warn "error loading messages from #{source}: #{source.error.message}"
- return
+ source.poll do |sym, args|
+ case sym
+ when :add
+ m = Message.build_from_source source, args[:info]
+ old_m = Index.build_message m.id
+ m.labels += args[:labels]
+ m.labels.delete :inbox if source.archived?
+ m.labels.delete :unread if source.read?
+ m.labels.delete :unread if m.source_marked_read? # preserve read status if possible
+ m.labels.each { |l| LabelManager << l }
+ m.labels = old_m.labels + (m.labels - [:unread, :inbox]) if old_m
+ m.locations = old_m.locations + m.locations if old_m
+ HookManager.run "before-add-message", :message => m
+ yield :add, m, old_m if block_given?
+ Index.sync_message m, true
+ UpdateManager.relay self, :added, m
+ when :delete
+ Index.each_message :location => [source.id, args[:info]] do |m|
+ m.locations.delete Location.new(source, args[:info])
+ yield :delete, m, [source,args[:info]] if block_given?
+ Index.sync_message m, false
+ #UpdateManager.relay self, :deleted, m
+ end
end
-
- m = Message.build_from_source source, offset
- m.labels += source_labels + (source.archived? ? [] : [:inbox])
- m.labels.delete :unread if m.source_marked_read? # preserve read status if possible
- m.labels.each { |l| LabelManager << l }
-
- HookManager.run "before-add-message", :message => m
- yield m
end
rescue SourceError => e
warn "problem getting messages from #{source}: #{e.message}"
- Redwood::report_broken_sources :force_to_top => true
end
end
- ## TODO: see if we can do this within PollMode rather than by calling this
- ## method.
- ##
- ## a wrapper around Index.add_message that calls the proper hooks,
- ## does the gui callback stuff, etc.
- def add_new_message m
- Index.add_message m
- UpdateManager.relay self, :added, m
- end
-
def handle_idle_update sender, idle_since; @should_clear_running_totals = false; end
def handle_unidle_update sender, idle_since; @should_clear_running_totals = true; clear_running_totals; end
def clear_running_totals; @running_totals = {:num => 0, :numi => 0, :loaded_labels => Set.new}; end
diff --git a/lib/sup/sent.rb b/lib/sup/sent.rb
@@ -19,29 +19,24 @@ class SentManager
end
def default_source
- @source = Recoverable.new SentLoader.new
+ @source = SentLoader.new
@source_uri = @source.uri
@source
end
def write_sent_message date, from_email, &block
@source.store_message date, from_email, &block
-
- PollManager.each_message_from(@source) do |m|
- m.remove_label :unread
- m.add_label :sent
- PollManager.add_new_message m
- end
+ PollManager.poll_from @source
end
end
-class SentLoader < MBox::Loader
- yaml_properties :cur_offset
+class SentLoader < MBox
+ yaml_properties
- def initialize cur_offset=0
+ def initialize
@filename = Redwood::SENT_FN
File.open(@filename, "w") { } unless File.exists? @filename
- super "mbox://" + @filename, cur_offset, true, true
+ super "mbox://" + @filename, true, true
end
def file_path; @filename end
@@ -51,6 +46,8 @@ class SentLoader < MBox::Loader
def id; 9998; end
def labels; [:inbox, :sent]; end
+ def default_labels; []; end
+ def read?; true; end
end
end
diff --git a/lib/sup/server.rb b/lib/sup/server.rb
@@ -97,13 +97,10 @@ private
SentManager.source.store_message Time.now, "test@example.com" do |io|
io.write raw
end
- m2 = nil
- PollManager.each_message_from(SentManager.source) do |m|
- PollManager.add_new_message m
- m2 = m
+ PollManager.poll_from SentManager.source do |sym,m,old_m|
+ next unless sym == :add
+ m.labels = labels
end
- m2.labels = Set.new(labels.map(&:to_sym))
- @index.update_message_state m2
nil
end
diff --git a/lib/sup/source.rb b/lib/sup/source.rb
@@ -59,51 +59,37 @@ class Source
## Examples for you to look at: mbox/loader.rb, imap.rb, and
## maildir.rb.
- ## let's begin!
- ##
- ## dirty? means cur_offset has changed, so the source info needs to
- ## be re-saved to sources.yaml.
- bool_reader :dirty
bool_accessor :usual, :archived
- attr_reader :uri, :cur_offset
+ attr_reader :uri
attr_accessor :id
- def initialize uri, initial_offset=nil, usual=true, archived=false, id=nil
+ def initialize uri, usual=true, archived=false, id=nil
raise ArgumentError, "id must be an integer: #{id.inspect}" unless id.is_a? Fixnum if id
@uri = uri
- @cur_offset = initial_offset
@usual = usual
@archived = archived
@id = id
- @dirty = false
end
## overwrite me if you have a disk incarnation (currently used only for sup-sync-back)
def file_path; nil end
def to_s; @uri.to_s; end
- def seek_to! o; self.cur_offset = o; end
- def reset!; seek_to! start_offset; end
def == o; o.uri == uri; end
- def done?; start_offset.nil? || (self.cur_offset ||= start_offset) >= end_offset; end
def is_source_for? uri; uri == @uri; end
- ## check should throw a FatalSourceError or an OutOfSyncSourcError
- ## if it can detect a problem. it is called when the sup starts up
- ## to proactively notify the user of any source problems.
- def check; end
+ def read?; false; end
- ## yields successive offsets and labels, starting at #cur_offset.
- ##
- ## when implementing a source, you can overwrite either #each or #next. the
- ## default #each just calls next over and over.
- def each
- self.cur_offset ||= start_offset
- until done?
- offset, labels = self.next
- yield offset, labels
- end
+ ## Yields values of the form [Symbol, Hash]
+ ## add: info, labels, progress
+ ## delete: info, progress
+ def poll
+ unimplemented
+ end
+
+ def valid? info
+ true
end
## utility method to read a raw email header from an IO stream and turn it
@@ -155,11 +141,6 @@ protected
def Source.expand_filesystem_uri uri
uri.gsub "~", File.expand_path("~")
end
-
- def cur_offset= o
- @cur_offset = o
- @dirty = true
- end
end
## if you have a @labels instance variable, include this
@@ -211,7 +192,7 @@ class SourceManager
def unusual_sources; sources.find_all { |s| !s.usual? }; end
def load_sources fn=Redwood::SOURCE_FN
- source_array = (Redwood::load_yaml_obj(fn) || []).map { |o| Recoverable.new o }
+ source_array = Redwood::load_yaml_obj(fn) || []
@source_mutex.synchronize do
@sources = Hash[*(source_array).map { |s| [s.id, s] }.flatten]
@sources_dirty = false
@@ -220,7 +201,7 @@ class SourceManager
def save_sources fn=Redwood::SOURCE_FN
@source_mutex.synchronize do
- if @sources_dirty || @sources.any? { |id, s| s.dirty? }
+ if @sources_dirty
bakfn = fn + ".bak"
if File.exists? fn
File.chmod 0600, fn
diff --git a/lib/sup/util.rb b/lib/sup/util.rb
@@ -4,6 +4,7 @@ require 'mime/types'
require 'pathname'
require 'set'
require 'enumerator'
+require 'benchmark'
## time for some monkeypatching!
class Symbol
@@ -184,6 +185,13 @@ class Object
EOF
end
end
+
+ def benchmark s, &b
+ ret = nil
+ times = Benchmark.measure { ret = b.call }
+ debug "benchmark #{s}: #{times}"
+ ret
+ end
end
class String
@@ -599,40 +607,6 @@ module Singleton
end
end
-## wraps an object. if it throws an exception, keeps a copy.
-class Recoverable
- def initialize o
- @o = o
- @error = nil
- @mutex = Mutex.new
- end
-
- attr_accessor :error
-
- def clear_error!; @error = nil; end
- def has_errors?; !@error.nil?; end
-
- def method_missing m, *a, &b; __pass m, *a, &b end
-
- def id; __pass :id; end
- def to_s; __pass :to_s; end
- def to_yaml x; __pass :to_yaml, x; end
- def is_a? c; @o.is_a? c; end
-
- def respond_to?(m, include_private=false)
- @o.respond_to?(m, include_private)
- end
-
- def __pass m, *a, &b
- begin
- @o.send(m, *a, &b)
- rescue Exception => e
- @error ||= e
- raise
- end
- end
-end
-
## acts like a hash with an initialization block, but saves any
## newly-created value even upon lookup.
##
diff --git a/test/test_header_parsing.rb b/test/test_header_parsing.rb
@@ -106,7 +106,7 @@ EOS
end
def test_from_line_splitting
- l = MBox::Loader.new StringIO.new(<<EOS)
+ l = MBox.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
@@ -132,7 +132,7 @@ EOS
end
def test_more_from_line_splitting
- l = MBox::Loader.new StringIO.new(<<EOS)
+ l = MBox.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