#! /usr/bin/ruby -w # qmanip - manipulate sendmail queues using sendmail-type locking # Copyright (C) 2001 Ed Cashin # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # run "qmanip --help" for usage info require 'getoptlong' require 'find' class Feedback # runtime error and warning message support def Feedback::warn(msg = "warning") me = $0.sub(%r{.*/}, "") msg = "#{me} Warning: #{msg}" msg = "#{msg}: $!" if $! $stderr.puts msg end def Feedback::die(msg = "exiting") me = $0.sub(%r{.*/}, "") msg = "#{me} Error: #{msg}" msg = "#{msg}: $!" if $! $stderr.puts msg exit 1 end end class QEntry # queue entry constructor attr_writer :verbose, :test def initialize(f) @dir = f.sub(%r{^(.*)\/.*}, '\1') # directory where queue entry is @id = f.sub(%r{^.*\/[qd]f}, "") # e.g., "PAA15930" for qfPAA15930 @msginfo = nil @headers = nil @sender = nil @verbose = nil @test = nil end end class QEntry # IO and data processing def read_info f = File.new(@dir + "/" + "qf" + @id) @msginfo = f.gets(nil).sub("\n\s+", " ").split("\n") f.close self end def match(re) return true unless re read_info unless @msginfo @msginfo.detect {|s| s =~ re } end def bodymatch(match) fname = "#{@dir}/df#{@id}" # the df file has the body return nil unless test(?r, fname) # todo: maybe warn? re = Regexp.new(match, Regexp::MULTILINE) f = File.new(fname) return nil unless f result = f.gets(nil).grep(re).size > 0 f.close result end end class QEntry # actions on associated files def lock # -------- sendmail, according to qtool.pl, locks the df file filename = @dir + "/df" + @id unless fh = File.open(filename, "r+") and fh.flock(File::LOCK_EX | File::LOCK_NB) Feedback::warn("could not lock file (#{filename})") return nil end fh end # --------------- with locking, process each file associated with id def each return nil unless fh = lock Dir[@dir + "/*" + @id].each do |f| yield(f) end fh.flock(File::LOCK_UN) fh.close end def move(dest) each { |f| tofile = dest + "/" + f.sub(%r{^.*\/}, "") puts "#{f} --> #{tofile}" if @verbose File.rename(f, tofile) unless @test } end def unlink each { |f| puts "unlink #{f}" if @verbose File.unlink(f) unless @test } end end class QManip # queue manipulator constructor def initialize @opts = Hash.new opts = GetoptLong.new(["--src", "-s", GetoptLong::REQUIRED_ARGUMENT], ["--dest", "-d", GetoptLong::REQUIRED_ARGUMENT], ["--unlink", "-u", GetoptLong::NO_ARGUMENT], ["--test", "-t", GetoptLong::NO_ARGUMENT], ["--verbose", "-v", GetoptLong::NO_ARGUMENT], ["--help", "-h", GetoptLong::NO_ARGUMENT], ["--match", "-m", GetoptLong::REQUIRED_ARGUMENT], ["--bodymatch", "-b", GetoptLong::REQUIRED_ARGUMENT] ) opts.each do |opt, arg| opt.sub!("^--", "") @opts[opt] = arg end if @opts['help'] usage exit end for opt in %w{src dest} next if (opt == "dest" and @opts['unlink']) raise "Error: unspecified #{opt}" unless @opts[opt] @opts[opt].sub!(%r{\/\/+}, "/") # collapse multiple consecutive slashes @opts[opt].sub!(%r{\/$}, "") # remove trailing slash raise "Error: #{opt} is not a directory" unless File.stat(@opts[opt]).directory? end @opts['verbose'] = true if @opts['test'] # test implies verbose end end class QManip # methods def run Find.find(@opts["src"]) do |f| next unless f =~ /^.*\/qf([A-Z]{3}\d{5})/ # e.g. qfPAA15930 #/ msg = QEntry.new(f) msg.verbose = @opts['verbose'] msg.test = @opts['test'] if msg.match(@opts['match']) next if b = @opts["bodymatch"] and ! msg.bodymatch(b) if @opts["unlink"] msg.unlink else msg.move(@opts['dest']) end end end end end class QManip def usage print <<'EOUSAGE' USAGE: qmanip --src={queue} --dest={queue} --match={rexexp} [--test] [--verbose] qmanip --src={queue} --match={rexexp} --unlink [--test] [--verbose] DESCRIPTION: qmanip allows arbitrary matching on a queue entry's envelope and headers. You specify perl-like regular expressions to match lines in the qf* files or df* files. If no "match" option is supplied, all messages match. Continued lines in the qf data are collapsed onto one line for convenience during matching. Matching messages will have all associated files in the queue moved to the specified destination or, with the "unlink" option, will be unlinked. OPTIONS: --src, -s specify source directory where queue files are --dest, -d specify destination ("move to") directory --unlink, -u delete matching files instead of moving them --test, -t do not really move or unlink files (implies verbose) (still does file locking) --verbose, -v print messages about what qmanip is doing --help, -h show this help --match, -m specify a match for the qf* file's info that will be used to select which files to move --bodymatch, -b specify a match for the df* file's info that will be used to select which files to move (if the qf* match wasn't specified or was successful) EXAMPLES: Testing before wacking a big queue: qmanip --src=/tmp/tmpqueue/ --dest=/tmp/destqueue \ --match='H\?x\?Full-Name: Ronald McDonald' --test You can also match the body of the message: qmanip --src=/tmp/tmpqueue/ --dest=/tmp/destqueue \ --match='H\?x\?Full-Name: Ronald McDonald' --bodymatch='love tryst' Like qtool, you can delete selected messages instead of moving them: qmanip --src=/tmp/tmpqueue \ --match='H\?x\?Full-Name: Ronald McDonald' --unlink --verbose EOUSAGE # ' end end qm = QManip.new qm.run