+++ /dev/null
-CONTRIBUTORS
-HACKING
-History.txt
-LICENSE
-Manifest.txt
-README.txt
-Rakefile
-ReleaseNotes
-bin/sup
-bin/sup-add
-bin/sup-config
-bin/sup-dump
-bin/sup-recover-sources
-bin/sup-sync
-bin/sup-sync-back
-bin/sup-tweak-labels
-doc/FAQ.txt
-doc/Hooks.txt
-doc/NewUserGuide.txt
-doc/Philosophy.txt
-lib/sup.rb
-lib/sup/account.rb
-lib/sup/buffer.rb
-lib/sup/colormap.rb
-lib/sup/contact.rb
-lib/sup/crypto.rb
-lib/sup/draft.rb
-lib/sup/hook.rb
-lib/sup/horizontal-selector.rb
-lib/sup/imap.rb
-lib/sup/index.rb
-lib/sup/keymap.rb
-lib/sup/label.rb
-lib/sup/logger.rb
-lib/sup/maildir.rb
-lib/sup/mbox.rb
-lib/sup/mbox/loader.rb
-lib/sup/mbox/ssh-file.rb
-lib/sup/mbox/ssh-loader.rb
-lib/sup/message-chunks.rb
-lib/sup/message.rb
-lib/sup/mode.rb
-lib/sup/modes/buffer-list-mode.rb
-lib/sup/modes/completion-mode.rb
-lib/sup/modes/compose-mode.rb
-lib/sup/modes/contact-list-mode.rb
-lib/sup/modes/edit-message-mode.rb
-lib/sup/modes/file-browser-mode.rb
-lib/sup/modes/forward-mode.rb
-lib/sup/modes/help-mode.rb
-lib/sup/modes/inbox-mode.rb
-lib/sup/modes/label-list-mode.rb
-lib/sup/modes/label-search-results-mode.rb
-lib/sup/modes/line-cursor-mode.rb
-lib/sup/modes/log-mode.rb
-lib/sup/modes/person-search-results-mode.rb
-lib/sup/modes/poll-mode.rb
-lib/sup/modes/reply-mode.rb
-lib/sup/modes/resume-mode.rb
-lib/sup/modes/scroll-mode.rb
-lib/sup/modes/search-results-mode.rb
-lib/sup/modes/text-mode.rb
-lib/sup/modes/thread-index-mode.rb
-lib/sup/modes/thread-view-mode.rb
-lib/sup/person.rb
-lib/sup/poll.rb
-lib/sup/rfc2047.rb
-lib/sup/sent.rb
-lib/sup/source.rb
-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
else; "libc.so.6"
end
- Redwood::log "dynamically loading setlocale() from #{setlocale_lib}"
+ debug "dynamically loading setlocale() from #{setlocale_lib}"
begin
dlload setlocale_lib
extern "void setlocale(int, const char *)"
- Redwood::log "setting locale..."
+ debug "setting locale..."
LibC.setlocale(6, "") # LC_ALL == 6
rescue RuntimeError => e
- Redwood::log "cannot dlload setlocale(); ncurses wide character support probably broken."
- Redwood::log "dlload error was #{e.class}: #{e.message}"
+ warn "cannot dlload setlocale(); ncurses wide character support probably broken."
+ warn "dlload error was #{e.class}: #{e.message}"
if Config::CONFIG['arch'] =~ /bsd/
- Redwood::log "BSD variant detected. You may have to install a compat6x package to acquire libc."
+ warn "BSD variant detected. You may have to install a compat6x package to acquire libc."
end
end
end
end
module_function :start_cursing, :stop_cursing
-Index.new
-begin
- Index.lock
-rescue Index::LockError => e
- require 'highline'
-
- h = HighLine.new
- h.wrap_at = :auto
- h.say Index.fancy_lock_error_message_for(e)
-
- case h.ask("Should I ask that process to kill itself? ")
- when /^\s*y(es)?\s*$/i
- h.say "Ok, suggesting seppuku..."
- FileUtils.touch Redwood::SUICIDE_FN
- sleep SuicideManager::DELAY * 2
- FileUtils.rm_f Redwood::SUICIDE_FN
- h.say "Let's try that again."
- retry
- else
- h.say <<EOS
-Ok, giving up. If the process crashed and left a stale lockfile, you
-can fix this by manually deleting #{Index.lockfile}.
-EOS
- exit
- end
-end
+Index.init
+Index.lock_interactively or exit
begin
Redwood::start
Index.load
+ $die = false
+ trap("TERM") { |x| $die = true }
+ trap("WINCH") { |x| BufferManager.sigwinch_happened! }
+
if(s = Redwood::SourceManager.source_for DraftManager.source_name)
DraftManager.source = s
else
- Redwood::log "no draft source, auto-adding..."
+ debug "no draft source, auto-adding..."
Redwood::SourceManager.add_source DraftManager.new_source
end
HookManager.run "startup"
- log "starting curses"
+ debug "starting curses"
+ Redwood::Logger.remove_sink $stderr
start_cursing
- bm = BufferManager.new
+ bm = BufferManager.init
Colormap.new.populate_colormap
- log "initializing mail index buffer"
+ debug "initializing log buffer"
+ lmode = Redwood::LogMode.new "system log"
+ lmode.on_kill { Logger.clear! }
+ Logger.add_sink lmode
+ Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
+
+ debug "initializing inbox buffer"
imode = InboxMode.new
ibuf = bm.spawn "Inbox", imode
- log "ready for interaction!"
- Logger.make_buf
+ debug "ready for interaction!"
bm.draw_screen
begin
s.connect
rescue SourceError => e
- Redwood::log "fatal error loading from #{s}: #{e.message}"
+ error "fatal error loading from #{s}: #{e.message}"
end
end
end unless $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]
unless $opts[:no_threads]
PollManager.start
- SuicideManager.start
Index.start_lock_update_thread
end
SearchResultsMode.spawn_from_query $opts[:search]
end
- until Redwood::exceptions.nonempty? || SuicideManager.die?
- c =
- begin
- Ncurses.nonblocking_getch
- rescue Exception => e
- if e.is_a?(Interrupt)
- raise if BufferManager.ask_yes_or_no("Die ungracefully now?")
- bm.draw_screen
- nil
- end
- end
- next unless c
+ until Redwood::exceptions.nonempty? || $die
+ c = begin
+ Ncurses.nonblocking_getch
+ rescue Interrupt => e
+ raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
+ BufferManager.draw_screen
+ nil
+ end
+
+ if c.nil?
+ if BufferManager.sigwinch_happened?
+ debug "redrawing screen on sigwinch"
+ BufferManager.completely_redraw_screen
+ end
+ next
+ end
+
+ if c == 410
+ ## this is ncurses's way of telling us it's detected a refresh.
+ ## since we have our own sigwinch handler, we don't do anything.
+ next
+ end
+
bm.erase_flash
- action =
- begin
- if bm.handle_input c
- :nothing
- else
- bm.resolve_input_with_keymap c, global_keymap
- end
- rescue InputSequenceAborted
+ action = begin
+ if bm.handle_input c
:nothing
+ else
+ bm.resolve_input_with_keymap c, global_keymap
end
+ rescue InputSequenceAborted
+ :nothing
+ end
case action
when :quit_now
break if bm.kill_all_buffers_safely
bm.draw_screen
end
- bm.kill_all_buffers if SuicideManager.die?
+ bm.kill_all_buffers if $die
rescue Exception => e
Redwood::record_exception e, "main"
ensure
unless $opts[:no_threads]
PollManager.stop if PollManager.instantiated?
- SuicideManager.stop if PollManager.instantiated?
Index.stop_lock_update_thread
end
Redwood::finish
stop_cursing
- Redwood::log "stopped cursing"
+ Redwood::Logger.remove_all_sinks!
+ Redwood::Logger.add_sink $stderr, false
+ debug "stopped cursing"
- if SuicideManager.instantiated? && SuicideManager.die?
- Redwood::log "I've been ordered to commit seppuku. I obey!"
+ if $die
+ info "I've been ordered to commit seppuku. I obey!"
end
if Redwood::exceptions.empty?
- Redwood::log "no fatal errors. good job, william."
+ debug "no fatal errors. good job, william."
Index.save
else
- Redwood::log "oh crap, an exception"
+ error "oh crap, an exception"
end
Index.unlock
$terminal.wrap_at = :auto
Redwood::start
-index = Redwood::Index.new
+index = Redwood::Index.init
-index.lock_or_die
+index.lock_interactively or exit
begin
Redwood::SourceManager.load_sources
Options:
EOS
-end #' stupid ruby-mode
+end
def axe q, default=nil
- ans =
- if default && !default.empty?
- ask "#{q} (enter for \"#{default}\"): "
- else
- ask "#{q}: "
- end
+ ans = if default && !default.empty?
+ ask "#{q} (enter for \"#{default}\"): "
+ else
+ ask "#{q}: "
+ end
ans.empty? ? default : ans
end
while true do
say "Ok, now for the details."
- default_labels, components =
- case type
- when :mbox
- $last_fn ||= ENV["MAIL"]
- fn = axe "What's the full path to the mbox file?", $last_fn #"srm
- return if fn.nil? || fn.empty?
-
- $last_fn = fn
- [Redwood::MBox::Loader.suggest_labels_for(fn),
- { :scheme => "mbox", :path => fn }]
- when :maildir
- $last_fn ||= ENV["MAIL"]
- fn = axe "What's the full path to the maildir directory?", $last_fn #"srm
- return if fn.nil? || fn.empty?
-
- $last_fn = fn
- [Redwood::Maildir.suggest_labels_for(fn),
- { :scheme => "maildir", :path => fn }]
- when :mboxssh
- $last_server ||= "localhost"
- srv = axe "What machine is the mbox file located on?", $last_server
- return if srv.nil? || srv.empty?
- $last_server = srv
-
- fn = axe "What's the path to the mbox file?", $last_fn #" stupid ruby-mode
- return if fn.nil? || fn.empty?
- $last_fn = fn
- fn = "/#{fn}" # lame
- [Redwood::MBox::SSHLoader.suggest_labels_for(fn),
- { :scheme => "mbox+ssh", :host => srv, :path => fn }]
- when :imap, :imaps
- $last_server ||= "localhost"
- srv = axe "What is the IMAP server (host, or host:port notation)?", $last_server
- return if srv.nil? || srv.empty?
- $last_server = srv
-
- $last_folder ||= "INBOX"
- fn = axe "What's the folder path?", $last_folder #"srm
- return if fn.nil? || fn.empty?
- $last_folder = fn
-
- fn = "/#{fn}" # lame
- if srv =~ /^(\S+):(\d+)$/
- host, port = $1, $2.to_i
- else
- host, port = srv, nil
- end
- [Redwood::IMAP.suggest_labels_for(fn),
- { :scheme => type.to_s, :host => host, :port => port, :path => fn }]
- end
-
- uri =
- begin
- URI::Generic.build components
- rescue URI::Error => e
- say "Whoopsie! I couldn't build a URI from that: #{e.message}"
- if axe_yes("Try again?") then next else return end
+ default_labels, components = case type
+ when :mbox
+ $last_fn ||= ENV["MAIL"]
+ fn = axe "What's the full path to the mbox file?", $last_fn
+ return if fn.nil? || fn.empty?
+
+ $last_fn = fn
+ [Redwood::MBox::Loader.suggest_labels_for(fn),
+ { :scheme => "mbox", :path => fn }]
+ when :maildir
+ $last_fn ||= ENV["MAIL"]
+ fn = axe "What's the full path to the maildir directory?", $last_fn
+ return if fn.nil? || fn.empty?
+
+ $last_fn = fn
+ [Redwood::Maildir.suggest_labels_for(fn),
+ { :scheme => "maildir", :path => fn }]
+ when :mboxssh
+ $last_server ||= "localhost"
+ srv = axe "What machine is the mbox file located on?", $last_server
+ return if srv.nil? || srv.empty?
+ $last_server = srv
+
+ fn = axe "What's the path to the mbox file?", $last_fn
+ return if fn.nil? || fn.empty?
+ $last_fn = fn
+ fn = "/#{fn}" # lame
+ [Redwood::MBox::SSHLoader.suggest_labels_for(fn),
+ { :scheme => "mbox+ssh", :host => srv, :path => fn }]
+ when :imap, :imaps
+ $last_server ||= "localhost"
+ srv = axe "What is the IMAP server (host, or host:port notation)?", $last_server
+ return if srv.nil? || srv.empty?
+ $last_server = srv
+
+ $last_folder ||= "INBOX"
+ fn = axe "What's the folder path?", $last_folder
+ return if fn.nil? || fn.empty?
+ $last_folder = fn
+
+ fn = "/#{fn}"
+ if srv =~ /^(\S+):(\d+)$/
+ host, port = $1, $2.to_i
+ else
+ host, port = srv, nil
end
+ [Redwood::IMAP.suggest_labels_for(fn),
+ { :scheme => type.to_s, :host => host, :port => port, :path => fn }]
+ end
+
+ uri = begin
+ URI::Generic.build components
+ rescue URI::Error => e
+ say "Whoopsie! I couldn't build a URI from that: #{e.message}"
+ if axe_yes("Try again?") then next else return end
+ end
say "I'm going to add this source: #{uri}"
unless axe("Does that look right?", "y") =~ /^y|yes$/i
labels_str = axe("Enter any labels to be automatically added to all messages from this source, separated by spaces (or 'none')", default_labels.join(","))
- labels =
- if labels_str =~ /^\s*none\s*$/i
- nil
- else
- labels_str.split(/\s+/)
- end
-
+ labels = if labels_str =~ /^\s*none\s*$/i
+ nil
+ else
+ labels_str.split(/\s+/)
+ end
+
cmd = build_cmd "sup-add"
cmd += " --unusual" unless usual
cmd += " --archive" if archive
$terminal.wrap_at = :auto
Redwood::start
-index = Redwood::Index.new
+index = Redwood::Index.init
Redwood::SourceManager.load_sources
say <<EOS
Just answer these simple questions and you'll be on your way.
EOS
-#' stupid ruby-mode
account = $config[:accounts][:default]
name = axe "What's your name?", account[:name]
-email = axe "What's your (primary) email address?", account[:email] #'srm
+email = axe "What's your (primary) email address?", account[:email]
-say "Ok, your header will look like this:"
+say "Ok, your from header will look like this:"
say " From: #{name} <#{email}>"
say "\nDo you have any alternate email addresses that also receive email?"
$config[:accounts][:default][:signature] = sigfn
$config[:editor] = editor
-
done = false
until done
say "\nNow, we'll tell Sup where to find all your email."
choose do |menu|
menu.prompt = "Store my sent mail in? "
+ menu.choice('Default (an mbox in ~/.sup, aka sup://sent)') { $config[:sent_source] = 'sup://sent'} unless have_sup_sent
+
valid_sents = Redwood::SourceManager.sources.each do |s|
have_sup_sent = true if s.to_s.eql?('sup://sent')
-
menu.choice(s.to_s) { $config[:sent_source] = s.to_s } if s.respond_to? :store_message
end
-
- menu.choice('Default (sup://sent)') { $config[:sent_source] = 'sup://sent'} unless have_sup_sent
-
end
end
say <<EOS
-Ok. The final step is to import all your messages into the Sup index.
+The final step is to import all your messages into the Sup index.
Depending on how many messages are in the sources, this could take
quite a while.
EOS
-#'
+
if axe_yes "Run sup-sync to import all messages now?"
while true
cmd = build_cmd("sup-sync") + " --all-sources"
EOS
end
-index = Redwood::Index.new
-Redwood::SourceManager.new
+index = Redwood::Index.init
+Redwood::SourceManager.init
index.load
index.each_message :load_spam => true, :load_deleted => true, :load_killed => true do |m|
- puts "#{m.id} (#{m.labels * ' '})"
+ puts "#{m.id} (#{m.labels.to_a * ' '})"
end
require "sup"
Redwood::start
puts "loading index..."
-index = Redwood::Index.new
+index = Redwood::Index.init
index.load
puts "loaded index of #{index.size} messages"
class Float
def to_s; sprintf '%.2f', self; end
- def to_time_s
- infinite? ? "unknown" : super
- end
+ def to_time_s; infinite? ? "unknown" : super end
end
class Numeric
end
end
+class Set
+ def to_s; to_a * ',' end
+end
+
def time
startt = Time.now
yield
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. (In the case of mbox sources, this includes all messages AFTER an altered message.)"
+ 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
opt :discard, "Discard any message state in the index and use the default source state. Dangerous!", :short => :none
opt :archive, "When using the default source state, mark messages as archived.", :short => "-x"
opt :read, "When using the default source state, mark messages as read."
- opt :extra_labels, "When using the default source state, also apply these user-defined labels. Should be a comma-separated list.", :type => String, :short => :none
+ opt :extra_labels, "When using the default source state, also apply these user-defined labels (a comma-separated list)", :default => "", :short => :none
text <<EOS
op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis
Redwood::start
-index = Redwood::Index.new
-
-restored_state =
- if opts[:restore]
- dump = {}
- $stderr.puts "Loading state dump from #{opts[:restore]}..."
- IO.foreach opts[:restore] do |l|
- l =~ /^(\S+) \((.*?)\)$/ or raise "Can't read dump line: #{l.inspect}"
- mid, labels = $1, $2
- dump[mid] = labels.symbolistize
- end
- $stderr.puts "Read #{dump.size} entries from dump file."
- dump
- else
- {}
+index = Redwood::Index.init
+
+restored_state = if opts[:restore]
+ dump = {}
+ $stderr.puts "Loading state dump from #{opts[:restore]}..."
+ IO.foreach opts[:restore] do |l|
+ l =~ /^(\S+) \((.*?)\)$/ or raise "Can't read dump line: #{l.inspect}"
+ mid, labels = $1, $2
+ dump[mid] = labels.to_set_of_symbols
end
+ $stderr.puts "Read #{dump.size} entries from dump file."
+ dump
+else
+ {}
+end
seen = {}
-index.lock_or_die
+index.lock_interactively or exit
begin
index.load
- sources = ARGV.map do |uri|
- Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
+ sources = if opts[:all_sources]
+ Redwood::SourceManager.sources
+ elsif ARGV.empty?
+ Redwood::SourceManager.usual_sources
+ else
+ ARGV.map do |uri|
+ Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
+ end
end
-
- sources = Redwood::SourceManager.usual_sources if sources.empty?
- sources = Redwood::SourceManager.sources if opts[:all_sources]
+ ## 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.each { |s| s.reset! }
end
end
-
+
sources.each do |source|
$stderr.puts "Scanning #{source}..."
num_added = num_updated = num_scanned = num_restored = 0
last_info_time = start_time = Time.now
- Redwood::PollManager.add_messages_from source, :force_overwrite => true do |m_old, m, offset|
+ Redwood::PollManager.each_message_from source do |m|
num_scanned += 1
seen[m.id] = true
-
- if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
- last_info_time = Time.now
- elapsed = last_info_time - start_time
- start = opts[:start_at] || source.start_offset
- pctdone = 100.0 * (source.cur_offset - start).to_f / (source.end_offset - start).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
+ 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.source.id == m.source.id && old_m.source_info == 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
- ## skip 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 target == :changed && m_old && m_old.source.id == source.id && m_old.source_info == offset
-
- ## get the state currently in the index
- index_state = m_old.labels.dup if m_old
-
- ## skip if we're operating on restored messages, and this one
- ## ain't.
- next if target == :restored && (!restored_state[m.id] || (index_state && restored_state[m.id].sort_by { |s| s.to_s } == index_state.sort_by { |s| s.to_s }))
-
- ## m.labels is the default source labels. tweak these according
- ## to default source state modification flags.
- m.labels -= [:inbox] if opts[:archive]
- m.labels -= [:unread] if opts[:read]
- m.labels += opts[:extra_labels].strip.split(/\s*,\s*/).map { |x| x.intern } if opts[:extra_labels]
-
- ## assign message labels based on the operation we're performing
- case op
- when :asis
- m.labels = ((m.labels - [:unread, :inbox]) + index_state).uniq if index_state
- when :restore
- ## if the entry exists on disk
- if restored_state[m.id]
- m.labels = restored_state[m.id]
- num_restored += 1
- elsif index_state
- m.labels = index_state
+ ## 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] && old_m && (old_m.labels != restored_state[m.id])
+ num_restored += 1
+ [:update_message_state, restored_state[m.id]]
+ when (op == :restore) && restored_state[m.id] && !old_m
+ num_restored += 1
+ m.labels = restored_state[m.id]
+ :add_message
+ when op == :discard
+ if old_m && (old_m.labels != m.labels)
+ [:update_message_state, m.labels]
+ else
+ # don't do anything
end
- when :discard
- ## nothin! use default source labels
+ 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
+ else
+ :add_message
+ end
+ end
+
+ ## now, actually do the operation
+ case dothis
+ when :add_message
+ $stderr.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
+ $stderr.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
+ $stderr.puts "Changing flags for #{source}##{m.source_info} from #{m.labels} to #{new_labels}"
+ m.labels = new_labels
+ index.update_message_state m unless opts[:dry_run]
+ num_updated += 1
end
if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
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
- else
- puts "Updating message #{source}##{offset}, source #{m_old.source.id} => #{source.id}, offset #{m_old.source_info} => #{offset}, state {#{index_state * ', '}} => {#{m.labels * ', '}}" if opts[:verbose]
- num_updated += 1
- end
-
- opts[:dry_run] ? nil : m
end
+
$stderr.puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated} messages from #{source}."
$stderr.puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
end
end
Redwood::start
-index = Redwood::Index.new
-index.lock_or_die
+index = Redwood::Index.init
+index.lock_interactively or exit
deleted_fp, spam_fp = nil
unless opts[:dry_run]
source.reset!
num_dropped = num_moved = num_scanned = 0
-
+
out_fp = Tempfile.new "sup-sync-back-#{source.id}"
- Redwood::PollManager.add_messages_from source do |m_old, m, offset|
+ Redwood::PollManager.each_message_from source do |m|
num_scanned += 1
- if m_old
+ if(m_old = index.build_message(m.id))
labels = m_old.labels
if labels.member? :deleted
if opts[:drop_deleted]
- puts "Dropping deleted message #{source}##{offset}" if opts[:verbose]
+ puts "Dropping deleted message #{source}##{m.source_info}" if opts[:verbose]
num_dropped += 1
elsif opts[:move_deleted] && labels.member?(:deleted)
- puts "Moving deleted message #{source}##{offset}" if opts[:verbose]
+ puts "Moving deleted message #{source}##{m.source_info}" if opts[:verbose]
save m, deleted_fp unless opts[:dry_run]
num_moved += 1
end
elsif labels.member? :spam
if opts[:drop_spam]
- puts "Dropping spam message #{source}##{offset}" if opts[:verbose]
+ puts "Dropping spam message #{source}##{m.source_info}" if opts[:verbose]
num_dropped += 1
elsif opts[:move_spam] && labels.member?(:spam)
- puts "Moving spam message #{source}##{offset}" if opts[:verbose]
+ puts "Moving spam message #{source}##{m.source_info}" if opts[:verbose]
save m, spam_fp unless opts[:dry_run]
num_moved += 1
end
else
save m, out_fp unless opts[:dry_run]
end
-
- nil # don't actually add anything!
end
$stderr.puts "Scanned #{num_scanned}, dropped #{num_dropped}, moved #{num_moved} messages from #{source}."
modified_sources << source if num_dropped > 0 || num_moved > 0
Options:
EOS
- opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :type => String
- opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :type => String
+ opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :default => ""
+ opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :default => ""
opt :query, "A Sup search query", :type => String
text <<EOS
end
opts[:verbose] = true if opts[:very_verbose]
-add_labels = (opts[:add] || "").split(",").map { |l| l.intern }.uniq
-remove_labels = (opts[:remove] || "").split(",").map { |l| l.intern }.uniq
+add_labels = opts[:add].to_set_of_symbols ","
+remove_labels = opts[:remove].to_set_of_symbols ","
Trollop::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
+Trollop::die "no sources specified" if ARGV.empty?
Redwood::start
+index = Redwood::Index.init
+index.lock_interactively or exit
begin
- index = Redwood::Index.new
index.load
- source_ids =
- if opts[:all_sources]
- Redwood::SourceManager.sources
- else
- ARGV.map do |uri|
- Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
- end
+ source_ids = if opts[:all_sources]
+ Redwood::SourceManager.sources
+ else
+ ARGV.map do |uri|
+ Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
+ end
end.map { |s| s.id }
Trollop::die "nothing to do: no sources" if source_ids.empty?
num_scanned += 1
m = index.build_message id
- old_labels = m.labels.clone
+ old_labels = m.labels.dup
m.labels += add_labels
m.labels -= remove_labels
- m.labels = m.labels.uniq
- unless m.labels.sort_by { |s| s.to_s } == old_labels.sort_by { |s| s.to_s }
+ unless m.labels == old_labels
num_changed += 1
puts "From #{m.from}, subject: #{m.subj}" if opts[:very_verbose]
- puts "#{m.id}: {#{old_labels.join ','}} => {#{m.labels.join ','}}" if opts[:verbose]
+ puts "#{m.id}: {#{old_labels.to_a.join ','}} => {#{m.labels.to_a.join ','}}" if opts[:verbose]
puts if opts[:very_verbose]
- index.sync_message m unless opts[:dry_run]
+ index.update_message_state m unless opts[:dry_run]
end
if Time.now - last_info_time > 60
vars = props.map { |p| "@#{p}" }
klass = self
path = klass.name.gsub(/::/, "/")
-
+
klass.instance_eval do
define_method(:to_yaml_properties) { vars }
define_method(:to_yaml_type) { "!#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}/#{path}" }
module_function :reporting_thread, :record_exception, :exceptions
## one-stop shop for yamliciousness
- def save_yaml_obj object, fn, safe=false
+ def save_yaml_obj o, fn, safe=false
+ o = if o.is_a?(Array)
+ o.map { |x| (x.respond_to?(:before_marshal) && x.before_marshal) || x }
+ elsif o.respond_to? :before_marshal
+ o.before_marshal
+ else
+ o
+ end
+
if safe
safe_fn = "#{File.dirname fn}/safe_#{File.basename fn}"
mode = File.stat(fn).mode if File.exists? fn
- File.open(safe_fn, "w", mode) { |f| f.puts object.to_yaml }
+ File.open(safe_fn, "w", mode) { |f| f.puts o.to_yaml }
FileUtils.mv safe_fn, fn
else
- File.open(fn, "w") { |f| f.puts object.to_yaml }
+ File.open(fn, "w") { |f| f.puts o.to_yaml }
end
end
def load_yaml_obj fn, compress=false
- if File.exists? fn
+ o = if File.exists? fn
if compress
Zlib::GzipReader.open(fn) { |f| YAML::load f }
else
YAML::load_file fn
end
end
+ if o.is_a?(Array)
+ o.each { |x| x.after_unmarshal! if x.respond_to?(:after_unmarshal!) }
+ else
+ o.after_unmarshal! if o.respond_to?(:after_unmarshal!)
+ end
+ o
end
def start
- Redwood::SentManager.new $config[:sent_source] || 'sup://sent'
- Redwood::ContactManager.new Redwood::CONTACT_FN
- Redwood::LabelManager.new Redwood::LABEL_FN
- Redwood::AccountManager.new $config[:accounts]
- Redwood::DraftManager.new Redwood::DRAFT_DIR
- Redwood::UpdateManager.new
- Redwood::PollManager.new
- Redwood::SuicideManager.new Redwood::SUICIDE_FN
- Redwood::CryptoManager.new
- Redwood::UndoManager.new
- Redwood::SourceManager.new
+ Redwood::SentManager.init $config[:sent_source] || 'sup://sent'
+ Redwood::ContactManager.init Redwood::CONTACT_FN
+ Redwood::LabelManager.init Redwood::LABEL_FN
+ Redwood::AccountManager.init $config[:accounts]
+ Redwood::DraftManager.init Redwood::DRAFT_DIR
+ Redwood::UpdateManager.init
+ Redwood::PollManager.init
+ Redwood::CryptoManager.init
+ Redwood::UndoManager.init
+ Redwood::SourceManager.init
end
def finish
## set up default configuration file
if File.exists? Redwood::CONFIG_FN
$config = Redwood::load_yaml_obj Redwood::CONFIG_FN
+ abort "#{Redwood::CONFIG_FN} is not a valid configuration file (it's a #{$config.class}, not a hash)" unless $config.is_a?(Hash)
else
require 'etc'
require 'socket'
## we have to initialize this guy first, because other classes must
## reference it in order to register hooks, and they do that at parse
## time.
-Redwood::HookManager.new Redwood::HOOK_DIR
+Redwood::HookManager.init Redwood::HOOK_DIR
## everything we need to get logging working
-require "sup/buffer"
-require "sup/keymap"
-require "sup/mode"
-require "sup/modes/scroll-mode"
-require "sup/modes/text-mode"
-require "sup/modes/log-mode"
require "sup/logger"
-module Redwood
- def log s; Logger.log s; end
- module_function :log
-end
+Redwood::Logger.init.add_sink $stderr
+include Redwood::LogsStuff
## determine encoding and character set
$encoding = Locale.current.charset
if $encoding
- Redwood::log "using character set encoding #{$encoding.inspect}"
+ debug "using character set encoding #{$encoding.inspect}"
else
- Redwood::log "warning: can't find character set by using locale, defaulting to utf-8"
+ warn "can't find character set by using locale, defaulting to utf-8"
$encoding = "UTF-8"
end
-## now everything else (which can feel free to call Redwood::log at load time)
+require "sup/buffer"
+require "sup/keymap"
+require "sup/mode"
+require "sup/modes/scroll-mode"
+require "sup/modes/text-mode"
+require "sup/modes/log-mode"
require "sup/update"
-require "sup/suicide"
require "sup/message-chunks"
require "sup/message"
require "sup/source"
require "sup/person"
require "sup/account"
require "sup/thread"
+require "sup/interactive-lock"
require "sup/index"
require "sup/textfield"
require "sup/colormap"
add_account accounts[:default], true
accounts.each { |k, v| add_account v, false unless k == :default }
-
- self.class.i_am_the_instance self
end
def user_accounts; @accounts.keys; end
def mutex; @mutex ||= Mutex.new; end
def sync &b; mutex.synchronize(&b); end
- ## magically, this stuff seems to work now. i could swear it didn't
- ## before. hm.
def nonblocking_getch
- if IO.select([$stdin], nil, nil, 1)
- Ncurses.getch
- else
- nil
+ ## INSANTIY
+ ## it is NECESSARY to wrap Ncurses.getch in a select() otherwise all
+ ## background threads will be BLOCKED. (except in very modern versions
+ ## of libncurses-ruby. the current one on ubuntu seems to work well.)
+ if IO.select([$stdin], nil, nil, 0.5)
+ c = Ncurses.getch
end
end
def content_height; @height - 1; end
def content_width; @width; end
- def resize rows, cols
+ def resize rows, cols
return if cols == @width && rows == @height
@width = cols
@height = rows
@flash = nil
@shelled = @asking = false
@in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
-
- self.class.i_am_the_instance self
+ @sigwinch_happened = false
+ @sigwinch_mutex = Mutex.new
end
+ def sigwinch_happened!; @sigwinch_mutex.synchronize { @sigwinch_happened = true } end
+ def sigwinch_happened?; @sigwinch_mutex.synchronize { @sigwinch_happened } end
+
def buffers; @name_map.to_a; end
def focus_on buf
## have to change this. but it's not clear that we will ever actually
## do that.
def roll_buffers
- @buffers.last.force_to_top = false
- raise_to_front @buffers.first
+ bufs = rollable_buffers
+ bufs.last.force_to_top = false
+ raise_to_front bufs.first
end
def roll_buffers_backwards
- return unless @buffers.length > 1
- @buffers.last.force_to_top = false
- raise_to_front @buffers[@buffers.length - 2]
+ bufs = rollable_buffers
+ return unless bufs.length > 1
+ bufs.last.force_to_top = false
+ raise_to_front bufs[bufs.length - 2]
+ end
+
+ def rollable_buffers
+ @buffers.select { |b| !b.system? || @buffers.last == b }
end
def handle_input c
def completely_redraw_screen
return if @shelled
+ ## this magic makes Ncurses get the new size of the screen
+ Ncurses.endwin
+ Ncurses.stdscr.keypad 1
+ Ncurses.curs_set 0
+ Ncurses.refresh
+ @sigwinch_mutex.synchronize { @sigwinch_happened = false }
+ debug "new screen size is #{Ncurses.rows} x #{Ncurses.cols}"
+
status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
Ncurses.sync do
end
if answer
- answer =
+ answer =
if answer.empty?
spawn_modal "file browser", FileBrowserMode.new
elsif File.directory?(answer)
## returns an array of labels
def ask_for_labels domain, question, default_labels, forbidden_labels=[]
default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
- default = default_labels.join(" ")
+ default = default_labels.to_a.join(" ")
default += " " unless default.empty?
# here I would prefer to give more control and allow all_labels instead of
return unless answer
- user_labels = answer.symbolistize
+ user_labels = answer.to_set_of_symbols
user_labels.each do |l|
if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
BufferManager.flash "'#{l}' is a reserved label!"
def ask_for_contacts domain, question, default_contacts=[]
default = default_contacts.map { |s| s.to_s }.join(" ")
default += " " unless default.empty?
-
+
recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
@next_id = (@next_id + 1) % NUM_COLORS
@next_id += 1 if @next_id == 0 # 0 is always white on black
id = @next_id
- Redwood::log "colormap: for color #{sym}, using id #{id} -> #{fg}, #{bg}"
+ debug "colormap: for color #{sym}, using id #{id} -> #{fg}, #{bg}"
Curses.init_pair id, fg, bg or raise ArgumentError,
"couldn't initialize curses color pair #{fg}, #{bg} (key #{id})"
## delete the old mapping, if it exists
if @users[cp]
@users[cp].each do |usym|
- Redwood::log "dropping color #{usym} (#{id})"
+ warn "dropping color #{usym} (#{id})"
@entries[usym][3] = nil
end
@users[cp] = []
## to the default ones.
def populate_colormap
user_colors = if File.exists? Redwood::COLOR_FN
- Redwood::log "loading user colors from #{Redwood::COLOR_FN}"
+ debug "loading user colors from #{Redwood::COLOR_FN}"
Redwood::load_yaml_obj Redwood::COLOR_FN
end
fg = Curses.const_get "COLOR_#{ufg.upcase}"
rescue NameError
error ||= "Warning: there is no color named \"#{ufg}\", using fallback."
- Redwood::log "Warning: there is no color named \"#{ufg}\""
+ warn "there is no color named \"#{ufg}\""
end
end
bg = Curses.const_get "COLOR_#{ubg.upcase}"
rescue NameError
error ||= "Warning: there is no color named \"#{ubg}\", using fallback."
- Redwood::log "Warning: there is no color named \"#{ubg}\""
+ warn "there is no color named \"#{ubg}\""
end
end
Curses.const_get "A_#{a.upcase}"
rescue NameError
error ||= "Warning: there is no attribute named \"#{a}\", using fallback."
- Redwood::log "Warning: there is no attribute named \"#{a}\", using fallback."
+ warn "there is no attribute named \"#{a}\", using fallback."
end
end
end
@a2p[aalias] = p unless aalias.nil? || aalias.empty?
end
end
-
- self.class.i_am_the_instance self
end
def contacts; @p2a.keys end
def initialize
@mutex = Mutex.new
- self.class.i_am_the_instance self
bin = `which gpg`.chomp
-
@cmd =
case bin
when /\S/
- Redwood::log "crypto: detected gpg binary in #{bin}"
+ debug "crypto: detected gpg binary in #{bin}"
"#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
else
- Redwood::log "crypto: no gpg binary detected"
+ debug "crypto: no gpg binary detected"
nil
end
end
output = run_gpg "--decrypt #{payload_fn.path}"
if $?.success?
- decrypted_payload, sig_lines =
- if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
- [$1, $2]
+ decrypted_payload, sig_lines = if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
+ [$1, $2]
+ else
+ [output, nil]
+ end
+
+ sig = if sig_lines # encrypted & signed
+ if sig_lines =~ /^gpg: (Good signature from .*$)/
+ Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
else
- [output, nil]
- end
-
- sig =
- if sig_lines # encrypted & signed
- if sig_lines =~ /^gpg: (Good signature from .*$)/
- Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
- else
- Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
- end
+ Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
end
+ end
notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
- [RMail::Parser.read(decrypted_payload), sig, notice]
+ [notice, sig, RMail::Parser.read(decrypted_payload)]
else
- notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
- [nil, nil, notice]
+ Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
end
end
def unknown_status lines=[]
Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
end
-
+
def cant_find_binary
["Can't find gpg binary in path."]
end
def run_gpg args
cmd = "#{@cmd} #{args} 2> /dev/null"
- #Redwood::log "crypto: running: #{cmd}"
output = `#{cmd}`
- #Redwood::log "crypto: output: #{output.inspect}" unless $?.success?
output
end
end
def initialize dir
@dir = dir
@source = nil
- self.class.i_am_the_instance self
end
def self.source_name; "sup://drafts"; end
File.open(fn, "w") { |f| yield f }
my_message = nil
- @source.each do |thisoffset, theselabels|
- m = Message.build_from_source @source, thisoffset
- m.labels = theselabels
- Index.sync_message m
- UpdateManager.relay self, :added, m
- my_message = m if thisoffset == offset
+ PollManager.each_message_from(@source) do |m|
+ PollManager.add_new_message m
+ my_message = m
end
my_message
def load_index dir=File.join(@dir, "ferret")
if File.exists? dir
- Redwood::log "loading index..."
+ debug "loading index..."
@index_mutex.synchronize do
@index = Ferret::Index::Index.new(:path => dir, :analyzer => @analyzer, :id_field => 'message_id')
- Redwood::log "loaded index of #{@index.size} messages"
+ debug "loaded index of #{@index.size} messages"
end
else
- Redwood::log "creating index..."
+ debug "creating index..."
@index_mutex.synchronize do
field_infos = Ferret::Index::FieldInfos.new :store => :yes
field_infos.add_field :message_id, :index => :untokenized
end
end
+ def add_message m; sync_message m end
+ def update_message m; sync_message m end
+ def update_message_state m; sync_message m end
+
def sync_message m, opts={}
entry = @index[m.id]
## written in this manner to support previous versions of the index which
## did not keep around the entry body. upgrading is thus seamless.
entry ||= {}
- labels = m.labels.uniq # override because this is the new state, unless...
+ labels = m.labels # override because this is the new state, unless...
## if we are a later version of a message, ignore what's in the index,
## but merge in the labels.
if entry[:source_id] && entry[:source_info] && entry[:label] &&
((entry[:source_id].to_i > source_id) || (entry[:source_info].to_i < m.source_info))
- labels = (entry[:label].symbolistize + m.labels).uniq
- #Redwood::log "found updated version of message #{m.id}: #{m.subj}"
- #Redwood::log "previous version was at #{entry[:source_id].inspect}:#{entry[:source_info].inspect}, this version at #{source_id.inspect}:#{m.source_info.inspect}"
- #Redwood::log "merged labels are #{labels.inspect} (index #{entry[:label].inspect}, message #{m.labels.inspect})"
+ labels += entry[:label].to_set_of_symbols
+ #debug "found updated version of message #{m.id}: #{m.subj}"
+ #debug "previous version was at #{entry[:source_id].inspect}:#{entry[:source_info].inspect}, this version at #{source_id.inspect}:#{m.source_info.inspect}"
+ #debug "merged labels are #{labels.inspect} (index #{entry[:label].inspect}, message #{m.labels.inspect})"
entry = {}
end
:date => (entry[:date] || m.date.to_indexable_s),
:body => (entry[:body] || m.indexable_content),
:snippet => snippet, # always override
- :label => labels.uniq.join(" "),
+ :label => labels.to_a.join(" "),
:attachments => (entry[:attachments] || m.attachments.uniq.join(" ")),
## always override :from and :to.
@index.add_document d
end
end
+ private :sync_message
def save_index fn=File.join(@dir, "ferret")
# don't have to do anything, apparently
while true
limit = (query[:limit])? [EACH_BY_DATE_NUM, query[:limit] - offset].min : EACH_BY_DATE_NUM
results = @index_mutex.synchronize { @index.search ferret_query, :sort => "date DESC", :limit => limit, :offset => offset }
- Redwood::log "got #{results.total_hits} results for query (offset #{offset}) #{ferret_query.inspect}"
+ debug "got #{results.total_hits} results for query (offset #{offset}) #{ferret_query.inspect}"
results.hits.each do |hit|
yield @index_mutex.synchronize { @index[hit.doc][:message_id] }, lambda { build_message hit.doc }
end
SAME_SUBJECT_DATE_LIMIT = 7
MAX_CLAUSES = 1000
def each_message_in_thread_for m, opts={}
- #Redwood::log "Building thread for #{m.id}: #{m.subj}"
+ #debug "Building thread for #{m.id}: #{m.subj}"
messages = {}
searched = {}
num_queries = 0
q = build_ferret_query :qobj => q
p1 = @index_mutex.synchronize { @index.search(q).hits.map { |hit| @index[hit.doc][:message_id] } }
- Redwood::log "found #{p1.size} results for subject query #{q}"
+ debug "found #{p1.size} results for subject query #{q}"
p2 = @index_mutex.synchronize { @index.search(q.to_s, :limit => :all).hits.map { |hit| @index[hit.doc][:message_id] } }
- Redwood::log "found #{p2.size} results in string form"
+ debug "found #{p2.size} results in string form"
pending = (pending + p1 + p2).uniq
end
end
mid = @index[docid][:message_id]
unless messages.member?(mid)
- #Redwood::log "got #{mid} as a child of #{id}"
+ #debug "got #{mid} as a child of #{id}"
messages[mid] ||= lambda { build_message docid }
refs = @index[docid][:refs].split
pending += refs.select { |id| !searched[id] }
end
if killed
- #Redwood::log "thread for #{m.id} is killed, ignoring"
+ #debug "thread for #{m.id} is killed, ignoring"
false
else
- #Redwood::log "ran #{num_queries} queries to build thread of #{messages.size} messages for #{m.id}: #{m.subj}" if num_queries > 0
+ #debug "ran #{num_queries} queries to build thread of #{messages.size} messages for #{m.id}: #{m.subj}" if num_queries > 0
messages.each { |mid, builder| yield mid, builder }
true
end
}
m = Message.new :source => source, :source_info => doc[:source_info].to_i,
- :labels => doc[:label].symbolistize,
+ :labels => doc[:label].to_set_of_symbols,
:snippet => doc[:snippet]
m.parse_header fake_header
m
end
q.add_query Ferret::Search::TermQuery.new(:label, "spam"), :must_not
- Redwood::log "contact search: #{q}"
+ debug "contact search: #{q}"
contacts = {}
num = h[:num] || 20
@index_mutex.synchronize do
@index.search_each q, :sort => "date DESC", :limit => :all do |docid, score|
break if contacts.size >= num
- #Redwood::log "got message #{docid} to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
+ #debug "got message #{docid} to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
f = @index[docid][:from]
t = @index[docid][:to]
field, name = $1, ($3 || $4)
case field
when "filename"
- Redwood::log "filename - translated #{field}:#{name} to attachments:(#{name.downcase})"
+ debug "filename: translated #{field}:#{name} to attachments:(#{name.downcase})"
"attachments:(#{name.downcase})"
when "filetype"
- Redwood::log "filetype - translated #{field}:#{name} to attachments:(*.#{name.downcase})"
+ debug "filetype: translated #{field}:#{name} to attachments:(*.#{name.downcase})"
"attachments:(*.#{name.downcase})"
end
end
if realdate
case field
when "after"
- Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.end}"
+ debug "chronic: translated #{field}:#{datestr} to #{realdate.end}"
"date:(>= #{sprintf "%012d", realdate.end.to_i})"
when "before"
- Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
+ debug "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
"date:(<= #{sprintf "%012d", realdate.begin.to_i})"
else
- Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate}"
+ debug "chronic: translated #{field}:#{datestr} to #{realdate}"
"date:(<= #{sprintf "%012d", realdate.end.to_i}) date:(>= #{sprintf "%012d", realdate.begin.to_i})"
end
else
end
def log s
- Redwood::log "hook[#@__name]: #{s}"
+ info "hook[#@__name]: #{s}"
end
def ask_yes_or_no q
@tags = {}
Dir.mkdir dir unless File.exists? dir
-
- self.class.i_am_the_instance self
end
attr_reader :tags
end
def log m
- Redwood::log("hook: " + m)
+ info("hook: " + m)
end
end
require 'time'
require 'rmail'
require 'cgi'
+require 'set'
## TODO: remove synchronized method protector calls; use a Monitor instead
## (ruby's reentrant mutex)
module Redwood
class IMAP < Source
+ include SerializeLabelsNicely
SCAN_INTERVAL = 60 # seconds
## upon these errors we'll try to rereconnect a few times
@imap_state = {}
@ids = []
@last_scan = nil
- @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
+ @labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
@say_id = nil
@mutex = Mutex.new
end
return if last_id == @ids.length
range = (@ids.length + 1) .. last_id
- Redwood::log "fetching IMAP headers #{range}"
+ debug "fetching IMAP headers #{range}"
fetch(range, ['RFC822.SIZE', 'INTERNALDATE', 'FLAGS']).each do |v|
id = make_id v
@ids << id
@imap_state[id] = { :id => v.seqno, :flags => v.attr["FLAGS"] }
end
- Redwood::log "done fetching IMAP headers"
+ debug "done fetching IMAP headers"
end
synchronized :scan_mailbox
if good_results.empty?
raise FatalSourceError, "no IMAP response for #{ids} containing all fields #{fields.join(', ')} (got #{results.size} results)"
elsif good_results.size < results.size
- Redwood::log "Your IMAP server sucks. It sent #{results.size} results for a request for #{good_results.size} messages. What are you using, Binc?"
+ warn "Your IMAP server sucks. It sent #{results.size} results for a request for #{good_results.size} messages. What are you using, Binc?"
end
good_results
raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=CRAM-MD5"
@imap.authenticate 'CRAM-MD5', @username, @password
rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
- Redwood::log "CRAM-MD5 authentication failed: #{e.class}. Trying LOGIN auth..."
+ debug "CRAM-MD5 authentication failed: #{e.class}. Trying LOGIN auth..."
begin
raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=LOGIN"
@imap.authenticate 'LOGIN', @username, @password
rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
- Redwood::log "LOGIN authentication failed: #{e.class}. Trying plain-text LOGIN..."
+ debug "LOGIN authentication failed: #{e.class}. Trying plain-text LOGIN..."
@imap.login @username, @password
end
end
def say s
@say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
- Redwood::log s
+ info s
end
def shutup
rescue *RECOVERABLE_ERRORS => e
if (retries += 1) <= 3
@imap = nil
- Redwood::log "got #{e.class.name}: #{e.message.inspect}"
+ warn "got #{e.class.name}: #{e.message.inspect}"
sleep 2
retry
end
require 'chronic'
$have_chronic = true
rescue LoadError => e
- Redwood::log "optional 'chronic' library not found (run 'gem install chronic' to install)"
+ debug "optional 'chronic' library not found; date-time query restrictions disabled"
$have_chronic = false
end
module Redwood
class BaseIndex
+ include InteractiveLock
+
class LockError < StandardError
def initialize h
@h = h
def initialize dir=BASE_DIR
@dir = dir
@lock = Lockfile.new lockfile, :retries => 0, :max_age => nil
- self.class.i_am_the_instance self
end
def lockfile; File.join @dir, "lock" end
def lock
- Redwood::log "locking #{lockfile}..."
+ debug "locking #{lockfile}..."
begin
@lock.lock
rescue Lockfile::MaxTriesLockError
@lock_update_thread = nil
end
- def possibly_pluralize number_of, kind
- "#{number_of} #{kind}" +
- if number_of == 1 then "" else "s" end
- end
-
- def fancy_lock_error_message_for e
- secs = (Time.now - e.mtime).to_i
- mins = secs / 60
- time =
- if mins == 0
- possibly_pluralize secs , "second"
- else
- possibly_pluralize mins, "minute"
- end
-
- <<EOS
-Error: the sup index is locked by another process! User '#{e.user}' on
-host '#{e.host}' is running #{e.pname} with pid #{e.pid}. The process was alive
-as of #{time} ago.
-EOS
- end
-
- def lock_or_die
- begin
- lock
- rescue LockError => e
- $stderr.puts fancy_lock_error_message_for(e)
- $stderr.puts <<EOS
-
-You can wait for the process to finish, or, if it crashed and left a
-stale lock file behind, you can manually delete #{@lock.path}.
-EOS
- exit
- end
- end
-
def unlock
if @lock && @lock.locked?
- Redwood::log "unlocking #{lockfile}..."
+ debug "unlocking #{lockfile}..."
@lock.unlock
end
end
end
def save
- Redwood::log "saving index and sources..."
+ debug "saving index and sources..."
FileUtils.mkdir_p @dir unless File.exists? @dir
SourceManager.save_sources
save_index
unimplemented
end
- ## Syncs the message to the index, replacing any previous version. adding
- ## either way. Index state will be determined by the message's #labels
- ## accessor.
- def sync_message m, opts={}
- unimplemented
- end
+ def add_message m; unimplemented end
+ def update_message m; unimplemented end
+ def update_message_state m; unimplemented end
def save_index fn
unimplemented
else fail "unknown index type #{index_name.inspect}"
end
Index = Redwood.const_get "#{index_name.capitalize}Index"
-Redwood::log "using index #{Index.name}"
+debug "using index #{Index.name}"
end
--- /dev/null
+require 'fileutils'
+
+module Redwood
+
+## wrap a nice interactive layer on top of anything that has a #lock method
+## which throws a LockError which responds to #user, #host, #mtim, #pname, and
+## #pid.
+
+module InteractiveLock
+ def pluralize number_of, kind; "#{number_of} #{kind}" + (number_of == 1 ? "" : "s") end
+
+ def time_ago_in_words time
+ secs = (Time.now - time).to_i
+ mins = secs / 60
+ time = if mins == 0
+ pluralize secs, "second"
+ else
+ pluralize mins, "minute"
+ end
+ end
+
+ DELAY = 5 # seconds
+
+ def lock_interactively stream=$stderr
+ begin
+ Index.lock
+ rescue Index::LockError => e
+ stream.puts <<EOS
+Error: the index is locked by another process! User '#{e.user}' on
+host '#{e.host}' is running #{e.pname} with pid #{e.pid}.
+The process was alive as of at least #{time_ago_in_words e.mtime} ago.
+
+EOS
+ stream.print "Should I ask that process to kill itself (y/n)? "
+ stream.flush
+
+ success = if $stdin.gets =~ /^\s*y(es)?\s*$/i
+ stream.puts "Ok, trying to kill process..."
+
+ begin
+ Process.kill "TERM", e.pid.to_i
+ sleep DELAY
+ rescue Errno::ESRCH # no such process
+ stream.puts "Hm, I couldn't kill it."
+ end
+
+ stream.puts "Let's try that again."
+ begin
+ Index.lock
+ rescue Index::LockError => e
+ stream.puts "I couldn't lock the index. The lockfile might just be stale."
+ stream.print "Should I just remove it and continue? (y/n) "
+ stream.flush
+
+ if $stdin.gets =~ /^\s*y(es)?\s*$/i
+ FileUtils.rm e.path
+
+ stream.puts "Let's try that one more time."
+ begin
+ Index.lock
+ true
+ rescue Index::LockError => e
+ end
+ end
+ end
+ end
+
+ stream.puts "Sorry, couldn't unlock the index." unless success
+ success
+ end
+ end
+end
+
+end
@new_labels = {}
@modified = false
labels.each { |t| @labels[t] = true }
-
- self.class.i_am_the_instance self
end
def new_label? l; @new_labels.include?(l) end
l
end
end
-
+
def << t
- t = t.intern unless t.is_a? Symbol
+ raise ArgumentError, "expecting a symbol" unless t.is_a? Symbol
unless @labels.member?(t) || RESERVED_LABELS.member?(t)
@labels[t] = true
@new_labels[t] = true
+require "sup"
+require 'stringio'
+require 'thread'
+
module Redwood
+## simple centralized logger. outputs to multiple sinks by calling << on them.
+## also keeps a record of all messages, so that adding a new sink will send all
+## previous messages to it by default.
class Logger
- @@instance = nil
+ include Singleton
- attr_reader :buf
+ LEVELS = %w(debug info warn error) # in order!
- def initialize
- raise "only one Log can be defined" if @@instance
- @@instance = self
- @mode = LogMode.new
- @respawn = true
- @spawning = false # to prevent infinite loops!
+ def initialize level=nil
+ level ||= ENV["SUP_LOG_LEVEL"] || "info"
+ @level = LEVELS.index(level) or raise ArgumentError, "invalid log level #{level.inspect}: should be one of #{LEVELS * ', '}"
+ @mutex = Mutex.new
+ @buf = StringIO.new
+ @sinks = []
end
- ## must be called if you want to see anything!
- ## once called, will respawn if killed...
- def make_buf
- return if @mode.buffer || !BufferManager.instantiated? || !@respawn || @spawning
- @spawning = true
- @mode.buffer = BufferManager.instance.spawn "log", @mode, :hidden => true, :system => true
- @spawning = false
+ def level; LEVELS[@level] end
+
+ def add_sink s, copy_current=true
+ @mutex.synchronize do
+ @sinks << s
+ s << @buf.string if copy_current
+ end
end
- def log s
-# $stderr.puts s
- make_buf
- prefix = "#{Time.now}: "
- padding = " " * prefix.length
- first = true
- s.split(/[\r\n]/).each do |l|
- l = l.chomp
- if first
- first = false
- @mode << "#{prefix}#{l}\n"
- else
- @mode << "#{padding}#{l}\n"
+ def remove_sink s; @mutex.synchronize { @sinks.delete s } end
+ def remove_all_sinks!; @mutex.synchronize { @sinks.clear } end
+ def clear!; @mutex.synchronize { @buf = StringIO.new } end
+
+ LEVELS.each_with_index do |l, method_level|
+ define_method(l) do |s|
+ if method_level >= @level
+ send_message format_message(l, Time.now, s)
end
end
- $stderr.puts "[#{Time.now}] #{s.chomp}" unless BufferManager.instantiated? && @mode.buffer
end
-
- def self.method_missing m, *a
- @@instance = Logger.new unless @@instance
- @@instance.send m, *a
+
+ ## send a message regardless of the current logging level
+ def force_message m; send_message format_message(nil, Time.now, m) end
+
+private
+
+ ## level can be nil!
+ def format_message level, time, msg
+ prefix = case level
+ when "warn"; "WARNING: "
+ when "error"; "ERROR: "
+ else ""
+ end
+ "[#{time.to_s}] #{prefix}#{msg}\n"
end
- def self.buffer
- @@instance.buf
+ ## actually distribute the message
+ def send_message m
+ @mutex.synchronize do
+ @sinks.each { |sink| sink << m }
+ @buf << m
+ end
end
end
+## include me to have top-level #debug, #info, etc. methods.
+module LogsStuff
+ Logger::LEVELS.each { |l| define_method(l) { |s| Logger.instance.send(l, s) } }
end
+end
## pathnames on disk.
class Maildir < Source
+ include SerializeLabelsNicely
SCAN_INTERVAL = 30 # seconds
MYHOSTNAME = Socket.gethostname
raise ArgumentError, "maildir URI must have a path component" unless uri.path
@dir = uri.path
- @labels = (labels || []).freeze
+ @labels = Set.new(labels || [])
@ids = []
@ids_to_fns = {}
@last_scan = nil
initial_poll = @ids.empty?
- Redwood::log "scanning maildir #@dir..."
+ debug "scanning maildir #@dir..."
begin
@mtimes.each_key do |d|
subdir = File.join(@dir, d)
@ids_to_fns[id] = fn
end
else
- Redwood::log "no poll on #{d}. mtime on indicates no new messages."
+ debug "no poll on #{d}. mtime on indicates no new messages."
end
end
@ids = @dir_ids.values.flatten.uniq.sort!
raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
end
- Redwood::log "done scanning maildir"
+ debug "done scanning maildir"
@last_scan = Time.now
end
synchronized :scan_mailbox
Time.parse time, 0
true
rescue NoMethodError
- Redwood::log "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
+ warn "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
false
end
end
require 'rmail'
require 'uri'
+require 'set'
module Redwood
module MBox
class Loader < Source
+ include SerializeLabelsNicely
yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
- attr_accessor :labels
+
+ attr_reader :labels
## uri_or_fp is horrific. need to refactor.
- def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=[]
+ def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=nil
@mutex = Mutex.new
- @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
+ @labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
case uri_or_fp
when String
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
end
self.cur_offset = next_offset
- [returned_offset, (self.labels + [:unread]).uniq]
+ [returned_offset, (labels + [:unread])]
end
end
## all of the methods here can throw SSHFileErrors, SocketErrors,
## Net::SSH::Exceptions and Errno::ENOENTs.
-## debugging TODO: remove me
-def debug s
- Redwood::log s
-end
-module_function :debug
-
## a simple buffer of contiguous data
class Buffer
def initialize
## TODO: share this code with imap
def say s
@say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
- Redwood::log s
+ info s
end
def shutup
def initial_state; :open end
def viewable?; @lines.nil? end
def view_default! path
- cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}' 2>/dev/null"
- Redwood::log "running: #{cmd.inspect}"
- system cmd
+ cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}'"
+ debug "running: #{cmd.inspect}"
+ BufferManager.shell_out(cmd)
$? == 0
end
@snippet = opts[:snippet]
@snippet_contains_encrypted_content = false
@have_snippet = !(opts[:snippet].nil? || opts[:snippet].empty?)
- @labels = (opts[:labels] || []).to_set_of_symbols
+ @labels = Set.new(opts[:labels] || [])
@dirty = false
@encrypted = false
@chunks = nil
else
id = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
from = header["from"]
- #Redwood::log "faking non-existent message-id for message from #{from}: #{id}"
+ #debug "faking non-existent message-id for message from #{from}: #{id}"
id
end
header["from"]
else
name = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
- #Redwood::log "faking non-existent sender for message #@id: #{name}"
+ #debug "faking non-existent sender for message #@id: #{name}"
name
end)
begin
Time.parse date
rescue ArgumentError => e
- #Redwood::log "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
+ #debug "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}"
+ #debug "faking non-existent date header for #{@id}"
Time.now
end
@list_unsubscribe = header["list-unsubscribe"]
end
+ ## Expected index entry format:
+ ## :message_id, :subject => String
+ ## :date => Time
+ ## :refs, :replytos => Array of String
+ ## :from => Person
+ ## :to, :cc, :bcc => Array of Person
+ def load_from_index! entry
+ @id = entry[:message_id]
+ @from = entry[:from]
+ @date = entry[:date]
+ @subj = entry[:subject]
+ @to = entry[:to]
+ @cc = entry[:cc]
+ @bcc = entry[:bcc]
+ @refs = (@refs + entry[:refs]).uniq
+ @replytos = entry[:replytos]
+
+ @replyto = nil
+ @list_address = nil
+ @recipient_email = nil
+ @source_marked_read = false
+ @list_subscribe = nil
+ @list_unsubscribe = nil
+ end
+
def add_ref ref
@refs << ref
@dirty = true
## don't tempt me.
def sanitize_message_id mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end
- def save index
+ def save_state index
return unless @dirty
- index.sync_message self
+ index.update_message_state self
@dirty = false
true
end
def has_label? t; @labels.member? t; end
- def add_label t
- return if @labels.member? t
- @labels = (@labels + [t]).to_set_of_symbols
+ def add_label l
+ l = l.to_sym
+ return if @labels.member? l
+ @labels << l
@dirty = true
end
- def remove_label t
- return unless @labels.member? t
- @labels.delete t
+ def remove_label l
+ l = l.to_sym
+ return unless @labels.member? l
+ @labels.delete l
@dirty = true
end
end
def labels= l
- @labels = l.to_set_of_symbols
+ raise ArgumentError, "not a set" unless l.is_a?(Set)
+ raise ArgumentError, "not a set of labels" unless l.all? { |ll| ll.is_a?(Symbol) }
+ return if @labels == l
+ @labels = l
@dirty = true
end
parse_header @source.load_header(@source_info)
message_to_chunks @source.load_message(@source_info)
rescue SourceError, SocketError => e
- Redwood::log "problem getting messages from #{@source}: #{e.message}"
+ 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
begin
yield
rescue SourceError => e
- Redwood::log "problem getting messages from #{@source}: #{e.message}"
+ warn "problem getting messages from #{@source}: #{e.message}"
@source.error ||= e
Redwood::report_broken_sources :force_to_top => true
error_message e.message
def multipart_signed_to_chunks m
if m.body.size != 2
- Redwood::log "warning: multipart/signed with #{m.body.size} parts (expecting 2)"
+ warn "multipart/signed with #{m.body.size} parts (expecting 2)"
return
end
payload, signature = m.body
if signature.multipart?
- Redwood::log "warning: multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
+ warn "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
return
end
## this probably will never happen
if payload.header.content_type == "application/pgp-signature"
- Redwood::log "warning: multipart/signed with payload content type #{payload.header.content_type}"
+ warn "multipart/signed with payload content type #{payload.header.content_type}"
return
end
if signature.header.content_type != "application/pgp-signature"
## unknown signature type; just ignore.
- #Redwood::log "warning: multipart/signed with signature content type #{signature.header.content_type}"
+ #warn "multipart/signed with signature content type #{signature.header.content_type}"
return
end
def multipart_encrypted_to_chunks m
if m.body.size != 2
- Redwood::log "warning: multipart/encrypted with #{m.body.size} parts (expecting 2)"
+ warn "multipart/encrypted with #{m.body.size} parts (expecting 2)"
return
end
control, payload = m.body
if control.multipart?
- Redwood::log "warning: multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
+ warn "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
return
end
if payload.header.content_type != "application/octet-stream"
- Redwood::log "warning: multipart/encrypted with payload content type #{payload.header.content_type}"
+ warn "multipart/encrypted with payload content type #{payload.header.content_type}"
return
end
if control.header.content_type != "application/pgp-encrypted"
- Redwood::log "warning: multipart/encrypted with control content type #{signature.header.content_type}"
+ warn "multipart/encrypted with control content type #{signature.header.content_type}"
return
end
- decryptedm, sig, notice = CryptoManager.decrypt payload
- children = message_to_chunks(decryptedm, true) if decryptedm
- [notice, sig, children].flatten.compact
+ notice, sig, decryptedm = CryptoManager.decrypt payload
+ if decryptedm # managed to decrypt
+ children = message_to_chunks(decryptedm, true)
+ [notice, sig, children]
+ else
+ [notice]
+ end
end
## takes a RMail::Message, breaks it into Chunk:: classes.
chunks
elsif m.header.content_type == "message/rfc822"
- payload = RMail::Parser.read(m.body)
- from = payload.header.from.first
- from_person = from ? Person.from_address(from.format) : nil
- [Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
- message_to_chunks(payload, encrypted)
+ if m.body
+ payload = RMail::Parser.read(m.body)
+ from = payload.header.from.first
+ from_person = from ? Person.from_address(from.format) : nil
+ [Chunk::EnclosedMessage.new(from_person, payload.to_s)] +
+ message_to_chunks(payload, encrypted)
+ else
+ [Chunk::EnclosedMessage.new(nil, "")]
+ end
else
filename =
## first, paw through the headers looking for a filename
unless err.empty?
message = err.first.read
if message =~ /^\s*$/
- Redwood::log "error running #{command} (but no error message)"
+ warn "error running #{command} (but no error message)"
BufferManager.flash "Error running #{command}!"
else
- Redwood::log "error running #{command}: #{message}"
+ warn "error running #{command}: #{message}"
BufferManager.flash "Error: #{message}"
end
return
BufferManager.flash "Message sent!"
true
rescue SystemCallError, SendmailCommandFailed, CryptoManager::Error => e
- Redwood::log "Problem sending mail: #{e.message}"
+ warn "Problem sending mail: #{e.message}"
BufferManager.flash "Problem sending mail: #{e.message}"
false
end
-require 'thread'
+require 'sup'
module Redwood
@@instance = self
end
- def is_relevant? m
- m.has_label?(:inbox) && ([:spam, :deleted, :killed] & m.labels).empty?
- end
+ def is_relevant? m; (m.labels & [:spam, :deleted, :killed, :inbox]) == Set.new([:inbox]) end
## label-list-mode wants to be able to raise us if the user selects
## the "inbox" label, so we need to keep our singletonness around
## TODO make the labelmanager responsible for label counts
## and then it can listen to labeled and unlabeled events, etc.
if total == 0 && !LabelManager::RESERVED_LABELS.include?(label) && !LabelManager.new_label?(label)
- Redwood::log "no hits for label #{label}, deleting"
+ debug "no hits for label #{label}, deleting"
LabelManager.delete label
next
end
+require 'stringio'
module Redwood
+## a variant of text mode that allows the user to automatically follow text,
+## and respawns when << is called if necessary.
+
class LogMode < TextMode
register_keymap do |k|
k.add :toggle_follow, "Toggle follow mode", 'f'
end
- def initialize
+ def initialize buffer_name
@follow = true
- super
+ @buffer_name = buffer_name
+ @on_kill = []
+ super()
end
+ ## register callbacks for when the buffer is killed
+ def on_kill &b; @on_kill << b end
+
def toggle_follow
@follow = !@follow
- if buffer
- if @follow
- jump_to_line lines - buffer.content_height + 1 # leave an empty line at bottom
- end
- buffer.mark_dirty
+ if @follow
+ jump_to_line(lines - buffer.content_height + 1) # leave an empty line at bottom
end
+ buffer.mark_dirty
end
- def text= t
- super
- if buffer && @follow
- follow_top = lines - buffer.content_height + 1
- jump_to_line follow_top if topline < follow_top
+ def << s
+ unless buffer
+ BufferManager.spawn @buffer_name, self, :hidden => true, :system => true
end
- end
- def << line
- super
- if buffer && @follow
+ s.split("\n").each { |l| super(l + "\n") } # insane. different << semantics.
+
+ if @follow
follow_top = lines - buffer.content_height + 1
jump_to_line follow_top if topline < follow_top
end
def status
super + " (follow: #@follow)"
end
+
+ def cleanup
+ @on_kill.each { |cb| cb.call self }
+ self.text = ""
+ super
+ end
end
end
class PollMode < LogMode
def initialize
@new = true
- super
- end
-
- def puts s=""
- self << s + "\n"
+ super "poll for new messages"
end
def poll
- puts unless @new
- @new = false
- puts "Poll started at #{Time.now}"
- PollManager.do_poll { |s| puts s }
+ unless @new
+ @new = false
+ self << "\n"
+ end
+ self << "Poll started at #{Time.now}\n"
+ PollManager.do_poll { |s| self << (s + "\n") }
end
end
## don't check that it's an Account, though; assume they know what they're
## doing.
if hook_reply_from && !(hook_reply_from.is_a? Person)
- Redwood::log "reply-from returned non-Person, using default from."
+ info "reply-from returned non-Person, using default from."
hook_reply_from = nil
end
class ScrollMode < Mode
## we define topline and botline as the top and bottom lines of any
## content in the currentview.
-
+
## we left leftcol and rightcol as the left and right columns of any
## content in the current view. but since we're operating in a
## line-centric fashion, rightcol is always leftcol + the buffer
raise "nil text for color '#{color}'" if text.nil? # good for debugging
l = text.display_length
no_fill = i != a.size - 1
-
+
if xpos + l < @leftcol
buffer.write ln - @topline, 0, "", :color => color,
:highlight => opts[:highlight]
buffer.mark_dirty if buffer
super()
end
-
+
def save_to_disk
fn = BufferManager.ask_for_filename :filename, "Save to file: ", @filename
save_to_file(fn) { |f| f.puts text } if fn
@lines << @text.length
if buffer
ensure_mode_validity
- buffer.mark_dirty
+ buffer.mark_dirty
end
end
BufferManager.say("Saving threads...") do |say_id|
dirty_threads.each_with_index do |t, i|
BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
- t.save Index
+ t.save_state Index
end
end
end
keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl, @hidden_labels
-
return unless user_labels
- thread.labels = keepl + user_labels
+
+ thread.labels = Set.new(keepl) + user_labels
user_labels.each { |l| LabelManager << l }
update_text_for_line curpos
bt = to.size > 1 ? "#{to.size} recipients" : to.to_s
if BufferManager.ask_yes_or_no "Really bounce to #{bt}?"
- Redwood::log "Bounce Command: #{cmd}"
+ debug "bounce command: #{cmd}"
begin
IO.popen(cmd, 'w') do |sm|
sm.puts m.raw_message
end
raise SendmailCommandFailed, "Couldn't execute #{cmd}" unless $? == 0
rescue SystemCallError, SendmailCommandFailed => e
- Redwood::log "Problem sending mail: #{e.message}"
+ warn "problem sending mail: #{e.message}"
BufferManager.flash "Problem sending mail: #{e.message}"
end
end
new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels
return unless new_labels
- @thread.labels = (reserved_labels + new_labels).uniq
+ @thread.labels = Set.new(reserved_labels) + new_labels
new_labels.each { |l| LabelManager << l }
update
UpdateManager.relay self, :labeled, @thread.first
@thread = nil
@last_poll = nil
@polling = false
-
- self.class.i_am_the_instance self
- end
-
- def buffer
- b, new = BufferManager.spawn_unless_exists("poll for new messages", :hidden => true, :system => true) { PollMode.new }
- b
+ @mode = nil
end
def poll
return if @polling
@polling = true
+ @mode ||= PollMode.new
HookManager.run "before-poll"
BufferManager.flash "Polling for new messages..."
- num, numi, from_and_subj, from_and_subj_inbox = buffer.mode.poll
+ num, numi, from_and_subj, from_and_subj_inbox = @mode.poll
if num > 0
BufferManager.flash "Loaded #{num.pluralize 'new message'}, #{numi} to inbox."
else
begin
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}"
+ warn "problem getting messages from #{source}: #{e.message}"
Redwood::report_broken_sources :force_to_top => true
next
end
num = 0
numi = 0
- add_messages_from source do |m_old, m, offset|
- ## always preserve the labels on disk.
- m.labels = ((m.labels - [:unread, :inbox]) + m_old.labels).uniq if m_old
- yield "Found message at #{offset} with labels {#{m.labels * ', '}}"
- unless m_old
+ each_message_from source do |m|
+ yield "Found message at #{m.source_info} with labels {#{m.labels.to_a * ', '}}"
+ old_m = Index.build_message m.id
+ if old_m
+ if old_m.source.id != source.id || old_m.source_info != 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
+ Index.update_message m
+ 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
num += 1
from_and_subj << [m.from && m.from.longname, m.subj]
- if m.has_label?(:inbox) && ([:spam, :deleted, :killed] & m.labels).empty?
+ if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
from_and_subj_inbox << [m.from && m.from.longname, m.subj]
- numi += 1
+ numi += 1
end
end
m
[total_num, total_numi, from_and_subj, from_and_subj_inbox]
end
- ## this is the main mechanism for adding new messages to the
- ## index. it's called both by sup-sync and by PollMode.
- ##
- ## for each message in the source, starting from the source's
- ## starting offset, this methods yields the message, the source
- ## offset, and the index entry on disk (if any). it expects the
- ## yield to return the message (possibly altered in some way), and
- ## then adds it (if new) or updates it (if previously seen).
+ ## like Source#each, but yields successive Message objects, which have their
+ ## labels and offsets set correctly.
##
- ## the labels of the yielded message are the default source
- ## labels. it is likely that callers will want to replace these with
- ## the index labels, if they exist, so that state is not lost when
- ## e.g. a new version of a message from a mailing list comes in.
- def add_messages_from source, opts={}
+ ## this is the primary mechanism for iterating over messages from a source.
+ def each_message_from source, opts={}
begin
return if source.done? || source.has_errors?
- source.each do |offset, default_labels|
+ source.each do |offset, source_labels|
if source.has_errors?
- Redwood::log "error loading messages from #{source}: #{source.error.message}"
+ warn "error loading messages from #{source}: #{source.error.message}"
return
end
- m_new = Message.build_from_source source, offset
- m_old = Index.build_message m_new.id
+ 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 }
- m_new.labels += default_labels + (source.archived? ? [] : [:inbox])
- m_new.labels << :sent if source.uri.eql?(SentManager.source_uri)
- m_new.labels.delete :unread if m_new.source_marked_read?
- m_new.labels.each { |l| LabelManager << l }
-
- HookManager.run "before-add-message", :message => m_new
- m_ret = yield(m_old, m_new, offset) or next if block_given?
- Index.sync_message m_ret, opts
- UpdateManager.relay self, :added, m_ret unless m_old
+ HookManager.run "before-add-message", :message => m
+ yield m
end
rescue SourceError => e
- Redwood::log "problem getting messages from #{source}: #{e.message}"
+ 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
end
end
def initialize source_uri
@source = nil
@source_uri = source_uri
- self.class.i_am_the_instance self
- Redwood::log "SentManager intialized with source uri: #@source_uri"
end
def source_id; @source.id; end
def default_source
@source = Recoverable.new SentLoader.new
- Redwood::log "SentManager initializing default source: #@source."
@source_uri = @source.uri
@source
end
def write_sent_message date, from_email, &block
@source.store_message date, from_email, &block
- PollManager.add_messages_from(@source) do |m_old, m, offset|
+ PollManager.each_message_from(@source) do |m|
m.remove_label :unread
- m
+ m.add_label :sent
+ PollManager.add_new_message m
end
end
end
def uri; 'sup://sent' end
def id; 9998; end
- def labels; [:inbox]; end
+ def labels; [:inbox, :sent]; end
end
end
## To write a new source, subclass this class, and implement:
##
## - start_offset
- ## - end_offset (exclusive!)
+ ## - end_offset (exclusive!) (or, #done?)
## - load_header offset
## - load_message offset
## - raw_header offset
## - raw_message offset
- ## - check
+ ## - check (optional)
## - next (or each, if you prefer): should return a message and an
## array of labels.
##
@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
## to proactively notify the user of any source problems.
def check; 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?
- n, labels = self.next
- raise "no message" unless n
- yield n, labels
+ offset, labels = self.next
+ yield offset, labels
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.
+ ## utility method to read a raw email header from an IO stream and turn it
+ ## into a hash of key-value pairs. minor special semantics for certain headers.
##
- ## 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.
+ ## 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
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.
+ ## regular header: overwrite (not that we should see more than one)
+ ## TODO: figure out whether just using the first occurrence changes
+ ## anything (which would simplify the logic slightly)
when /^([^:\s]+):\s*(.*?)\s*$/i; header[last = $1.downcase] = $2
- when /^\r*$/; break
+ when /^\r*$/; break # blank line signifies end of header
else
if last
header[last] << " " unless header[last].empty?
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}"
+ #debug "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
v
end
end
## 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("~")
end
end
end
+## if you have a @labels instance variable, include this
+## to serialize them nicely as an array, rather than as a
+## nasty set.
+module SerializeLabelsNicely
+ def before_marshal # can return an object
+ c = clone
+ c.instance_eval { @labels = @labels.to_a.map { |l| l.to_s } }
+ c
+ end
+
+ def after_unmarshal!
+ @labels = Set.new(@labels.map { |s| s.to_sym })
+ end
+end
+
class SourceManager
include Singleton
@sources = {}
@sources_dirty = false
@source_mutex = Monitor.new
- self.class.i_am_the_instance self
end
def [](id)
File.chmod 0600, fn
FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
end
- Redwood::save_yaml_obj sources.sort_by { |s| s.id.to_i }, fn, true
+ Redwood::save_yaml_obj sources, fn, true
File.chmod 0600, fn
end
@sources_dirty = false
+++ /dev/null
-module Redwood
-
-class SuicideManager
- include Singleton
-
- DELAY = 5
-
- def initialize fn
- @fn = fn
- @die = false
- @thread = nil
- self.class.i_am_the_instance self
- FileUtils.rm_f @fn
- end
-
- bool_reader :die
-
- def start
- @thread = Redwood::reporting_thread("suicide watch") do
- while true
- sleep DELAY
- if File.exists? @fn
- FileUtils.rm_f @fn
- @die = true
- end
- end
- end
- end
-
- def stop
- @thread.kill if @thread
- @thread = nil
- end
-end
-
-end
unless @history.empty?
value = get_cursed_value
@i ||= @history.size
- #Redwood::log "history before #{@history.inspect}"
+ #debug "history before #{@history.inspect}"
@history[@i] = value #unless value =~ /^\s*$/
@i = (@i + (c == Ncurses::KEY_UP ? -1 : 1)) % @history.size
@value = @history[@i]
- #Redwood::log "history after #{@history.inspect}"
+ #debug "history after #{@history.inspect}"
set_cursed_value @value
Ncurses::Form::REQ_END_FIELD
end
## a faked root object tying them all together into one tree
## structure.
+require 'set'
+
module Redwood
class Thread
def toggle_label label
if has_label? label
remove_label label
- return false
+ false
else
apply_label label
- return true
+ true
end
end
def set_labels l; each { |m, *o| m && m.labels = l }; end
-
def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
- def save index; each { |m, *o| m && m.save(index) }; end
+ def save_state index; each { |m, *o| m && m.save_state(index) }; end
def direct_participants
map { |m, *o| [m.from] + m.to if m }.flatten.compact.uniq
def size; map { |m, *o| m ? 1 : 0 }.sum; end
def subj; argfind { |m, *o| m && m.subj }; end
- def labels
- map { |m, *o| m && m.labels }.flatten.compact.uniq.sort_by { |t| t.to_s }
- end
+ def labels; inject(Set.new) { |s, (m, *o)| m ? s | m.labels : s } end
def labels= l
- each { |m, *o| m && m.labels = l.clone }
+ raise ArgumentError, "not a set" unless l.is_a?(Set)
+ each { |m, *o| m && m.labels = l.dup }
end
def latest_message
- inject(nil) do |a, b|
+ inject(nil) do |a, b|
b = b.first
if a.nil?
b
@id = id
@message, @parent, @thread = nil, nil, nil
@children = []
- end
+ end
def each_with_stuff parent=nil
yield self, 0, parent
def initialize
@@actionlist = []
- self.class.i_am_the_instance self
end
def register desc, *actions, &b
def initialize
@targets = {}
- self.class.i_am_the_instance self
end
def register o; @targets[o] = true; end
require 'lockfile'
require 'mime/types'
require 'pathname'
+require 'set'
## time for some monkeypatching!
class Lockfile
def lockinfo_on_disk
h = load_lock_id IO.read(path)
h['mtime'] = File.mtime path
+ h['path'] = path
h
end
class Range
## only valid for integer ranges (unless I guess it's exclusive)
- def size
+ def size
last - first + (exclude_end? ? 0 : 1)
end
end
## 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"
+ if $encoding == "UTF-8" || $encoding == "utf8"
scan(/./u).size
else
size
newpos = case state
when :escaped_instring, :escaped_outstring then pos
else index(/[,"\\]/, pos)
- end
-
+ end
+
if newpos
char = self[newpos]
else
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.
- def symbolistize; split.map { |x| x.intern } end
+ ## takes a 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.
+ ##
+ ## split_on will be passed to String#split, so you can leave this nil for space.
+ def to_set_of_symbols split_on=nil; Set.new split(split_on).map { |x| x.strip.intern } end
end
class Numeric
def last= e; self[-1] = e end
def nonempty?; !empty? end
-
- def to_set_of_symbols
- map { |x| x.is_a?(Symbol) ? x : x.intern }.uniq
- end
end
class Time
end
end
-## simple singleton module. far less complete and insane than the ruby
-## standard library one, but automatically forwards methods calls and
-## allows for constructors that take arguments.
+## simple singleton module. far less complete and insane than the ruby standard
+## library one, but it automatically forwards methods calls and allows for
+## constructors that take arguments.
##
-## You must have #initialize call "self.class.i_am_the_instance self"
-## at some point or everything will fail horribly.
+## classes that inherit this can define initialize. however, you cannot call
+## .new on the class. To get the instance of the class, call .instance;
+## to create the instance, call init.
module Singleton
module ClassMethods
def instance; @instance; end
def instantiated?; defined?(@instance) && !@instance.nil?; end
def deinstantiate!; @instance = nil; end
def method_missing meth, *a, &b
- raise "no instance defined!" unless defined? @instance
+ raise "no #{name} instance defined in method call to #{meth}!" unless defined? @instance
## if we've been deinstantiated, just drop all calls. this is
## useful because threads that might be active during the
@instance.send meth, *a, &b
end
- def i_am_the_instance o
+ def init *args
raise "there can be only one! (instance)" if defined? @instance
- @instance = o
+ @instance = new(*args)
end
end
def self.included klass
+ klass.private_class_method :allocate, :new
klass.extend ClassMethods
end
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
begin
Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]
rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::InvalidCharacter, Iconv::IllegalSequence => e
- Redwood::log "warning: error (#{e.class.name}) decoding text from #{charset} to #{target}: #{text[0 ... 20]}"
+ warn "couldn't transcode text from #{charset} to #{target} (\"#{text[0 ... 20]}\"...) (got #{e.message}); using original as is"
text
end
end
require 'xapian'
-require 'gdbm'
require 'set'
module Redwood
# for searching due to precomputing thread membership.
class XapianIndex < BaseIndex
STEM_LANGUAGE = "english"
+ INDEX_VERSION = '1'
## 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
end
def load_index
- @entries = MarshalledGDBM.new File.join(@dir, "entries.db")
- @docids = MarshalledGDBM.new File.join(@dir, "docids.db")
- @thread_members = MarshalledGDBM.new File.join(@dir, "thread_members.db")
- @thread_ids = MarshalledGDBM.new File.join(@dir, "thread_ids.db")
- @assigned_docids = GDBM.new File.join(@dir, "assigned_docids.db")
-
- @xapian = Xapian::WritableDatabase.new(File.join(@dir, "xapian"), Xapian::DB_CREATE_OR_OPEN)
+ path = File.join(@dir, 'xapian')
+ if File.exists? path
+ @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
+ db_version = @xapian.get_metadata 'version'
+ db_version = '0' if db_version.empty?
+ if 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)."
+ end
+ else
+ @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_CREATE)
+ @xapian.set_metadata 'version', INDEX_VERSION
+ end
@term_generator = Xapian::TermGenerator.new()
@term_generator.stemmer = Xapian::Stem.new(STEM_LANGUAGE)
@enquire = Xapian::Enquire.new @xapian
end
def contains_id? id
- synchronize { @entries.member? id }
+ synchronize { find_docid(id) && true }
end
def source_for_id id
- synchronize { @entries[id][:source_id] }
+ synchronize { get_entry(id)[:source_id] }
end
def delete id
- synchronize { @xapian.delete_document @docids[id] }
+ synchronize { @xapian.delete_document mkterm(:msgid, id) }
end
def build_message id
- entry = synchronize { @entries[id] }
+ entry = synchronize { get_entry id }
return unless entry
source = SourceManager[entry[:source_id]]
raise "invalid source #{entry[:source_id]}" unless source
- mk_addrs = lambda { |l| l.map { |e,n| "#{n} <#{e}>" } * ', ' }
- mk_refs = lambda { |l| l.map { |r| "<#{r}>" } * ' ' }
- fake_header = {
- 'message-id' => entry[:message_id],
- 'date' => Time.at(entry[:date]),
- 'subject' => entry[:subject],
- 'from' => mk_addrs[[entry[:from]]],
- 'to' => mk_addrs[entry[:to]],
- 'cc' => mk_addrs[entry[:cc]],
- 'bcc' => mk_addrs[entry[:bcc]],
- 'reply-tos' => mk_refs[entry[:replytos]],
- 'references' => mk_refs[entry[:refs]],
- }
-
- m = Message.new :source => source, :source_info => entry[:source_info],
- :labels => entry[:labels],
- :snippet => entry[:snippet]
- m.parse_header fake_header
- m
+ m = Message.new :source => source, :source_info => entry[:source_info],
+ :labels => entry[:labels], :snippet => entry[:snippet]
+
+ mk_person = lambda { |x| Person.new(*x.reverse!) }
+ entry[:from] = mk_person[entry[:from]]
+ entry[:to].map!(&mk_person)
+ entry[:cc].map!(&mk_person)
+ entry[:bcc].map!(&mk_person)
+
+ m.load_from_index! entry
+ m
end
+ def add_message m; sync_message m end
+ def update_message m; sync_message m end
+ def update_message_state m; sync_message m end
+
def sync_message m, opts={}
- entry = synchronize { @entries[m.id] }
+ entry = synchronize { get_entry m.id }
snippet = m.snippet
entry ||= {}
labels = m.labels
:source_info => m.source_info,
:date => (entry[:date] || m.date),
:snippet => snippet,
- :labels => labels.uniq,
+ :labels => labels,
:from => (entry[:from] || [m.from.email, m.from.name]),
:to => (entry[:to] || m.to.map { |p| [p.email, p.name] }),
:cc => (entry[:cc] || m.cc.map { |p| [p.email, p.name] }),
:replytos => (entry[:replytos] || m.replytos),
}
- m.labels.each { |l| LabelManager << l }
+ labels.each { |l| LabelManager << l }
synchronize do
- index_message m, opts
- union_threads([m.id] + m.refs + m.replytos)
- @entries[m.id] = d
+ index_message m, d, opts
end
true
end
+ private :sync_message
def num_results_for query={}
xapian_query = build_xapian_query query
def each_message_in_thread_for m, opts={}
# TODO thread by subject
# TODO handle killed threads
- ids = synchronize { @thread_members[@thread_ids[m.id]] } || []
- ids.select { |id| contains_id? id }.each { |id| yield id, lambda { build_message id } }
+ return unless doc = find_doc(m.id)
+ queue = doc.value(THREAD_VALUENO).split(',')
+ msgids = [m.id]
+ seen_threads = Set.new
+ seen_messages = Set.new [m.id]
+ while not queue.empty?
+ thread_id = queue.pop
+ next if seen_threads.member? thread_id
+ return false if thread_killed? thread_id
+ seen_threads << thread_id
+ docs = term_docids(mkterm(:thread, thread_id)).map { |x| @xapian.document x }
+ docs.each do |doc|
+ msgid = doc.value MSGID_VALUENO
+ next if seen_messages.member? msgid
+ msgids << msgid
+ seen_messages << msgid
+ queue.concat doc.value(THREAD_VALUENO).split(',')
+ end
+ end
+ msgids.each { |id| yield id, lambda { build_message id } }
true
end
field, name = $1, ($3 || $4)
case field
when "filename"
- Redwood::log "filename - translated #{field}:#{name} to attachment:\"#{name.downcase}\""
+ debug "filename: translated #{field}:#{name} to attachment:\"#{name.downcase}\""
"attachment:\"#{name.downcase}\""
when "filetype"
- Redwood::log "filetype - translated #{field}:#{name} to attachment_extension:#{name.downcase}"
+ debug "filetype: translated #{field}:#{name} to attachment_extension:#{name.downcase}"
"attachment_extension:#{name.downcase}"
end
end
if realdate
case field
when "after"
- Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.end}"
+ debug "chronic: translated #{field}:#{datestr} to #{realdate.end}"
"date:#{realdate.end.to_i}..#{lastdate}"
when "before"
- Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
+ debug "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
"date:#{firstdate}..#{realdate.end.to_i}"
else
- Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate}"
+ debug "chronic: translated #{field}:#{datestr} to #{realdate}"
"date:#{realdate.begin.to_i}..#{realdate.end.to_i}"
end
else
'label' => 'L',
'source_id' => 'I',
'attachment_extension' => 'O',
+ 'msgid' => 'Q',
+ 'thread' => 'H',
+ 'ref' => 'R',
}
PREFIX = NORMAL_PREFIX.merge BOOLEAN_PREFIX
- DATE_VALUENO = 0
+ MSGID_VALUENO = 0
+ THREAD_VALUENO = 1
+ DATE_VALUENO = 2
MAX_TERM_LENGTH = 245
def assign_docid m, truncated_date
t = (truncated_date.to_i - MIDDLE_DATE.to_i).to_f
docid = (DOCID_SCALE - DOCID_SCALE/(Math::E**(-(t/TIME_SCALE)) + 1)).to_i
+ while docid > 0 and docid_exists? docid
+ docid -= 1
+ end
+ docid > 0 ? docid : nil
+ end
+
+ # XXX is there a better way?
+ def docid_exists? docid
begin
- while @assigned_docids.member? [docid].pack("N")
- docid -= 1
- end
- rescue
+ @xapian.doclength docid
+ true
+ rescue RuntimeError #Xapian::DocNotFoundError
+ raise unless $!.message =~ /DocNotFoundError/
+ false
end
- @assigned_docids[[docid].pack("N")] = ''
- docid
+ end
+
+ def term_docids term
+ @xapian.postlist(term).map { |x| x.docid }
+ end
+
+ def find_docid id
+ docids = term_docids(mkterm(:msgid,id))
+ fail unless docids.size <= 1
+ docids.first
+ end
+
+ def find_doc id
+ return unless docid = find_docid(id)
+ @xapian.document docid
+ end
+
+ def get_id docid
+ return unless doc = @xapian.document(docid)
+ doc.value MSGID_VALUENO
+ end
+
+ def get_entry id
+ return unless doc = find_doc(id)
+ Marshal.load doc.data
+ end
+
+ def thread_killed? thread_id
+ not run_query(Q.new(Q::OP_AND, mkterm(:thread, thread_id), mkterm(:label, :Killed)), 0, 1).empty?
end
def synchronize &b
def run_query_ids xapian_query, offset, limit
matchset = run_query xapian_query, offset, limit
- matchset.matches.map { |r| r.document.data }
+ matchset.matches.map { |r| r.document.value MSGID_VALUENO }
end
Q = Xapian::Query
end
end
- def index_message m, opts
+ def index_message m, entry, opts
terms = []
text = []
terms << mkterm(:date,m.date) if m.date
m.labels.each { |t| terms << mkterm(:label,t) }
terms << mkterm(:type, 'mail')
+ terms << mkterm(:msgid, m.id)
terms << mkterm(:source_id, m.source.id)
m.attachments.each do |a|
a =~ /\.(\w+)$/ or next
terms << t
end
+ ## Thread membership
+ children = term_docids(mkterm(:ref, m.id)).map { |docid| @xapian.document docid }
+ parent_ids = m.refs + m.replytos
+ parents = parent_ids.map { |id| find_doc id }.compact
+ thread_members = SavingHash.new { [] }
+ (children + parents).each do |doc2|
+ thread_ids = doc2.value(THREAD_VALUENO).split ','
+ thread_ids.each { |thread_id| thread_members[thread_id] << doc2 }
+ end
+
+ thread_ids = thread_members.empty? ? [m.id] : thread_members.keys
+
+ thread_ids.each { |thread_id| terms << mkterm(:thread, thread_id) }
+ parent_ids.each do |ref|
+ terms << mkterm(:ref, ref)
+ end
+
# Full text search content
text << [subject_text, PREFIX['subject']]
text << [subject_text, PREFIX['body']]
m.attachments.each { |a| text << [a, PREFIX['attachment']] }
truncated_date = if m.date < MIN_DATE
- Redwood::log "warning: adjusting too-low date #{m.date} for indexing"
+ debug "warning: adjusting too-low date #{m.date} for indexing"
MIN_DATE
elsif m.date > MAX_DATE
- Redwood::log "warning: adjusting too-high date #{m.date} for indexing"
+ debug "warning: adjusting too-high date #{m.date} for indexing"
MAX_DATE
else
m.date
Xapian.sortable_serialise 0
end
- doc = Xapian::Document.new
- docid = @docids[m.id] || assign_docid(m, truncated_date)
+ docid = nil
+ unless doc = find_doc(m.id)
+ doc = Xapian::Document.new
+ if not docid = assign_docid(m, truncated_date)
+ # Could be triggered by spam
+ Redwood::log "warning: docid underflow, dropping #{m.id.inspect}"
+ return
+ end
+ else
+ doc.clear_terms
+ doc.clear_values
+ docid = doc.docid
+ end
@term_generator.document = doc
text.each { |text,prefix| @term_generator.index_text text, 1, prefix }
terms.each { |term| doc.add_term term if term.length <= MAX_TERM_LENGTH }
+ doc.add_value MSGID_VALUENO, m.id
+ doc.add_value THREAD_VALUENO, (thread_ids * ',')
doc.add_value DATE_VALUENO, date_value
- doc.data = m.id
+ doc.data = Marshal.dump entry
@xapian.replace_document docid, doc
- @docids[m.id] = docid
end
# Construct a Xapian term
PREFIX['source_id'] + args[0].to_s.downcase
when :attachment_extension
PREFIX['attachment_extension'] + args[0].to_s.downcase
+ when :msgid, :ref, :thread
+ PREFIX[type.to_s] + args[0][0...(MAX_TERM_LENGTH-1)]
else
raise "Invalid term type #{type}"
end
end
- # Join all the given message-ids into a single thread
- def union_threads ids
- seen_threads = Set.new
- related = Set.new
-
- # Get all the ids that will be in the new thread
- ids.each do |id|
- related << id
- thread_id = @thread_ids[id]
- if thread_id && !seen_threads.member?(thread_id)
- thread_members = @thread_members[thread_id]
- related.merge thread_members
- seen_threads << thread_id
- end
- end
-
- # Pick a leader and move all the others to its thread
- a = related.to_a
- best, *rest = a.sort_by { |x| x.hash }
- @thread_members[best] = a
- @thread_ids[best] = best
- rest.each do |x|
- @thread_members.delete x
- @thread_ids[x] = best
- end
- end
end
end
-
-class MarshalledGDBM < GDBM
- def []= k, v
- super k, Marshal.dump(v)
- end
-
- def [] k
- v = super k
- v ? Marshal.load(v) : nil
- end
-end