fef37f54ce
passing in -l for when your term windows is more than 200 chars wide will show more detail
371 lines
13 KiB
Perl
Executable File
371 lines
13 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Cwd;
|
|
use Data::Dumper;
|
|
use File::Tail;
|
|
use Getopt::Std;
|
|
|
|
$Data::Dumper::Sortkeys = 1;
|
|
|
|
our $opt_l = 0;
|
|
getopts('l');
|
|
|
|
my (%plugins, %plugin_aliases, %seen_plugins, %pids);
|
|
my %hide_plugins = map { $_ => 1 } qw/ hostname /;
|
|
|
|
my $qpdir = get_qp_dir();
|
|
my $file = "$qpdir/log/main/current";
|
|
populate_plugins_from_registry();
|
|
my @sorted_plugins =
|
|
sort { $plugins{$a}{id} <=> $plugins{$b}{id} } keys %plugins;
|
|
|
|
my $fh = File::Tail->new(
|
|
name => $file,
|
|
interval => 1,
|
|
maxinterval => 1,
|
|
debug => 1,
|
|
tail => 1000
|
|
);
|
|
my $printed = 0;
|
|
my $has_cleanup;
|
|
|
|
my %formats = (
|
|
ip => "%-15.15s",
|
|
hostname => "%-20.20s",
|
|
distance => "%5.5s",
|
|
'ident::geoip' => $opt_l ? "%-20.20s" : "%-6.6s",
|
|
'ident::p0f' => "%-10.10s",
|
|
count_unrecognized_commands => "%-5.5s",
|
|
unrecognized_commands => "%-5.5s",
|
|
dnsbl => "%-3.3s",
|
|
rhsbl => "%-3.3s",
|
|
relay => "%-3.3s",
|
|
karma => "%-3.3s",
|
|
fcrdns => "%-3.3s",
|
|
earlytalker => "%-3.3s",
|
|
check_earlytalker => "%-3.3s",
|
|
helo => "%-3.3s",
|
|
tls => "%-3.3s",
|
|
'auth::auth_vpopmail' => "%-3.3s",
|
|
'auth::auth_vpopmaild' => "%-3.3s",
|
|
'auth::auth_vpopmail_sql' => "%-3.3s",
|
|
'auth::auth_checkpassword' => "%-3.3s",
|
|
badmailfrom => "%-3.3s",
|
|
check_badmailfrom => "%-3.3s",
|
|
sender_permitted_from => "%-3.3s",
|
|
resolvable_fromhost => "%-3.3s",
|
|
'queue::qmail-queue' => "%-3.3s",
|
|
connection_time => "%-4.4s",
|
|
);
|
|
|
|
my %formats3 = (
|
|
%formats,
|
|
badrcptto => "%-3.3s",
|
|
check_badrcptto => "%-3.3s",
|
|
qmail_deliverable => "%-3.3s",
|
|
rcpt_ok => "%-3.3s",
|
|
check_basicheaders => "%-3.3s",
|
|
headers => "%-3.3s",
|
|
uribl => "%-3.3s",
|
|
bogus_bounce => "%-3.3s",
|
|
check_bogus_bounce => "%-3.3s",
|
|
domainkeys => "%-3.3s",
|
|
dkim => "%-3.3s",
|
|
dmarc => "%-3.3s",
|
|
spamassassin => "%-3.3s",
|
|
dspam => "%-3.3s",
|
|
'virus::clamdscan' => "%-3.3s",
|
|
);
|
|
|
|
while (defined(my $line = $fh->read)) {
|
|
chomp $line;
|
|
next if !$line;
|
|
my ($type, $pid, $hook, $plugin, $message) = parse_line($line);
|
|
next if !$type;
|
|
next if $type =~ /^(info|unknown|response|tcpserver)$/;
|
|
next if $type eq 'init'; # doesn't occur in all deployment models
|
|
|
|
if (!$pids{$pid}) { # haven't seen this pid
|
|
next if $type ne 'connect'; # ignore unless connect
|
|
my ($host, $ip) = split /\s/, $message;
|
|
$ip = substr $ip, 1, -1;
|
|
foreach (keys %seen_plugins, qw/ helo_host from to /) {
|
|
$pids{$pid}{$_} = '';
|
|
}
|
|
$pids{$pid}{ip} = $ip;
|
|
$pids{$pid}{hostname} = $host if $host ne 'Unknown';
|
|
}
|
|
|
|
if ($type eq 'close') {
|
|
next if $has_cleanup; # it'll get handled later
|
|
print_auto_format($pid, $line);
|
|
delete $pids{$pid};
|
|
}
|
|
elsif ($type eq 'cleanup') {
|
|
print_auto_format($pid, $line);
|
|
delete $pids{$pid};
|
|
}
|
|
elsif ($type eq 'plugin') {
|
|
next if $plugin eq 'naughty'; # housekeeping only
|
|
if (!$pids{$pid}{$plugin}) { # first entry for this plugin
|
|
$pids{$pid}{$plugin} = $message;
|
|
}
|
|
else { # subsequent log entry for this plugin
|
|
if ($pids{$pid}{$plugin} !~ /^(?:pass|fail|skip)/i) {
|
|
$pids{$pid}{$plugin} = $message; # overwrite 1st
|
|
}
|
|
else {
|
|
#print "ignoring subsequent hit on $plugin: $message\n";
|
|
}
|
|
}
|
|
|
|
if ($plugin eq 'ident::geoip') {
|
|
if (length $message < 3) {
|
|
$formats{'ident::geoip'} = "%-3.3s";
|
|
$formats3{'ident::geoip'} = "%-3.3s";
|
|
}
|
|
else {
|
|
my ($gip, $distance) = $message =~ /(.*?),\s+([\d]+)\skm/;
|
|
if ($distance) {
|
|
$pids{$pid}{$plugin} = $gip;
|
|
$pids{$pid}{distance} = $distance;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
elsif ($type eq 'reject') { }
|
|
elsif ($type eq 'connect') { }
|
|
elsif ($type eq 'dispatch') {
|
|
if ($message =~ /^dispatching MAIL FROM/i) {
|
|
my ($from) = $message =~ /<(.*?)>/;
|
|
$pids{$pid}{from} = $from;
|
|
}
|
|
elsif ($message =~ /^dispatching RCPT TO/i) {
|
|
my ($to) = $message =~ /<(.*?)>/;
|
|
$pids{$pid}{to} = $to;
|
|
}
|
|
elsif ($message =~ m/dispatching (EHLO|HELO) (.*)/) {
|
|
$pids{$pid}{helo_host} = $2;
|
|
}
|
|
elsif ($message eq 'dispatching DATA') { }
|
|
elsif ($message eq 'dispatching QUIT') { }
|
|
elsif ($message eq 'dispatching STARTTLS') { }
|
|
elsif ($message eq 'dispatching RSET') {
|
|
print_auto_format($pid, $line);
|
|
}
|
|
else {
|
|
# anything here is likely an unrecognized command
|
|
#print "$message\n";
|
|
}
|
|
}
|
|
else {
|
|
print "$type $pid $hook $plugin $message\n";
|
|
}
|
|
}
|
|
|
|
sub parse_line {
|
|
my $line = shift;
|
|
my ($tai, $pid, $message) = split /\s+/, $line, 3;
|
|
return if !$message; # garbage in the log file
|
|
|
|
# lines seen many times per connection
|
|
return parse_line_plugin($line) if substr($message, 0, 1) eq '(';
|
|
return ('dispatch', $pid, undef, undef, $message)
|
|
if substr($message, 0, 12) eq 'dispatching ';
|
|
return ('response', $pid, undef, undef, $message)
|
|
if $message =~ /^[2|3]\d\d/;
|
|
return ('tcpserver', $pid, undef, undef, undef)
|
|
if substr($pid, 0, 10) eq 'tcpserver:';
|
|
|
|
# lines seen about once per connection
|
|
return ('init', $pid, undef, undef, $message)
|
|
if substr($message, 0, 19) eq 'Accepted connection';
|
|
return ('connect', $pid, undef, undef, substr($message, 16))
|
|
if substr($message, 0, 15) eq 'Connection from';
|
|
return ('close', $pid, undef, undef, $message)
|
|
if substr($message, 0, 6) eq 'close ';
|
|
return ('close', $pid, undef, undef, $message)
|
|
if substr($message, 0, 20) eq 'click, disconnecting';
|
|
return parse_line_cleanup($line)
|
|
if substr($message, 0, 11) eq 'cleaning up';
|
|
|
|
# lines seen less than once per connection
|
|
return ('info', $pid, undef, undef, $message)
|
|
if $message eq 'spooling message to disk';
|
|
return ('reject', $pid, undef, undef, $message) if $message =~ /^[4|5]\d\d/;
|
|
return ('reject', $pid, undef, undef, $message)
|
|
if substr($message, 0, 14) eq 'deny mail from';
|
|
return ('reject', $pid, undef, undef, $message)
|
|
if substr($message, 0, 18) eq 'denysoft mail from';
|
|
return ('info', $pid, undef, undef, $message)
|
|
if substr($message, 0, 15) eq 'Lost connection';
|
|
return ('info', $pid, undef, undef, $message)
|
|
if $message eq 'auth success cleared naughty';
|
|
return ('info', $pid, undef, undef, $message)
|
|
if substr($message, 0, 15) eq 'Running as user';
|
|
return ('info', $pid, undef, undef, $message)
|
|
if substr($message, 0, 16) eq 'Loaded Qpsmtpd::';
|
|
return ('info', $pid, undef, undef, $message)
|
|
if substr($message, 0, 24) eq 'Permissions on spool_dir';
|
|
return ('info', $pid, undef, undef, $message)
|
|
if substr($message, 0, 13) eq 'Listening on ';
|
|
|
|
return ('err', $pid, undef, undef, $message)
|
|
if $line =~ /at [\S]+ line \d/; # generic perl error
|
|
print "UNKNOWN LINE: $line\n";
|
|
return ('unknown', $pid, undef, undef, $message);
|
|
}
|
|
|
|
sub parse_line_plugin {
|
|
my ($line) = @_;
|
|
|
|
# @tai 13486 (connect) ident::p0f: Windows (XP/2000 (RFC1323+, w, tstamp-))
|
|
# @tai 13681 (connect) dnsbl: fail, NAUGHTY
|
|
# @tai 15787 (connect) karma: pass, no penalty (0 naughty, 3 nice, 3 connects)
|
|
# @tai 77603 (queue) queue::qmail_2dqueue: (for 77590) Queuing to /var/qmail/bin/qmail-queue
|
|
my ($tai, $pid, $hook, $plugin, $message) = split /\s/, $line, 5;
|
|
$plugin =~ s/:$//;
|
|
if ($plugin =~ /_3a/) {
|
|
($plugin) = split /_3a/, $plugin; # trim :N off the plugin log entry
|
|
}
|
|
$plugin =~ s/_2d/-/g;
|
|
|
|
$plugin = $plugin_aliases{$plugin}
|
|
if $plugin_aliases{$plugin}; # map alias to master
|
|
if ($hook eq '(queue)') {
|
|
($pid) = $message =~ /\(for ([\d]+)\)\s/;
|
|
$message = 'pass';
|
|
}
|
|
|
|
return ('plugin', $pid, $hook, $plugin, $message);
|
|
}
|
|
|
|
sub parse_line_cleanup {
|
|
my ($line) = @_;
|
|
|
|
# @tai 85931 cleaning up after 3210
|
|
my $pid = (split /\s+/, $line)[-1];
|
|
$has_cleanup++;
|
|
return ('cleanup', $pid, undef, undef, $line);
|
|
}
|
|
|
|
sub print_auto_format {
|
|
my ($pid, $line) = @_;
|
|
|
|
my $format;
|
|
my @headers;
|
|
my @values;
|
|
|
|
foreach my $plugin (qw/ ip hostname distance /, @sorted_plugins) {
|
|
if (defined $pids{$pid}{$plugin}) {
|
|
if (!$seen_plugins{$plugin}) { # first time seeing this plugin
|
|
$printed = 0; # force header print
|
|
}
|
|
$seen_plugins{$plugin}++;
|
|
}
|
|
|
|
next if !$seen_plugins{$plugin}; # hide unused plugins
|
|
if ($hide_plugins{$plugin}) { # user doesn't want to see
|
|
delete $pids{$pid}{$plugin};
|
|
next;
|
|
}
|
|
|
|
my $wide = $opt_l ? 20 : 8;
|
|
|
|
if (defined $pids{$pid}{helo_host} && $plugin =~ /helo/) {
|
|
$format .= " %-$wide.${wide}s";
|
|
push @values, substr(delete $pids{$pid}{helo_host}, -$wide, $wide);
|
|
push @headers, 'HELO';
|
|
}
|
|
elsif (defined $pids{$pid}{from} && $plugin =~ /from/) {
|
|
$format .= " %-$wide.${wide}s";
|
|
push @values, substr(delete $pids{$pid}{from}, -$wide, $wide);
|
|
push @headers, 'MAIL FROM';
|
|
}
|
|
elsif (defined $pids{$pid}{to} && $plugin =~ /to|rcpt|recipient/) {
|
|
$format .= " %-$wide.${wide}s";
|
|
push @values, delete $pids{$pid}{to};
|
|
push @headers, 'RCPT TO';
|
|
}
|
|
|
|
$format .= $formats3{$plugin} ? " $formats3{$plugin}" : " %-10.10s";
|
|
|
|
if (defined $pids{$pid}{$plugin}) {
|
|
push @values, show_symbol(delete $pids{$pid}{$plugin});
|
|
}
|
|
else {
|
|
push @values, '';
|
|
}
|
|
push @headers,
|
|
($plugins{$plugin}{abb3} ? $plugins{$plugin}{abb3} : $plugin);
|
|
}
|
|
$format .= "\n";
|
|
printf("\n$format", @headers) if (!$printed || $printed % 20 == 0);
|
|
printf($format, @values);
|
|
#print Data::Dumper::Dumper($pids{$pid}) if keys %{$pids{$pid}};
|
|
$printed++;
|
|
}
|
|
|
|
sub show_symbol {
|
|
my $mess = shift;
|
|
return ' o' if $mess eq 'TLS setup returning';
|
|
return ' o' if $mess eq 'pass';
|
|
return ' -' if $mess eq 'skip';
|
|
return ' X' if $mess eq 'fail';
|
|
return ' -' if $mess =~ /^skip[,:\s]/i;
|
|
return ' o' if $mess =~ /^pass[,:\s]/i;
|
|
return ' X' if $mess =~ /^fail[,:\s]/i;
|
|
return ' x' if $mess =~ /^negative[,:\s]/i;
|
|
return ' o' if $mess =~ /^positive[,:\s]/i;
|
|
return ' !' if $mess =~ /^error[,:\s]/i;
|
|
$mess =~ s/\s\s/ /g;
|
|
return $mess;
|
|
}
|
|
|
|
sub get_qp_dir {
|
|
foreach my $user (qw/ qpsmtpd smtpd /) {
|
|
my ($homedir) = (getpwnam($user))[7] or next;
|
|
|
|
if (-d "$homedir/plugins") {
|
|
return "$homedir";
|
|
}
|
|
foreach my $s (qw/ smtpd qpsmtpd qpsmtpd-dev /) {
|
|
if (-d "$homedir/$s/plugins") {
|
|
return "$homedir/$s";
|
|
}
|
|
}
|
|
}
|
|
if (-d "./plugins") {
|
|
return Cwd::getcwd();
|
|
}
|
|
}
|
|
|
|
sub populate_plugins_from_registry {
|
|
|
|
my $file = "$qpdir/plugins/registry.txt";
|
|
if (!-f $file) {
|
|
die "unable to find plugin registry\n";
|
|
}
|
|
|
|
open my $F, '<', $file;
|
|
while (defined(my $line = <$F>)) {
|
|
next if $line =~ /^#/; # discard comments
|
|
chomp $line;
|
|
next if ! $line;
|
|
my ($id, $name, $abb3, $abb5, $aliases) = split /\s+/, $line;
|
|
next if !defined $name;
|
|
$plugins{$name} = {id => $id, abb3 => $abb3, abb5 => $abb5};
|
|
|
|
next if !$aliases;
|
|
$aliases =~ s/\s+//g;
|
|
$plugins{$name}{aliases} = $aliases;
|
|
foreach my $a (split /,/, $aliases) {
|
|
$plugin_aliases{$a} = $name;
|
|
}
|
|
}
|
|
}
|
|
|