From: William Morgan Date: Tue, 25 Aug 2009 13:53:04 +0000 (-0400) Subject: Merge branch 'buffer-rolling' X-Git-Url: https://git.cworth.org/git?a=commitdiff_plain;h=c7aa3fcad08082fa8d897349eb8d9b98735e0445;hp=a8e3572ee5ed91d205c2de0dd98cf8e610d04493;p=sup Merge branch 'buffer-rolling' --- diff --git a/Manifest.txt b/Manifest.txt index be633d7..09d867e 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -32,6 +32,7 @@ lib/sup/index.rb lib/sup/keymap.rb lib/sup/label.rb lib/sup/logger.rb +lib/sup/interactive-lock.rb lib/sup/maildir.rb lib/sup/mbox.rb lib/sup/mbox/loader.rb @@ -67,7 +68,6 @@ 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 diff --git a/bin/sup b/bin/sup index 1febefd..bbb6c17 100755 --- a/bin/sup +++ b/bin/sup @@ -78,6 +78,7 @@ global_keymap = Keymap.new do |k| k.add :compose, "Compose new message", 'm', 'c' k.add :nothing, "Do nothing", :ctrl_g k.add :recall_draft, "Edit most recent draft message", 'R' + k.add :show_inbox, "Show the Inbox buffer", 'I' end ## the following magic enables wide characters when used with a ruby @@ -95,17 +96,17 @@ module LibC 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 @@ -129,41 +130,21 @@ def stop_cursing 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 < 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] @@ -209,7 +196,6 @@ begin unless $opts[:no_threads] PollManager.start - SuicideManager.start Index.start_lock_update_thread end @@ -217,30 +203,40 @@ begin 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 @@ -296,6 +292,8 @@ begin b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] } b.mode.load_threads :num => b.content_height if new end + when :show_inbox + BufferManager.raise_to_front ibuf when :nothing, InputSequenceAborted when :redraw bm.completely_redraw_screen @@ -306,13 +304,12 @@ begin 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 @@ -320,17 +317,19 @@ ensure 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 diff --git a/bin/sup-add b/bin/sup-add index 3ab7c4d..e27a0eb 100755 --- a/bin/sup-add +++ b/bin/sup-add @@ -77,9 +77,9 @@ end $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 diff --git a/bin/sup-config b/bin/sup-config index 9fcbee6..bd55fc1 100755 --- a/bin/sup-config +++ b/bin/sup-config @@ -151,7 +151,7 @@ end $terminal.wrap_at = :auto Redwood::start -index = Redwood::Index.new +index = Redwood::Index.init Redwood::SourceManager.load_sources say < true, :load_deleted => true, :load_killed => true do |m| - puts "#{m.id} (#{m.labels * ' '})" + puts "#{m.id} (#{m.labels.to_a * ' '})" end diff --git a/bin/sup-recover-sources b/bin/sup-recover-sources index db75b11..43fa5f6 100755 --- a/bin/sup-recover-sources +++ b/bin/sup-recover-sources @@ -50,7 +50,7 @@ end.parse(ARGV) require "sup" Redwood::start puts "loading index..." -index = Redwood::Index.new +index = Redwood::Index.init index.load puts "loaded index of #{index.size} messages" diff --git a/bin/sup-sync b/bin/sup-sync index 44ff3b2..2aa00c3 100755 --- a/bin/sup-sync +++ b/bin/sup-sync @@ -9,9 +9,7 @@ PROGRESS_UPDATE_INTERVAL = 15 # seconds 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 @@ -21,6 +19,10 @@ class Numeric end end +class Set + def to_s; to_a * ',' end +end + def time startt = Time.now yield @@ -54,7 +56,7 @@ by running "sup-add --help". Options controlling WHICH messages sup-sync operates on: EOS opt :new, "Operate on new messages only. Don't scan over the entire source. (Default.)", :short => :none - opt :changed, "Scan over the entire source for messages that have been deleted, altered, or moved from another source. (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 @@ -68,7 +70,7 @@ EOS 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 < 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]) + [:update_message_state, restored_state[m.id]] + 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 @@ -191,17 +219,8 @@ begin 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 diff --git a/bin/sup-sync-back b/bin/sup-sync-back index 56ac4eb..6298c97 100755 --- a/bin/sup-sync-back +++ b/bin/sup-sync-back @@ -65,8 +65,8 @@ EOS 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] @@ -108,30 +108,30 @@ EOS 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 @@ -141,8 +141,6 @@ EOS 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 diff --git a/bin/sup-tweak-labels b/bin/sup-tweak-labels index 8ae5c26..90f6a57 100755 --- a/bin/sup-tweak-labels +++ b/bin/sup-tweak-labels @@ -38,8 +38,8 @@ be seen by running "sup-add --help". 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 < {#{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 diff --git a/lib/sup.rb b/lib/sup.rb index 54de73f..16c2b3f 100644 --- a/lib/sup.rb +++ b/lib/sup.rb @@ -24,7 +24,7 @@ class Module 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}" } @@ -85,39 +85,50 @@ module Redwood 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 } + else + o.respond_to?(:before_marshal) && o.before_marshal + 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 @@ -230,33 +241,29 @@ require "sup/hook" ## 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" @@ -266,6 +273,7 @@ require "sup/imap" require "sup/person" require "sup/account" require "sup/thread" +require "sup/interactive-lock" require "sup/index" require "sup/textfield" require "sup/colormap" diff --git a/lib/sup/account.rb b/lib/sup/account.rb index 6f86129..eed2794 100644 --- a/lib/sup/account.rb +++ b/lib/sup/account.rb @@ -25,8 +25,6 @@ class AccountManager 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 diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb index 77a0e1e..d85090a 100644 --- a/lib/sup/buffer.rb +++ b/lib/sup/buffer.rb @@ -25,13 +25,13 @@ module Ncurses 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 @@ -70,7 +70,7 @@ class Buffer 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 @@ -196,10 +196,13 @@ EOS @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 @@ -267,6 +270,14 @@ EOS 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 @@ -474,7 +485,7 @@ EOS end if answer - answer = + answer = if answer.empty? spawn_modal "file browser", FileBrowserMode.new elsif File.directory?(answer) @@ -490,7 +501,7 @@ EOS ## 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 @@ -501,7 +512,7 @@ EOS 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!" @@ -514,7 +525,7 @@ EOS 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] } diff --git a/lib/sup/colormap.rb b/lib/sup/colormap.rb index 38787cd..fbbbfc9 100644 --- a/lib/sup/colormap.rb +++ b/lib/sup/colormap.rb @@ -129,7 +129,7 @@ class Colormap @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})" @@ -137,7 +137,7 @@ class Colormap ## 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] = [] @@ -155,7 +155,7 @@ class Colormap ## 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 @@ -171,7 +171,7 @@ class Colormap 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 @@ -180,7 +180,7 @@ class Colormap 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 @@ -190,7 +190,7 @@ class Colormap 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 diff --git a/lib/sup/contact.rb b/lib/sup/contact.rb index 51fd0e9..c489aaf 100644 --- a/lib/sup/contact.rb +++ b/lib/sup/contact.rb @@ -22,8 +22,6 @@ class ContactManager @a2p[aalias] = p unless aalias.nil? || aalias.empty? end end - - self.class.i_am_the_instance self end def contacts; @p2a.keys end diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb index 8ec277b..7f044b9 100644 --- a/lib/sup/crypto.rb +++ b/lib/sup/crypto.rb @@ -13,17 +13,15 @@ class CryptoManager 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 @@ -116,21 +114,19 @@ class CryptoManager 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] @@ -145,7 +141,7 @@ private 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 @@ -158,9 +154,7 @@ private 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 diff --git a/lib/sup/draft.rb b/lib/sup/draft.rb index dd4574d..5ea2935 100644 --- a/lib/sup/draft.rb +++ b/lib/sup/draft.rb @@ -7,7 +7,6 @@ class DraftManager def initialize dir @dir = dir @source = nil - self.class.i_am_the_instance self end def self.source_name; "sup://drafts"; end @@ -20,12 +19,9 @@ class DraftManager 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 diff --git a/lib/sup/ferret_index.rb b/lib/sup/ferret_index.rb index f3d4147..df1139d 100644 --- a/lib/sup/ferret_index.rb +++ b/lib/sup/ferret_index.rb @@ -18,13 +18,13 @@ class FerretIndex < BaseIndex 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 @@ -45,6 +45,10 @@ class FerretIndex < BaseIndex 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] @@ -79,16 +83,16 @@ class FerretIndex < BaseIndex ## 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 @@ -103,7 +107,7 @@ class FerretIndex < BaseIndex :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. @@ -125,6 +129,7 @@ class FerretIndex < BaseIndex @index.add_document d end end + private :sync_message def save_index fn=File.join(@dir, "ferret") # don't have to do anything, apparently @@ -146,7 +151,7 @@ class FerretIndex < BaseIndex 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 @@ -165,7 +170,7 @@ class FerretIndex < BaseIndex 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 @@ -186,10 +191,10 @@ class FerretIndex < BaseIndex 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 @@ -220,7 +225,7 @@ class FerretIndex < BaseIndex 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] } @@ -230,10 +235,10 @@ class FerretIndex < BaseIndex 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 @@ -259,7 +264,7 @@ class FerretIndex < BaseIndex } 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 @@ -280,13 +285,13 @@ class FerretIndex < BaseIndex 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] @@ -376,10 +381,10 @@ class FerretIndex < BaseIndex 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 @@ -391,13 +396,13 @@ class FerretIndex < BaseIndex 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 diff --git a/lib/sup/hook.rb b/lib/sup/hook.rb index 0a0a2f6..0c41162 100644 --- a/lib/sup/hook.rb +++ b/lib/sup/hook.rb @@ -40,7 +40,7 @@ class HookManager end def log s - Redwood::log "hook[#@__name]: #{s}" + info "hook[#@__name]: #{s}" end def ask_yes_or_no q @@ -79,8 +79,6 @@ class HookManager @tags = {} Dir.mkdir dir unless File.exists? dir - - self.class.i_am_the_instance self end attr_reader :tags @@ -148,7 +146,7 @@ private end def log m - Redwood::log("hook: " + m) + info("hook: " + m) end end diff --git a/lib/sup/imap.rb b/lib/sup/imap.rb index 6c04d88..bdb9e15 100644 --- a/lib/sup/imap.rb +++ b/lib/sup/imap.rb @@ -4,6 +4,7 @@ require 'stringio' require 'time' require 'rmail' require 'cgi' +require 'set' ## TODO: remove synchronized method protector calls; use a Monitor instead ## (ruby's reentrant mutex) @@ -47,6 +48,7 @@ require 'cgi' module Redwood class IMAP < Source + include SerializeLabelsNicely SCAN_INTERVAL = 60 # seconds ## upon these errors we'll try to rereconnect a few times @@ -69,7 +71,7 @@ class IMAP < Source @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 @@ -159,13 +161,13 @@ class IMAP < Source 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 @@ -224,7 +226,7 @@ private 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 @@ -252,12 +254,12 @@ private 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 @@ -274,7 +276,7 @@ private def say s @say_id = BufferManager.say s, @say_id if BufferManager.instantiated? - Redwood::log s + info s end def shutup @@ -331,7 +333,7 @@ private 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 diff --git a/lib/sup/index.rb b/lib/sup/index.rb index fb46eb0..ff03f19 100644 --- a/lib/sup/index.rb +++ b/lib/sup/index.rb @@ -6,13 +6,15 @@ begin 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 @@ -26,13 +28,12 @@ class BaseIndex 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 @@ -54,45 +55,9 @@ class BaseIndex @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 - - < e - $stderr.puts fancy_lock_error_message_for(e) - $stderr.puts < e + stream.puts < 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 diff --git a/lib/sup/label.rb b/lib/sup/label.rb index 47d632b..67474c2 100644 --- a/lib/sup/label.rb +++ b/lib/sup/label.rb @@ -22,8 +22,6 @@ class LabelManager @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 @@ -61,9 +59,9 @@ class LabelManager 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 diff --git a/lib/sup/logger.rb b/lib/sup/logger.rb index 4ac6551..ccaeae0 100644 --- a/lib/sup/logger.rb +++ b/lib/sup/logger.rb @@ -1,54 +1,73 @@ +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 diff --git a/lib/sup/maildir.rb b/lib/sup/maildir.rb index a2dbae4..2c33e3b 100644 --- a/lib/sup/maildir.rb +++ b/lib/sup/maildir.rb @@ -9,6 +9,7 @@ module Redwood ## pathnames on disk. class Maildir < Source + include SerializeLabelsNicely SCAN_INTERVAL = 30 # seconds MYHOSTNAME = Socket.gethostname @@ -23,7 +24,7 @@ class Maildir < Source 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 @@ -115,7 +116,7 @@ class Maildir < Source initial_poll = @ids.empty? - Redwood::log "scanning maildir #@dir..." + debug "scanning maildir #@dir..." begin @mtimes.each_key do |d| subdir = File.join(@dir, d) @@ -134,7 +135,7 @@ class Maildir < Source @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! @@ -142,7 +143,7 @@ class Maildir < Source 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 diff --git a/lib/sup/mbox.rb b/lib/sup/mbox.rb index e1e3a4d..3f3abad 100644 --- a/lib/sup/mbox.rb +++ b/lib/sup/mbox.rb @@ -15,7 +15,7 @@ module MBox 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 diff --git a/lib/sup/mbox/loader.rb b/lib/sup/mbox/loader.rb index ea277cf..0307594 100644 --- a/lib/sup/mbox/loader.rb +++ b/lib/sup/mbox/loader.rb @@ -1,17 +1,18 @@ 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 ## 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 @@ -47,7 +48,7 @@ class Loader < Source 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 @@ -168,7 +169,7 @@ class Loader < Source end self.cur_offset = next_offset - [returned_offset, (self.labels + [:unread]).uniq] + [returned_offset, (@labels + [:unread])] end end diff --git a/lib/sup/mbox/ssh-file.rb b/lib/sup/mbox/ssh-file.rb index d474636..4ae4bba 100644 --- a/lib/sup/mbox/ssh-file.rb +++ b/lib/sup/mbox/ssh-file.rb @@ -16,12 +16,6 @@ class SSHFileError < StandardError; 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 @@ -154,7 +148,7 @@ private ## 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 diff --git a/lib/sup/message-chunks.rb b/lib/sup/message-chunks.rb index 0d742d9..1eda174 100644 --- a/lib/sup/message-chunks.rb +++ b/lib/sup/message-chunks.rb @@ -132,7 +132,7 @@ EOS 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}" + debug "running: #{cmd.inspect}" system cmd $? == 0 end diff --git a/lib/sup/message.rb b/lib/sup/message.rb index 7c63746..ed27d3d 100644 --- a/lib/sup/message.rb +++ b/lib/sup/message.rb @@ -46,7 +46,7 @@ class Message @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 @@ -73,7 +73,7 @@ class Message 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 @@ -81,7 +81,7 @@ class Message header["from"] else name = "Sup Auto-generated Fake Sender " - #Redwood::log "faking non-existent sender for message #@id: #{name}" + #debug "faking non-existent sender for message #@id: #{name}" name end) @@ -92,11 +92,11 @@ class Message 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 @@ -157,22 +157,22 @@ class Message ## 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 + 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 + return unless @labels.member? l + @labels.delete l @dirty = true end @@ -181,7 +181,9 @@ class Message end def labels= l - @labels = l.to_set_of_symbols + raise ArgumentError, "not a set" unless l.is_a?(Set) + return if @labels == l + @labels = l @dirty = true end @@ -208,7 +210,7 @@ class Message 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 @@ -242,7 +244,7 @@ EOS 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 @@ -333,25 +335,25 @@ private 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 @@ -360,23 +362,23 @@ private 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 diff --git a/lib/sup/mode.rb b/lib/sup/mode.rb index 6433492..209ca45 100644 --- a/lib/sup/mode.rb +++ b/lib/sup/mode.rb @@ -92,10 +92,10 @@ EOS 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 diff --git a/lib/sup/modes/edit-message-mode.rb b/lib/sup/modes/edit-message-mode.rb index a48930a..8da316f 100644 --- a/lib/sup/modes/edit-message-mode.rb +++ b/lib/sup/modes/edit-message-mode.rb @@ -325,7 +325,7 @@ protected 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 diff --git a/lib/sup/modes/inbox-mode.rb b/lib/sup/modes/inbox-mode.rb index d8daeb9..ba095da 100644 --- a/lib/sup/modes/inbox-mode.rb +++ b/lib/sup/modes/inbox-mode.rb @@ -1,4 +1,4 @@ -require 'thread' +require 'sup' module Redwood @@ -15,9 +15,7 @@ class InboxMode < ThreadIndexMode @@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 diff --git a/lib/sup/modes/label-list-mode.rb b/lib/sup/modes/label-list-mode.rb index 53287c1..f65ec2e 100644 --- a/lib/sup/modes/label-list-mode.rb +++ b/lib/sup/modes/label-list-mode.rb @@ -73,7 +73,7 @@ protected ## 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 diff --git a/lib/sup/modes/log-mode.rb b/lib/sup/modes/log-mode.rb index de16b5e..07fa9dd 100644 --- a/lib/sup/modes/log-mode.rb +++ b/lib/sup/modes/log-mode.rb @@ -1,36 +1,40 @@ +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 @@ -39,6 +43,12 @@ class LogMode < TextMode def status super + " (follow: #@follow)" end + + def cleanup + @on_kill.each { |cb| cb.call self } + self.text = "" + super + end end end diff --git a/lib/sup/modes/poll-mode.rb b/lib/sup/modes/poll-mode.rb index 5849f3e..cf61343 100644 --- a/lib/sup/modes/poll-mode.rb +++ b/lib/sup/modes/poll-mode.rb @@ -3,18 +3,16 @@ module Redwood 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 diff --git a/lib/sup/modes/reply-mode.rb b/lib/sup/modes/reply-mode.rb index c79c5db..700dfc1 100644 --- a/lib/sup/modes/reply-mode.rb +++ b/lib/sup/modes/reply-mode.rb @@ -56,7 +56,7 @@ EOS ## 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 diff --git a/lib/sup/modes/scroll-mode.rb b/lib/sup/modes/scroll-mode.rb index 63b48ec..c131425 100644 --- a/lib/sup/modes/scroll-mode.rb +++ b/lib/sup/modes/scroll-mode.rb @@ -3,7 +3,7 @@ module Redwood 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 @@ -223,7 +223,7 @@ protected 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] diff --git a/lib/sup/modes/text-mode.rb b/lib/sup/modes/text-mode.rb index 273c028..7c9e7d8 100644 --- a/lib/sup/modes/text-mode.rb +++ b/lib/sup/modes/text-mode.rb @@ -14,7 +14,7 @@ class TextMode < ScrollMode 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 @@ -50,7 +50,7 @@ class TextMode < ScrollMode @lines << @text.length if buffer ensure_mode_validity - buffer.mark_dirty + buffer.mark_dirty end end diff --git a/lib/sup/modes/thread-index-mode.rb b/lib/sup/modes/thread-index-mode.rb index b671119..fb6b2ce 100644 --- a/lib/sup/modes/thread-index-mode.rb +++ b/lib/sup/modes/thread-index-mode.rb @@ -477,7 +477,7 @@ EOS 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 @@ -533,9 +533,9 @@ EOS 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 diff --git a/lib/sup/modes/thread-view-mode.rb b/lib/sup/modes/thread-view-mode.rb index 737f6f1..dfe30ff 100644 --- a/lib/sup/modes/thread-view-mode.rb +++ b/lib/sup/modes/thread-view-mode.rb @@ -212,14 +212,14 @@ EOS 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 @@ -253,7 +253,7 @@ EOS 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 diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb index 8a9d218..46fe5c5 100644 --- a/lib/sup/poll.rb +++ b/lib/sup/poll.rb @@ -35,22 +35,17 @@ EOS @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 @@ -88,23 +83,36 @@ EOS 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 @@ -121,47 +129,43 @@ EOS [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 diff --git a/lib/sup/sent.rb b/lib/sup/sent.rb index b750d71..9203dd6 100644 --- a/lib/sup/sent.rb +++ b/lib/sup/sent.rb @@ -8,8 +8,6 @@ class SentManager 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 @@ -22,7 +20,6 @@ class SentManager def default_source @source = Recoverable.new SentLoader.new - Redwood::log "SentManager initializing default source: #@source." @source_uri = @source.uri @source end @@ -30,9 +27,9 @@ class SentManager 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 + PollManager.add_new_message m end end end @@ -52,7 +49,7 @@ class SentLoader < MBox::Loader def uri; 'sup://sent' end def id; 9998; end - def labels; [:inbox]; end + def labels; [:inbox, :sent]; end end end diff --git a/lib/sup/source.rb b/lib/sup/source.rb index 1bb7797..78386ff 100644 --- a/lib/sup/source.rb +++ b/lib/sup/source.rb @@ -34,12 +34,12 @@ class Source ## 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. ## @@ -78,6 +78,7 @@ class Source @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 @@ -92,20 +93,23 @@ class Source ## 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 @@ -116,9 +120,11 @@ class Source 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? @@ -133,7 +139,7 @@ class Source 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 @@ -144,7 +150,7 @@ protected ## convenience function def parse_raw_email_header f; self.class.parse_raw_email_header f end - + def Source.expand_filesystem_uri uri uri.gsub "~", File.expand_path("~") end @@ -155,6 +161,21 @@ protected 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 @@ -162,7 +183,6 @@ class SourceManager @sources = {} @sources_dirty = false @source_mutex = Monitor.new - self.class.i_am_the_instance self end def [](id) @@ -204,7 +224,7 @@ class SourceManager 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 diff --git a/lib/sup/suicide.rb b/lib/sup/suicide.rb deleted file mode 100644 index 98b4346..0000000 --- a/lib/sup/suicide.rb +++ /dev/null @@ -1,36 +0,0 @@ -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 diff --git a/lib/sup/textfield.rb b/lib/sup/textfield.rb index c748c7a..76803bf 100644 --- a/lib/sup/textfield.rb +++ b/lib/sup/textfield.rb @@ -35,9 +35,9 @@ class TextField @completion_block = block @field = Ncurses::Form.new_field 1, @width - question.length, @y, @x + question.length, 256, 0 @form = Ncurses::Form.new_form [@field] - @value = default + @value = default || '' Ncurses::Form.post_form @form - set_cursed_value default if default + set_cursed_value @value end def position_cursor @@ -112,11 +112,11 @@ class TextField 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 diff --git a/lib/sup/thread.rb b/lib/sup/thread.rb index d395c35..2300305 100644 --- a/lib/sup/thread.rb +++ b/lib/sup/thread.rb @@ -24,6 +24,8 @@ ## a faked root object tying them all together into one tree ## structure. +require 'set' + module Redwood class Thread @@ -101,17 +103,16 @@ 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 @@ -123,15 +124,14 @@ class Thread 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 @@ -162,7 +162,7 @@ class Container @id = id @message, @parent, @thread = nil, nil, nil @children = [] - end + end def each_with_stuff parent=nil yield self, 0, parent @@ -357,7 +357,7 @@ class ThreadSet return if threads.size < 2 containers = threads.map do |t| - c = @messages.member?(c) ? @messages[t.first.id] : nil + c = @messages.member?(t.first.id) ? @messages[t.first.id] : nil raise "not in threadset: #{t.first.id}" unless c && c.message c end diff --git a/lib/sup/undo.rb b/lib/sup/undo.rb index 5a93c31..9ccf84a 100644 --- a/lib/sup/undo.rb +++ b/lib/sup/undo.rb @@ -12,7 +12,6 @@ class UndoManager def initialize @@actionlist = [] - self.class.i_am_the_instance self end def register desc, *actions, &b diff --git a/lib/sup/update.rb b/lib/sup/update.rb index 021b7e1..d386801 100644 --- a/lib/sup/update.rb +++ b/lib/sup/update.rb @@ -16,7 +16,6 @@ class UpdateManager def initialize @targets = {} - self.class.i_am_the_instance self end def register o; @targets[o] = true; end diff --git a/lib/sup/util.rb b/lib/sup/util.rb index 3f2c901..068ce6b 100644 --- a/lib/sup/util.rb +++ b/lib/sup/util.rb @@ -2,6 +2,7 @@ require 'thread' require 'lockfile' require 'mime/types' require 'pathname' +require 'set' ## time for some monkeypatching! class Lockfile @@ -24,6 +25,7 @@ class Lockfile def lockinfo_on_disk h = load_lock_id IO.read(path) h['mtime'] = File.mtime path + h['path'] = path h end @@ -90,7 +92,7 @@ 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 @@ -215,8 +217,8 @@ class String newpos = case state when :escaped_instring, :escaped_outstring then pos else index(/[,"\\]/, pos) - end - + end + if newpos char = self[newpos] else @@ -288,10 +290,12 @@ class String 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 @@ -419,10 +423,6 @@ class Array 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 @@ -496,19 +496,20 @@ 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 @@ -518,13 +519,14 @@ module Singleton @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 @@ -543,7 +545,7 @@ class Recoverable 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 @@ -652,7 +654,7 @@ class Iconv 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 diff --git a/lib/sup/xapian_index.rb b/lib/sup/xapian_index.rb index 861c2a3..dbf6643 100644 --- a/lib/sup/xapian_index.rb +++ b/lib/sup/xapian_index.rb @@ -87,6 +87,10 @@ class XapianIndex < BaseIndex 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] } snippet = m.snippet @@ -100,7 +104,7 @@ class XapianIndex < BaseIndex :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] }), @@ -110,7 +114,7 @@ class XapianIndex < BaseIndex :replytos => (entry[:replytos] || m.replytos), } - m.labels.each { |l| LabelManager << l } + labels.each { |l| LabelManager << l } synchronize do index_message m, opts @@ -119,6 +123,7 @@ class XapianIndex < BaseIndex end true end + private :sync_message def num_results_for query={} xapian_query = build_xapian_query query @@ -216,10 +221,10 @@ class XapianIndex < BaseIndex 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 @@ -233,13 +238,13 @@ class XapianIndex < BaseIndex 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 @@ -408,10 +413,10 @@ class XapianIndex < BaseIndex 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