1#!/usr/bin/perl -w 2############################################################ 3# 4# $Id$ 5# rrd-client-mta.pl - MTA data gathering script for rrd-server.pl 6# 7# Copyright 2007, 2008 Nicola Worthington 8# 9# Licensed under the Apache License, Version 2.0 (the "License"); 10# you may not use this file except in compliance with the License. 11# You may obtain a copy of the License at 12# 13# http://www.apache.org/licenses/LICENSE-2.0 14# 15# Unless required by applicable law or agreed to in writing, software 16# distributed under the License is distributed on an "AS IS" BASIS, 17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18# See the License for the specific language governing permissions and 19# limitations under the License. 20# 21############################################################ 22# vim:ts=4:sw=4:tw=78 23 24use constant MAIL_LOG => '/var/log/maillog'; 25use constant RRD_SERVER_URL => 'http://rrd.me.uk/cgi-bin/rrd-server.cgi'; 26use constant DEBUG => $ENV{DEBUG} ? 1 : 0; 27use constant RRD_STEPPING => 60; # seconds 28 29############################################################ 30# 31# NO USER SERVICABLE PARTS BEYOND THIS POINT 32# 33############################################################ 34 35 36use 5.6.1; 37use strict; 38use warnings; 39 40use Parse::Syslog qw(); 41use File::Tail qw(); 42use Getopt::Std qw(); 43use LWP::UserAgent qw(); 44use HTTP::Request::Common qw(); 45use Proc::DaemonLite qw(); 46 47our $VERSION = sprintf('%d.%02d', q$Revision: 1.1 $ =~ /(\d+)/g); 48 49my $this_minute; 50my %opt = ('ignore-localhost' => 1); 51my %sum = map { $_ => 0 } qw(sent received bounced rejected spam virus); 52 53#my $tail = File::Tail->new(name => MAIL_LOG, tail => -1); 54my $tail = File::Tail->new(name => MAIL_LOG, tail => 0); 55 56my $parser = new Parse::Syslog($tail, 57 year => (localtime(time))[5]+1900, 58 arrayref => 1, 59 type => 'syslog' 60 ); 61 62my $pid = Proc::DaemonLite::init_server(); 63 64while (my $sl = $parser->next) { 65 process_line($sl); 66} 67 68exit; 69 70 71sub process_line { 72 my $sl = shift; 73 my $time = $sl->[0]; 74 my $prog = $sl->[2]; 75 my $text = $sl->[4]; 76 77 if($prog eq 'exim') { 78 if($text =~ /^[0-9a-zA-Z]{6}-[0-9a-zA-Z]{6}-[0-9a-zA-Z]{2} <= \S+/) { 79 event($time, 'received'); 80 } 81 elsif($text =~ /^[0-9a-zA-Z]{6}-[0-9a-zA-Z]{6}-[0-9a-zA-Z]{2} => \S+/) { 82 event($time, 'sent'); 83 } 84# rejected after DATA: Your message scored 10.4 SpamAssassin point. Report follows: 85 elsif($text =~ / rejected because \S+ is in a black list at \S+/) { 86 if($opt{'rbl-is-spam'}) { 87 event($time, 'spam'); 88 } else { 89 event($time, 'rejected'); 90 } 91 } 92 elsif($text =~ / rejected RCPT \S+: (Sender verify failed|Unknown user)/) { 93 event($time, 'rejected'); 94 } 95 } 96 elsif($prog =~ /^postfix\/(.*)/) { 97 my $prog = $1; 98 if($prog eq 'smtp') { 99 if($text =~ /\bstatus=sent\b/) { 100 return if $opt{'ignore-localhost'} and 101 $text =~ /\brelay=[^\s\[]*\[127\.0\.0\.1\]/; 102 return if $opt{'ignore-host'} and 103 $text =~ /\brelay=[^\s,]*$opt{'ignore-host'}/oi; 104 event($time, 'sent'); 105 } 106 elsif($text =~ /\bstatus=bounced\b/) { 107 event($time, 'bounced'); 108 } 109 } 110 elsif($prog eq 'local') { 111 if($text =~ /\bstatus=bounced\b/) { 112 event($time, 'bounced'); 113 } 114 } 115 elsif($prog eq 'smtpd') { 116 if($text =~ /^[0-9A-Z]+: client=(\S+)/) { 117 my $client = $1; 118 return if $opt{'ignore-localhost'} and 119 $client =~ /\[127\.0\.0\.1\]$/; 120 return if $opt{'ignore-host'} and 121 $client =~ /$opt{'ignore-host'}/oi; 122 event($time, 'received'); 123 } 124 elsif($opt{'virbl-is-virus'} and $text =~ /^(?:[0-9A-Z]+: |NOQUEUE: )?reject: .*: 554.* blocked using virbl.dnsbl.bit.nl/) { 125 event($time, 'virus'); 126 } 127 elsif($opt{'rbl-is-spam'} and $text =~ /^(?:[0-9A-Z]+: |NOQUEUE: )?reject: .*: 554.* blocked using/) { 128 event($time, 'spam'); 129 } 130 elsif($text =~ /^(?:[0-9A-Z]+: |NOQUEUE: )?reject: /) { 131 event($time, 'rejected'); 132 } 133 } 134 elsif($prog eq 'error') { 135 if($text =~ /\bstatus=bounced\b/) { 136 event($time, 'bounced'); 137 } 138 } 139 elsif($prog eq 'cleanup') { 140 if($text =~ /^[0-9A-Z]+: (?:reject|discard): /) { 141 event($time, 'rejected'); 142 } 143 } 144 } 145 elsif($prog eq 'sendmail' or $prog eq 'sm-mta') { 146 if($text =~ /\bmailer=local\b/ ) { 147 event($time, 'received'); 148 } 149 elsif($text =~ /\bmailer=relay\b/) { 150 event($time, 'received'); 151 } 152 elsif($text =~ /\bstat=Sent\b/ ) { 153 event($time, 'sent'); 154 } 155 elsif($text =~ /\bmailer=esmtp\b/ ) { 156 event($time, 'sent'); 157 } 158 elsif($text =~ /\bruleset=check_XS4ALL\b/ ) { 159 event($time, 'rejected'); 160 } 161 elsif($text =~ /\blost input channel\b/ ) { 162 event($time, 'rejected'); 163 } 164 elsif($text =~ /\bruleset=check_rcpt\b/ ) { 165 event($time, 'rejected'); 166 } 167 elsif($text =~ /\bstat=virus\b/ ) { 168 event($time, 'virus'); 169 } 170 elsif($text =~ /\bruleset=check_relay\b/ ) { 171 if (($opt{'virbl-is-virus'}) and ($text =~ /\bivirbl\b/ )) { 172 event($time, 'virus'); 173 } elsif ($opt{'rbl-is-spam'}) { 174 event($time, 'spam'); 175 } else { 176 event($time, 'rejected'); 177 } 178 } 179 elsif($text =~ /\bsender blocked\b/ ) { 180 event($time, 'rejected'); 181 } 182 elsif($text =~ /\bsender denied\b/ ) { 183 event($time, 'rejected'); 184 } 185 elsif($text =~ /\brecipient denied\b/ ) { 186 event($time, 'rejected'); 187 } 188 elsif($text =~ /\brecipient unknown\b/ ) { 189 event($time, 'rejected'); 190 } 191 elsif($text =~ /\bUser unknown$/i ) { 192 event($time, 'bounced'); 193 } 194 elsif($text =~ /\bMilter:.*\breject=55/ ) { 195 event($time, 'rejected'); 196 } 197 } 198 elsif($prog eq 'amavis' || $prog eq 'amavisd') { 199 if( $text =~ /^\([0-9-]+\) (Passed|Blocked) SPAM(?:MY)?\b/) { 200 event($time, 'spam'); # since amavisd-new-2004xxxx 201 } 202 elsif($text =~ /^\([0-9-]+\) (Passed|Not-Delivered)\b.*\bquarantine spam/) { 203 event($time, 'spam'); # amavisd-new-20030616 and earlier 204 } 205 ### UNCOMMENT IF YOU USE AMAVISD-NEW <= 20030616 WITHOUT QUARANTINE: 206 #elsif($text =~ /^\([0-9-]+\) Passed, .*, Hits: (\d*\.\d*)/) { 207 # if ($1 >= 5.0) { # amavisd-new-20030616 without quarantine 208 # event($time, 'spam'); 209 # } 210 #} 211 elsif($text =~ /^\([0-9-]+\) (Passed |Blocked )?INFECTED\b/) { 212 if($text !~ /\btag2=/) { # ignore new per-recipient log entry (2.2.0) 213 event($time, 'virus');# Passed|Blocked inserted since 2004xxxx 214 } 215 } 216 elsif($text =~ /^\([0-9-]+\) (Passed |Blocked )?BANNED\b/) { 217 if($text !~ /\btag2=/) { 218 event($time, 'virus'); 219 } 220 } 221# elsif($text =~ /^\([0-9-]+\) Passed|Blocked BAD-HEADER\b/) { 222# event($time, 'badh'); 223# } 224 elsif($text =~ /^Virus found\b/) { 225 event($time, 'virus');# AMaViS 0.3.12 and amavisd-0.1 226 } 227 } 228 elsif($prog eq 'vagatefwd') { 229 # Vexira antivirus (old) 230 if($text =~ /^VIRUS/) { 231 event($time, 'virus'); 232 } 233 } 234 elsif($prog eq 'hook') { 235 # Vexira antivirus 236 if($text =~ /^\*+ Virus\b/) { 237 event($time, 'virus'); 238 } 239 # Vexira antispam 240 elsif($text =~ /\bcontains spam\b/) { 241 event($time, 'spam'); 242 } 243 } 244 elsif($prog eq 'avgatefwd' or $prog eq 'avmailgate.bin') { 245 # AntiVir MailGate 246 if($text =~ /^Alert!/) { 247 event($time, 'virus'); 248 } 249 elsif($text =~ /blocked\.$/) { 250 event($time, 'virus'); 251 } 252 } 253 elsif($prog eq 'avcheck') { 254 # avcheck 255 if($text =~ /^infected/) { 256 event($time, 'virus'); 257 } 258 } 259 elsif($prog eq 'spamd') { 260 if($text =~ /^(?:spamd: )?identified spam/) { 261 event($time, 'spam'); 262 } 263 # ClamAV SpamAssassin-plugin 264 elsif($text =~ /(?:result: )?CLAMAV/) { 265 event($time, 'virus'); 266 } 267 } 268 elsif($prog eq 'dspam') { 269 if($text =~ /spam detected from/) { 270 event($time, 'spam'); 271 } 272 } 273 elsif($prog eq 'spamproxyd') { 274 if($text =~ /^\s*SPAM/ or $text =~ /^identified spam/) { 275 event($time, 'spam'); 276 } 277 } 278 elsif($prog eq 'drweb-postfix') { 279 # DrWeb 280 if($text =~ /infected/) { 281 event($time, 'virus'); 282 } 283 } 284 elsif($prog eq 'BlackHole') { 285 if($text =~ /Virus/) { 286 event($time, 'virus'); 287 } 288 if($text =~ /(?:RBL|Razor|Spam)/) { 289 event($time, 'spam'); 290 } 291 } 292 elsif($prog eq 'MailScanner') { 293 if($text =~ /(Virus Scanning: Found)/ ) { 294 event($time, 'virus'); 295 } 296 elsif($text =~ /Bounce to/ ) { 297 event($time, 'bounced'); 298 } 299 elsif($text =~ /^Spam Checks: Found ([0-9]+) spam messages/) { 300 my $cnt = $1; 301 for (my $i=0; $i<$cnt; $i++) { 302 event($time, 'spam'); 303 } 304 } 305 } 306 elsif($prog eq 'clamsmtpd') { 307 if($text =~ /status=VIRUS/) { 308 event($time, 'virus'); 309 } 310 } 311 elsif($prog eq 'clamav-milter') { 312 if($text =~ /Intercepted/) { 313 event($time, 'virus'); 314 } 315 } 316 # uncommment for clamassassin: 317 #elsif($prog eq 'clamd') { 318 # if($text =~ /^stream: .* FOUND$/) { 319 # event($time, 'virus'); 320 # } 321 #} 322 elsif ($prog eq 'smtp-vilter') { 323 if ($text =~ /clamd: found/) { 324 event($time, 'virus'); 325 } 326 } 327 elsif($prog eq 'avmilter') { 328 # AntiVir Milter 329 if($text =~ /^Alert!/) { 330 event($time, 'virus'); 331 } 332 elsif($text =~ /blocked\.$/) { 333 event($time, 'virus'); 334 } 335 } 336 elsif($prog eq 'bogofilter') { 337 if($text =~ /Spam/) { 338 event($time, 'spam'); 339 } 340 } 341 elsif($prog eq 'filter-module') { 342 if($text =~ /\bspam_status\=(?:yes|spam)/) { 343 event($time, 'spam'); 344 } 345 } 346 elsif($prog eq 'sta_scanner') { 347 if($text =~ /^[0-9A-F]+: virus/) { 348 event($time, 'virus'); 349 } 350 } 351} 352 353sub event { 354 my ($t, $type) = @_; 355 update($t) && $sum{$type}++; 356} 357 358# returns 1 if $sum should be updated 359sub update($) { 360 my $t = shift; 361 my $m = $t - $t % RRD_STEPPING; 362 $this_minute = $m unless defined $this_minute; 363 return 1 if $m == $this_minute; 364 return 0 if $m < $this_minute; 365 366 my $data = ''; 367 for (sort keys %sum) { 368 $data .= "$this_minute.mail.traffic.$_ $sum{$_}\n"; 369 } 370 warn $data if DEBUG; 371 372 my $ua = LWP::UserAgent->new(agent => $0); 373 my $resp = $ua->request( 374 HTTP::Request::Common::POST( 375 RRD_SERVER_URL, 376 Content_Type => 'text/plain', 377 Content => $data 378 ) 379 ); 380 if ($resp->is_success) { 381 printf("%s\n",$resp->content); 382 } else { 383 warn 'Posting Error: '.$resp->status_line; 384 } 385 386 $this_minute = $m; 387 $sum{$_} = 0 for keys %sum; 388 return 1; 389} 390 391__END__ 392 393