-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspamfilter-stats-7.pl
More file actions
1896 lines (1612 loc) · 66.5 KB
/
Copy pathspamfilter-stats-7.pl
File metadata and controls
1896 lines (1612 loc) · 66.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/perl -w
#############################################################################
#
# This script provides daily SpamFilter statistics.
#
# This script was originally developed
# by Jesper Knudsen at http://sme.swerts-knudsen.dk
# and re-written by brian read at bjsystems.co.uk (with some help from the community - thanks guys)
#
# bjr - 02sept12 - Add in qpsmtpd failure code auth::auth_cvm_unix_local as per Bug 7089
# bjr - 10Jun15 - Sort out multiple files as input parameters as per bug 5613
# - Sort out geoip failure status as per Bug 4262
# - change final message about the DB (it is created automatically these days by the rpm)
# bjr - 17Jun15 - Add annotation showing Badcountries being eliminated
# - correct Spamfilter details extract, as per Bug 8656
# - Add analysis table of Geoip results
# bjr - 19Jun15 - Add totals for the League tables
# bjr and Unnilennium - 08Apr16 - Add in else for unrecognised plugin detection
# bjr - 08Apr16 - Add in link for SaneSecurity "extra" virus detection
# bjr - 14Jun16 - make compatible with qpsmtpd 0.96
# bjr - 16Jun16 - Add code to create an html equivalent of the text email (v0.7)
# bjr - 04Aug16 - Add code to log and count the blacklist RBL urls that have triggered, this (NFR) is Bugzilla 9717
# bjr - 04Aug16 - Add code to expand the junkmail table to include daily ham and spam and deleted spam for each user - (NFR bugzilla 9716)
# bjr - 05Aug16 - Add code to log remote relay incoming emails
# bjr - 10Oct16 - Add code to show stats for the smeoptimizer package
# bjr - 16dec16 - Fix dnsbl code to deal with psbl.surriel.com - Bug 9717
# bjr - 16Dec16 - Change geopip table code to show even if no exclusions found (assuming geoip data found) - Bug 9888
# bjr - 30Apr17 - Change Categ index code - Bug 9888 again
# bjr - 18Dec19 - Sort out a few format problems and also remove some debugging crud - Bug 10858
# bjr - 18Dec19 - change to fix truncation of email address in by email table - bug 10327
#
#############################################################################
#
# SMEServer DB usage
# ------------------
#
# mailstats / Status ("enabled"|"disabled")
# / <column header> ("yes"|"no"|"auto") - enable, supress or only show if nonzero
# / QpsmtpdCodes ("enabled"|"disabled")
# / SARules ("enabled"|"disabled")
# / GeoipTable ("enabled"|"disabled")
# / GeoipCutoffPercent (0.5%) - threshold to show Geoip country in league table
# / JunkMailList ("enabled"|"disabled")
# / SARulePercentThreshold (0.5) - threshold of SArules percentage for report cutoff
# / Email (admin) - email to send report
# / SaveDataToMySQL - save data to MySQL database (default is "no")
# / ShowLeagueTotals - Show totals row after league tables - (default is "yes")
# / DBHost - MySQL server hostname (default is "localhost").
# / DBPort - MySQL server post (default is "3306")
# / Interval - "daily", "weekly", "fortnightly", "monthly", "99999" - last is number of hours (default is daily)
# / Base - "Midnight", "Midday", "Now", "99" hour (0-23) (default is midnight)
# / HTMLEmail - "yes", "no", "both" - default is "No" - Send email in HTML
# NOT YET INUSE - WIP!
# / HTMLPage - "yes" / "no" - default is "yes" if HTMLEmail is "yes" or "both" otherwise "no"
#
#############################################################################
#
#
# TODO
#
# 1. Delete loglines records from any previous run of same table
# 2. Add tracking LogId for each cont in the table
# 3. Use link directory file to generate h1 / h2 tags for title and section headings
# 4. Ditto for links to underlying data
#
# internal modules (part of core perl distribution)
use strict;
use warnings;
use Getopt::Long;
use Pod::Usage;
use POSIX qw/strftime floor/;
use Time::Local;
use Date::Parse;
use Time::TAI64;
use esmith::ConfigDB;
use esmith::DomainsDB;
use Sys::Hostname;
use Switch;
use DBIx::Simple;
use URI::URL;
#use CGI;
#use HTML::TextToHTML;
my $hostname = hostname();
my $cdb = esmith::ConfigDB->open_ro or die "Couldn't open ConfigDB : $!\n";
my $true = 1;
my $false = 0;
#and see if mailstats are disabled
my $disabled;
if ($cdb->get('mailstats')){
$disabled = !(($cdb->get('mailstats')->prop('Status') || 'enabled') eq 'enabled');
} else {
my $db = esmith::ConfigDB->open; my $record = $db->new_record('mailstats', { type => 'report', Status => 'enabled', Email => 'admin' });
$cdb = esmith::ConfigDB->open_ro or die "Couldn't open ConfigDB : $!\n"; #Open up again to pick up new record
$disabled = $false;
}
#Configuration section
my %opt = (
version => '0.7.13', # please update at each change.
debug => 0, # guess what ?
sendmail => '/usr/sbin/sendmail', # Path to sendmail stub
from => 'spamfilter-stats', # Who is the mail from
mail => $cdb->get('mailstats')->prop('Email') || 'admin', # mailstats email recipient
timezone => `date +%z`,
);
my $FetchmailIP = '127.0.0.200'; #Apparent Ip address of fetchmail deliveries
my $WebmailIP = '127.0.0.1'; #Apparent Ip of Webmail sender
my $localhost = 'localhost'; #Apparent sender for webmail
my $FETCHMAIL = 'FETCHMAIL'; #Sender from fetchmail when Ip address not 127.0.0.200 - when qpsmtpd denies the email
my $MAILMAN = "bounces"; #sender when mailman sending when orig is localhost
my $DMARCDomain="dmarc"; #Pattern to recognised DMARC sent emails (this not very reliable, as the email address could be anything)
my $DMARCOkPattern="dmarc: pass"; #Pattern to use to detect DMARC approval
my $localIPregexp = ".*((127\.)|(10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(192\.168\.)).*";
my $MinCol = 6; #Minimum column width
my $HourColWidth = 16; #Date and time column width
my $SARulethresholdPercent = 10; #If Sa rules less than this of total emails, then cutoff reduced
my $maxcutoff = 1; #max percent cutoff applied
my $mincutoff = 0.2; #min percent cutoff applied
my $tstart = time;
#Local variables
my $YEAR = ( localtime(time) )[5]; # this is years since 1900
my $total = 0;
my $spamcount = 0;
my $spamavg = 0;
my $spamhits = 0;
my $hamcount = 0;
my $hamavg = 0;
my $hamhits = 0;
my $rejectspamavg = 0;
my $rejectspamhits= 0;
my $Accepttotal = 0;
my $localAccepttotal = 0; #Fetchmail connections
my $localsendtotal = 0; #Connections from local PCs
my $totalexamined = 0; #total download + RBL etc
my $WebMailsendtotal = 0; #total from Webmail
my $mailmansendcount = 0; #total from mailman
my $DMARCSendCount = 0; #total DMARC reporting emails sent (approx)
my $DMARCOkCount = 0; #Total emails approved through DMARC
my %found_viruses = ();
my %found_qpcodes = ();
my %found_SARules = ();
my %junkcount = ();
my %unrecog_plugin = ();
my %blacklistURL = (); #Count of use of each balcklist rhsbl
my %usercounts = (); #Count per received email of sucessful delivery, queued spam and deleted Spam, and rejected
# replaced by...
my %counts = (); #Hold all counts in 2-D matrix
my @display = (); #used to switch on and off columns - yes, no or auto for each category
my @colwidth = (); #width of each column
#(auto means only if non zero) - populated from possible db entries
my @finaldisplay = (); #final decision on display or not - true or false
#count column names, used for headings - also used for DB mailstats property names
my $CATHOUR='Hour';
my $CATFETCHMAIL='Fetchmail';
my $CATWEBMAIL='WebMail';
my $CATMAILMAN='Mailman';
my $CATLOCAL='Local';
my $CATRELAY="Relay";
# border between where it came from and where it ended..
my $countfromhere = 6; #Temp - Check this not moved!!
my $CATVIRUS='Virus';
my $CATRBLDNS='RBL/DNS';
my $CATEXECUT='Execut.';
my $CATNONCONF='Non.Conf.';
my $CATBADCOUNTRIES='Geoip.';
my $CATKARMA="Karma";
my $CATSPAMDEL='Del.Spam';
my $CATSPAM='Qued.Spam?';
my $CATHAM='Ham';
my $CATTOTALS='TOTALS';
my $CATPERCENT='PERCENT';
my $CATDMARC="DMARC Rej.";
my $CATLOAD="Rej.Load";
my @categs = ($CATHOUR,$CATFETCHMAIL,$CATWEBMAIL,$CATMAILMAN,$CATLOCAL,$CATRELAY,$CATDMARC,$CATVIRUS,$CATRBLDNS,$CATEXECUT,$CATBADCOUNTRIES,$CATNONCONF,$CATLOAD,$CATKARMA,$CATSPAMDEL,$CATSPAM,$CATHAM,$CATTOTALS,$CATPERCENT);
my $GRANDTOTAL = '99'; #subs for count arrays, for grand total
my $PERCENT = '98'; # for column percentages
my $categlen = @categs-2; #-2 to avoid the total and percent column
#
# Index for certain columns - check these do not move if we add columns
#
#my $BadCountryCateg=9;
#my $DMARCcateg = 5; #Not used.
#my $KarmaCateg=$BadCountryCateg+3;
my %categindex;
@categindex{@categs} = (0..$#categs);
my $BadCountryCateg=$categindex{$CATBADCOUNTRIES};
my $DMARCcateg = $categindex{$CATDMARC}; #Not used.
my $KarmaCateg=$categindex{$CATKARMA};
my $above15 = 0;
my $RBLcount = 0;
my $MiscDenyCount = 0;
my $PatternFilterCount = 0;
my $noninfectedcount = 0;
my $okemailcount = 0;
my $infectedcount = 0;
my $warnnoreject = " ";
my $rblnotset = ' ';
my %found_countries = ();
my $total_countries = 0;
my $BadCountries = ""; #From the DB
my $FS = "\t"; # field separator used by logterse plugin
my %log_items = ( "", "", "", "", "", "", "", "" );
my $score;
my %timestamp_items = ();
my $localflag = 0; #indicate if current email is local or not
my $WebMailflag = 0; #indicate if current mail is send from webmail
# some storage for by recipient domains stats (PS)
# my bad : I have to deal with multiple simoultaneous connections
# will play with the process number.
# my $currentrcptdomain = '' ;
my %currentrcptdomain ; # temporay store the recipient domain until end of mail processing
my %byrcptdomain ; # Store 'by domains stats'
my @extdomain ; # only useful in some MX-Backup case, when any subdomains are allowed
my $morethanonercpt = 0 ; # count every 'second' recipients for a mail.
my $recipcount = 0; # count every recipient email address received.
#
#Load up the emails curreently stored for DMARC reporting - so that we cna spot the reports being sent.
#Held in an slqite db, created by the DMARC perl lib.
#
my $dsn = "dbi:SQLite:dbname=/var/lib/qpsmtpd/dmarc/reports.sqlite"; #Taken from /etc/mail-dmarc.ini
# doesn't seem to need
my $user = "";
my $pass = "";
my $DMARC_Report_emails = ""; #Flat string of all email addresses
if (my $dbix = DBIx::Simple->connect( $dsn, $user, $pass )){
my $result = $dbix->query("select rua from report_policy_published;");
$result->bind(my ($emailaddress));
while ($result->fetch){
#remember email from logterse entry has chevrons round it - so we add them here to guarantee the alighment of the match
#Remove the mailto:
$emailaddress =~ s/mailto://g;
# and map any commas to ><
$emailaddress =~ s/,/></g;
$DMARC_Report_emails .= "<".$emailaddress.">\n"
}
$dbix->disconnect();
} else { $DMARC_Report_emails = "None found - DB not opened"}
# and setup list of local domains for spotting the local one in a list of email addresses (Remote station processing)
use esmith::DomainsDB;
my $d = esmith::DomainsDB->open_ro();
my @domains = $d->keys();
my $alldomains = "(";
foreach my $dom (@domains){$alldomains .= $dom."|"}
$alldomains .= ")";
# Saving the Log lines processed
my %LogLines = (); #Save all the log lines processed for writing to the DB
my %LogId = (); #Save the Log Ids.
my $CurrentLogId = "";
my $Sequence = 0;
# store the domain of interest. Every other records are stored in a 'Other' zone
my $ddb = esmith::DomainsDB->open_ro or die "Couldn't open DomainsDB : $!\n";
foreach my $domain( $ddb->get_all_by_prop( type => "domain" ) ) {
$byrcptdomain{ $domain->key }{ 'type' }='local';
}
$byrcptdomain{ $cdb->get('SystemName')->value . "."
. $cdb->get('DomainName')->value }{ 'type' } = 'local';
# is this system a MX-Backup ?
if ($cdb->get('mxbackup')){
if ( ( $cdb->get('mxbackup')->prop('status') || 'disabled' ) eq 'enabled' ) {
my %MXValues = split( /,/, ( $cdb->get('mxbackup')->prop('name') || '' ) ) ;
foreach my $data ( keys %MXValues ) {
$byrcptdomain{ $data }{ 'type' } = "mxbackup-$MXValues{ $data }" ;
if ( $MXValues{ $data } == 1 ) { # subdomains allowed, must take care of this
push @extdomain, $data ;
}
}
}
}
my ( $start, $end ) = analysis_period();
#
# First check current configuration for logging, DNS enable and Max threshold for spamassassin
#
my $LogLevel = $cdb->get('qpsmtpd')->prop('LogLevel');
my $HighLogLevel = ( $LogLevel > 6 );
my $RHSenabled =
( $cdb->get('qpsmtpd')->prop('RHSBL') eq 'enabled' );
my $DNSenabled =
( $cdb->get('qpsmtpd')->prop('DNSBL') eq 'enabled' );
my $SARejectLevel =
$cdb->get('spamassassin')->prop('RejectLevel');
my $SATagLevel =
$cdb->get('spamassassin')->prop('TagLevel');
my $DomainName =
$cdb->get('DomainName')->value;
# check that logterse is in use
#my pluginfile = '/var/service/qpsmtpd/config/peers/0';
if ( !$RHSenabled || !$DNSenabled ) {
$rblnotset = '*';
}
if ( $SARejectLevel == 0 ) {
$warnnoreject = "(*Warning* 0 = no reject)";
}
# get enable/disable subsections
my $enableqpsmtpdcodes;
my $enableSARules;
my $enableGeoiptable;
my $enablejunkMailList;
my $savedata;
my $enableblacklist; #Enabled according to setting in qpsmtpd
if ($cdb->get('mailstats')){
$enableqpsmtpdcodes = ($cdb->get('mailstats')->prop("QpsmtpdCodes") || "enabled") eq "enabled" || $false;
$enableSARules = ($cdb->get('mailstats')->prop("SARules") || "enabled") eq "enabled" || $false;
$enablejunkMailList = ($cdb->get('mailstats')->prop("JunkMailList") || "enabled") eq "enabled" || $false;
$enableGeoiptable = ($cdb->get('mailstats')->prop("Geoiptable") || "enabled") eq "enabled" || $false;
$savedata = ($cdb->get('mailstats')->prop("SaveDataToMySQL") || "no") eq "yes" || $false;
} else {
$enableqpsmtpdcodes = $true;
$enableSARules = $true;
$enablejunkMailList = $true;
$enableGeoiptable = $true;
$savedata = $false;
}
$enableblacklist = ($cdb->get('qpsmtpd')->prop("RHSBL") || "disabled") eq "enabled" || ($cdb->get('qpsmtpd')->prop("URIBL") || "disabled") eq "enabled";
my $makeHTMLemail = "no";
#if ($cdb->get('mailstats')){$makeHTMLemail = $cdb->get('mailstats')->prop('HTMLEmail') || "no"} #TEMP!!
my $makeHTMLpage = "no";
#if ($makeHTMLemail eq "yes" || $makeHTMLemail eq "both") {$makeHTMLpage = "yes"}
#if ($cdb->get('mailstats')){$makeHTMLpage = $cdb->get('mailstats')->prop('HTMLPage') || "no"}
# Init the hashes
my $nhour = floor( $start / 3600 );
my $ncateg;
while ( $nhour < $end / 3600 ) {
$counts{$nhour}=();
$ncateg = 0;
while ( $ncateg < @categs) {
$counts{$nhour}{$categs[$ncateg-1]} = 0;
$ncateg++
}
$nhour++;
}
# and grand totals, percent and display status from db entries, and column widths
$ncateg = 0;
my $colpadding = 0;
while ( $ncateg < @categs) {
$counts{$GRANDTOTAL}{$categs[$ncateg]} = 0;
$counts{$PERCENT}{$categs[$ncateg]} = 0;
if ($cdb->get('mailstats')){
$display[$ncateg] = lc($cdb->get('mailstats')->prop($categs[$ncateg])) || "auto";
} else {
$display[$ncateg] = 'auto'
}
if ($ncateg == 0) {
$colwidth[$ncateg] = $HourColWidth + $colpadding;
} else {
$colwidth[$ncateg] = length($categs[$ncateg])+1+$colpadding;
}
if ($colwidth[$ncateg] < $MinCol) {$colwidth[$ncateg] = $MinCol + $colpadding}
$ncateg++
}
my $starttai = Time::TAI64::unixtai64n($start);
my $endtai = Time::TAI64::unixtai64n($end);
my $sum_SARules = 0;
# we remove non valid files
my @ARGV2;
foreach ( map { glob } @ARGV){
push(@ARGV2,($_));
}
@ARGV=@ARGV2;
my $count = -1; #for loop reduction in debugging mode
#
#---------------------------------------
# Scan the qpsmtpd log file(s)
#---------------------------------------
my $CurrentMailId = "";
LINE: while (<>) {
next LINE if !(my($tai,$log) = split(' ',$_,2));
#If date specified, only process lines matching date
next LINE if ( $tai lt $starttai );
next LINE if ( $tai gt $endtai );
#Count lines and skip out if debugging
$count++;
#last LINE if ($opt{debug} && $count >= 100);
#Loglines to Saved String for later DB write
if ($savedata) {
my $CurrentLine = $_;
$CurrentLine = /^\@([0-9a-z]*) ([0-9]*) .*$/;
my $l = length($CurrentLine);
if ($l != 0){
if (defined($2)){
if ($2 ne $CurrentMailId) {
print "CL:$CurrentLine*\n" if !defined($1);
$CurrentLogId = $1."-".$2;
$CurrentMailId = $2;
$Sequence = 0;
} else {$Sequence++}
#$CurrentLogId .=":".$Sequence;
$LogLines{$CurrentLogId.":".$Sequence} = $_;
}
}
}
# pull out spamasassin rule lists
if ( $_ =~m/spamassassin: pass, Ham,(.*)</ )
#if ( $_ =~m/spamassassin plugin.*: check_spam:.*hits=(.*), required.*tests=(.*)/ )
{
#New version does not seem to have spammassasin tests in logs
#if (exists($2){
#my (@SAtests) = split(',',$2);
#foreach my $SAtest (@SAtests) {
#if (!$SAtest eq "") {
#$found_SARules{$SAtest}{'count'}++;
#$found_SARules{$SAtest}{'totalhits'} += $1;
#$sum_SARules++
#}
#}
#}
}
#Pull out Geoip countries for analysis table
if ( $_ =~m/check_badcountries: GeoIP Country: (.*)/ )
{
$found_countries{$1}++;
$total_countries++;
}
#Pull out DMARC approvals
if ( $_ =~m/.*$DMARCOkPattern.*/ )
{
$DMARCOkCount++;
}
#only select Logterse output
next LINE unless m/logging::logterse:/;
my $abstime = Time::TAI64::tai2unix($tai);
my $abshour = floor( $abstime / 3600 ); # Hours since the epoch
my ($timestamp_part, $log_part) = split('`',$_,2); #bjr 0.6.12
my (@log_items) = split $FS, $log_part;
my (@timestamp_items) = split(' ',$timestamp_part);
my $result= "rejected"; #Tag as rejected unti we know otherwise
# we store the more recent recipient domain, for domain statistics
# in fact, we only store the first recipient. Could be sort of headhache
# to obtain precise stats with many recipients on more than one domain !
my $proc = $timestamp_items[1] ; #numeric Id for the email
my $emailnum = $proc; #proc gets modified later...
if ($emailnum == 23244) {
}
$totalexamined++;
# first spot the fetchmail and local deliveries.
# Spot from local workstation
$localflag = 0;
$WebMailflag = 0;
if ( $log_items[1] =~ m/$DomainName/ ) { #bjr
$localsendtotal++;
$counts{$abshour}{$CATLOCAL}++;
$localflag = 1;
}
#Or a remote station
elsif ((!test_for_private_ip($log_items[0])) and (test_for_private_ip($log_items[2])) and ($log_items[5] eq "queued"))
{
#Remote user
$localflag = 1;
$counts{$abshour}{$CATRELAY}++;
}
elsif (($log_items[2] =~ m/$WebmailIP/) and (!test_for_private_ip($log_items[0]))) {
#Webmail
$localflag = 1;
$WebMailsendtotal++;
$counts{$abshour}{$CATWEBMAIL}++;
$WebMailflag = 1;
}
# see if from localhost
elsif ( $log_items[1] =~ m/$localhost/ ) {
# but not if it comes from fetchmail
if ( $log_items[3] =~ m/$FETCHMAIL/ ) { }
else {
$localflag = 1;
# might still be from mailman here
if ( $log_items[3] =~ m/$MAILMAN/ ) {
$mailmansendcount++;
$localsendtotal++;
$counts{$abshour}{$CATMAILMAN}++;
$localflag = 1;
}
else {
#Or sent to the DMARC server
#check for email address in $DMARC_Report_emails string
my $logemail = $log_items[4];
if ((index($DMARC_Report_emails,$logemail)>=0) or ($logemail =~ m/$DMARCDomain/)){
$localsendtotal++;
$DMARCSendCount++;
$localflag = 1;
}
else {
if (exists $log_items[8]){
# ignore incoming localhost spoofs
if ( $log_items[8] =~ m/msg denied before queued/ ) { }
else {
#Webmail
$localflag = 1;
$WebMailsendtotal++;
$counts{$abshour}{$CATWEBMAIL}++;
$WebMailflag = 1;
}
}
else {
$localflag = 1;
$WebMailsendtotal++;
$counts{$abshour}{$CATWEBMAIL}++;
$WebMailflag = 1;
}
}
}
}
}
# try to spot fetchmail emails
if ( $log_items[0] =~ m/$FetchmailIP/ ) {
$localAccepttotal++;
$counts{$abshour}{$CATFETCHMAIL}++;
}
elsif ( $log_items[3] =~ m/$FETCHMAIL/ ) {
$localAccepttotal++;
$counts{$abshour}{$CATFETCHMAIL}++;
}
# and adjust for recipient field if not set-up by denying plugin - extract from deny msg
if ( length( $log_items[4] ) == 0 ) {
if ( $log_items[5] eq 'check_goodrcptto' ) {
if ( $log_items[7] gt "invalid recipient" ) {
$log_items[4] =
substr( $log_items[7], 16 ); #Leave only email address
}
}
}
# if ( ( $currentrcptdomain{ $proc } || '' ) eq '' ) {
# reduce to lc and process each e,mail if a list, pseperatedy commas
my $recipientmail = lc( $log_items[4] );
if ( $recipientmail =~ m/.*,/ ) {
#comma - split the line and deal with each domain
# print $recipientmail."\n";
my ($recipients) = split( ',', $recipientmail );
foreach my $recip ($recipients) {
$proc = $proc . $recip;
# print $proc."\n";
$currentrcptdomain{$proc} = $recip;
add_in_domain($proc);
$recipcount++;
}
# print "*\n";
#count emails with more than one recipient
# $recipientmail =~ m/(.*),/;
# $currentrcptdomain{ $proc } = $1;
}
else {
$proc = $proc . $recipientmail;
$currentrcptdomain{$proc} = $recipientmail;
add_in_domain($proc);
$recipcount++;
}
# } else {
# # there more than a recipient for a mail, how many daily ?
# $morethanonercpt++;
# }
# then categorise the result
if (exists $log_items[5]) {
if ($log_items[5] eq 'naughty') {
my $rejreason = $log_items[7];
$rejreason = /.*(\(.*\)).*/;
if (!defined($1)){$rejreason = "unknown"}
else {$rejreason = $1}
$found_qpcodes{$log_items[5]."-".$rejreason}++}
else {$found_qpcodes{$log_items[5]}++} ##Count different qpsmtpd result codes
if ($log_items[5] eq 'check_earlytalker') {$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_relay') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_norelay') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'require_resolvable_fromhost') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_basicheaders') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'rhsbl') { $RBLcount++;$counts{$abshour}{$CATRBLDNS}++;mark_domain_rejected($proc);$blacklistURL{get_domain($log_items[7])}++}
elsif ($log_items[5] eq 'dnsbl') { $RBLcount++;$counts{$abshour}{$CATRBLDNS}++;mark_domain_rejected($proc);$blacklistURL{get_domain($log_items[7])}++}
elsif ($log_items[5] eq 'check_badmailfrom') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_badrcptto_patterns') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_badrcptto') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_spamhelo') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_goodrcptto extn') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'rcpt_ok') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'pattern_filter') { $PatternFilterCount++;$counts{$abshour}{$CATEXECUT}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'virus::pattern_filter') { $PatternFilterCount++;$counts{$abshour}{$CATEXECUT}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_goodrcptto') {$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_smtp_forward') {$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'count_unrecognized_commands') {$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_badcountries') {$MiscDenyCount++;$counts{$abshour}{$CATBADCOUNTRIES}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'tnef2mime') { } #Not expecting this one.
elsif ($log_items[5] eq 'spamassassin') { $above15++;$counts{$abshour}{$CATSPAMDEL}++;
# and extract the spam score
# if ($log_items[8] =~ "Yes, hits=(.*) required=([0-9\.]+)")
if ($log_items[8] =~ "Yes, score=(.*) required=([0-9\.]+)")
{$rejectspamavg += $1}
mark_domain_rejected($proc);
}
elsif (($log_items[5] eq 'virus::clamav') or ($log_items[5] eq 'virus::clamdscan')) { $infectedcount++;$counts{$abshour}{$CATVIRUS}++;
#extract the virus name
if ($log_items[7] =~ "Virus found: (.*)" ) {$found_viruses{$1}++;}
else {$found_viruses{$log_items[7]}++} #Some other message!!
mark_domain_rejected($proc);
}
elsif ($log_items[5] eq 'queued') { $Accepttotal++;
#extract the spam score
# Remove count for rejectred as it looks as if it might get through!!
$result= "queued";
if ($log_items[8] =~ ".*score=([+-]?\\d+\.?\\d*).* required=([0-9\.]+)") {
$score = trim($1);
if ($score =~ /^[+-]?\d+\.?\d*$/ ) #check its numeric
{
if ($score < $SATagLevel) { $hamcount++;$counts{$abshour}{$CATHAM}++;$hamavg += $score;}
else {$spamcount++;$counts{$abshour}{$CATSPAM}++;$spamavg += $score;$result= "spam";}
} else {
print "Unexpected non numeric found in $proc:".$log_items[8]."($score)\n";
}
} else {
# no SA score - treat it as ham
$hamcount++;$counts{$abshour}{$CATHAM}++;
}
if ( ( $currentrcptdomain{ $proc } || '' ) ne '' ) {
$byrcptdomain{ $currentrcptdomain{ $proc } }{ 'accept' }++ ;
$currentrcptdomain{ $proc } = '' ;
}
}
elsif ($log_items[5] eq 'tls') {$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'auth::auth_cvm_unix_local') {$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'earlytalker') {$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'uribl') {$RBLcount++;$counts{$abshour}{$CATRBLDNS}++;mark_domain_rejected($proc);$blacklistURL{get_domain($log_items[7])}++}
elsif ($log_items[5] eq 'naughty') {
#Naughty plugin seems to span a number of rejection reasons - so we have to use the next but one log_item[7] to identify
if ($log_items[7] =~ m/(karma)/) {
$MiscDenyCount++;$counts{$abshour}{$CATKARMA}++;mark_domain_rejected($proc)}
elsif ($log_items[7] =~ m/(dnsbl)/){
$RBLcount++;$counts{$abshour}{$CATRBLDNS}++;mark_domain_rejected($proc);$blacklistURL{get_domain($log_items[7])}++}
elsif ($log_items[7] =~ m/(helo)/){
$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
else {
#Unidentified Naughty rejection
$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc);$unrecog_plugin{$log_items[5]."-".$log_items[7]}++}
}
elsif ($log_items[5] eq 'resolvable_fromhost') {$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'loadcheck') {$MiscDenyCount++;$counts{$abshour}{$CATLOAD}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'karma') {$MiscDenyCount++;$counts{$abshour}{$CATKARMA}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'dmarc') {$MiscDenyCount++;$counts{$abshour}{$CATDMARC}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'relay') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'headers') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'mailfrom') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'badrcptto') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'helo') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'check_smtp_forward') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
elsif ($log_items[5] eq 'sender_permitted_from') { $MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc)}
#Treat it as Unconf if not recognised
else {$MiscDenyCount++;$counts{$abshour}{$CATNONCONF}++;mark_domain_rejected($proc);$unrecog_plugin{$log_items[5]}++}
} #Log[5] exists
#Entry if not local send
if ($localflag == 0) {
if (length($log_items[4]) > 0){
# Need to check here for multiple email addresses
my @emails = split(",",lc($log_items[4]));
if (scalar(@emails) > 1) {
#Just pick the first local address to hang it on.
# TEMP - just go for the first address until I can work out how to spot the 1st "local" one
$usercounts{$emails[0]}{$result}++;
$usercounts{$emails[0]}{"proc"} = $proc;
#Compare with @domains array until we get a local one
my $gotone = $false;
foreach my $email (@emails){
#Extract the domain from the email address
my $fullemail = $email;
$email = s/.*\@(.*)$/$1/;
#and see if it is local
if ($email =~ m/$alldomains/){
$usercounts{lc($fullemail)}{$result}++;
$usercounts{lc($fullemail)}{"proc"} = $proc;
$gotone = $true;
last;
}
}
if (!$gotone) {
$usercounts{'No internal email $proc'}{$result}++;
$usercounts{'No internal email $proc'}{"proc"} = $proc;
}
} else {
$usercounts{lc($log_items[4])}{$result}++;
$usercounts{lc($log_items[4])}{"proc"} = $proc;
}
}
}
#exit if $emailnum == 15858;
} #END OF MAIN LOOP
#total up grand total Columns
$nhour = floor( $start / 3600 );
while ( $nhour < $end / 3600 ) {
$ncateg = 0; #past the where it came from columns
while ( $ncateg < @categs) {
#total columns
$counts{$GRANDTOTAL}{$categs[$ncateg]} += $counts{$nhour}{$categs[$ncateg]};
# and total rows
if ( $ncateg < $categlen and $ncateg>=$countfromhere) {#skip initial columns of non final reasons
$counts{$nhour}{$categs[@categs-2]} += $counts{$nhour}{$categs[$ncateg]};
}
$ncateg++
}
$nhour++;
}
#Compute row totals and row percentages
$nhour = floor( $start / 3600 );
while ( $nhour < $end / 3600 ) {
$counts{$nhour}{$categs[@categs-1]} = $counts{$nhour}{$categs[@categs-2]}*100/$totalexamined if $totalexamined;
$nhour++;
}
#compute column percentages
$ncateg = 0;
while ( $ncateg < @categs) {
if ($ncateg == @categs-1) {
$counts{$PERCENT}{$categs[$ncateg]} = $counts{$GRANDTOTAL}{$categs[$ncateg-1]}*100/$totalexamined if $totalexamined;
} else {
$counts{$PERCENT}{$categs[$ncateg]} = $counts{$GRANDTOTAL}{$categs[$ncateg]}*100/$totalexamined if $totalexamined;
}
$ncateg++
}
#compute sum of row percentages
$nhour = floor( $start / 3600 );
while ( $nhour < $end / 3600 ) {
$counts{$GRANDTOTAL}{$categs[@categs-1]} += $counts{$nhour}{$categs[@categs-1]};
$nhour++;
}
my $QueryNoLogTerse = ($totalexamined==0); #might indicate logterse not installed in qpsmtpd plugins
#Calculate some numbers
$spamavg = $spamavg / $spamcount if $spamcount;
$rejectspamavg = $rejectspamavg / $above15 if $above15;
$hamavg = $hamavg / $hamcount if $hamcount;
# RBL etc percent of total SMTP sessions
my $rblpercent = ( ( $RBLcount / $totalexamined ) * 100 ) if $totalexamined;
my $PatternFilterpercent = ( ( $PatternFilterCount / $totalexamined ) * 100 ) if $totalexamined;
my $Miscpercent = ( ( $MiscDenyCount / $totalexamined ) * 100 ) if $totalexamined;
#Spam and virus percent of total email downloaded
#Expressed as a % of total examined
my $spampercent = ( ( $spamcount / $totalexamined ) * 100 ) if $totalexamined;
my $hampercent = ( ( $hamcount / $totalexamined ) * 100 ) if $totalexamined;
my $hrsinperiod = ( ( $end - $start ) / 3600 );
my $emailperhour = ( $totalexamined / $hrsinperiod ) if $totalexamined;
my $above15percent = ( $above15 / $totalexamined * 100 ) if $totalexamined;
my $infectedpercent = ( ( $infectedcount / ($totalexamined) ) * 100 ) if $totalexamined;
my $AcceptPercent = ( ( $Accepttotal / ($totalexamined) ) * 100 ) if $totalexamined;
my $oldfh;
#Open Sendmail if we are mailing it
if ( $opt{'mail'} and !$disabled ) {
open( SENDMAIL, "|$opt{'sendmail'} -oi -t -odq" )
or die "Can't open sendmail: $!\n";
print SENDMAIL "From: $opt{'from'}\n";
print SENDMAIL "To: $opt{'mail'}\n";
print SENDMAIL "Subject: Spam Filter Statistics from $hostname - ",
strftime( "%F", localtime($start) ), "\n\n";
$oldfh = select SENDMAIL;
}
my $telapsed = time - $tstart;
if ( !$disabled ) {
#Output results
# NEW - save the print to a variable so that it can be processed into html.
#
#Save current output selection and divert into variable
#
my $output;
my $tablestr="";
open(my $outputFH, '>', \$tablestr) or die; # This shouldn't fail
my $oldFH = select $outputFH;
print "SMEServer daily Anti-Virus and Spamfilter statistics from $hostname - ".strftime( "%F", localtime($start))."\n";
print "----------------------------------------------------------------------------------", "\n\n";
print "$0 Version : $opt{'version'}", "\n";
print "Period Beginning : ", strftime( "%c", localtime($start) ), "\n";
print "Period Ending : ", strftime( "%c", localtime($end) ), "\n";
print "Clam Version/DB Count/Last DB update: ",`freshclam -V`;
print "SpamAssassin Version : ",`spamassassin -V`;
printf "Tag level: %3d; Reject level: %-3d $warnnoreject\n", $SATagLevel,$SARejectLevel;
if ($HighLogLevel) {
printf "*Loglevel is set to: ".$LogLevel. " - you only need it set to 6\n";
printf "\tYou can set it this way:\n";
printf "\tconfig setprop qpsmtpd LogLevel 6\n";
printf "\tsignal-event email-update\n";
printf "\tsv t /var/service/qpsmtpd\n";
}
printf "Reporting Period : %-.2f hrs\n", $hrsinperiod;
printf "All SMTP connections accepted:%-8d \n", $totalexamined;
printf "Emails per hour : %-8.1f/hr\n", $emailperhour || 0;
printf "Average spam score (accepted): %-11.2f\n", $spamavg || 0;
printf "Average spam score (rejected): %-11.2f\n", $rejectspamavg || 0;
printf "Average ham score : %-11.2f\n", $hamavg || 0;
printf "Number of DMARC reporting emails sent:\t%-11d (not shown on table)\n", $DMARCSendCount || 0;
if ($hamcount != 0){ printf "Number of emails approved through DMARC:\t%-11d (%-3d%% of Ham count)\n", $DMARCOkCount|| 0,$DMARCOkCount*100/$hamcount || 0;}
my $smeoptimizerprog = "/usr/local/smeoptimizer/SMEOptimizer.pl";
if (-e $smeoptimizerprog) {
#smeoptimizer installed - get result of status
my @smeoptimizerlines = split(/\n/,`/usr/local/smeoptimizer/SMEOptimizer.pl -status`);
print("SMEOptimizer status:\n");
print("\t".$smeoptimizerlines[6]."\n");
print("\t".$smeoptimizerlines[7]."\n");
print("\t".$smeoptimizerlines[8]."\n");
print("\t".$smeoptimizerlines[9]."\n");
print("\t".$smeoptimizerlines[10]."\n");
}
print "\nStatistics by Hour:\n";
#
# start by working out which colunns to show - tag the display array
#
$ncateg = 1; ##skip the first column
$finaldisplay[0] = $true;
while ( $ncateg < $categlen) {
if ($display[$ncateg] eq 'yes') { $finaldisplay[$ncateg] = $true }
elsif ($display[$ncateg] eq 'no') { $finaldisplay[$ncateg] = $false }
else {
$finaldisplay[$ncateg] = ($counts{$GRANDTOTAL}{$categs[$ncateg]} != 0);
if ($finaldisplay[$ncateg]) {
#if it has been non zero and auto, then make it yes for the future.
esmith::ConfigDB->open->get('mailstats')->set_prop($categs[$ncateg],'yes')
}
}
$ncateg++
}
#make sure total and percentages are shown
$finaldisplay[@categs-2] = $true;
$finaldisplay[@categs-1] = $true;
# and put together the print lines
my $Line1; #Full Line across the page
my $Line2; #Broken Line across the page
my $Titles; #Column headers
my $Values; #Values
my $Totals; #Corresponding totals
my $Percent; # and column percentages
my $hour = floor( $start / 3600 );
$Line1 = '';
$Line2 = '';
$Titles = '';
$Values = '';
$Totals = '';
$Percent = '';
while ( $hour < $end / 3600 ) {
if ($hour == floor( $start / 3600 )){
#Do all the once only things
$ncateg = 0;