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