Server IP : 195.201.23.43 / Your IP : 18.189.32.37 Web Server : Apache System : Linux webserver2.vercom.be 5.4.0-192-generic #212-Ubuntu SMP Fri Jul 5 09:47:39 UTC 2024 x86_64 User : kdecoratie ( 1041) PHP Version : 7.1.33-63+ubuntu20.04.1+deb.sury.org+1 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals, MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /bin/ |
Upload File : |
#!/usr/bin/perl # postgreyreport by tbaker@bakerfl.org # bits and peices of code taken from postgrey 1.11 ( http://isg.ee.ethz.ch/tools/postgrey/ ) package postgreyreport; use strict; use BerkeleyDB; use Getopt::Long 2.25 qw(:config posix_default no_ignore_case); use Net::Server::Daemonize qw( get_uid get_gid set_uid set_gid ); use Pod::Usage; #use Net::RBLClient; my $VERSION='1.14.3 (20100321)'; # used in maillog processing my $RE_revdns_ip = qr/ ([^\[\s]+)\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]/; # ptr[1.2.3.4] my $RE_reject = qr/reject: /; my $RE_triplet = qr/$RE_revdns_ip: 450 .+from=<([^>]+)> to=<([^>]+)>/; my $dns; my %dns_cache; # used for --check_sender my $rbl = undef; # Net::RBLClient object select((select(STDOUT), $| = 1)[0]); # Unbuffer standard output. # default options, override via command line my %opt = ( user => 'postgrey', dbdir => '/var/lib/postgrey', delay => 300, return_string => 'Greylisted', # match on this string check_sender => '', # = mx,a,mx/24,a/24 # todo=spf - uses Net::DNS show_tries => 0, # number of greylist attempts within --delay separate_by_subnet => '', # if not blank output this string for every new /24 separate_by_ip => '', # if not blank output this string for every new IP single_line => 1, # output everything on a single line? (grouping enabled if false ) tab => 0, # use tabs as separators, not spaces (only in single line mode) show_time => 0, # show entry time in maillog skip_dnsbl => [], # list of DNSBL servers to check and skip reporting for skip_clients => [], # files of clients to skip reporting skip_pool => 0, # skip entries that appear to be a provider pool (last 2 ips in ptr) match_clients => [], # files of ONLY clients to report on v => 0, # verbose? used mainly for script debugging debug_db => 0, # output time() values from btree db debug_re => '', # but only for these hosts (separate by commas ) ); # start here sub main { GetOptions(\%opt, 'help|h', 'version', 'man', 'delay=s', 'user|u=s', 'dbdir=s', 'debug_db', 'debug_re=s', 'v+', 'return_string|greylist-text=s', 'show_tries', 'check_sender=s', 'separate_by_subnet=s', 'separate_by_ip=s', 'single_line!', 'tab', 'show_time', 'skip_dnsbl=s@','skip_clients=s@', 'match_clients=s@', 'skip_pool', ) or exit(1); if($opt{help}) { pod2usage(1) } if($opt{man}) { pod2usage(-exitstatus => 0, -verbose => 2) } if ($opt{version}) { print "postgreyreport $VERSION\n"; exit(0) } if (scalar(@{$opt{skip_dnsbl}}) > 0) { require Net::RBLClient; $rbl = Net::RBLClient->new ( lists => $opt{skip_dnsbl} ); } setup_debug(); # display key/value pairs from db read_client_files(); postgrey_fatal_report(); # do the work } ####################################################### # postgrey_fatal(): report on all fatal triplets # sub postgrey_fatal_report() { umask 0077; # mode 600 my %triplets; # hash of all triplets we will look at drop_priv($opt{user}); # change UID to 'postgrey' # convert --check_sender into hash: opt{do_checks}{VAL} if ($opt{check_sender}) { use Net::DNS; $dns = Net::DNS::Resolver->new; $opt{check_sender} = lc $opt{check_sender}; foreach my $check ( split(/,/,$opt{check_sender}) ) { $opt{do_checks}{$check}=1; print "Enabling Check: opt{do_checks}{$check} \n" if ($opt{v}); } } my $db = setup_dbm($opt{dbdir}); # connect to BerkeleyDB my @greyfatal = find_and_sort_fatal( \%{$db}, \%triplets ); # read STDIN and sort the fatal triplets # foreach: loop through (sorted) fatal triplets and display to STDOUT my ($last_ip,$last_subnet); # define now $opt{separate_by_ip} =~ s|\\n|\n|g; # do it once before the for loop $opt{separate_by_subnet} =~ s|\\n|\n|g; # "" foreach my $key (@greyfatal) { my ($ip,$sender,$recipient) = split(/\//,$key); # separate the triplet my $revdns = $triplets{$key}{revdns}; # we saved revdns during maillog parse, so we dont have to look it up # --check_sender=mx,mx/24,a,a/24 # dns lookups from Net::DNS are cached and only performed once per sender's @domain my $check_sender = ''; if ( $opt{do_checks}{mx} and check_sender_mx( $sender,$ip,'mx') ) { $check_sender='MX'; } elsif ( $opt{do_checks}{'mx/24'} and check_sender_mx( $sender,$ip,'mx/24') ) { $check_sender='MX/24'; } elsif ( $opt{do_checks}{a} and check_sender_a( $sender,$ip,'a') ) { $check_sender='A'; } elsif ( $opt{do_checks}{'a/24'} and check_sender_a( $sender,$ip,'a/24') ) { $check_sender='A/24'; } # if separate_by_ip or separate_by_subnet display configured text if ($last_subnet eq $triplets{$key}{subnet}) { print "$opt{separate_by_ip}" if ( ($last_ip ne $ip) and $opt{separate_by_ip}) ; } else { if ( $opt{separate_by_subnet} ) { print $opt{separate_by_subnet}; } elsif ( $opt{separate_by_ip} ) { print $opt{separate_by_ip}; } } # display output on single line or multi-line if ($opt{single_line}) { if ($opt{tab}) { printf "%s\t", $triplets{$key}{entrytime} if($opt{show_time}) ; printf "%s\t", $triplets{$key}{counter} if($opt{show_tries}) ; printf "%s\t", $check_sender if($opt{check_sender}) ; printf "%s\t", $ip ; printf "%s\t", $revdns ; printf "%s\t", $sender ; } else { printf "%s ", $triplets{$key}{entrytime} if($opt{show_time}) ; printf "%s ", $triplets{$key}{counter} if($opt{show_tries}) ; printf "%5s ", $check_sender if($opt{check_sender}) ; printf "%15s ", $ip ; printf "%s ", $revdns ; printf "%s ", $sender ; } printf "%s\n", $recipient; ; } else { ### multi-line ## only output PTR - IP if its a new IP (grouping) printf "%-77s ", $revdns if($last_ip ne $ip) ; printf "%15s" , $ip if($last_ip ne $ip) ; print "\n" if($last_ip ne $ip) ; ## always output the new pairs MX/A? (sender/recipient) # if sender was from MX or A of above IP printf "%5s " , $check_sender if($opt{check_sender}) ; printf " ", $check_sender if(! $opt{check_sender}); # tries or blank space printf " %2s ", $triplets{$key}{counter} if($opt{show_tries}) ; print " " if(! $opt{show_tries}) ; # sender - recipient printf " %40s ", $sender ; printf " %40s ", $recipient ; print "\n" ; } ($last_ip, $last_subnet) = ($ip, $triplets{$key}{subnet}); # save for next iteration } } ##################################################################### # find_and_sort_fatal( \%db, \%triplets ) # read STDIN (maillog) and remember any 4xx greylisted log entries # return array of fatal triplets (ip/sender/recipient) sorted by ip sub find_and_sort_fatal { my ($db, $triplets) = @_; # while(<>): STDIN is maillog.0, looking at reject: 4xx greylist entries and remembering all triplets MAILLOG: while (<>) { next unless (/$RE_reject/o); # only look at reject: lines next unless (/$opt{return_string}/o); # only look at greylisted lines next unless (/$RE_triplet/o); # extract the triplet my ($revdns,$ipaddr,$sender,$recipient) = ($1,$2,$3,$4); my @ip = split(/\./, $ipaddr); $sender = do_sender_substitutions($sender); my ($subnet) = do_client_substitutions($ipaddr,$revdns); # 1.2.3.0 my $key = lc "$ipaddr/$sender/$recipient"; # postgrey key my $subkey = lc "$subnet/$sender/$recipient"; # subnet key 1.2.3.0/sender/recipient # if we are wanting to dump first,last out of the db do it before we determine if its fatal if ( is_debug_host($revdns) ) { foreach my $testkey ( @{[$key,$subkey]} ) { my ($tfirst, $tlast) = split(/,/,$db->{$testkey}); my $tdiff = $tlast - $tfirst; print "$testkey : $db->{$testkey} = " .$tdiff . "s \n"; } } # if --match_clients was specified on command line then move on to the next line unless a match is found if ( scalar(@{$opt{match_clients}}) > 0 ) { next unless ( find_in_array($ipaddr, $opt{MATCH_CLIENT_IPS}) or find_in_array($revdns, $opt{MATCH_CLIENT_PTR}) ); } # if --skip_clients was specified on command line, skip to next line if a match is found next if ( find_in_array($ipaddr, $opt{SKIP_CLIENT_IPS}) or find_in_array($revdns, $opt{SKIP_CLIENT_PTR}) ); # if --skip_pool then if last 2 ips are in ptr skip to next line next if ( $opt{skip_pool} and defined $ip[3] and $revdns =~ /$ip[2]/ and $revdns =~ /$ip[3]/ ); # check the db, proceed if the triplet was fatal next MAILLOG unless is_fatal_triplet($db, $key, $subkey); # if --skip_dnsbl then do RBL lookups (slow!) if ( defined $rbl ) { $rbl->lookup($ipaddr); my @listed = $rbl->listed_by; next if ( scalar(@listed) > 0 ); } # we made it past all the filtering checks, remember the triplet as fatal $triplets->{$key}{counter}++; # increase counter for this triplet $triplets->{$key}{revdns}=$revdns; # save its ptr for later use $triplets->{$key}{ipaddr}=$ipaddr; # save IP in easy to access form $triplets->{$key}{subnet}=$subnet; # save subnet in easy to access form $triplets->{$key}{subkey}=$subkey; # save key in subnet form $triplets->{$key}{entrytime}=substr($_,0,15); } die "Debugging DB active, report shutdown" if ($opt{debug_db}); # don't do anything other than spit out key pairs and stop my @greyfatal = keys %{ $triplets }; # create an array containing all triplets in form: ip/sender/recipient # sort fatal triplets by IP address @greyfatal = sort { pack('C4' => $a =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) cmp pack('C4' => $b =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/) } @greyfatal; return @greyfatal; } sub find_in_array($$) { my ($var, $patterns) = @_; for my $w (@{$patterns}) { return 1 if $var =~ $w; } return 0; } sub is_fatal_triplet($$$) { my ($db, $key, $subkey) = @_; my ($lapsed_ip, $lapsed_subnet) = (undef,undef); # try lookup by key if ( $db->{$key} =~ /,/ ) { my ($tfirst,$tlast) = split(/,/,$db->{$key}); # time_first_seen,time_last_seen $lapsed_ip = $tlast - $tfirst; # difference is time lapsed } # try subnet lookup if ( $db->{$subkey} =~ /,/ ) { my ($tfirst,$tlast) = split(/,/,$db->{$subkey}); # time_first_seen,time_last_seen $lapsed_subnet = $tlast - $tfirst; } if ( ( defined $lapsed_ip or defined $lapsed_subnet ) and (!( ($lapsed_ip >= $opt{delay} ) or ($lapsed_subnet >= $opt{delay}) ) ) ) { #push (@greyfatal, $key); # if lapsed time less than --delay, then it was a fatal triplet return 1; } elsif (( ! defined $lapsed_ip ) and ( ! defined $lapsed_subnet )) { #push (@greyfatal, $key); # if neither is found in the db it must have been removed. return 1; } return 0; } ########################################################################### # check_sender_mx(sender, ip, subnet) # subnet='' or '/24' # return true if ip is in MX list for sender domain (or /24 if specified) # enable via --check_sender=mx or --check_sender=mx,mx/24 sub check_sender_mx($$$) { my ($sender, $ip, $subnet) = @_; my ($user, $hostname) = split(/\@/,$sender); my @iplist; if ( $dns_cache{$hostname}{mx} ) { @iplist = @{$dns_cache{$hostname}{mx}}; # use the cache for MX records } else { my @mxr = mx($dns, $hostname); # no cache existed, call out to Net::DNS # mx records if ($#mxr >= 0) { foreach my $mxrr (@mxr) { # print "MX for $hostname: ". $mxrr->exchange . "\n"; my $ipquery = $dns->search($mxrr->exchange); if ($ipquery) { foreach my $iprr ($ipquery->answer) { next unless ($iprr->type eq "A"); # print " IP=" . $iprr->address . "\n"; push (@iplist, $iprr->address); } } } } if ( $#iplist < 0 ) { push (@iplist, '0.0.0.0'); } # cache ip of all zero's so we dont keep calling net::dns if nothing is returned $dns_cache{$hostname}{mx} = [ @iplist ]; # cache the array IPs of the MX records into an hash location. } $subnet =~ s/^mx//i; return check_sender_ip_vs_list($ip, $subnet, \@iplist); } ########################################################################### # check_sender_a(sender, ip, subnet) # subnet='' or '/24' # return true if ip is in A record for sender domain (or /24 if specified) # enable via --check_sender=a or --check_sender=a,24 sub check_sender_a($$$) { my ($sender, $ip, $subnet) = @_; my ($user, $hostname) = split(/\@/,$sender); my @iplist; if ( $dns_cache{$hostname}{a} ) { @iplist = @{$dns_cache{$hostname}{a}}; # use the cache'd A records } else { my $ipquery = $dns->search($hostname); # no cache existed, call out to Net::DNS if ($ipquery) { foreach my $iprr ($ipquery->answer) { next unless ($iprr->type eq "A"); # print " IP=" . $iprr->address . "\n"; push (@iplist, $iprr->address); } } if ( $#iplist < 0 ) { push (@iplist, '0.0.0.0'); } # cache ip of all zero's so we dont keep calling net::dns if nothing is returned $dns_cache{$hostname}{a} = [ @iplist ]; # cache the array IPs of the A records into an hash location. } $subnet =~ s/^a//i; return check_sender_ip_vs_list($ip, $subnet, \@iplist); } ################################################### # used by check_sender_mx and check_sender_a # return true if IP is in list # if /24 then return true if first 3 octets match sub check_sender_ip_vs_list($$$) { my ($client_ip, $match, $iplist) = @_; foreach my $ipaddr ( @{$iplist} ) { return 1 if ($client_ip eq $ipaddr); return 0 if (! $match eq '/24'); $client_ip =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.)/; my $client_classaddr = $1; $ipaddr =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.)/; my $ipaddr_classaddr = $1; return 1 if ( $client_classaddr eq $ipaddr_classaddr ); } return 0 } ######################################### # drop_priv(username) # code from Net::Server sub drop_priv { my ($user) = @_; ### drop privileges eval{ if( $user ne $> ){ # print "Setting uid to \"$user\"\n"; set_uid( $user ); } }; if( $@ ){ if( $> == 0 ){ die $@; } elsif( $< == 0){ # print "NOTICE: Effective UID changed, but Real UID is 0: $@\n"; }else{ print $@."\n"; } } } ###########################################3 # setup_dbm(dbdir) # connect to BerkeleyDB *READ_ONLY*, return reference to db hash sub setup_dbm { my ($dbdir) = @_; my %db; tie(%db, 'BerkeleyDB::Btree', -Filename => "$dbdir/postgrey.db", -Flags => DB_RDONLY, ) or die "ERROR: can't find database $dbdir/postgrey.db: $!\n"; return \%db; } # from postgrey 1.14 http://isg.ee.ethz.ch/tools/postgrey/ sub do_sender_substitutions($) { my ($addr) = @_; my ($user, $domain) = split(/@/, $addr, 2); defined $domain or return $addr; # strip extension, used sometimes for mailing-list VERP $user =~ s/\+.*//; # replace numbers in VERP addresses with '#' so that # we don't create a new key for each mail $user =~ s/\b\d+\b/#/g; return "$user\@$domain"; } # from postgrey 1.14 http://isg.ee.ethz.ch/tools/postgrey/ sub do_client_substitutions($$) { my ($ip, $revdns) = @_; # --lookup-by-subnet: return ($ip, undef) if $revdns eq 'unknown'; my @ip=split(/\./, $ip); return ($ip, undef) unless defined $ip[3]; # skip if it contains the last two IP numbers in the hostname # (we assume it is a pool of dialup addresses of a provider) return ($ip, undef) if $revdns =~ /$ip[2]/ and $revdns =~ /$ip[3]/; return (join('.', @ip[0..2], '0'), $ip[3]); } ## used code from postgrey for read_client_whitelists() to import client files sub read_client_files() { my @skip_client_ips; my @skip_client_ptr; my @match_client_ips; my @match_client_ptr; for my $f (@{$opt{'skip_clients'}}) { if(open(CLIENTS, $f)) { while(<CLIENTS>) { s/^\s+//; s/\s+$//; next if $_ eq '' or /^#/; if(/^\/(\S+)\/$/) { # regular expression push @skip_client_ptr, qr{$1}i; } elsif(/^\d{1,3}(?:\.\d{1,3}){0,3}$/) { # IP address or part of it push @skip_client_ips, qr{^$_}; } # note: we had ^[^\s\/]+$ but it triggers a bug in perl 5.8.0 elsif(/^\S+$/) { push @skip_client_ptr, qr{\Q$_\E$}i; } else { warn "WARNING: $f line $.: doesn't look like a hostname\n"; } } } } $opt{SKIP_CLIENT_PTR} = \@skip_client_ptr; $opt{SKIP_CLIENT_IPS} = \@skip_client_ips; for my $f (@{$opt{'match_clients'}}) { if(open(CLIENTS, $f)) { while(<CLIENTS>) { s/^\s+//; s/\s+$//; next if $_ eq '' or /^#/; if(/^\/(\S+)\/$/) { # regular expression push @match_client_ptr, qr{$1}i; } elsif(/^\d{1,3}(?:\.\d{1,3}){0,3}$/) { # IP address or part of it push @match_client_ips, qr{^$_}; } # note: we had ^[^\s\/]+$ but it triggers a bug in perl 5.8.0 elsif(/^\S+$/) { push @match_client_ptr, qr{\Q$_\E$}i; } else { warn "WARNING: $f line $.: doesn't look like a hostname\n"; } } } } $opt{MATCH_CLIENT_PTR} = \@match_client_ptr; $opt{MATCH_CLIENT_IPS} = \@match_client_ips; } sub setup_debug() { if ($opt{debug_db} or $opt{search_db}) { die "\nDebugging_DB Activated, but no matching RE's defined. use --debug_re also! \n " if (! $opt{debug_re} ); print "\nDebugging_DB Active, Displaying hosting matching REs: "; foreach my $RE ( split(/,/,$opt{debug_re}) ) { print "$RE ; "; push ( @{ $opt{debug_RE} }, qr/$RE/i ); } print "\n\n"; } } sub is_debug_host($) { my ($host) = @_; foreach my $RE ( @{$opt{debug_RE}} ) { return 1 if ($host =~ /$RE/); } return 0; } main(); exit 0; __END__ =head1 NAME postgreyreport - Fatal report for Postfix Greylisting Policy Server =head1 SYNOPSIS B<postgreyreport> [I<options>...] -h, --help display this help and exit --version display version and exit --user=USER run as USER (default: postgrey) --dbdir=PATH find db files in PATH (default: /var/lib/postgrey) --delay=N report triplets that did not try again after N seconds (default: 300) --greylist-text=TXT text to match on for greylist maillog lines --skip_pool Skip report for 'subscriber pools' ( last 2 octets of IP found in PTR name ) --skip_dnsbl=RBL RBL server to query and skip reporting for any listed hosts (SLOW!!) --skip_clients=FILE PTR or IP or REGEXP of clients to skip in report --match_clients=FILE *ONLY* report if fatal *AND* PTR/IP of client matches --show_tries display the number of attempts failed triplets made in first column --show_time show entry time in maillog (single line only) --tab use tabs as separators for easy cut(1)ting --nosingle_line display sender/recipients grouped by ptr - ip --separate_by_subnet=TXT display TXT for every new /24 (ex: "=================\n" ) --separate_by_ip=TXT display TXT for every new IP (ex: "\n") --check_sender=LIST one or more of: mx,mx/24,a,a/24 does DNS/A lookups for sender @domain and compares sending IP if match displays "MX" "A" or "MX/24" or "A/24" depending on LIST Note that --(skip|match)_clients can be specified multiple times and there are no default files. Same rules apply as postgrey's --whitelist-clients, see postgrey doc for more info. --skip_dnsbl can also be specified multiple times to query multiple DNSBL servers. =head1 DESCRIPTION postgreyreport opens postgrey.db as read-only; reads a maillog via STDIN, extracts the triplets for any Greylisted lines and looks them up in postgrey.db. if the difference in first and last time seen is less than --delay=N then the triplet is considered fatal and displayed to STDOUT The report sorts by client IP address =head2 Note: unless you are using --lookup_by_subnet or excluding all known MTA pools you will likely have false fatal reports for "BigISPs". A message that was tried from every IP in SMTP pool before making it through will show up in the report for all of the attempted source IPs =head2 USAGE It is best to run postgreyreport against a maillog that is at least several hours old (yesterdays?) ( you be the judge on how old is acceptable ). if you run the report against a live maillog you are not giving legit MTA's enough time to try again and you will have lots of inaccurate information. =over =item * Ex usage: zcat /var/log/maillog.0.gz | ./postgreyreport [options] > postgreyreport.log or zcat /var/log/maillog.0.gz | \ ./postgreyreport --nosingle_line --check_sender=mx,a \ --separate_by_subnet=":==================\n" # 94 "=" total, some were omitted for clarity =item * Ex Output: ( POD wrapping will mess this up, view source ) :============================================================================================ unknown 4.29.43.31 marissa_mcclendonuu@abit.com.tw user1@recipient1.com jake_meyerdt@ali.com.tw user2@recipient1.com jenny_banks_sh@translate.ru user1@recipient2.com rvazquezpo@ali.com.tw user3@recipient1.com aep@notimexico.com user2@recipient1.com brittneystanley_ei@cetra.org.tw user2@recipient1.com brendasheehan_cw@lib.ru user2@recipient1.com :============================================================================================ lsanca1-ar5-127-189.biz.dsl.gtei.net 4.33.127.189 A fokkensr@lsanca1-ar5-127-189.biz.dsl.gtei.net user2@recipient1.com cyxlfrfwciercu@publicist.com user3@recipient4.com :============================================================================================ smtpout.mac.com 17.250.248.83 do_not_reply@apple.com user4@recipient5.com smtpout.mac.com 17.250.248.88 MX legituser@mac.com user6@recipient7.com :============================================================================================ =back =head1 HISTORY B<1.14.3 20100321> =over 4 Some additions, Leonard den Ottolander <leonard.den.ottolander.nl> New option: --tab Use tabs as separator in single line mode New option: --show_time Show entry time in maillog in single line mode =back B<1.14.2 20040715> =over 4 BUGFIX: (automatic) lookup-by-subnet support was broken, fixed. BUGFIX: corrected a few spelling errors new Option: --skip_pool Skip report for 'subscriber pools' =back B<1.14.1 20040712> =over 4 Changed --return-string to --greylist-text to match postgrey new Option: --skip_clients=FILE new Option: --match_clients=FILE new Option: --skip_dnsbl=RBL.DNS.NAME All 3 of the new options can be specified multiple times. Updated do_*_subsititions again to match postgrey =back B<1.11.1 20040701> =over 4 missing keys from DB are considered fatal triplets and included in report Changed --delay testing from "greater than" to "greater than or equal to" Fixed --help and --man switches Removed setuid Notice =back B<1.6.4 20040618> =over 4 Initial Public Version (postgrey/contrib) =back =head1 AUTHOR S<Tom Baker E<lt>tbaker@bakerfl.orgE<gt>> =cutPrivate