sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit a7c254f315328a2160f2a9a105e29b06cb19d83b
parent be93a14806472849414fe38565cc1bdd31d03d6a
Author: Rich Lane <rlane@club.cc.cmu.edu>
Date:   Tue, 23 Mar 2010 21:48:18 -0700

remove mutable source state

Diffstat:
M bin/sup-add | 4 ++--
M bin/sup-sync | 71 ++---------------------------------------------------------------------
M lib/sup/maildir.rb | 25 ++++++-------------------
M lib/sup/mbox.rb | 91 ++++++++++++++++++++++++++++++++++++++++---------------------------------------
M lib/sup/poll.rb | 5 ++---
M lib/sup/sent.rb | 6 +++---
M lib/sup/source.rb | 38 ++++++--------------------------------
7 files changed, 67 insertions(+), 173 deletions(-)
diff --git a/bin/sup-add b/bin/sup-add
@@ -97,9 +97,9 @@ begin
     source = 
       case parsed_uri.scheme
       when "maildir"
-        Redwood::Maildir.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
+        Redwood::Maildir.new uri, !$opts[:unusual], $opts[:archive], nil, labels
       when "mbox"
-        Redwood::MBox::Loader.new uri, nil, !$opts[:unusual], $opts[:archive], nil, labels
+        Redwood::MBox::Loader.new uri, !$opts[:unusual], $opts[:archive], nil, labels
       when nil
         Trollop::die "Sources must be specified with an URI"
       else
diff --git a/bin/sup-sync b/bin/sup-sync
@@ -53,16 +53,6 @@ where <source>* is zero or more source URIs. If no sources are given,
 sync from all usual sources. Supported source URI schemes can be seen
 by running "sup-add --help".
 
-Options controlling WHICH messages sup-sync operates on:
-EOS
-  opt :new, "Operate on new messages only. Don't scan over the entire source. (Default.)", :short => :none
-  opt :changed, "Scan over the entire source for messages that have been deleted, altered, or moved from another source."
-  opt :restored, "Operate only on those messages included in a dump file as specified by --restore which have changed state."
-  opt :all, "Operate on all messages in the source, regardless of newness or changedness."
-  opt :start_at, "For --changed, --restored and --all, start at a particular offset.", :type => :int
-
-text <<EOS
-
 Options controlling HOW message state is altered:
 EOS
   opt :asis, "If the message is already in the index, preserve its state. Otherwise, use default source state. (Default.)", :short => :none
@@ -82,16 +72,9 @@ EOS
   opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
   opt :version, "Show version information", :short => :none
 
-  conflicts :changed, :all, :new, :restored
   conflicts :asis, :restore, :discard
 end
-Trollop::die :restored, "requires --restore" if opts[:restored] unless opts[:restore]
-if opts[:start_at]
-  Trollop::die :start_at, "must be non-negative" if opts[:start_at] < 0
-  Trollop::die :start_at, "requires either --changed, --restored or --all" unless opts[:changed] || opts[:restored] || opts[:all]
-end
 
-target = [:new, :changed, :all, :restored].find { |x| opts[x] } || :new
 op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis
 
 Redwood::start
@@ -126,18 +109,6 @@ begin
     end
   end
 
-  ## for all target specifications except for only-new messages, reset the
-  ## source to the beginning (or to the user-specified starting point.)
-  unless target == :new
-    if opts[:start_at]
-      Trollop::die :start_at, "can only be used on one source" unless sources.size == 1
-      sources.first.seek_to! opts[:start_at]
-      sources.first.correct_offset! if sources.first.respond_to?(:correct_offset!)
-    else
-      sources.each { |s| s.reset! }
-    end
-  end
-
   sources.each do |source|
     puts "Scanning #{source}..."
     num_added = num_updated = num_scanned = num_restored = 0
@@ -148,24 +119,6 @@ begin
       seen[m.id] = true
       old_m = index.build_message m.id
 
-      case target
-      when :changed
-        ## skip this message if we're operating only on changed messages, the
-        ## message is in the index, and it's unchanged from what the source is
-        ## reporting.
-        next if old_m && old_m.locations.member?([m.source, m.source_info])
-      when :restored
-        ## skip if we're operating on restored messages, and this one
-        ## ain't (or we wouldn't be making a change)
-        next unless old_m && restored_state[m.id] && restored_state[m.id] != old_m.labels
-      when :new
-        ## nothing to do; we'll consider all messages starting at the start offset, which
-        ## hasn't been changed.
-      when :all
-        ## nothing to do; we'll consider all messages starting at the start offset, which
-        ## was reset to the beginning above.
-      end
-
       ## tweak source labels according to commandline arguments if necessary
       m.labels.delete :inbox if opts[:archive]
       m.labels.delete :unread if opts[:read]
@@ -226,9 +179,9 @@ begin
       if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
         last_info_time = Time.now
         elapsed = last_info_time - start_time
-        pctdone = source.respond_to?(:pct_done) ? source.pct_done : 100.0 * (source.cur_offset.to_f - source.start_offset).to_f / (source.end_offset - source.start_offset).to_f
+        pctdone = source.pct_done
         remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
-        printf "## read %dm (~%.0f%%) @ %.1fm/s. %s elapsed, ~%s remaining, offset #{source.cur_offset}\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
+        printf "## read %dm (~%.0f%%) @ %.1fm/s. %s elapsed, ~%s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
       end
     end
 
@@ -236,26 +189,6 @@ begin
     puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
   end
 
-  ## delete any messages in the index that claim they're from one of
-  ## these sources, but that we didn't see.
-  if (target == :all || target == :changed)
-    puts "Deleting missing messages from the index..."
-    num_del, num_scanned = 0, 0
-    sources.each do |source|
-      raise "no source id for #{source}" unless source.id
-      index.each_message :source_id => source.id, :load_spam => true, :load_deleted => true, :load_killed => true do |m|
-        num_scanned += 1
-        unless seen[m.id]
-          next unless m.source_info >= opts[:start_at] if opts[:start_at]
-          puts "Deleting #{m.id}" if opts[:verbose]
-          index.delete m.id unless opts[:dry_run]
-          num_del += 1
-        end
-      end
-    end
-    puts "Deleted #{num_del} / #{num_scanned} messages"
-  end
-
   index.save
 
   if opts[:optimize]
diff --git a/lib/sup/maildir.rb b/lib/sup/maildir.rb
@@ -14,9 +14,9 @@ class Maildir < Source
   MYHOSTNAME = Socket.gethostname
 
   ## remind me never to use inheritance again.
-  yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels, :mtimes
-  def initialize uri, last_date=nil, usual=true, archived=false, id=nil, labels=[], mtimes={}
-    super uri, last_date, usual, archived, id
+  yaml_properties :uri, :usual, :archived, :id, :labels
+  def initialize uri, usual=true, archived=false, id=nil, labels=[]
+    super uri, usual, archived, id
     uri = URI(Source.expand_filesystem_uri(uri))
 
     raise ArgumentError, "not a maildir URI" unless uri.scheme == "maildir"
@@ -32,7 +32,7 @@ class Maildir < Source
     #the mtime from the subdirs in the maildir with the unix epoch as default.
     #these are used to determine whether scanning the directory for new mail
     #is a worthwhile effort
-    @mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }.merge(mtimes || {})
+    @mtimes = { 'cur' => Time.at(0), 'new' => Time.at(0) }
     @dir_ids = { 'cur' => [], 'new' => [] }
   end
 
@@ -40,13 +40,6 @@ class Maildir < Source
   def self.suggest_labels_for path; [] end
   def is_source_for? uri; super || (URI(Source.expand_filesystem_uri(uri)) == URI(self.uri)); end
 
-  def check
-    scan_mailbox
-    return unless start_offset
-
-    start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
-  end
-
   def store_message date, from_email, &block
     stored = false
     new_fn = new_maildir_basefn + ':2,S'
@@ -150,13 +143,7 @@ class Maildir < Source
 
   def each
     scan_mailbox
-    return unless start_offset
-
-    start = @ids.index(cur_offset || start_offset) or raise OutOfSyncSourceError, "Unknown message id #{cur_offset || start_offset}." # couldn't find the most recent email
-
-    start.upto(@ids.length - 1) do |i|
-      id = @ids[i]
-      self.cur_offset = id
+    @ids.each do |id|
       yield id, @labels + (seen?(id) ? [] : [:unread]) + (trashed?(id) ? [:deleted] : []) + (flagged?(id) ? [:starred] : [])
     end
   end
@@ -171,7 +158,7 @@ class Maildir < Source
     @ids.last + 1
   end
 
-  def pct_done; 100.0 * (@ids.index(cur_offset) || 0).to_f / (@ids.length - 1).to_f; end
+  def pct_done; 100.0 * (0).to_f / (@ids.length - 1).to_f; end
 
   def draft? msg; maildir_data(msg)[2].include? "D"; end
   def flagged? msg; maildir_data(msg)[2].include? "F"; end
diff --git a/lib/sup/mbox.rb b/lib/sup/mbox.rb
@@ -8,12 +8,12 @@ class MBox < Source
   BREAK_RE = /^From \S+ (.+)$/
 
   include SerializeLabelsNicely
-  yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
+  yaml_properties :uri, :usual, :archived, :id, :labels
 
   attr_reader :labels
 
   ## uri_or_fp is horrific. need to refactor.
-  def initialize uri_or_fp, start_offset=nil, usual=true, archived=false, id=nil, labels=nil
+  def initialize uri_or_fp, usual=true, archived=false, id=nil, labels=nil
     @mutex = Mutex.new
     @labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
 
@@ -30,8 +30,8 @@ class MBox < Source
       @path = uri_or_fp.path
     end
 
-    start_offset ||= 0
-    super uri_or_fp, start_offset, usual, archived, id
+    @offset = 0
+    super uri_or_fp, usual, archived, id
   end
 
   def file_path; @path end
@@ -47,15 +47,6 @@ class MBox < Source
     end
   end
 
-  def check
-    if (cur_offset ||= start_offset) > end_offset
-      raise OutOfSyncSourceError, "mbox file is smaller than last recorded message offset. Messages have probably been deleted by another client."
-    end
-  end
-
-  def start_offset; 0; end
-  def end_offset; File.size @f; end
-
   def load_header offset
     header = nil
     @mutex.synchronize do
@@ -88,12 +79,12 @@ class MBox < Source
   ## scan forward until we're at the valid start of a message
   def correct_offset!
     @mutex.synchronize do
-      @f.seek cur_offset
+      @f.seek @offset
       string = ""
       until @f.eof? || MBox::is_break_line?(l = @f.gets)
         string << l
       end
-      self.cur_offset += string.length
+      @offset += string.length
     end
   end
 
@@ -139,41 +130,51 @@ class MBox < Source
     end
   end
 
-  def next
-    returned_offset = nil
-    next_offset = cur_offset
+  def pct_done
+    (@offset.to_f / File.size(@f)) * 100
+  end
 
-    begin
-      @mutex.synchronize do
-        @f.seek cur_offset
-
-        ## cur_offset could be at one of two places here:
-
-        ## 1. before a \n and a mbox separator, if it was previously at
-        ##    EOF and a new message was added; or,
-        ## 2. at the beginning of an mbox separator (in all other
-        ##    cases).
-
-        l = @f.gets or return nil
-        if l =~ /^\s*$/ # case 1
-          returned_offset = @f.tell
-          @f.gets # now we're at a BREAK_RE, so skip past it
-        else # case 2
-          returned_offset = cur_offset
-          ## we've already skipped past the BREAK_RE, so just go
-        end
+  def each
+    end_offset = File.size @f
+    while @offset < end_offset
+      returned_offset = nil
+      next_offset = @offset
 
-        while(line = @f.gets)
-          break if MBox::is_break_line? line
-          next_offset = @f.tell
+      begin
+        @mutex.synchronize do
+          @f.seek @offset
+
+          ## @offset could be at one of two places here:
+
+          ## 1. before a \n and a mbox separator, if it was previously at
+          ##    EOF and a new message was added; or,
+          ## 2. at the beginning of an mbox separator (in all other
+          ##    cases).
+
+          l = @f.gets or break
+          if l =~ /^\s*$/ # case 1
+            returned_offset = @f.tell
+            @f.gets # now we're at a BREAK_RE, so skip past it
+          else # case 2
+            returned_offset = @offset
+            ## we've already skipped past the BREAK_RE, so just go
+          end
+
+          while(line = @f.gets)
+            break if MBox::is_break_line? line
+            next_offset = @f.tell
+          end
         end
+      rescue SystemCallError, IOError => e
+        raise FatalSourceError, "Error reading #{@f.path}: #{e.message}"
       end
-    rescue SystemCallError, IOError => e
-      raise FatalSourceError, "Error reading #{@f.path}: #{e.message}"
+
+      @offset = next_offset
+      yield returned_offset, (labels + [:unread])
     end
+  end
 
-    self.cur_offset = next_offset
-    [returned_offset, (labels + [:unread])]
+  def next
   end
 
   def self.is_break_line? l
@@ -190,7 +191,7 @@ class MBox < Source
   end
 
   class Loader < self
-    yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
+    yaml_properties :uri, :usual, :archived, :id, :labels
   end
 end
 end
diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb
@@ -102,9 +102,8 @@ EOS
 
     @mutex.synchronize do
       @poll_sources.each do |source|
-#        yield "source #{source} is done? #{source.done?} (cur_offset #{source.cur_offset} >= #{source.end_offset})"
         begin
-          yield "Loading from #{source}... " unless source.done? || (source.respond_to?(:has_errors?) && source.has_errors?)
+          yield "Loading from #{source}... " unless source.has_errors?
         rescue SourceError => e
           warn "problem getting messages from #{source}: #{e.message}"
           Redwood::report_broken_sources :force_to_top => true
@@ -160,7 +159,7 @@ EOS
   ## 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?
+      return if source.has_errors?
 
       source.each do |offset, source_labels|
         if source.has_errors?
diff --git a/lib/sup/sent.rb b/lib/sup/sent.rb
@@ -36,12 +36,12 @@ class SentManager
 end
 
 class SentLoader < MBox::Loader
-  yaml_properties :cur_offset
+  yaml_properties
 
-  def initialize cur_offset=0
+  def initialize
     @filename = Redwood::SENT_FN
     File.open(@filename, "w") { } unless File.exists? @filename
-    super "mbox://" + @filename, cur_offset, true, true
+    super "mbox://" + @filename, true, true
   end
 
   def file_path; @filename end
diff --git a/lib/sup/source.rb b/lib/sup/source.rb
@@ -59,50 +59,29 @@ class Source
   ## Examples for you to look at: mbox/loader.rb, imap.rb, and
   ## maildir.rb.
 
-  ## let's begin!
-  ##
-  ## dirty? means cur_offset has changed, so the source info needs to
-  ## be re-saved to sources.yaml.
-  bool_reader :usual, :archived, :dirty
-  attr_reader :uri, :cur_offset
+  bool_reader :usual, :archived
+  attr_reader :uri
   attr_accessor :id
 
-  def initialize uri, initial_offset=nil, usual=true, archived=false, id=nil
+  def initialize uri, usual=true, archived=false, id=nil
     raise ArgumentError, "id must be an integer: #{id.inspect}" unless id.is_a? Fixnum if id
 
     @uri = uri
-    @cur_offset = initial_offset
     @usual = usual
     @archived = archived
     @id = id
-    @dirty = false
   end
 
   ## overwrite me if you have a disk incarnation (currently used only for sup-sync-back)
   def file_path; nil end
 
   def to_s; @uri.to_s; end
-  def seek_to! o; self.cur_offset = o; end
-  def reset!; seek_to! start_offset; end
   def == o; o.uri == uri; end
-  def done?; start_offset.nil? || (self.cur_offset ||= start_offset) >= end_offset; end
   def is_source_for? uri; uri == @uri; end
 
-  ## check should throw a FatalSourceError or an OutOfSyncSourcError
-  ## if it can detect a problem. it is called when the sup starts up
-  ## to proactively notify the user of any source problems.
-  def check; end
-
-  ## 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.
+  ## yields successive offsets and labels
   def each
-    self.cur_offset ||= start_offset
-    until done?
-      offset, labels = self.next
-      yield offset, labels
-    end
+    unimplemented
   end
 
   ## utility method to read a raw email header from an IO stream and turn it
@@ -154,11 +133,6 @@ protected
   def Source.expand_filesystem_uri uri
     uri.gsub "~", File.expand_path("~")
   end
-
-  def cur_offset= o
-    @cur_offset = o
-    @dirty = true
-  end
 end
 
 ## if you have a @labels instance variable, include this
@@ -219,7 +193,7 @@ class SourceManager
 
   def save_sources fn=Redwood::SOURCE_FN
     @source_mutex.synchronize do
-      if @sources_dirty || @sources.any? { |id, s| s.dirty? }
+      if @sources_dirty
         bakfn = fn + ".bak"
         if File.exists? fn
           File.chmod 0600, fn