sup

A curses threads-with-tags style email client

sup.git

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

bin/sup-tweak-labels (4253B) - raw

      1 #!/usr/bin/env ruby
      2 
      3 $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
      4 
      5 require 'optimist'
      6 require "sup"
      7 
      8 class Float
      9   def to_s; sprintf '%.2f', self; end
     10    def to_time_s
     11      infinite? ? "unknown" : super
     12    end
     13 end
     14 
     15 class Numeric
     16   def to_time_s
     17     i = to_i
     18     sprintf "%d:%02d:%02d", i / 3600, (i / 60) % 60, i % 60
     19   end
     20 end
     21 
     22 def time
     23   startt = Time.now
     24   yield
     25   Time.now - startt
     26 end
     27 
     28 opts = Optimist::options do
     29   version "sup-tweak-labels (sup #{Redwood::VERSION})"
     30   banner <<EOS
     31 Batch modification of message state for messages already in the index.
     32 
     33 Usage:
     34   sup-tweak-labels [options] <source>*
     35 
     36 where <source>* is zero or more source URIs. Supported source URI schemes can
     37 be seen by running "sup-add --help".
     38 
     39 Options:
     40 EOS
     41   opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :default => ""
     42   opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :default => ""
     43   opt :query, "A Sup search query", :type => String
     44 
     45   text <<EOS
     46 
     47 Other options:
     48 EOS
     49   opt :verbose, "Print message ids as they're processed."
     50   opt :very_verbose, "Print message names and subjects as they're processed."
     51   opt :all_sources, "Scan over all sources.", :short => :none
     52   opt :dry_run, "Don't actually modify the index. Probably only useful with --verbose.", :short => "-n"
     53   opt :no_sync_back, "Do not sync back to the original Maildir."
     54   opt :version, "Show version information", :short => :none
     55 end
     56 opts[:verbose] = true if opts[:very_verbose]
     57 
     58 add_labels = opts[:add].to_set_of_symbols ","
     59 remove_labels = opts[:remove].to_set_of_symbols ","
     60 
     61 Optimist::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
     62 
     63 Redwood::start
     64 index = Redwood::Index.init
     65 index.lock_interactively or exit
     66 
     67 begin
     68   index.load
     69 
     70   source_ids = if opts[:all_sources]
     71     Redwood::SourceManager.sources
     72   else
     73     ARGV.map do |uri|
     74       Redwood::SourceManager.source_for uri or Optimist::die "Unknown source: #{uri}. Did you add it with sup-add first?"
     75     end
     76   end.map { |s| s.id }
     77   Optimist::die "nothing to do: no sources" if source_ids.empty?
     78 
     79   query = "(" + source_ids.map { |id| "source_id:#{id}" }.join(" OR ") + ")"
     80   if add_labels.empty?
     81     ## if all we're doing is removing labels, we can further restrict the
     82     ## query to only messages with those labels
     83     query += " (" + remove_labels.map { |l| "label:#{l}" }.join(" OR ") + ")"
     84   end
     85   query += ' AND ' + opts[:query] if opts[:query]
     86 
     87   parsed_query = index.parse_query query
     88   parsed_query.merge! :load_spam => true, :load_deleted => true, :load_killed => true
     89   ids = index.to_enum(:each_id, parsed_query)
     90   num_total = index.num_results_for parsed_query
     91 
     92   $stderr.puts "Found #{num_total} documents across #{source_ids.length} sources. Scanning..."
     93 
     94   num_changed = num_scanned = 0
     95   last_info_time = start_time = Time.now
     96   ids.each do |id|
     97     num_scanned += 1
     98 
     99     m = index.build_message id
    100     old_labels = m.labels.dup
    101 
    102     m.labels += add_labels
    103     m.labels -= remove_labels
    104 
    105     unless m.labels == old_labels
    106       num_changed += 1
    107       puts "From #{m.from}, subject: #{m.subj}" if opts[:very_verbose]
    108       puts "#{m.id}: {#{old_labels.to_a.join ','}} => {#{m.labels.to_a.join ','}}" if opts[:verbose]
    109       puts if opts[:very_verbose]
    110       unless opts[:dry_run]
    111         index.update_message_state [m, false]
    112         m.sync_back unless opts[:no_sync_back]
    113       end
    114     end
    115 
    116     if Time.now - last_info_time > 60
    117       last_info_time = Time.now
    118       elapsed = last_info_time - start_time
    119       pctdone = 100.0 * num_scanned.to_f / num_total.to_f
    120       remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
    121       $stderr.puts "## #{num_scanned} (#{pctdone}%) read; #{elapsed.to_time_s} elapsed; #{remaining.to_time_s} remaining"
    122     end
    123   end
    124   $stderr.puts "Scanned #{num_scanned} / #{num_total} messages and changed #{num_changed}."
    125 
    126   unless num_changed == 0
    127     $stderr.puts "Optimizing index..."
    128     index.optimize unless opts[:dry_run]
    129   end
    130 
    131 rescue Exception => e
    132   File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace }
    133   raise
    134 ensure
    135   index.save
    136   Redwood::finish
    137   index.unlock
    138 end
    139