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