diff --git a/quarantineck b/quarantineck index 8b48810..75431a6 100644 --- a/quarantineck +++ b/quarantineck @@ -4,12 +4,41 @@ # Check for non-empty quarantine queue & report -SMQ="`dirname $0`/smqdecode" -QD="/var/spool/mqueue" +# Locate smqdecode - check same place as this script, then try PATH -if [ -x "$SMQ" -a -d "$QD" ]; then +SMQ="$(dirname "$0")/smqdecode" +if ! [ -x "$SMQ" ]; then + SMQ="$(which smqdecode 2>/dev/null)" +fi +QD= + +# Look for Queue directory in sendmail.cf, then default + +if [[ -n "$SMQ" && -x "$SMQ" ]]; then + SMCONFIG= + for DIR in /opt/etc/mail /usr/local/etc/mail /etc/mail; do + if [ -f "$DIR/sendmail.cf" ]; then + SMCONFIG="$DIR/sendmail.cf" + break; + fi + done + + if [[ -n "$SMCONFIG" && -f "$SMCONFIG" ]]; then + QD="$(grep '^O QueueDirectory=' /etc/mail/sendmail.cf | tail -n 1)" + if [[ "$QD" =~ ^O\ QueueDirectory=(.*)$ ]]; then + QD="${BASH_REMATCH[1]}" + else + QD= + fi + fi + [ -z "$QD" ] && QD="/var/spool/mqueue" +fi + +# If have smqdecode && queue directory, look there for quarantined mail + +if [[ -n "$QD" && -d "$QD" ]]; then FOUND=0 - while read ; do + while read -r ; do if [ "$FOUND" == 0 ]; then echo "Quarantined e-mail"; fi echo ((FOUND++)) @@ -30,14 +59,14 @@ if [ -x "$SMQ" -a -d "$QD" ]; then # Command to remove from quarantine & delete immediately - echo " Delete : rm -f $QD/{d,h}${REPLY:1}" + echo " Delete : $SMQ --queue=$QD --delete ${REPLY:2}" # Command to release from quarantine & deliver echo " Release: sendmail -qQ -qI${REPLY:2}" done < <( find "$QD" -maxdepth 1 -type f -name "hf*" -printf '%f\n' ) - if [ $FOUND -gt 0 ]; then + if [ "$FOUND" -gt 0 ]; then exit 0 elif [ -z "$CRONJOB" ]; then echo "Quarantine is empty" @@ -49,11 +78,10 @@ fi # Fallback to mailq -L="`mailq -qQ 2>&1`" -if [ -n "$CRONJOB" ] && echo "$L" | grep -qP '^\s*Total\srequests:\s+0'; then +L="$(mailq -qQ 2>&1)" +if [ -n "$CRONJOB" ] && grep -qP '^\s*Total\srequests:\s+0' <<<"$L" ; then exit 0 fi -cat < Use --man for license information. COPYRIGHT -our $VERSION = 'V1.004'; +our $VERSION = 'V1.005'; # License is in the POD at the end of this file. @@ -133,10 +133,9 @@ my @meta = ( sub { my( $qe, $k, $v ) = @_; - return sprintf( - "%s Size: %u, Permissions: %s", - $qe->{_fname}, $v, - _pperms( $qe->{_qperm} ) ); + return sprintf( "%s Size: %u, Permissions: %s", + $qe->{_fname}, $v, + _pperms( $qe->{_qperm} ) ); } ], _type => [ @@ -177,11 +176,10 @@ my @meta = ( sub { my( $qe, $k, $v ) = @_; - return sprintf( - '%s delta: %s mode: %s', - _pdate( $qe, $k, $v->{due} ), - _pdelta( $qe, $k, $v->{delta}, 1, '+' ), - _pflags( \%rtnFlags, $v->{mode}, 1 ) ); + return sprintf( '%s delta: %s mode: %s', + _pdate( $qe, $k, $v->{due} ), + _pdelta( $qe, $k, $v->{delta}, 1, '+' ), + _pflags( \%rtnFlags, $v->{mode}, 1 ) ); } ], @@ -196,10 +194,9 @@ my @meta = ( my $txt = ''; $txt = "$qe=>{d}/" if( defined $qe->{d} ); - return $txt . sprintf( - "%s Size: %u, Permissions: %s", - $v, $qe->{_dsize}, - _pperms( $qe->{_dperm} ) ); + return $txt . sprintf( "%s Size: %u, Permissions: %s", + $v, $qe->{_dsize}, + _pperms( $qe->{_dperm} ) ); } ], I => [ @@ -223,10 +220,9 @@ my @meta = ( 'Orphan ORCPT' => sub { my $v = $_[2]; - return sprintf( - 'Type: %s Addr: %s', - defined $v->[0] ? $v->[0] : 'unspecified', - $v->[1] ); + return sprintf( 'Type: %s Addr: %s', + defined $v->[0] ? $v->[0] : 'unspecified', + $v->[1] ); } ], R => [ @@ -288,10 +284,9 @@ my @meta = ( sub { my( $qe, $k, $v ) = @_; - return sprintf( - "%s Size: %u, Permissions: %s", - $v, $qe->{_xsize}, - _pperms( $qe->{_xperm} ) ); + return sprintf( "%s Size: %u, Permissions: %s", + $v, $qe->{_xsize}, + _pperms( $qe->{_xperm} ) ); } ], ); @@ -305,6 +300,7 @@ sub new { my $type = substr( $fn, 0, 1 ); my $qe = { B => '7BIT', + _debug => $opts->{debug}, _dname => 'd' . substr( $fn, 1 ), _fname => $fn, _type => $type, }; @@ -314,7 +310,9 @@ sub new { # Queue control file - CORE::open( my $fh, '<', File::Spec->catfile( $qfd, $fn ) ) or do { + my $qfn = File::Spec->catfile( $qfd, $fn ); + $qe->{_qfn} = $qfn; + CORE::open( my $fh, '<', $qfn ) or do { return if( $opts->{skip} ); die( "$fn: open - $!\n" ); }; @@ -348,23 +346,22 @@ QF: my $unm = getpwuid( $uid ); my $gnm = getgrgid( $gid ); - $qe->{C} = sprintf( - "%s:%s.%s%s", - $user, - defined $unm ? $unm : $uid, - defined $gnm ? $gnm : $gid, - defined $eaddr ? ":$eaddr" : '' ); + $qe->{C} = sprintf( "%s:%s.%s%s", + $user, + defined $unm ? $unm : $uid, + defined $gnm ? $gnm : $gid, + defined $eaddr ? ":$eaddr" : '' ); next; } - if( m,^([Dd])(.+)$, ) { # data file name, path + if( m,^([Dd])(.+)$, ) { # data file name, path $qe->{$1} = $2; next; } - if( /^F(.*)$/ ) { # Flags + if( /^F(.*)$/ ) { # Flags $qe->{F} = { map { $_ => 1 } split( //, $1 ) }; next; } - if( /^H(?:\?([^?]*)\?)?(.+)$/ ) { # Header (optional delivery flags) + if( /^H(?:\?([^?]*)\?)?(.+)$/ ) { # Header (optional delivery flags) push @{ $qe->{H} }, { F => { map { $_ => 1 } split( //, defined $1 ? $1 : '' ) }, H => $2 }; @@ -374,7 +371,7 @@ QF: } last QF; } - if( m,^I(\d+)/(\d+)/(\d+)$, ) { # Data file recovery + if( m,^I(\d+)/(\d+)/(\d+)$, ) { # Data file recovery $qe->{I} = { major => $1, minor => $2, inode => $3 }; next; } @@ -503,7 +500,7 @@ QF: my $dfn = File::Spec->catfile( ( $qe->{d} || $dfd ), $qe->{_dname} ); if( CORE::open( my $df, '<', $dfn ) ) { $r->{dfh} = $df; - @{$qe}{qw/_dperm _dsize/} = ( stat $df )[ 2, 7 ]; + @{$qe}{qw/_dfn _dperm _dsize/} = ( $dfn, ( stat $df )[ 2, 7 ] ); } else { warn( "$dfn: $!\n" ); } @@ -512,9 +509,10 @@ QF: my $xfn = $fn; substr( $xfn, 0, 1, 'x' ); - if( CORE::open( my $xf, '<', File::Spec->catfile( $xfd, $xfn ) ) ) { + my $xfp = File::Spec->catfile( $xfd, $xfn ); + if( CORE::open( my $xf, '<', $xfp ) ) { $r->{xfh} = $xf; - @{$qe}{qw/_xf _xperm _xsize/} = ( $xfn, ( stat $xf )[ 2, 7 ] ); + @{$qe}{qw/_xfn _xf _xperm _xsize/} = ( $xfp, $xfn, ( stat $xf )[ 2, 7 ] ); } # Return holding lock and file handles @@ -562,7 +560,9 @@ sub AUTOLOAD { qname => '_fname', qperm => '_qperm', qsize => '_qsize', xname => '_xf', xperm => '_xperm', xsize => '_xsize', - age => '_age', elapsed => '_elapsed', }->{$name}; + age => '_age', elapsed => '_elapsed', + qpath => '_qfn', dpath => '_dfn', + xpath => '_xfn', debug => '_debug', }->{$name}; croak( qq(Unknown access method "$name" called\n) ) unless( defined $k ); no strict 'refs'; ## no critic ProhibitNoStrict *$AUTOLOAD = sub { my $qf = shift; return $qf->{qe}{$k}; }; @@ -584,9 +584,9 @@ sub dumpmeta { my $lwid = 0; for( my $i = 0; $i < @meta; $i += 2 ) { my( $k, $r ) = @meta[ $i .. $i + 1 ]; - $r = $meta[ $i + 1 ] = [ $meta[ $i + 1 ], '%s' ] unless( ref $r ); + $r = $meta[ $i + 1 ] = [ $meta[ $i + 1 ], '%s' ] unless( ref $r ); $r->[1] = '%s' if( @$r <= 1 ); - $lwid = length( $r->[0] ) if( exists $qe->{$k} + $lwid = length( $r->[0] ) if( exists $qe->{$k} && length( $r->[0] ) > $lwid ); } @@ -718,6 +718,25 @@ sub dumptranscript { return 1; } +sub deleteentry { + my $qf = shift; + my( $ofh ) = @_; + + $ofh ||= \*STDOUT; + my $xv = 0; + + my @files = ( grep { defined } ( $qf->xpath, $qf->dpath, $qf->qpath ) ); + foreach my $fn ( @files ) { + if( unlink( $fn ) ) { + $xv++; + printf $ofh ( "Deleted: %s\n", $fn ) if( $qf->{_debug} ); + } else { + printf $ofh ( "%s: %s\n", $fn, $! ); + } + } + return !$xv; +} + # Print flags ( $table, $hash, $one-line ) sub _pflags { @@ -728,9 +747,8 @@ sub _pflags { foreach my $f ( sort keys %$v ) { if( $ft->{$f} ) { - $txt .= sprintf( - "%s - %s%s", - $f, $ft->{$f}, $eol ); + $txt .= sprintf( "%s - %s%s", + $f, $ft->{$f}, $eol ); } else { $txt .= "$f - $eol"; } @@ -1034,9 +1052,8 @@ unless( __FILE__ =~ /\.pm$/ ) { Getopt::Long->import( qw/GetOptions :config bundling/ ); my( $meta, $headers, $body, $transcript, - $flags, $limit, $debug, $submit, - $help, $man, $version, ) - = ( 0, 1, 0, 0, ); + $flags, $limit, $debug, $submit, $delete, + $help, $man, $version, ); my %opts = ( type => qr/[q]/ ); @@ -1044,6 +1061,7 @@ unless( __FILE__ =~ /\.pm$/ ) { 'body|b!' => \$body, 'config|C=s' => \$opts{config}, 'debug|d!' => \$debug, + 'delete|rm!' => \$delete, 'flags|f!' => \$flags, 'headers|h!' => \$headers, 'help|?' => \$help, @@ -1056,15 +1074,14 @@ unless( __FILE__ =~ /\.pm$/ ) { 'version' => \$version, 'warnings|w!' => \$opts{warn}, 'message|M!' => sub { $headers = $body = $_[1]; undef $limit if( $_[1] ); }, - 'limit-body|l=s' => sub { $limit = _matchnum( @_ ) }, + 'limit-body|l=s' => sub { $limit = _matchnum( @_ ) }, 'type|T=s' => sub { - $opts{type} = _matchval( - @_[ 0, 1 ], - all => qr/[qht]/, - pended => qr/[q]/, - quarantined => qr/[h]/, - queued => qr/[q]/, - temporary => qr/[t]/, ); + $opts{type} = _matchval( @_[ 0, 1 ], + all => qr/[qht]/, + pended => qr/[q]/, + quarantined => qr/[h]/, + queued => qr/[q]/, + temporary => qr/[t]/, ); }, ) or die( "Command line error, --help for usage\n" ); @@ -1077,7 +1094,7 @@ unless( __FILE__ =~ /\.pm$/ ) { require Pod::Usage; } or die( "Install Pod::Usage or use 'perldoc $0'\n" ); - Pod::Usage::pod2usage( 1 ) if $help; + Pod::Usage::pod2usage( 1 ) if $help; Pod::Usage::pod2usage( -exitval => 0, -verbose => 2 ) if $man; } if( $version ) { @@ -1085,6 +1102,8 @@ unless( __FILE__ =~ /\.pm$/ ) { exit; } + $headers = 1 unless( defined $headers || $delete ); + $dcfg = $scfg if( $submit ); $opts{debug} = $debug; @@ -1130,6 +1149,9 @@ unless( __FILE__ =~ /\.pm$/ ) { local $Data::Dumper::Sortkeys = 1; print Data::Dumper::Dumper( $qf->{qe} ); } + if( $delete ) { + $qf->deleteentry; + } } exit; } @@ -1178,6 +1200,7 @@ smqdecode - Decode and extract sendmail queue entries smqdecode --body --config --flags --headers --submission --limit-body=[N,none] --message --metadata --skip-busy --queue --warnings --type=[all,pended,quarantined,temporary] + --delete --help --man --version smqdecode --meta x0VKKEWx032646 qfx0VKKEWx032647 /foo/hfx0VKKEWx032649 @@ -1218,6 +1241,8 @@ Output the message body as transmitted. Output any processing transcript associated with a message. +=item delete the message from the queue + =back =head1 OPTIONS @@ -1226,7 +1251,9 @@ The following options are provided. The short and long forms of each option have the same effect. The long form of a boolean option can be negated by specifying it as B<-->noB