1 |
dpavlin |
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 |
|
|
|