sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/

bin/sup-sync (7076B) - raw

      1 #!/usr/bin/env ruby
      2 
      3 $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
      4 
      5 require 'uri'
      6 require 'optimist'
      7 require "sup"
      8 
      9 PROGRESS_UPDATE_INTERVAL = 15 # seconds
     10 
     11 class Float
     12   def to_s; sprintf '%.2f', self; end
     13   def to_time_s; infinite? ? "unknown" : super end
     14 end
     15 
     16 class Numeric
     17   def to_time_s
     18     i = to_i
     19     sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60
     20   end
     21 end
     22 
     23 class Set
     24   def to_s; to_a * ',' end
     25 end
     26 
     27 def time
     28   startt = Time.now
     29   yield
     30   Time.now - startt
     31 end
     32 
     33 opts = Optimist::options do
     34   version "sup-sync (sup #{Redwood::VERSION})"
     35   banner <<EOS
     36 Synchronizes the Sup index with one or more message sources by adding
     37 messages, deleting messages, or changing message state in the index as
     38 appropriate.
     39 
     40 "Message state" means read/unread, archived/inbox, starred/unstarred,
     41 and all user-defined labels on each message.
     42 
     43 "Default source state" refers to any state that a source itself has
     44 keeps about a message. Sup-sync uses this information when adding a
     45 new message to the index. The source state is typically limited to
     46 read/unread, archived/inbox status and a single label based on the
     47 source name. Messages using the default source state are placed in
     48 the inbox (i.e. not archived) and unstarred.
     49 
     50 Usage:
     51   sup-sync [options] <source>*
     52 
     53 where <source>* is zero or more source URIs. If no sources are given,
     54 sync from all usual sources. Supported source URI schemes can be seen
     55 by running "sup-add --help".
     56 
     57 Options controlling HOW message state is altered:
     58 EOS
     59   opt :asis, "If the message is already in the index, preserve its state. Otherwise, use default source state. (Default.)", :short => :none
     60   opt :restore, "Restore message state from a dump file created with sup-dump. If a message is not in this dumpfile, act as --asis.", :type => String, :short => :none
     61   opt :discard, "Discard any message state in the index and use the default source state. Dangerous!", :short => :none
     62   opt :archive, "When using the default source state, mark messages as archived.", :short => "-x"
     63   opt :read, "When using the default source state, mark messages as read."
     64   opt :extra_labels, "When using the default source state, also apply these user-defined labels (a comma-separated list)", :default => "", :short => :none
     65 
     66 text <<EOS
     67 
     68 Other options:
     69 EOS
     70   opt :verbose, "Print message ids as they're processed."
     71   opt :optimize, "As the final operation, optimize the index."
     72   opt :all_sources, "Scan over all sources.", :short => :none
     73   opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
     74   opt :version, "Show version information", :short => :none
     75 
     76   conflicts :asis, :restore, :discard
     77 end
     78 
     79 op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis
     80 
     81 Redwood::start
     82 index = Redwood::Index.init
     83 
     84 restored_state = if opts[:restore]
     85   dump = {}
     86   puts "Loading state dump from #{opts[:restore]}..."
     87   IO.foreach opts[:restore] do |l|
     88     l =~ /^(\S+) \((.*?)\)$/ or raise "Can't read dump line: #{l.inspect}"
     89     mid, labels = $1, $2
     90     dump[mid] = labels.to_set_of_symbols
     91   end
     92   puts "Read #{dump.size} entries from dump file."
     93   dump
     94 else
     95   {}
     96 end
     97 
     98 seen = {}
     99 index.lock_interactively or exit
    100 begin
    101   index.load
    102 
    103   if(s = Redwood::SourceManager.source_for Redwood::SentManager.source_uri)
    104     Redwood::SentManager.source = s
    105   else
    106     Redwood::SourceManager.add_source Redwood::SentManager.default_source
    107   end
    108 
    109   sources = if opts[:all_sources]
    110     Redwood::SourceManager.sources
    111   elsif ARGV.empty?
    112     Redwood::SourceManager.usual_sources
    113   else
    114     ARGV.map do |uri|
    115       Redwood::SourceManager.source_for uri or Optimist::die "Unknown source: #{uri}. Did you add it with sup-add first?"
    116     end
    117   end
    118 
    119   sources.each do |source|
    120     puts "Scanning #{source}..."
    121     num_added = num_updated = num_deleted = num_scanned = num_restored = 0
    122     last_info_time = start_time = Time.now
    123 
    124     Redwood::PollManager.poll_from source do |action,m,old_m,progress|
    125       num_scanned += 1
    126       if action == :delete
    127         num_deleted += 1
    128         puts "Deleting #{m.id}" if opts[:verbose]
    129       elsif action == :add
    130         seen[m.id] = true
    131 
    132         ## tweak source labels according to commandline arguments if necessary
    133         m.labels.delete :inbox if opts[:archive]
    134         m.labels.delete :unread if opts[:read]
    135         m.labels += opts[:extra_labels].to_set_of_symbols(",")
    136 
    137         ## decide what to do based on message labels and the operation we're performing
    138         dothis = case
    139         when (op == :restore) && restored_state[m.id]
    140           if old_m && (old_m.labels != restored_state[m.id])
    141             num_restored += 1
    142             m.labels = restored_state[m.id]
    143             :update_message_state
    144           elsif old_m.nil?
    145             num_restored += 1
    146             m.labels = restored_state[m.id]
    147             :add_message
    148           else
    149             # labels are the same; don't do anything
    150           end
    151         when op == :discard
    152           if old_m && (old_m.labels != m.labels)
    153             :update_message_state
    154           else
    155             # labels are the same; don't do anything
    156           end
    157         else
    158           if old_m
    159             :update_message
    160           else
    161             :add_message
    162           end
    163         end
    164 
    165         ## now, actually do the operation
    166         case dothis
    167         when :add_message
    168           puts "Adding new message #{source}##{m.source_info} with labels #{m.labels}" if opts[:verbose]
    169           num_added += 1
    170         when :update_message
    171           puts "Updating message #{source}##{m.source_info}; labels #{old_m.labels} => #{m.labels}; offset #{old_m.source_info} => #{m.source_info}" if opts[:verbose]
    172           num_updated += 1
    173         when :update_message_state
    174           puts "Changing flags for #{source}##{m.source_info} from #{old_m.labels} to #{m.labels}" if opts[:verbose]
    175           num_updated += 1
    176         end
    177       else fail "sup-sync cannot handle :update's"
    178       end
    179 
    180       if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
    181         last_info_time = Time.now
    182         elapsed = last_info_time - start_time
    183         pctdone = progress * 100.0
    184         remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
    185         printf "## scanned %dm (~%.0f%%) @ %.1fm/s. %s elapsed, ~%s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
    186       end
    187       next if opts[:dry_run]
    188     end
    189 
    190     puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated}, deleted #{num_deleted} messages from #{source}."
    191     puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
    192   end
    193 
    194   index.save
    195 
    196   if opts[:optimize]
    197     puts "Optimizing index..."
    198     optt = time { index.optimize unless opts[:dry_run] }
    199     puts "Optimized index of size #{index.size} in #{optt}s."
    200   end
    201 rescue Redwood::FatalSourceError => e
    202   $stderr.puts "Sorry, I couldn't communicate with a source: #{e.message}"
    203 rescue Exception => e
    204   File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
    205   raise
    206 ensure
    207   Redwood::finish
    208   index.unlock
    209 end