#!/usr/bin/ruby -w$LOAD_PATH.unshift '/home/tnks/src/builds/sup/lib'require 'rubygems'require 'chronic'require 'ftools'require 'singleton'require 'sup'require 'tempfile'require 'trollop'require 'uri'class Redwood::Maildir attr_reader :ids_to_fnsendclass Redwood::Index public :parse_user_query_stringendclass Redwood::Message def maildir_filepath raise "message not in a Maildir" if not @source.is_a? Redwood::Maildir @source.scan_mailbox return @source.ids_to_fns[@source_info] endendclass SupTweakMaildir include Singleton VERSION = 0.1 PROG = $0.split("/")[-1] USAGE = <<EOSManipulate large amounts of Maildir mail regardless of source.Usage: #{PROG} [options] <query string>All messages Maildir messages matching the supplied query string areidentified. These messages can then be trashed (-t), purged (-p), or havetheir labels modifed (-a or -r).Note: * The supplied query string can be of any format valid in the Sup query buffer. * You don't have to quote the query string to keep it one argument, but quoting may be useful if you don't want to escape special shell characters like '(' or ')'. * There are four switches that effect changes to either the index or backend Maildirs: -t, -p, -a, and -r. Not specifying one of these switches will just show the messages that would be acted upon, but do nothing to the permanent stores (a dry run). * When using the -t or -p option, you may want to exempt messages in threads that have certain labels. Use the -e option for this, for example "-e starred,save". * This script /only/ considers messages stored on a Maildir backend. * This script does /not/ skip over Spam, Deleted, or Killed mail.Options:EOS def initialize self.class.i_am_the_instance self end def parse_options @opts = Trollop::options do version "#{PROG} v#{VERSION}" banner USAGE opt :trash, "trash matching messages from index and backend", :short => "-t" opt :backup, "when trashing move messages to Maildir <s>; created if" \ " missing", :type => String, :short => "-b", :default => Time.now.strftime("./#{PROG}.trash.%y%m%d.%H%M%S") opt :purge, "like --trash, but without moving messages to a backup" \ " directory", :short => "-p" opt :add_labels, "add labels in <s> (comma-separated) to messages", :type => String, :short => "-a" opt :remove_labels, "remove labels in <s> (comma-separated) to messages", :type => String, :short => "-r" opt :exempt_threads_labeled, "exempt messages of threads with labels in <s> (comma-separated)", :type => String, :short => "-e" opt :thread_by_subj, "use subject for threading", :short => "-s" opt :file, "dump backend filenames of matches to file <s> (one per line)", :type => String, :short => "-f" conflicts :backup, :purge, :add_labels conflicts :backup, :purge, :remove_labels conflicts :trash, :purge, :add_labels conflicts :trash, :purge, :remove_labels end Trollop::die "query string required" if ARGV.empty? end def gather_msgs # building query # query = ARGV.join " " real_query, opts = @index.parse_user_query_string query opts = { :load_spam => true, :load_deleted => true, :skip_killed => false } # getting messages matching query # hits = Hash.new if @opts[:exempt_threads_labeled] thread_set = Redwood::ThreadSet.new @index, @opts[:thread_by_subj] end @index.ferret.search_each(real_query, :limit => :all) do |doc_id, score| msg = @index.build_message doc_id next if not msg.source.is_a? Redwood::Maildir hits[msg.maildir_filepath] = {"msg" => msg, "doc_id" => doc_id} if @opts[:exempt_threads_labeled] thread_set.load_thread_for_message msg, opts end end Redwood::log "hits found: #{hits.length}" # pruning threads as per specifications from command line before returning # if @opts[:exempt_threads_labeled] thread_must_not_have = @opts[:exempt_threads_labeled].tr(",", " ").split. map {|elem| elem.downcase.intern} thread_set.threads.each do |thread| thread_must_not_have.each do |label| if thread.has_label? label thread.each do |msg, depth, parent| hits.delete msg.maildir_filepath end break end end end Redwood::log "removing messages in threads labeled: "\ "#{thread_must_not_have}" Redwood::log "hits remaining: #{hits.length}" end return hits end def perform_action hits if @opts[:file] Redwood::log "dumping filenames of matches to '#{@opts[:file]}'" dump_file = File.open @opts[:file], "w" hits.keys.each {|filepath| dump_file.puts filepath} dump_file.close end if @opts[:trash] Redwood::log "action: trashing messages" path = @opts[:backup] File.makedirs path, "#{path}/tmp", "#{path}/cur", "#{path}/new" hits.each do |filepath, record| filename = filepath.split("/")[-1] File.move filepath, "#{path}/new/#{filename}" @index.ferret.delete record["doc_id"] end elsif @opts[:purge] Redwood::log "action: purging messages" hits.each do |filepath, record| File.unlink filepath @index.ferret.delete record["doc_id"] end elsif @opts[:add_labels] or @opts[:remove_labels] Redwood::log "action: modifying labels." hits.values.each do |record| msg = record["msg"] labels = Hash.new msg.labels.each do |label| labels[label] = true end (@opts[:add_labels] or "").tr(",", " ").split.each do |label| labels[label.downcase.intern] = true end (@opts[:remove_labels] or "").tr(",", " ").split.each do |label| labels.delete(label.downcase.intern) end msg.labels = labels.keys @index.sync_message msg end else Redwood::log "no action: doing nothing to permanent stores" end end def main parse_options Redwood::start @index = Redwood::Index.new @index.lock_or_die begin @index.load hits = gather_msgs perform_action hits @index.save rescue Exception => e File.open("sup-exception-log.txt", "w") { |f| f.puts e.backtrace } raise ensure Redwood::finish @index.unlock end endendSupTweakMaildir.new.main if $0 == __FILE__