117 Commits

Author SHA1 Message Date
34fd81cd51 Fix crash when no emails found 2025-06-18 06:29:38 +01:00
1258b41ad8 Move log to /var/log, make most messages DEBUG and log INFO start and finish 2025-04-12 12:37:09 +01:00
9453031df3 Restore greyed print date (regression) 2025-04-12 12:03:15 +01:00
756e0f94c3 Make text version a bit more readable 2025-04-12 11:52:07 +01:00
f885ab684e Supress prev and next links when inside iframe 2025-04-10 19:48:59 +01:00
7504f32ce7 Add tooltips to black lists 2025-04-10 12:18:05 +01:00
9dc7d3d7a1 forgot mailstats-custom changes 2025-04-10 09:17:59 +01:00
89475c0aa3 Enable/disable rbl,sbl,uribl setting according to db property 2025-04-10 09:02:57 +01:00
b513dfc9be Make country codes update respect any extra ones added manaually 2025-04-09 11:00:44 +01:00
32d91c4a24 Sort out text version of table etc 2025-04-08 18:57:30 +01:00
2d1824553b Typo in TagLevel processing 2025-04-07 18:18:29 +01:00
e610abd351 Fix up various typos caused by global editting 2025-04-07 18:11:31 +01:00
8d8c97d1fa Get table display by date working on front page of mailstats panel 2025-04-07 14:31:16 +01:00
a58191f667 Update to mailstats panel 2025-04-06 20:32:49 +01:00
7fcdfccfa6 Add in code for mailstats panel in SM2 - not complete 2025-04-05 12:19:40 +01:00
cd4a0b1725 Move to use logging lib for logs, update runmailstats.sh for no manipulation of logs files 2025-04-04 10:20:07 +01:00
3b0b574171 Fix SQL creation of user and move log anaysis to use python systemd - journal lib 2025-04-03 20:15:41 +01:00
da71021889 Switch graphs to matplotlib which does not require internet access 2025-03-31 08:44:27 +01:00
4d29da7f3d Correct links to mailstats webpage, add -q to pip3 in spec 2025-03-28 12:22:55 +00:00
736315d47e Add in Blacklist sub report 2025-03-28 11:19:01 +00:00
40daa827c4 Add threshold for subtables 2025-03-28 05:42:48 +00:00
2d54c4f7f5 Sort out switch to pymysql and add SME10 log convert and runmailstats for SME10 2025-01-13 18:50:30 +00:00
fce93e1dcd Sort out css and html for subtables 2025-01-12 16:38:56 +00:00
eff56815da Minor change to format for port count label format 2025-01-12 07:30:45 +00:00
d1ddf5d04c Stremaline some table headings and add counts of port used by incoming email 2025-01-11 16:19:27 +00:00
f2f4078bb8 Comment out W3c icons on table until I've finished and can check it 2025-01-11 11:35:16 +00:00
9bfaa754e6 Take out extraneous characters from python version 2025-01-11 11:29:50 +00:00
9739f78b19 Fix up format according to actual log 2025-01-11 11:12:02 +00:00
d4961059b6 Fix junk mail counts table 2025-01-10 09:30:26 +00:00
93b5eb22ab Fix virus counts in sub table 2025-01-10 09:18:33 +00:00
f57b0c6e43 Fix recipient email counts 2025-01-09 10:41:12 +00:00
dce1df37db changes to SMTP connect type stats 2025-01-06 16:48:43 +00:00
e1250779de Fix up recipient email and qpsmtpd plugin counts 2025-01-05 18:15:42 +00:00
372d2b45dd Add in recipient email table 2025-01-02 09:50:44 +00:00
9be485a1a9 Add in q/s/u psmtpd counts 2024-12-31 15:51:48 +00:00
9ebe02b80e More fixes for log format and make perl and python run from same datafile 2024-12-31 13:34:23 +00:00
8be2103dec Update python requirements in spec file 2024-12-30 16:23:44 +00:00
20a8d3b4ef Edit python mailstats.py to accomodate new log format 2024-12-30 15:20:28 +00:00
51dd523249 update runmailstats.sh for new log names 2024-12-30 14:29:39 +00:00
a20b570e11 Sort out dates and hours for table 2024-12-30 14:27:35 +00:00
382489ecac * Mon Dec 30 2024 Brian Read <brianr@koozali.org> 1.1-19.sme
- Update mailstats.pl to accomodate change in log format for SME11 [SME: ]
2024-12-30 07:07:45 +00:00
c1c4251361 add pip3 commands to spec file 2024-12-18 11:03:22 +01:00
1fc294e8f6 Update with change in date format in logs 2024-12-18 06:36:42 +01:00
076e844898 Update spec file with python requirements 2024-12-18 06:35:42 +01:00
136a9416ec Take out patches and add python3-mysql as a requirement 2024-12-17 20:01:57 +01:00
f2f570c87e Add in uqpsmtpd log directory 2024-12-17 19:40:26 +01:00
1132efb828 Update README with specific Bugzilla links 2024-10-27 15:31:49 +00:00
edb71ad684 revert 8a506beb0f
revert Update README with specific Bugzilla links
2024-10-27 16:31:16 +01:00
8a506beb0f Update README with specific Bugzilla links 2024-10-27 15:05:08 +00:00
c335e93def Add in php routine to convert Tai64n string to date and time - untested 2024-07-26 06:24:51 +01:00
471cb25ad1 fix click link to stacked bar graph 2024-07-16 08:19:35 +01:00
b661e436fb grey out web page printed date 2024-07-15 21:59:54 +01:00
3c1dc868aa Add detailed level of drill down into logs 2024-07-15 16:15:39 +01:00
54ceac1ee8 Adjust summary displays 2024-07-15 11:06:44 +01:00
7eb3ff0048 Ignore the mailstats email 2024-07-15 10:26:52 +01:00
51912e5525 Fix link to underlying summary records for all dates 2024-07-15 09:36:06 +01:00
a1c9a698ee Arrange to remeber the tab selected in web page 2024-07-15 06:35:15 +01:00
c7cf518477 Fix nav header for email 2024-07-14 12:33:40 +01:00
7ad144e3b0 More refinements to graphs 2024-07-14 12:13:41 +01:00
aa0ebc76e9 Move graph data and nav html inline to template 2024-07-14 08:20:09 +01:00
a9be56deae Refine the stacked bar graph 2024-07-13 11:12:18 +01:00
e014d91060 derive graphs from main table 2024-07-12 20:09:13 +01:00
ddcde8fa07 Arrange that zero entries are blank 2024-07-12 10:35:44 +01:00
b0c63e61fd Update comment on front of file 2024-07-01 09:14:22 +01:00
4997bbffa2 more format for the table plus supress links when no database saved 2024-07-01 08:54:19 +01:00
ae0e133918 make counts on main table link to SummaryLogs page - WIP 2024-06-30 15:38:42 +01:00
ce1db0a31b Delete sql records for current day stats 2024-06-30 12:19:31 +01:00
f4f4c8173e slight change in the format 2024-06-30 12:10:57 +01:00
61f9872e66 enhance display of summary log line 2024-06-30 12:05:54 +01:00
ebff1f3d78 fix select for all hours on Summary logs page 2024-06-30 09:10:57 +01:00
e94c96cb26 Increase table width to 98% 2024-06-30 08:30:13 +01:00
fcc2a6fce8 update httpd fragment to add in private public option 2024-06-28 14:57:57 +01:00
c5a708a382 Add in soft failure for sql delete 2024-06-28 10:41:04 +01:00
6cb877d358 Allow php execute in mailstats html dir 2024-06-28 05:56:48 +01:00
55575811e7 First shot at php page to drill down to summary logs 2024-06-28 05:30:49 +01:00
906448378f Option to write out the summary logs to DB 2024-06-27 14:58:38 +01:00
68928375d8 Add in rejected flag in Badcountry table 2024-06-25 14:20:11 +01:00
1ef07f3acc Typo fix 2024-06-25 12:53:58 +01:00
85dc97aa05 Fix up counts in main table 2024-06-25 12:52:12 +01:00
0947689c0f FRix counts for naughty failures - dns, karma and helo 2024-06-19 22:44:21 +01:00
44b811d09e Correct names of fields in summary record 2024-06-19 09:14:57 +01:00
6e81bf1600 Get sort working in saub tables 2024-06-18 16:31:11 +01:00
1adf1b83db Sort out some of the stats and fix space between header and table 2024-06-18 15:45:03 +01:00
49095c3830 Add build time into --version string 2024-06-18 12:09:47 +01:00
9c6aae8ea7 Add in parameter to supress txt file creation 2024-06-18 09:54:55 +01:00
9db68b263d Properly align links at top of web page and add header text to page 2024-06-17 23:04:49 +01:00
3d7f2407b6 Remove some debug prints 2024-06-16 18:54:14 +01:00
d5c387d12e Sort out failure on spamstats not found 2024-06-16 17:15:23 +01:00
767ade0e0d Make anaysis date a parameter to mailstats 2024-06-16 09:15:38 +01:00
5e77fd4c82 Streamline overnight commans to only pass yesterdays data 2024-06-16 08:40:56 +01:00
1917053811 Bold percent cells and add percentage sign 2024-06-15 11:58:08 +01:00
cfe5d57656 Sort out border collapse for email 2024-06-14 18:31:48 +01:00
765bcb5896 Align top nav links 2024-06-14 17:46:07 +01:00
528bebf7a7 Deal with unexpected chars in log 2024-06-14 16:00:52 +01:00
b5970977e6 Update Readme.md from Chatgpt 2024-06-12 15:46:16 +02:00
4a22b47580 Moved css to own directory 2024-06-11 16:32:06 +01:00
389175c392 More changes to responsive tables and make top liks depend on email or web 2024-06-07 10:35:28 +01:00
52c1bfba48 Make table responsive for web 2024-06-06 16:52:17 +01:00
0bc452c38a Get email report working 2024-06-06 14:56:19 +01:00
c0cc10f417 Add in email sending from e-smith DB configuration details 2024-06-05 16:17:23 +01:00
997de8ca9f Add in percentages to main table 2024-06-05 10:09:28 +01:00
6de00553c2 Add routines to open and interrogate e-smith db 2024-06-04 12:06:52 +01:00
5f737912e4 Untested fragment for apache conf to add web access to mailstats 2024-06-04 10:34:39 +01:00
4a0c17e1c0 Move html and template files to /opt/mailstats 2024-06-04 10:12:01 +01:00
1dd11f04f1 Add nav in html and css from previous goes at mailstats html 2024-06-04 05:04:59 +01:00
c647574cb0 Finally got secondary scan covering all log entries 2024-06-03 20:44:16 +01:00
5db9ddf82f Refactor sub table render into def and add geoip results 2024-06-03 17:31:24 +01:00
b2440be6d0 Refactor import and categorise for re-use of data in second scan 2024-06-03 16:15:27 +01:00
ad1962753b First insert of sub tables 2024-06-01 07:48:35 +01:00
f64ff2feea Add in check for version of html2text 2024-05-30 21:47:57 +01:00
a5a38bae43 Added convert html to text using html2text program 2024-05-30 19:05:06 +01:00
5768306bc8 Fix up html with footer and row headers 2024-05-30 12:05:31 +01:00
d7ae6e9106 update html - add title 2024-05-29 18:30:39 +01:00
67f4621dd7 Add in all the plugin error counts 2024-05-29 18:16:22 +01:00
731233cce1 Main table in html 2024-05-29 16:46:58 +01:00
02deabb6af Read in and extract fields from logs 2024-05-29 10:15:23 +01:00
eefac0a502 Start of malistats re-write in python 2024-05-28 19:28:13 +01:00
26 changed files with 4337 additions and 96 deletions

10
.gitignore vendored
View File

@@ -2,3 +2,13 @@
*.log
*spec-20*
*.tgz
current.*
*.xz
current1
current2
*.html
*.txt
accounts
configuration
domains
hosts

View File

@@ -6,12 +6,49 @@ SMEServer Koozali developed git repo for smeserver-mailstats smecontribs
<br />https://wiki.koozali.org/Mailstats
## Bugzilla
Show list of outstanding bugs: [here](https://bugs.koozali.org/buglist.cgi?component=smeserver-mailstats&product=SME%20Contribs&query_format=advanced&limit=0&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=CONFIRMED)
Show list of outstanding bugs:
[All](https://bugs.koozali.org/buglist.cgi?action=wrap&bug_status=UNCONFIRMED&bug_status=CONFIRMED&bug_status=NEEDINFO&bug_status=IN_PROGRESS&bug_status=RESOLVED&bug_status=VERIFIED&classification=Contribs&component=smeserver-mailstats&list_id=105781&order=changeddate+DESC%2Ccomponent%2Cpriority%2Cbug_severity&product=SME+Contribs&query_format=advanced)
[Confirmed](https://bugs.koozali.org/buglist.cgi?action=wrap&bug_status=CONFIRMED&classification=Contribs&component=smeserver-mailstats&list_id=105781&order=changeddate+DESC%2Ccomponent%2Cpriority%2Cbug_severity&product=SME+Contribs&query_format=advanced)
[Unconfirmed](https://bugs.koozali.org/buglist.cgi?action=wrap&bug_status=UNCONFIRMED&classification=Contribs&component=smeserver-mailstats&list_id=105781&order=changeddate+DESC%2Ccomponent%2Cpriority%2Cbug_severity&product=SME+Contribs&query_format=advanced)
[Need Info](https://bugs.koozali.org/buglist.cgi?action=wrap&bug_status=NEEDINFO&classification=Contribs&component=smeserver-mailstats&list_id=105781&order=changeddate+DESC%2Ccomponent%2Cpriority%2Cbug_severity&product=SME+Contribs&query_format=advanced)
[In Progress](https://bugs.koozali.org/buglist.cgi?action=wrap&bug_status=IN_PROGRESS&classification=Contribs&component=smeserver-mailstats&list_id=105781&order=changeddate+DESC%2Ccomponent%2Cpriority%2Cbug_severity&product=SME+Contribs&query_format=advanced)
[Verified](https://bugs.koozali.org/buglist.cgi?action=wrap&bug_status=VERIFIED&classification=Contribs&component=smeserver-mailstats&list_id=105781&order=changeddate+DESC%2Ccomponent%2Cpriority%2Cbug_severity&product=SME+Contribs&query_format=advanced)
[Resolved](https://bugs.koozali.org/buglist.cgi?action=wrap&bug_status=RESOLVED&classification=Contribs&component=smeserver-mailstats&list_id=105781&order=changeddate+DESC%2Ccomponent%2Cpriority%2Cbug_severity&product=SME+Contribs&query_format=advanced)
## Description
## Overview
`smeserver-mailstats` is a package designed for the SME Server environment to provide comprehensive email traffic statistics and monitoring. It helps administrators track and analyze email usage, ensuring effective email management and enhanced security.
<br />*This description has been generated by an LLM AI system and cannot be relied on to be fully correct.*
*Once it has been checked, then this comment will be deleted*
<br />
## Functions
The Mailstats contrib is an invaluable tool for small and medium-sized businesses that use the SME Server operating system. This software is designed to give detailed statistics about an email server, providing crucial information such as the number of emails sent and received, and the average response time. The data it provides is essential for businesses to ensure their email system is running as efficiently as possible. Mailstats is an easy-to-use, graphical interface that gives clear, comprehensive information at a glance, enabling businesses to quickly identify any potential problems or areas that may need improvement. The Mailstats contrib is an invaluable asset to any business running the SME Server operating system, and can ensure the smooth running of their email system in the most efficient way possible.
1. **Email Traffic Analysis**:
- Tracks all inbound and outbound email traffic, providing detailed statistics on email usage patterns.
2. **User Activity Logging**:
- Logs email activities for each user, enabling detailed monitoring and accountability.
3. **Reporting**:
- Generates clear, reports that offer visual insights into email traffic data.
4. **Historical Data Maintenance**:
- Maintains historical email statistics, allowing for trend analysis over different periods.
5. **Customizable Alerts**:
- Enables administrators to set up alerts for specific conditions, such as volume thresholds or unusual email activity.
6. **Security Monitoring**:
- Helps identify unusual email patterns or spikes, which may indicate spam attacks or unauthorized access.
7. **Performance Optimization**:
- Provides data that helps in optimizing server performance by identifying resource bottlenecks.
8. **User-Friendly Interface**:
- Offers an easy-to-use interface for configuring and accessing email statistics and reports.
## Benefits
- **Enhanced Security**: Quickly identify and respond to potential email-based threats.
- **Resource Management**: Efficiently manage server resources based on email traffic analysis.
- **User Accountability**: Maintain detailed logs for compliance and auditing.
- **Proactive Administration**: Set up alerts to stay ahead of potential issues.
For more detailed information, refer to the official [SME Server wiki](https://wiki.contribs.org/Main_Page) and the respective package documentation.

View File

@@ -8,6 +8,7 @@ $event = 'smeserver-mailstats-update';
foreach my $file (qw(
/etc/systemd/system-preset/49-koozali.preset
/etc/e-smith/sql/init/99smeserver-mailstats.sql
/etc/httpd/conf/httpd.conf
))
{
templates2events( $file, $event );
@@ -18,7 +19,7 @@ event_link('systemd-reload', $event, '50');
#action specific to this package
#event_link('action', $event, '30');
#services we need to restart
#safe_symlink('restart', 'root/etc/e-smith/events/$event/services2adjust/<service>)
safe_symlink('restart', "root/etc/e-smith/events/$event/services2adjust/httpd-e-smith");
#and Server Mmanager panel link
#panel_link('somefunction', 'manager');

View File

@@ -91,4 +91,7 @@ CREATE TABLE IF NOT EXISTS `time` (
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
grant all privileges on mailstats.* to 'mailstats'@'localhost' identified by 'mailstats';
CREATE USER 'mailstats'@'localhost' IDENTIFIED BY 'mailstats';
GRANT ALL PRIVILEGES ON mailstats.* TO 'mailstats'@'localhost';
FLUSH PRIVILEGES;

View File

@@ -0,0 +1,45 @@
{
# mailstats
my $status = $mailstats{'Status'} || 'disabled';
if ($status eq 'enabled')
{
$OUT .="#-------------------------------------------------\n";
$OUT .="# mailstats settings from smeserver-mailstats\n";
$OUT .="#-------------------------------------------------\n";
$OUT .="\n";
$OUT .= qq(
# Alias for mailstats
Alias "/mailstats/js" "/opt/mailstats/js"
Alias "/mailstats/css" "/opt/mailstats/css"
Alias "/mailstats" "/opt/mailstats/html"
<Directory "/opt/mailstats/html">
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
);
$OUT .= (($mailstats{access} || 'private' ) eq "public" ) ? " Require all granted": " Require ip $localAccess $externalSSLAccess";
$OUT .= qq(
<FilesMatch .php\$\>
SetHandler "proxy:unix:/var/run/php-fpm/php74.sock|fcgi://localhost"
</FilesMatch>
</Directory>
<Directory "/opt/mailstats/css">
AllowOverride None
Require all granted
</Directory>
<Directory "/opt/mailstats/js">
AllowOverride None
Require all granted
</Directory>
);
}
else
{
$OUT .= "# mailstats is disabled";
}
}

View File

@@ -0,0 +1,210 @@
table {
xxborder:2px solid;
xxborder-collapse:collapse;
}
tr.row-total, tr.row-percent , td.col-15, td.col-16 {
font-weight: bold;
}
table {
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
border-collapse: collapse;
}
.headerpanel {
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.1);
border:1px solid;
width:98%;
}
.innerheaderpanel {
padding:10px;
}
tr,td,th {
border:1px solid;
}
.mailstats-detail-1stcol{
width:174px;
}
thead tr {
border-bottom :2px solid;
color:black;
background-color:darkgrey;
}
tfoot tr {
border-top:2px solid;
color:black;
font-weight: bold;
background-color:darkgrey;
}
.stripes tbody tr:nth-child(odd) {background-color: #dfdfdf}
.no-stripes tbody tr:nth-child(odd) {
background-color: transparent; /* or whatever background color you want */
}
div.linksattop {
display: flex;
justify-content: space-between;
}
a.prevlink {
text-align: left;
}
div.divshowindex {
flex-grow: 1;
text-align: center;
}
a.nextlink {
text-align: right;
}
/* Basic styling for the tab container */
.tab-container {
display: flex;
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
}
/* Styling for the tabs */
.tab {
padding: 10px 20px;
cursor: pointer;
border: 1px solid #ccc;
border-bottom: none;
background-color: #f1f1f1;
}
/* Styling for the active tab */
.tab-active {
background-color: #ffffff;
border-top: 2px solid #007bff;
font-weight: bold;
}
/* Hide all content sections by default */
.tab-content {
display: none;
}
/* Display the active content section */
.tab-content-active {
display: block;
}
.cssclass1 {background-color:#ffff99;}
.cssclass2 {background-color:lightcoral;}
.cssclass3 {background-color:lightcyan;}
.cssclass4 {background-color:lightgoldenrodyellow;}
.cssclass5 {background-color:lightgray;}
.cssclass6 {background-color:lightgreen;}
.cssclass7 {background-color:lightpink;}
.cssclass8 {background-color:lightsalmon;}
.cssclass9 {background-color:lightseagreen;}
.cssclass10 {background-color:lightskyblue;}
.cssclass11 {background-color:lightslategray;}
.cssclass12 {background-color:lightsteelblue;}
p.cssvalid,p.htmlvalid {float:left;margin-right:20px}
.maintable {}
.subtables {
display: flex;
flex-wrap: nowrap; /* Use wrap if you want the tables to wrap to the next line when the screen is too narrow */
gap: 10px; /* Optional: Adds space between the tables */
}
.subtables > div {
flex: 1; /* Equal width tables, remove or adjust based on your preference */
margin-left:0px;
}
.footer {}
.iframe-container {
width: 100%;
height: 500px; /* Adjust as needed */
border: none;
}
.parent-container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.greyed-out {
color: grey;
}
.subtables {
display: flex;
flex-wrap: wrap; /* Allows the items to wrap onto the next line */
justify-content: space-between; /* Distributes space between the tables */
margin: -10px; /* Negative margin to offset the table margins */
}
.table-container {
display: flex;
flex-wrap: wrap; /* Allowing wrapping of the tables */
width: 100%; /* Makes the container full width */
box-sizing: border-box; /* Adjust size calculations to include padding and borders */
}
.Incoming, .Junk, .Geoip, .Qpsmtpd, .Viruses, .Blacklist {
flex: 0 1 calc(25% - 20px); /* Each table will take 25% of the width minus margins */
margin: 10px; /* Margin for spacing */
box-sizing: border-box; /* Include padding and border in the element's total width and height */
}
/* Ensure tables adapt on smaller screens */
/* Default styling for large screens (5 columns) */
.Incoming, .Junk, .Geoip, .Qpsmtpd, .Viruses, .Blacklist {
flex: 0 1 calc(20% - 20px); /* 20% width for 5 columns */
margin: 10px;
box-sizing: border-box;
}
/* 4 columns layout */
@media (max-width: 1600px) {
.Incoming, .Junk, .Geoip, .Qpsmtpd, .Viruses, .Blacklist {
flex: 0 1 calc(25% - 20px); /* 25% width for 4 columns */
}
}
/* 3 columns layout */
@media (max-width: 1200px) {
.Incoming, .Junk, .Geoip, .Qpsmtpd, .Viruses, .Blacklist {
flex: 0 1 calc(33.333% - 20px); /* 33.333% width for 3 columns */
}
}
/* 2 columns layout */
@media (max-width: 600px) {
.Incoming, .Junk, .Geoip, .Qpsmtpd, .Viruses, .Blacklist {
flex: 0 1 calc(50% - 20px); /* 50% width for 2 columns */
}
}
/* 1 column layout for mobile */
@media (max-width: 300px) {
.Incoming, .Junk, .Geoip, .Qpsmtpd, .Viruses, .Blacklist{
flex: 0 1 100%; /* 100% width for 1 column */
}
}
/* Taken from inline in the chameleon templates */
.maindiv {width:100%;overflow-x:auto;font-size:1cqw}
.traffictable {border-collapse:collapse;width:98%}
.divseeinbrowser{text-align:center;}
.bordercollapse{border-collapse:collapse;}

View File

@@ -0,0 +1,51 @@
<?php
header('Content-Type: text/plain');
$input_param = isset($_GET['id']) ? $_GET['id'] : '9999';
// Set the directory and file names
$directory = "/opt/mailstats/logs";
$files = ['current1', 'current2'];
function process_file($file_path, $input_param) {
$file = fopen($file_path, 'r');
$match = "/ $input_param /";
$endmatch = "/cleaning up after $input_param/";
while (($line = fgets($file)) !== false) {
// Check if the line contains the input_parameter
if (preg_match($match,$line) === 1) {
echo $line;
} elseif (preg_match($endmatch,$line) === 1) {
echo $line;
exit();
}
}
fclose($file);
}
function tai64nToDate($tai64n) {
// Check if the input TAI64N string is valid
if (preg_match('/^@([0-9a-f]{8})([0-9a-f]{8})$/', $tai64n, $matches)) {
// First part: seconds since epoch
$sec_hex = $matches[1];
// Second part: nanoseconds in hex
$nsec_hex = $matches[2];
// Convert hex to decimal
$seconds = hexdec($sec_hex);
$nanoseconds = hexdec($nsec_hex);
// Calculate the full timestamp in seconds
$timestamp = $seconds + ($nanoseconds / 1e9); // Nanoseconds to seconds
// Format timestamp to 'Y-m-d H:i:s'
return date('Y-m-d H:i:s', $timestamp);
} else {
throw new InvalidArgumentException("Invalid TAI64N format.");
}
}
chdir($directory);
foreach ($files as $file) {
process_file($file, $input_param);
}
?>

View File

@@ -0,0 +1,166 @@
<?php
// Database configuration
$servername = "localhost";
$username = "mailstats";
$password = "mailstats";
$dbname = "mailstats";
// Default date to yesterday
$date = isset($_GET['date']) ? $_GET['date'] : date('Y-m-d', strtotime('-1 day'));
// Default hour to 99 (means all the hours)
$hour = isset($_GET['hour']) ? $_GET['hour'] : 99;
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Prepare and execute the query
if ($hour == 99){
$sql = "SELECT * FROM SummaryLogs WHERE Date = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $date);
} else {
$sql = "SELECT * FROM SummaryLogs WHERE Date = ? AND Hour = ?";
$stmt = $conn->prepare($sql);
$stmt->bind_param("si", $date, $hour);
}
$stmt->execute();
$result = $stmt->get_result();
$result_count = $result->num_rows;
function generateLogDataTable($logData) {
$data = json_decode($logData, true);
if (is_null($data)) {
return "Invalid JSON data";
}
//// Remove entries with the key "logterse"
//if (isset($data['logterse'])) {
//unset($data['logterse']);
//}
// Remove entries with the key "logterse" and remove entries with empty values
foreach ($data as $key => $value) {
if ($key === 'logterse' || empty($value)) {
unset($data[$key]);
}
}
// Handle adjacent duplicates by merging keys
$mergedData = [];
$previousValue = null;
foreach ($data as $key => $value) {
if ($value === $previousValue) {
// Merge the current key with the previous key
end($mergedData);
$lastKey = key($mergedData);
$newKey = "$lastKey/$key";
$mergedData[$newKey] = $value;
// Remove the old entry
unset($mergedData[$lastKey]);
} else {
// Otherwise, add a new entry
$mergedData[$key] = $value;
}
$previousValue = $value;
}
$keys = array_keys($mergedData);
$values = array_values($mergedData);
$output = '<table class="stripes" style="border-collapse: collapse; width:95%;overflow-x:auto; margin: 0.6% auto 0.6% auto;"><tbody>';
#$output = '<table class="stripes" style="border-collapse: collapse; width:95%;overflow-x:auto; margin:2%"><tbody>';
// Divide keys and values into sets of 6
$chunks = array_chunk($keys, 6);
foreach ($chunks as $chunkIndex => $chunk) {
if ($chunkIndex > 0) {
// Add spacing between different sets
#$output .= '<tr><td colspan="6" style="height: 1em;"></td></tr>';
}
$output .= '<tr>';
foreach ($chunk as $key) {
$output .= '<th>' . htmlspecialchars($key) . '</th>';
}
$output .= '</tr><tr>';
foreach ($chunk as $i => $key) {
$val = htmlspecialchars($values[$chunkIndex * 6+ $i]);
if ($key == 'id'){
$output .= '<td>' . "<a href='./ShowDetailedLogs.php?id=".$val."'</a>".$val."</td>";
} else {
$output .= '<td>' . $val . '</td>';
}
}
$output .= '</tr>';
}
$output .= '</tbody></table>';
return $output;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel='stylesheet' type='text/css' href='css/mailstats.css' />
<title>Summary Logs</title>
<!-- <style>
table {
xxwidth: 100%;
xxborder-collapse: collapse;
}
table, th, td {
xxborder: 1px solid black;
}
th, td {
xxpadding: 8px;
xxtext-align: left;
}
</style>
-->
</head>
<body>
<div style="width:100%;overflow-x:auto;font-size:0.726cqw">"
<h1>Summary Logs for Date: <?= htmlspecialchars($date) ?> <?= $hour == 99 ? 'for All Hours' : 'and Hour: ' . htmlspecialchars($hour) ?></h1>
<h3>Found <?= $result_count ?> records.</h3>
<table style="border-collapse:collapse;width:98%">
<thead>
<tr>
<th>Id</th>
<!--<th>Date</th>-->
<!--<th>Hour</th>-->
<th>Log Data</th>
</tr>
</thead>
<tbody>
<?php if ($result->num_rows > 0): ?>
<?php while($row = $result->fetch_assoc()): ?>
<tr>
<td><?= htmlspecialchars($row['id']) ?></td>
<td><?= generateLogDataTable($row['logData']) ?></td>
</tr>
<?php endwhile; ?>
<?php else: ?>
<tr>
<td colspan="4">No records found for the specified date and hour.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
// Close the connection
$stmt->close();
$conn->close();
?>
</body>
</html>

View File

@@ -0,0 +1,98 @@
document.addEventListener("DOMContentLoaded", () => {
doNavs(); // Your initialization code
});
//function openTab(event, tabId){
//// Get all elements with class="tab-content" and hide them
//const tabContents = document.querySelectorAll('.tab-content');
//tabContents.forEach(content => content.classList.remove('tab-content-active'));
//// Get all elements with class="tab" and remove the class "tab-active"
//const tabs = document.querySelectorAll('.tab');
//tabs.forEach(tab => tab.classList.remove('tab-active'));
//// Show the current tab content, and add an "active" class to the clicked tab
//document.getElementById(tabId).classList.add('tab-content-active');
//event.target.classList.add('tab-active');}
//function LinkCheck(url){
//var http = new XMLHttpRequest();
//http.open('HEAD', url, false);
//http.send();
//return http.status!=404;
//}
async function LinkCheck(url) {
//Not allowed by CSP rules.
//try {
//const response = await fetch(url, { mode: "no-cors" });
//return response.ok; // Returns true if the resource exists
//} catch (error) {
//console.error("Error checking link:", error);
//return false;
//}
return true;
}
function doNavs() {
const isInIframe = window.self !== window.top;
var aTags = document.getElementsByTagName('a'),
atl = aTags.length,i;
for (i = 0; i < atl; i++) {
if (aTags[i].innerText == "Previous") {
if (isInIframe){ //!LinkCheck(aTags[i].href)) {
aTags[i].style.visibility = "hidden";
} else {
aTags[i].style.visibility = "visible";
}
} else if (aTags[i].innerText == "Next") {
if (isInIframe){ //!LinkCheck(aTags[i].href)) {
aTags[i].style.visibility = "hidden";
} else {
aTags[i].style.visibility = "visible";
}
} else if (aTags[i].innerText == "Index of files") {
if (isInIframe){ //!LinkCheck(aTags[i].href)) {
aTags[i].style.visibility = "hidden";
} else {
aTags[i].style.visibility = "visible";
}
}
}
}
function openTab(evt, tabName) {
// Declare all variables
var i, tab_content, tab;
// Get all elements with class="tab_content" and hide them
tab_content = document.getElementsByClassName("tab-content");
for (i = 0; i < tab_content.length; i++) {
tab_content[i].style.display = "none";
}
// Get all elements with class="tab" and remove the class "active"
tab = document.getElementsByClassName("tab");
for (i = 0; i < tab.length; i++) {
tab[i].className = tab[i].className.replace(" tab-active", "");
}
// Show the current tab, and add an "active" class to the link that opened the tab
document.getElementById(tabName).style.display = "block";
evt.currentTarget.className += " tab-active";
// Store the active tab index
sessionStorage.setItem('activeTab', evt.currentTarget.getAttribute("data-index"));
}
document.addEventListener("DOMContentLoaded", function() {
// Attach click event handler for all divs with the class "tab"
document.querySelectorAll(".tab").forEach(function(tab) {
tab.addEventListener("click", function(event) {
// Get the data-index attribute value
const tabIndex = this.getAttribute("data-index");
// Dynamically call openTab with the correct tab parameter
openTab(event, `tab${tabIndex}`);
});
});
});

0
root/opt/mailstats/logs/.gitignore vendored Normal file
View File

View File

@@ -0,0 +1,22 @@
<div class="${classname}">
<h2>${title}</h2>
<tal:block condition="threshold != 0">
<span class='greyed-out'>Display threshold set to ${threshold}%</span>
</tal:block>
<tal:block condition="threshold == 0">
<br>
</tal:block>
<table class="bordercollapse">
<thead>
<tr>
<th tal:repeat="header column_headers">${header}</th>
</tr>
</thead>
<tbody>
<tr tal:repeat="item array_2d">
<td tal:repeat="cell item">${cell}</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>SMEServer Mailstats</title>
<link rel='stylesheet' type='text/css' href='css/mailstats.css' />
<!-- Check links -->
<!--css here-->
</head>
<body>
<div class=maindiv>
<!---Navigation here-->
<div class='linksattop'>
<a class='prevlink' href='http://${SystemName}.${DomainName}/mailstats/mailstats_for_${PreviousDate}.html'>Previous</a>
<div class='divshowindex'><a class='showindex' href='http://${SystemName}.${DomainName}/mailstats/'>Index of files</a></div>
<a class='nextlink' href='http://${SystemName}.${DomainName}/mailstats/mailstats_for_${NextDate}.html'>Next</a></div> <!--format important here - used to insert see in browser for email -->
<br />
<h2>${structure:title}</h2>
<br />
<div class="headerpanel">
<div class = "innerheaderpanel">
<!---Add in header information here -->
</div>
</div>
<br />
<br />
<!--Tabs -->
<div class="tab-container">
<div class="tab tab-active" data-index="0" >Table</div>
<div tal:condition="enable_graphs" class="tab" data-index="1">Line Graph</div>
<div tal:condition="enable_graphs" class="tab" data-index="2">Stacked Bar Graph</div>
<div tal:condition="enable_graphs" class="tab" data-index="3">Scatter Graph</div>
<div tal:condition="enable_graphs" class="tab" data-index="4">Pie Chart</div>
</div>
<div id="tab0" class="tab-content tab-content-active">
<div class = "maintable">
<table class="traffictable">
<thead>
<tr>
<th>Date/Time</th>
<th tal:repeat="header column_headers" tal:content="header">Header</th>
</tr>
</thead>
<tbody>
<tr tal:repeat="row array_2d" tal:attributes="class python: 'row-total' if repeat.row.index == 24 else 'row-percent' if repeat.row.index == 25 else None">
<td tal:condition="repeat.row.index == 24" tal:attributes="class python:'col-total'" tal:content="'TOTALS'">Totals</td>
<td tal:condition="repeat.row.index == 25" tal:attributes="class python:'col-percent'" tal:content="'PERCENT'">Percent</td>
<td tal:condition="repeat.row.index < 24" tal:content="string:${reporting_date}, ${repeat.row.index}">Hour</td>
<td tal:repeat="cell row" tal:attributes="class python: 'col-' + str(repeat.cell.index)">
<!-- Check if 'nolinks' is true. If not, generate links for rows 0 to 23 except 'PERCENT' column -->
<tal:case tal:condition="not: nolinks">
<tal:case tal:condition="repeat.row.index >= 0 and repeat.row.index < 24 and repeat.cell.index != 16">
<a tal:attributes="href string:./showSummaryLogs.php?date=${reporting_date}&hour=${repeat.row.index}">
<!-- Check if cell value is zero and print "" -->
<tal:case tal:condition="cell != 0" tal:content="cell">Cell</tal:case>
<tal:case tal:condition="cell == 0" tal:content="''">-</tal:case>
</a>
</tal:case>
<!-- For 'PERCENT' column or other rows, just display the cell content -->
<tal:case tal:condition="not (repeat.row.index >= 0 and repeat.row.index < 24 and repeat.cell.index != 16)">
<!-- Check if cell value is zero and print "" -->
<tal:case tal:condition="cell != 0" tal:content="cell">Cell</tal:case>
<tal:case tal:condition="cell == 0" tal:content="''">-</tal:case>
</tal:case>
</tal:case>
<tal:case tal:condition="nolinks">
<!-- Display cell content without link if 'nolinks' is true -->
<tal:case tal:condition="cell != 0" tal:content="cell">Cell</tal:case>
<tal:case tal:condition="cell == 0" tal:content="''">-</tal:case>
</tal:case>
</td>
</tr>
</tbody>
</table>
</div>
<div class = "subtables">
<div class="table-container">
<!---Add in sub tables here -->
</div>
</div>
</div>
<!-- Next Tab-->
<div tal:condition="enable_graphs" id="tab1" class="tab-content">
<img src="line_graph_${reporting_date}.png">
</div>
<!-- Next Tab-->
<div tal:condition="enable_graphs" id="tab2" class="tab-content">
<img src="bar_graph_${reporting_date}.png">
</div>
<!-- Next Tab-->
<div tal:condition="enable_graphs" id="tab3" class="tab-content">
<img src="scatter_graph_${reporting_date}.png">
</div>
<!-- Next Tab-->
<div tal:condition="enable_graphs" id="tab4" class="tab-content">
<img src="pie_chart_${reporting_date}.png">
</div>
<br />
<footer class="footer">${version}</footer>
<script type='text/javascript' src='js/mailstats.js' ></script>
<!--
<p class="cssvalid">
<a href="http://jigsaw.w3.org/css-validator/check/referer">
<img style="border:0;width:88px;height:31px"
src="http://jigsaw.w3.org/css-validator/images/vcss"
alt="Valid CSS!" />
</a>
</p>
<p class="htmlvalid">
<a href="https://validator.w3.org/check?uri=referer"><img
src="http://www.w3.org/Icons/valid-xhtml10"
alt="Valid XHTML 1.0!" height="31" width="88" /></a>
</p>
</div>
-->
</body>
</html>

View File

@@ -0,0 +1,97 @@
import os
import re
from datetime import datetime, timedelta
import glob # Import the glob module
def tai64n_to_datetime(tai64n):
"""Convert TAI64N formatted timestamp to a datetime object."""
if len(tai64n) < 16:
raise ValueError(f"Invalid TAI64N timestamp length: {tai64n}")
high_bits = int(tai64n[:15], 16)
low_bits = int(tai64n[15:23], 16)
seconds_since_epoch = high_bits
nanoseconds = low_bits
# Create datetime object
epoch = datetime(1970, 1, 1)
dt = epoch + timedelta(seconds=seconds_since_epoch)
dt += timedelta(microseconds=nanoseconds // 1000)
return dt
def convert_log(file_paths, output_path):
host_name = "sme11"
total_files = 0
total_lines = 0
# Input file validation
for file_path in file_paths:
if not os.path.isfile(file_path):
print(f"Input file {file_path} does not exist.")
return
with open(output_path, 'w') as output_file:
for file_path in file_paths:
print(f"{file_path}")
# Determine the process name based on the file being read
if "sqpsmtpd" in file_path:
process_name = "sqpsmtpd-forkserver"
else:
process_name = "qpsmtpd-forkserver"
with open(file_path, 'r', encoding='latin1') as log_file:
total_files += 1
try:
for line in log_file:
total_lines += 1
match = re.match(r'@(\w+) (\d+) \((.*?)\) (.*)', line.strip())
if match:
tai64n_timestamp, pid, context, message = match.groups()
try:
log_time = tai64n_to_datetime(tai64n_timestamp[1:]) # Ignore '@'
formatted_time = log_time.strftime('%b %d %H:%M:%S')
# Replace "bjsystems.co.uk" with "thereadclan.me.uk" in the message
#message = message.replace("bjsystems.co.uk", "thereadclan.me.uk")
# Correctly format the output line
formatted_line = f"{formatted_time} {host_name} {process_name}[{pid}]: {pid} ({context}) {message}\n"
output_file.write(formatted_line)
except Exception as e:
with open("error_log.txt", 'a') as error_file:
error_file.write(f"Could not convert timestamp {tai64n_timestamp}: {e}\n")
print(f"Error logged for timestamp {tai64n_timestamp}.")
else:
#does not mathc the logterse line, but still needed
match = re.match(r'@(\w+) (\d+) (.*)', line.strip())
if match:
tai64n_timestamp, pid, message = match.groups()
try:
log_time = tai64n_to_datetime(tai64n_timestamp[1:]) # Ignore '@'
formatted_time = log_time.strftime('%b %d %H:%M:%S')
# Replace "bjsystems.co.uk" with "thereadclan.me.uk" in the message
#message = message.replace("bjsystems.co.uk", "thereadclan.me.uk")
# Correctly format the output line
formatted_line = f"{formatted_time} {host_name} {process_name}[{pid}]: {pid} {message}\n"
output_file.write(formatted_line)
except Exception as e:
with open("error_log.txt", 'a') as error_file:
error_file.write(f"Could not convert timestamp {tai64n_timestamp}: {e}\n")
print(f"Error logged for timestamp {tai64n_timestamp}.")
except Exception as e:
print(f"Error reading file {file_path}: {e}")
continue
print(f"Processed {total_files} files and {total_lines} lines.")
# Specify the input and output file paths
# Use glob to expand file patterns
input_log_files = (
glob.glob("/var/log/qpsmtpd/@*.s") +
["/var/log/qpsmtpd/current", "/var/log/sqpsmtpd/current"] +
glob.glob("/var/log/sqpsmtpd/@*.s") # Adjust the asterisk * as needed
)
output_log_file = "output_log.txt" # Specify your desired output file path
# Convert the log
convert_log(input_log_files, output_log_file)
print(f"Log conversion complete. Check the output at: {output_log_file}")

View File

@@ -33,6 +33,7 @@ use strict;
# bjr - 18Oct20 - Alter use of lc to avoid uninitialised messages - bug 11044
# bjr - 02Apr21 - Fix up lc to try to avoif uninit messages - and alter warning status - bug 11519
# bjr - 15Feb23 - Add in auth::auth_imap after change to use dovecot as incoming authorisation Bugzilla 12327
# bjr - 29Dec24 - Convert to SME11 log date format
#
#############################################################################
#
@@ -70,6 +71,11 @@ use strict;
#
# internal modules (part of core perl distribution)
#
# New format for SME11
#
#Dec 17 19:34:34 sme11 qpsmtpd-forkserver[441318]: 441318 (deny) logging::logterse: ` 192.168.1.4 pc-00004.thereadclan.me.uk bjsystems.co.uk <biodiversityadvanced@bjsystems.co.uk> check_goodrcptto 901 relaying denied smeserver@thereadclan.me.uk msg denied before queued
use strict;
use warnings;
use Getopt::Long;
@@ -105,7 +111,7 @@ if ($cdb->get('mailstats')){
#Configuration section
my %opt = (
version => '0.7.16', # please update at each change.
version => '0.8.01', # 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
@@ -370,17 +376,30 @@ my $makeHTMLpage = "no";
# Init the hashes
my $nhour = floor( $start / 3600 );
my $nhour = 0;
#print "Hour:".$nhour."\n";
my $ncateg;
while ( $nhour < $end / 3600 ) {
$counts{$nhour}=();
$ncateg = 0;
while ( $ncateg < @categs) {
$counts{$nhour}{$categs[$ncateg-1]} = 0;
$ncateg++
while ( $nhour < 24 ) {
$counts{$nhour} = {}; # Initialize as a hash reference
#print "Hour:".$nhour."\n";
my $ncateg = 0; # Reset $ncateg for each hour
while ( $ncateg < @categs ) {
$counts{$nhour}{$categs[$ncateg]} = 0; # Corrected index
$ncateg++; # Increment $ncateg
}
$nhour++;
$nhour++; # Increment $nhour
}
#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;
@@ -402,8 +421,17 @@ while ( $ncateg < @categs) {
$ncateg++
}
my $starttai = Time::TAI64::unixtai64n($start);
my $endtai = Time::TAI64::unixtai64n($end);
#foreach my $hour (sort keys %counts) {
#printf "Hour: %2d\n", $hour; # Right-aligned hour
#foreach my $categ (sort keys %{ $counts{$hour} }) {
#printf " %-12s: %d\n", $categ, $counts{$hour}{$categ}; # Left-aligned category
#}
#}
#die("die");
#my $starttai = Time::TAI64::unixtai64n($start);
#my $endtai = Time::TAI64::unixtai64n($end);
my $sum_SARules = 0;
# we remove non valid files
@@ -424,13 +452,23 @@ my $count = -1; #for loop reduction in debugging mode
my $CurrentMailId = "";
LINE: while (<>) {
chomp;
next LINE if !(my($tai,$log) = split(' ',$_,2));
# Use a regex to extract the date, time, and the rest of the log
if (/^(\w+\s+\d+\s+\d{2}:\d{2}:\d{2})\s+\S+\s+\S+\[(\d+)\]:\s+(.*)/) {
my $datetime = $1; # Get the date and time
my $log_id = $2; # Get the log ID
my $log_entry = $3; # The rest of the log line
# Convert datetime to epoch
my $current_epoch = convert_to_epoch($datetime);
#If date specified, only process lines matching date
next LINE if ( $tai lt $starttai );
next LINE if ( $tai gt $endtai );
# If date specified, only process lines matching date
#print $datetime." ".$current_epoch." ".$start." ".$end."\n";
next LINE if ($current_epoch < $start);
next LINE if ($current_epoch > $end);
#print $datetime."\n";
#Count lines and skip out if debugging
$count++;
@@ -493,20 +531,36 @@ LINE: while (<>) {
#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 $abstime = Time::TAI64::tai2unix($tai);
#my $abshour = floor( $abstime / 3600 ); # Hours since the epoch
# Create a timestamp for the previous hour
my $previous_hour_epoch = $current_epoch; # - 3600; # Subtract 3600 seconds (1 hour)
# Convert epoch time to local time
my ($sec, $min, $hour) = localtime($previous_hour_epoch);
#print $sec." ".$min." ".$hour."\n";
#$hour = ($hour==23)?0:$hour;
my $abshour = $hour;
#print "Abs:".$abshour." ".strftime('%Y-%m-%dT%H:%M:%SZ',gmtime($previous_hour_epoch))."\n";
my ($timestamp_part, $log_part) = split('`',$_,2); #bjr 0.6.12
my (@log_items) = split $FS, $log_part;
#print "0".$log_items[0]."\n";
#print "1".$log_items[1]."\n";
#print "5".$log_items[5]."\n";
#print "7".$log_items[7]."\n";
my (@timestamp_items) = split(' ',$timestamp_part);
#print $timestamp_items[5];
#print "\n";
my $result= "rejected"; #Tag as rejected unti we know otherwise
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 $proc = $timestamp_items[5] ; #numeric Id for the email
my $emailnum = $proc; #proc gets modified later...
if ($emailnum == 23244) {
@@ -534,7 +588,7 @@ LINE: while (<>) {
$counts{$abshour}{$CATRELAY}++;
}
elsif (($log_items[2] =~ m/$WebmailIP/) and (!test_for_private_ip($log_items[0]))) {
elsif (($log_items[2] =~ m/$WebmailIP/) and (!test_for_private_ip($log_items[0]))) {
#Webmail
$localflag = 1;
$WebMailsendtotal++;
@@ -556,33 +610,33 @@ LINE: while (<>) {
$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;
}
}
#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;
}
}
}
}
}
@@ -716,22 +770,24 @@ LINE: while (<>) {
#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";}
if (defined($log_items[8])){
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 {
print "Unexpected non numeric found in $proc:".$log_items[8]."($score)\n";
# no SA score - treat it as ham
$hamcount++;$counts{$abshour}{$CATHAM}++;
}
if ( ( $currentrcptdomain{ $proc } || '' ) ne '' ) {
$byrcptdomain{ $currentrcptdomain{ $proc } }{ 'accept' }++ ;
$currentrcptdomain{ $proc } = '' ;
}
} else {
# no SA score - treat it as ham
$hamcount++;$counts{$abshour}{$CATHAM}++;
}
if ( ( $currentrcptdomain{ $proc } || '' ) ne '' ) {
$byrcptdomain{ $currentrcptdomain{ $proc } }{ 'accept' }++ ;
$currentrcptdomain{ $proc } = '' ;
}
}
@@ -822,12 +878,16 @@ LINE: while (<>) {
}
}
#exit if $emailnum == 15858;
#print "Counts:*$abshour* ".$counts{$abshour}{$CATNONCONF}."\n";
;
} # end of if regexp
} #END OF MAIN LOOP
#total up grand total Columns
$nhour = floor( $start / 3600 );
while ( $nhour < $end / 3600 ) {
$nhour = 0;
while ( $nhour < 24 ) {
$ncateg = 0; #past the where it came from columns
while ( $ncateg < @categs) {
#total columns
@@ -846,8 +906,8 @@ while ( $nhour < $end / 3600 ) {
#Compute row totals and row percentages
$nhour = floor( $start / 3600 );
while ( $nhour < $end / 3600 ) {
$nhour = 0; #floor( $start / 3600 );
while ( $nhour < 24 ){ #}$end / 3600 ) {
$counts{$nhour}{$categs[@categs-1]} = $counts{$nhour}{$categs[@categs-2]}*100/$totalexamined if $totalexamined;
$nhour++;
@@ -865,8 +925,8 @@ while ( $nhour < $end / 3600 ) {
}
#compute sum of row percentages
$nhour = floor( $start / 3600 );
while ( $nhour < $end / 3600 ) {
$nhour = 0; #floor( $start / 3600 );
while ( $nhour < 24 ){ #}$end / 3600 ) {
$counts{$GRANDTOTAL}{$categs[@categs-1]} += $counts{$nhour}{$categs[@categs-1]};
$nhour++;
@@ -995,15 +1055,15 @@ if ( !$disabled ) {
my $Totals; #Corresponding totals
my $Percent; # and column percentages
my $hour = floor( $start / 3600 );
my $hour = 0; #floor( $start / 3600 );
$Line1 = '';
$Line2 = '';
$Titles = '';
$Values = '';
$Totals = '';
$Percent = '';
while ( $hour < $end / 3600 ) {
if ($hour == floor( $start / 3600 )){
while ( $hour < 24){ #}$end / 3600 ) {
if ($hour == 0){ #}floor( $start / 3600 )){
#Do all the once only things
$ncateg = 0;
while ( $ncateg < @categs) {
@@ -1030,10 +1090,11 @@ if ( !$disabled ) {
}
$ncateg = 0;
my $table_start_time = $start;
while ( $ncateg < @categs) {
if ($finaldisplay[$ncateg]){
if ($ncateg == 0) {
$Values .= strftime( "%F, %H", localtime( $hour * 3600 ) )." "
$Values .= strftime( "%F, %H", localtime( $table_start_time + $hour * 3600 ) )." "
} elsif ($ncateg == @categs-1) {
#percentages in last column
$Values .= sprintf('%'.($colwidth[$ncateg]-2).'.1f',$counts{$hour}{$categs[$ncateg]})."%";
@@ -1289,13 +1350,16 @@ sub analysis_period {
$sec=0;$min=0;$hour=$base;
};
#$mday="05"; #$mday="03"; #$mday="16"; #Temp!!
#print $sec." ".$min." ".$hour." ".$mday." ".$mon." ".$year." ".$wday." ".$yday."\n";
$time = timelocal($sec,$min,$hour,$mday,$mon,$year);
#print $time."\n"
}
my $start = str2time( $startdate );
my $end = $enddate ? str2time( $enddate ) :
$startdate ? $start + $secsininterval : $time;
$start = $startdate ? $start : $end - $secsininterval;
#print "Analysis".$start." ".$end;
return ( $start > $end ) ? ( $end, $start ) : ( $start, $end );
}
@@ -1667,7 +1731,7 @@ sub save_data
"mailstats", "mailstats" )
or die "Cannot open mailstats db - has it beeen created?";
my $hour = floor( $start / 3600 );
my $hour = 0; #floor( $start / 3600 );
my $reportdate = strftime( "%F", localtime( $hour * 3600 ) );
my $dateid = get_dateid($dbh,$reportdate);
my $reccount = 0; #count number of records written
@@ -1740,9 +1804,9 @@ sub save_data
}
# finally - the hourly breakdown
# need to remember here that the date might change during the 24 hour span
my $nhour = floor( $start / 3600 );
my $nhour = 0; #floor( $start / 3600 );
my $ncateg;
while ( $nhour < $end / 3600 ) {
while ( $nhour < 24){ #}$end / 3600 ) {
#see if the time record has been created
# print strftime("%H",localtime( $nhour * 3600 ) ).":00:00\n";
my $sth =
@@ -1891,10 +1955,32 @@ sub get_dateid
#} else { return 0}
#}
sub convert_to_epoch {
my ($sec, $min, $hour1, $mday, $mon, $year) = localtime();
$year += 1900; # localtime returns year as years since 1900
my ($datetime) = @_;
my ($month, $day, $time) = split(' ', $datetime);
my ($hour, $minute, $second) = split(':', $time);
my $month_num = {
Jan => 0, Feb => 1, Mar => 2, Apr => 3,
May => 4, Jun => 5, Jul => 6, Aug => 7,
Sep => 8, Oct => 9, Nov => 10, Dec => 11,
}->{$month};
return timelocal($second, $minute, $hour, $day, $month_num, $year); # Adjust the year as necessary
}
sub test_for_private_ip {
use NetAddr::IP;
$_ = shift;
# Remove leading whitespace
s/^\s+//;
return unless /(\d+\.\d+\.\d+\.\d+)/;
my $ip = NetAddr::IP->new($1);
return unless $ip;

1914
root/usr/bin/mailstats.py Normal file

File diff suppressed because it is too large Load Diff

22
root/usr/bin/runmailstats.sh Normal file → Executable file
View File

@@ -1,3 +1,21 @@
#!/bin/bash
exec 1> >(logger -t $(basename $0)) 2>&1
perl /usr/bin/mailstats.pl /var/log/qpsmtpd/\@* /var/log/qpsmtpd/current /var/log/sqpsmtpd/\@* /var/log/sqpsmtpd/current
# Validate date format (YYYY-MM-DD)
validate_date() {
local date_regex="^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$"
if [[ ! $1 =~ $date_regex ]]; then
echo "Error: Invalid date format. Use YYYY-MM-DD" >&2
exit 1
fi
}
# Set date (default: yesterday)
if [ -n "$1" ]; then
run_date="$1"
validate_date "$run_date"
else
run_date=$(date -d "yesterday" +%F)
fi
# Run mailstats with validated date
python3 /usr/bin/mailstats.py -d "$run_date"

View File

@@ -0,0 +1,17 @@
#!/bin/bash
#exec 1> >(logger -t $(basename $0)) 2>&1
perl /usr/bin/mailstats.pl /var/log/qpsmtpd/\@* /var/log/qpsmtpd/current /var/log/sqpsmtpd/\@* /var/log/sqpsmtpd/current
# and run new python one - start by copying and decoding log files
yesterday_date=$(date -d "yesterday" +'%mm %d')
#cd /var/log/qpsmtpd
#cat \@* current >/opt/mailstats/logs/current1 2>/dev/null
#cd /var/log/sqpsmtpd
#cat \@* current >/opt/mailstats/logs/current2 2>/dev/null
cd /opt/mailstats/logs
#cat current1 current2 2>/dev/null | /usr/local/bin/tai64nlocal | grep "$yesterday_date" > current1.log
python3 /usr/bin/mailstats-convert-log-sme10-to-sme11.py
yesterday_date=$(date -d "yesterday" +'%b %d')
cat output_log.txt | grep "$yesterday_date" | sort >current.log
ls -l
python3 /usr/bin/mailstats.py
echo "Done"

View File

@@ -0,0 +1,321 @@
#
# Generated by SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-04-04 12:46:00
#
#
# Routines to be edited by the developer to provide content and validation for parameters
# and provison of the control data for table(s)
#
use esmith::util;
use esmith::util::network;
use esmith::ConfigDB;
use esmith::HostsDB;
use esmith::AccountsDB;
use esmith::NetworksDB;
use esmith::DomainsDB;
use POSIX 'strftime';
use constant FALSE => 0;
use constant TRUE => 1;
#The most common ones
#my $cdb
#my $adb
#my $ndb
#my $hdb
#my $ddb
# Validation routines - parameters for each panel
sub validate_TABLE {
my $c = shift;
my $prefix_data = shift; #Data hash as parameter
# Validation for each field
my $ret = "";
if (! TRUE) #validate $c->param('StatsDate')
{$ret .= 'Validation for StatsDate failed';}
if ($ret eq "") {$ret = 'ok';}
return $ret;
}
sub validate_CONFIG {
my $c = shift;
my $prefix_data = shift; #Data hash as parameter
# Validation for each field
my $ret = "";
if (! TRUE) #validate $c->param('TextorHTML')
{$ret .= 'Validation for TextorHTML failed';}
if (! TRUE) #validate $c->param('Email')
{$ret .= 'Validation for Email failed';}
if (! TRUE) #validate $c->param('EmailHost')
{$ret .= 'Validation for EmailHost failed';}
if (! TRUE) #validate $c->param('EmailUser')
{$ret .= 'Validation for EmailUser failed';}
if (! TRUE) #validate $c->param('DBSave')
{$ret .= 'Validation for DBSave failed';}
if (! TRUE) #validate $c->param('DBHost')
{$ret .= 'Validation for DBHost failed';}
if (! TRUE) #validate $c->param('DBUser')
{$ret .= 'Validation for DBUser failed';}
if (! TRUE) #validate $c->param('CountrySelect')
{$ret .= 'Validation for CountrySelect failed';}
if (! TRUE) #validate $c->param('AccumCountryCodes')
{$ret .= 'Validation for AccumCountryCodes failed';}
if (! TRUE) #validate $c->param('EnableRHSBL')
{$ret .= 'Validation for EnableRHSBL failed';}
if (! TRUE) #validate $c->param('EnableRHSBL')
{$ret .= 'Validation for EnableRHSBL failed';}
if (! TRUE) #validate $c->param('RBLLIST')
{$ret .= 'Validation for RBLLIST failed';}
if (! TRUE) #validate $c->param('SBLLIST')
{$ret .= 'Validation for SBLLIST failed';}
if (! TRUE) #validate $c->param('UBLLIST')
{$ret .= 'Validation for UBLLIST failed';}
if ($ret eq "") {$ret = 'ok';}
return $ret;
}
# Get singleton data for each panel
sub get_data_for_panel_TABLE {
# Return a hash with the fields required which will be loaded into the shared data
my $c = shift;
my %ret = (
'Data1'=>'Data for TABLE', #Example
# fields from Inputs in TABLE $fields['TABLE']
'StatsDate'=>'StatsDate contents',
);
return %ret;
}
sub get_data_for_panel_CONFIG {
# Return a hash with the fields required which will be loaded into the shared data
my $c = shift;
my $cdb = esmith::ConfigDB->open() || die("Couldn't open config db");
my $key = 'mailstats';
my %ret = (
'Data1'=>'Data for CONFIG', #Example
# fields from Inputs in CONFIG $fields['CONFIG']
'TextorHTML'=>$cdb->get_prop($key,'TextorHTML') || 'HTML',
'Email'=>$cdb->get_prop($key,'Email') || 'admin',
'EmailHost'=>$cdb->get_prop($key,'EmailHost') || 'localhost',
'EmailPort'=>$cdb->get_prop($key,'EmailPORT') || '25',
'EmailUser'=>$cdb->get_prop($key,'EmailUser') || 'admin',
'DBSave'=>$cdb->get_prop($key,'SaveDataToMySQL') || 'yes',
'DBHost'=>$cdb->get_prop($key,'DBHost') || 'localhost',
'DBUser'=>$cdb->get_prop($key,'DBUser') || 'admin',
'DBPort'=>$cdb->get_prop($key,'DBPort') || '3306',
#'CountrySelect'=>'CountrySelect ',
'AccumCountryCodes'=>$cdb->get_prop('qpsmtpd','BadCountries'),
'EnableRHSBL'=>$cdb->get_prop('qpsmtpd','RHSBL'),
'EnableDNSBL'=>$cdb->get_prop('qpsmtpd','DNSBL'),
'EnableURIBL'=>$cdb->get_prop('qpsmtpd','URIBL'),
'RBLList'=>$cdb->get_prop('qpsmtpd','RBLList'),
'SBLList'=>$cdb->get_prop('qpsmtpd','SBLList'),
'UBLList'=>$cdb->get_prop('qpsmtpd','UBLList'),
'TagLevel'=>$cdb->get_prop('spamassassin','TagLevel') || '5',
'RejectLevel'=>$cdb->get_prop('spamassassin','RejectLevel') || '12'
);
return %ret;
}
# Get control data for table(s)
# Return hash with values from row in which link clicked on table
sub get_selected_TABLE {
my $c = shift;
my $selected = shift; #Parameter is name of selected row.
my $is_new_record = shift; #Indicates new record required (defaults)
my %ret = {};
return %ret;
}
sub get_selected_CONFIG {
my $c = shift;
my $selected = shift; #Parameter is name of selected row.
my $is_new_record = shift; #Indicates new record required (defaults)
my %ret = {};
return %ret;
}
#after sucessful modify or create or whatever and submit then perfom (if the params validate)
sub perform_TABLE {
my $c = shift;
my $prefix_data = shift; #Data hash as parameter
my $ret = "";
my $db = $cdb; #maybe one of the others
my $dbkey = 'ChangeThis';
# To make it write to DB as comment, delete this (regex) string in each if statement "TRUE\) \#copy or perform with value: .* e.g."
if (! TRUE) #copy or perform with value: StatsDate e.g. $db->set_prop($dbkey,'StatsDate',$c->param('StatsDate'),type=>'service'))
{$ret .= 'Perform/save failed for StatsDate';}
if ($ret eq "") {$ret = 'ok';}
return $ret;
}
sub perform_CONFIG {
my $c = shift;
my $prefix_data = shift; #Data hash as parameter
my $ret = "";
my $cdb = esmith::ConfigDB->open() || die("Couldn't open config db");
my $db = $cdb; #maybe one of the others
my $dbkey = 'mailstats';
# To make it write to DB as comment, delete this (regex) string in each if statement "TRUE\) \#copy or perform with value: .* e.g."
if (! $db->set_prop($dbkey,'TextorHTML',$c->param('TextorHTML'),type=>'service'))
{$ret .= 'Perform/save failed for TextorHTML';}
if (! $db->set_prop($dbkey,'Email',$c->param('Email'),type=>'service'))
{$ret .= 'Perform/save failed for Email';}
if (! $db->set_prop($dbkey,'EmailHost',$c->param('EmailHost'),type=>'service'))
{$ret .= 'Perform/save failed for EmailHost';}
if (! $db->set_prop($dbkey,'EmailPort',$c->param('EmailPort'),type=>'service'))
{$ret .= 'Perform/save failed for EmailPort';}
if (! $db->set_prop($dbkey,'EmailUser',$c->param('EmailUser'),type=>'service'))
{$ret .= 'Perform/save failed for EmailUser';}
if (! $db->set_prop($dbkey,'SaveDataToMySQL',$c->param('DBSave'),type=>'service'))
{$ret .= 'Perform/save failed for DBSave';}
if (! $db->set_prop($dbkey,'DBHost',$c->param('DBHost'),type=>'service'))
{$ret .= 'Perform/save failed for DBHost';}
if (! $db->set_prop($dbkey,'DBUser',$c->param('DBUser'),type=>'service'))
{$ret .= 'Perform/save failed for DBUser';}
if (! $db->set_prop($dbkey,'DBPort',$c->param('DBPort'),type=>'service'))
{$ret .= 'Perform/save failed for DBPort';}
#if (! $db->set_prop($dbkey,'CountrySelect',$c->param('CountrySelect'),type=>'service'))
# {$ret .= 'Perform/save failed for CountrySelect';}
if (! $db->set_prop('qpsmtpd','BadCountries',$c->param('AccumCountryCodes'),type=>'service'))
{$ret .= 'Perform/save failed for AccumCountryCodes';}
if (! $db->set_prop('qpsmtpd','RHSBL',$c->param('EnableRHSBL'),type=>'service'))
{$ret .= 'Perform/save failed for EnableRHSBL';}
if (! $db->set_prop('qpsmtpd','DNSBL',$c->param('EnableDNSBL'),type=>'service'))
{$ret .= 'Perform/save failed for EnableDNSBL';}
if (! $db->set_prop('qpsmtpd','URIBL',$c->param('EnableURIBL'),type=>'service'))
{$ret .= 'Perform/save failed for EnableURIBL';}
if (! $db->set_prop('qpsmtpd','RBLList',$c->param('RBLList'),type=>'service'))
{$ret .= 'Perform/save failed for RBLLIST';}
if (! $db->set_prop('qpsmtpd','SBLList',$c->param('SBLList'),type=>'service'))
{$ret .= 'Perform/save failed for SBLLIST';}
if (! $db->set_prop('qpsmtpd','UBLList',$c->param('UBLList'),type=>'service'))
{$ret .= 'Perform/save failed for UBLLIST';}
if (! $db->set_prop('spamassassin','TagLevel',$c->param('TagLevel'),type=>'service'))
{$ret .= 'Perform/save failed for TagLevel';}
if (! $db->set_prop('spamassassin','RejectLevel',$c->param('RejectLevel'),type=>'service'))
{$ret .= 'Perform/save failed for RejectLevel';}
if ($ret eq "") {$ret = 'ok';}
return $ret;
}
sub create_link{
# WIP
my ($c,$route, $panel, $index) = @_;
my $link = "$route?trt=$panel&Selected=$index";
return $link;
}
sub get_StatsDate{
return ['yesterday']
}
sub get_CountryCodes {
return [
['Afghanistan' => 'AF'], # Frequent political/malware spam
['Argentina' => 'AR'], # 52% spam call rate (highest globally)
['Brazil' => 'BR'], # Top spam call origin
['China' => 'CN'], # High spam volume
['France' => 'FR'], # 43% spam call rate, top email spam source
['Germany' => 'DE'], # Significant email spam volume
['India' => 'IN'], # High spam call/text volume
['Indonesia' => 'ID'], # 51.5% spam call rate
['Italy' => 'IT'], # 35.1% spam call rate, email spam source
['Malaysia' => 'MY'], # 63% scam calls
['Mexico' => 'MX'], # Emerging spam source
['Nigeria' => 'NG'], # "Nigerian prince" scams
['Pakistan' => 'PK'], # Phishing campaigns
['Peru' => 'PE'], # Emerging spam source
['Russia' => 'RU'], # Cybercrime associations
['Saudi Arabia' => 'SA'], # High spam volume
['Spain' => 'ES'], # 43.9% spam call rate, email spam
['Turkey' => 'TR'], # Significant spam activity
['Ukraine' => 'UA'], # Spam proxy servers
['United States' => 'US'], # High email spam volume
['Viet Nam' => 'VN'] # Phishing/malware origins
];
}
sub get_SBL_lists {
return [
['sbl.spamhaus.org' => 'sbl.spamhaus.org', title => 'Spamhaus Blocklist'],
['xbl.spamhaus.org' => 'xbl.spamhaus.org', title => 'eXploits Blocklist'],
['pbl.spamhaus.org' => 'pbl.spamhaus.org', title => 'Policy Blocklist'],
['auth.spamhaus.org' => 'auth.spamhaus.org', title => 'Auth Blocklist'],
['multi.surbl.org' => 'multi.surbl.org', title => 'SURBL\'s multi-level URI checker (domains in email content)'],
['rhsbl.sorbs.net' => 'rhsbl.sorbs.net', title => 'Right-Hand Side Blocklist (domain-based, not IP-based)']
];
}
sub get_URIBL_lists {
return [
['uribl.com' => 'uribl.com', title => 'Primary URIBL service'],
['multi.uribl.com' => 'multi.uribl.com', title => 'Combined URIBL checks'],
['black.uribl.com' => 'black.uribl.com', title => 'Aggressive blocking list'],
['grey.uribl.com' => 'grey.uribl.com', title => 'Suspicious URI list'],
['white.uribl.com' => 'white.uribl.com', title => 'Verified safe URI list']
];
}
sub get_RBL_lists {
return [
['zen.spamhaus.org' => 'zen.spamhaus.org', title => 'Combines SBL, XBL, and PBL'],
['bl.spamcop.net' => 'bl.spamcop.net', title => 'SpamCop Blocklist (user-reported spam)'],
['cbl.abuseat.org' => 'cbl.abuseat.org', title => 'Composite Blocking List (bot-infected hosts)'],
['b.barracudacentral.org' => 'b.barracudacentral.org', title => 'Barracuda Reputation Blocklist'],
['dun.dnsrbl.net' => 'dun.dnsrbl.net', title => 'DNSRBL (DNS-based blacklist)'],
['psbl.surriel.com' => 'psbl.surriel.com', title => 'Passive Spam Block List (passive spam traps)'],
['backscatterer.org' => 'backscatterer.org', title => 'Backscatter/Out-of-Bounce spam sources'],
['dronebl.org' => 'dronebl.org', title => 'Drones/Proxy/DDoS sources'],
['dnsbl-1.uceprotect.net' => 'dnsbl-1.uceprotect.net', title => 'UCEPROTECT Level 1 (entry-level blocking)'],
['dnsbl-2.uceprotect.net' => 'dnsbl-2.uceprotect.net', title => 'UCEPROTECT Level 2 (more aggressive)']
];
}
sub get_mailstat_dates {
my ($directory) = '/opt/mailstats/html';
my @date_pairs;
# Find all matching files in directory
opendir(my $dh, $directory) or die "Can't open directory: $!";
while (my $file = readdir($dh)) {
next unless $file =~ /mailstats_for_(\d{4}-\d{2}-\d{2})\.html$/;
my $date = $1;
if ($date =~ /^(\d{4})-(\d{2})-(\d{2})$/) {
my $formatted_date = strftime("%B %-d %Y", 0, 0, 0, $3, $2-1, $1-1900);
push @date_pairs, [$formatted_date, $date];
}
}
closedir($dh);
# Sort dates chronologically
@date_pairs = sort { $a->[1] cmp $b->[1] } @date_pairs;
return \@date_pairs;
}
1;

View File

@@ -0,0 +1,315 @@
package SrvMngr::Controller::Mailstats;
#
# Generated by SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-04-05 11:59:08
#
#----------------------------------------------------------------------
# heading : Investigation
# description : Mailstats
# navigation : 4000 700
#
# name : mailstats, method : get, url : /mailstats, ctlact : Mailstats#main
# name : mailstatsu, method : post, url : /mailstatsu, ctlact : Mailstats#do_update
# name : mailstatsd, method : get, url : /mailstatsd, ctlact : Mailstats#do_display
#
# routes : end
#
# Documentation: https://wiki.contribs.org/Mailstats
#----------------------------------------------------------------------
#
# Scheme of things:
#
# TBA!!
use strict;
use warnings;
use Mojo::Base 'Mojolicious::Controller';
use constant FALSE => 0;
use constant TRUE => 1;
use Locale::gettext;
use SrvMngr::I18N;
use SrvMngr qw(theme_list init_session);
use Data::Dumper;
use esmith::util;
use esmith::util::network;
use esmith::ConfigDB;
use esmith::AccountsDB;
use esmith::NetworksDB;
use esmith::HostsDB;
use esmith::DomainsDB;
my $cdb;
my $adb;
my $ndb;
my $hdb;
my $ddb;
require '/usr/share/smanager/lib/SrvMngr/Controller/Mailstats-Custom.pm'; #The code that is to be added by the developer
sub main {
#
# Initial entry - route is "/<whatever>"
#
#set initial panel
#for initial panel:
#Specifiy panel to enter
#load up _data hash with DB fields
#load up stash with pointer(s) to control fields hash(= get-))
#and a pointer to the prefix_data hash
#render initial panel
my $c = shift;
$c->app->log->info( $c->log_req );
#The most common ones
$cdb = esmith::ConfigDB->open() || die("Couldn't open config db");
$adb = esmith::AccountsDB->open() || die("Couldn't open Accounts db");
$ndb = esmith::NetworksDB->open() || die("Couldn't open Network db");
$hdb = esmith::HostsDB->open() || die("Couldn't open Hosts db");
$ddb = esmith::DomainsDB->open() || die("Couldn't open Domains db");
my %mst_data = ();
my $title = $c->l('mst_Mailstats');
my $modul = '';
$mst_data{'trt'} = 'TABLE';
#Load any DB entries into the <prefix>_data area so as they are preset in the form
# which DB - this only really works if the initial panel is a PARAMS type panel and not a TABLE
my $db = $cdb; #pickup local or global db or Default to config
$c->do_display($mst_data{'trt'});
}
# Post request with params - submit from the form
sub do_update {
#
# Return after submit pushed on panel (this is a post) - route is "/<whatever>u"
# parameters in the params hash.
#
#load up all params into prefix_data hash:
#By panel (series of if statements - only one executed):
#call validate-PANEL() - return ret = ok or error message
#if validation not ok:
#render back to current panel with error message in stash
#otherwise:
#By panel (series of if statements - only one executed):
#do whatever is required: call perform-PANEL() - return "ok" or Error Message
#call signal-event for any global actions specified (check it exists - error and continue?)
#if action smeserver-<whatever>-update exists
#signal_event smeserver-<whatever>-update
#call signal-event for any specific actions for thids panel (check it exists first - error and continue)
#set success in stash
#if no "nextpanel" entry:
#set firstpanel
#else
#set nextpanel
#call render
my $c = shift;
$c->app->log->info($c->log_req);
my $modul = '';
#The most common ones - you might want to comment out any not used.
$cdb = esmith::ConfigDB->open() || die("Couldn't open config db");
$adb = esmith::AccountsDB->open() || die("Couldn't open Accounts db");
$ndb = esmith::NetworksDB->open() || die("Couldn't open Network db");
$hdb = esmith::HostsDB->open() || die("Couldn't open Hosts db");
$ddb = esmith::DomainsDB->open() || die("Couldn't open Domains db");
my %mst_data = ();
my $title = $c->l('mst_Mailstats');
# Accessing all POST/GET parameters
my $params = $c->req->params->to_hash;
# Get number of POST parameters
#my $num_params = keys scaler %$params;
#Params are available in the hash "params" - copy to the prefix_data hash
#while (my ($key, $value) = each %{$c->req->params->to_hash}) {
# $mst_data{$key} = $value;
#}
# the value of trt will tell you which panel has returned
my $trt = $c->param('trt') || 'TABLE'; #hidden control on every form.
my $ret = 'ok';
#Validate the parameters in a custom sub one for each panel (although only one of these will be executed)
my $thispanel;
if ($trt eq 'TABLE'){
#Validate form parameters for panel TABLE
$ret = $c->validate_TABLE(\%mst_data);
$thispanel = 'TABLE';
}
if ($trt eq 'CONFIG'){
#Validate form parameters for panel CONFIG
$ret = $c->validate_CONFIG(\%mst_data);
$thispanel = 'CONFIG';
}
if ($ret ne "ok"){
$c->do_display($thispanel);
} else {
#Do whatever is needed, including writing values to the DB
if ($trt eq 'TABLE'){
#do whatever is required ...
$ret = $c->perform_TABLE(\%mst_data);
if ($ret ne "ok") {
# return to the panel with error message
$c->stash(error => $c->l($ret));
$c->stash(
title => $title,
modul => $modul,
mst_data => \%mst_data
);
$c->render(template => "mailstats");
} else {
$c->stash( success => $c->l('mst_TABLE_panel_action_was_successful')); #A bit bland - edit it in the lex file
}
}
if ($trt eq 'CONFIG'){
#do whatever is required ...
$ret = $c->perform_CONFIG(\%mst_data);
if ($ret ne "ok") {
# return to the panel with error message
$c->stash(error => $c->l($ret));
$c->stash(
title => $title,
modul => $modul,
mst_data => \%mst_data
);
$c->render(template => "mailstats");
} else {
$c->stash( success => $c->l('mst_CONFIG_panel_action_was_successful')); #A bit bland - edit it in the lex file
}
}
# and call any signal-events needed
#TBD
# Setup shared data and call panel
if ('none' eq 'none') {
$mst_data{'trt'} = 'TABLE';
} else {
$mst_data{'trt'} = 'none';
}
$c->do_display($mst_data{'trt'});
}
}
sub do_display {
#
# Return after link clicked in table (this is a get) - route is "/<whatever>d"
# Expects ?trt=PANEL&selected="TableRowName" plus any other required
#
# OR it maybe a post from the main panel to add a new record
#
#load up all supplied params into prefix_data hash
#call get-selected-PANEL() - returns hash of all relevent parameters
#load up returned hash into prefix_data
#render - to called panel
my ($c,$trt) = @_;
$c->app->log->info($c->log_req);
#The most common ones - you might want to comment out any not used.
$cdb = esmith::ConfigDB->open() || die("Couldn't open config db");
$adb = esmith::AccountsDB->open() || die("Couldn't open Accounts db");
$ndb = esmith::NetworksDB->open() || die("Couldn't open Network db");
$hdb = esmith::HostsDB->open() || die("Couldn't open Hosts db");
$ddb = esmith::DomainsDB->open() || die("Couldn't open Domains db");
my %mst_data = ();
my $title = $c->l('mst_Mailstats');
my $modul = "";
# Accessing all parameters
my %params = $c->req->params->to_hash;
# Get number of parameters
my $num_params = keys %params;
#Tag as Post or Get (ie. create new entry or edit existing one
my $is_new_record = ($c->req->method() eq 'POST');
#Params are available in the hash "params" - copy to the prefix_data hash
#while (my ($key, $value) = each %{$c->req->params->to_hash}) {
# $mst_data{$key} = $value;
#}
# the value of trt will tell you which panel has returned
if (! $trt){
$trt = $c->param('trt') || 'TABLE'; #Indicates where to go now
}
# Now add in the params from the selected row from the table
my %selectedrow;
if ($trt eq 'TABLE'){
#Validate Get selected row (if applicable) TABLE
%selectedrow = $c->get_selected_TABLE($mst_data{'Selected'},$is_new_record);
}
if ($trt eq 'CONFIG'){
#Validate Get selected row (if applicable) CONFIG
%selectedrow = $c->get_selected_CONFIG($mst_data{'Selected'},$is_new_record);
}
#Copy in the selected row params to the prefix_data hash to pass to the panel
while (my ($key, $value) = each %selectedrow){
$mst_data{$key} = $value;
}
# Where to go now
$mst_data{'trt'} = $trt;
# Set up other shared data according to the panel to go to
if ($trt eq 'TABLE'){
# pickup any other contents needed and load them into hash shared with panel
my %returned_hash;
# subroutine returns a hash directly
%returned_hash = $c->get_data_for_panel_TABLE();
# Copy each key-value pair from the returned hash to the prefix data hash
while (my ($key, $value) = each %returned_hash) {
$mst_data{$key} = $value;
}
}
if ($trt eq 'CONFIG'){
# pickup any other contents needed and load them into hash shared with panel
my %returned_hash;
# subroutine returns a hash directly
%returned_hash = $c->get_data_for_panel_CONFIG();
# Copy each key-value pair from the returned hash to the prefix data hash
while (my ($key, $value) = each %returned_hash) {
$mst_data{$key} = $value;
}
}
# and table control fields
# Data for panel
$c->stash(
title => $title,
modul => $modul,
mst_data => \%mst_data
);
$c->render(template => "mailstats");
}
1;

View File

@@ -0,0 +1,40 @@
#
# Generated by SM2Gen version: SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-04-05 11:59:08
#
'mst_Host_name_for_DB_server' => 'Host name for DB server',
'mst_Enable_RHSBL_checking' => 'Enable RHSBL checking',
'mst_RBL_Servers_to_use' => 'RBL Servers to use',
'mst_Port_number_for_email_server' => 'Port number for email server',
'mst_User_name_for_DB_sending' => 'User name for DB sending',
'mst_Table_of_email_status' => '',
'mst_Score_to_fully_reject_emmail' => 'Score to fully reject email',
'mst_User_Password_for_email_sending' => 'User Password for email sending',
'mst_UBL_Servers_to_use' => 'UBL Servers to use',
'mst_Would_you_like_to_save' => 'Would you like to save data in the DB?',
'mst_Specify_if_you_would_like' => 'Specify if you would like to receive email in text or HTML form',
'mst_Port_number_for_DB_server' => 'Port number for DB server',
'mst_Mailstats' => 'Daily Email Status Table',
'mst_Email_filtering_/_exclusion' => 'Email filtering / exclusion',
'mst_Score_for_tagging_as_spam,' => 'Score for tagging as spam but queued',
'mst_Accumulated_country_codes_(editable)' => 'Accumulated country codes editable',
'mst_Date_for_Stats_display' => 'Date for Stats display',
'mst_Enable_URIBL_checking' => 'Enable URIBL checking',
'mst_CONFIG_panel_action_was_successful' => 'CONFIG panel action was successful',
'mst_Email_details' => 'Email details',
'mst_User_Password_for_DB_sending' => 'User Password for DB sending',
'mst_Email_for_stats' => 'Email for stats',
'mst_Select_the_countries_you_would' => 'Select the countries you would like to reject',
'mst_Spamassassin_scores_-_tag_and' => 'Spamassassin scores - tag and reject levels',
'mst_SBL_Servers_to_use' => 'SBL Servers to use',
'mst_Save' => 'Save',
'mst_Descriptive_paragraph' => 'The Mailstats contrib analyzes your qpsmtpd log files and creates a webpage and sends a periodic email to the address you specify summarizing your server\'s email activity.
<br>You can use the <I>Configure Mailstats</I> button to set up mailstats and some associated qpsmtpd and spamassassin properties. ',
'mst_APPLY' => 'Apply',
'mst_Enable_DNSBL_checking' => 'Enable DNSBL checking',
'mst_Host_name_for_email_server' => 'Host name for email server',
'mst_Details_for_connection_to_database' => 'Details for connection to database for saving email status',
'mst_TABLE_panel_action_was_successful' => 'TABLE panel action was successful',
'mst_Configure_Mailstats' => 'Configure Mailstats',
'mst_User_name_for_email_sending' => 'User name for email sending',
'mst_Full_Window' => 'Full Window',
'mst_Use_Cursor_keys' => '(Use the cursor keys to select the date)'

View File

@@ -0,0 +1,81 @@
/*
Generated by: SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-04-05 11:59:08
*/
object {
border: 1px,solid, darkgrey;
}
.inline-buttons {
display: flex; /* Use flexbox to arrange items horizontally */
gap: 10px; /* Optional: Add space between buttons */
}
.inline-buttons .link {
/* Additional styling can be added here if needed */
}
.inline-buttons .link {
display: inline-block; /* Keep links as inline-block for button shape */
padding: 7px 14px; /* Adjusted padding to approximate 70% of the original */
margin: 0; /* Remove margin */
background-color: #efefef; /* Light gray background color */
color: black; /* Text color */
text-decoration: none; /* Remove underline */
border: 2px solid #bbb; /* Thin, light gray border */
border-radius: 3px; /* Slightly rounded corners */
font-size: 11.2px; /* Adjusted font size to approximate 70% of the original */
text-align: center; /* Center the text */
cursor: pointer; /* Pointer cursor on hover */ }
/* Hover and active effects for better interaction */
.inline-buttons .link:hover {
background-color: #d9d9d9; /* Darker shade on hover */
}
.inline-buttons .link:active {
background-color: #c0c0c0; /* Even darker shade on click */
}
.Mailstats-panel {}
.name {}
.rout {}
.grou1 {}
.link1 {}
.endg1 {}
.subh1 {}
.para1 {}
.sele1 {}
.obje2 {}
.name {}
.rout {}
.sele4 {}
.grou1 {}
.subh {}
.emai3 {}
.text5 {}
.numb6 {}
.text7 {}
.pass8 {}
.endg1 {}
.sele9 {}
.grou2 {}
.subh2 {}
.text10 {}
.numb11 {}
.text12 {}
.pass13 {}
.endg2 {}
.subh3 {}
.mult14 {}
.text15 {}
.sele16 {}
.sele17 {}
.sele24 {}
.text18 {}
.text19 {}
.text20 {}
.subh4 {}
.numb21 {}
.numb22 {}
.subm23 {}

View File

@@ -0,0 +1,224 @@
//
//Generated by: SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-04-05 11:17:38
//
$(document).ready(function() {
});
// Initialize multiselect with existing text values
// Initialize multiselect based on text input values
function initMultiselect(multiselectId, textInputId) {
const multiselect = document.getElementById(multiselectId);
const textInput = document.getElementById(textInputId);
if (!multiselect || !textInput) {
console.error('Could not find elements with provided IDs');
return;
}
textInput.value.split(',').forEach(value => {
const option = Array.from(multiselect.options)
.find(opt => opt.value === value.trim());
if (option) option.selected = true;
});
}
// Update text input when multiselect changes
function updateTextInput(multiselectId, textInputId) {
const multiselect = document.getElementById(multiselectId);
const textInput = document.getElementById(textInputId);
if (!multiselect || !textInput) {
console.error('Could not find elements with provided IDs');
return;
}
const selectedValues = Array.from(multiselect.selectedOptions)
.map(option => option.value);
const currentValues = textInput.value.split(',')
.map(code => code.trim())
.filter(code => code !== '');
const updatedValuesSet = new Set(currentValues);
selectedValues.forEach(value => updatedValuesSet.add(value));
currentValues.forEach(code => {
if (!selectedValues.includes(code) &&
multiselect.querySelector(`option[value="${code}"]`)) {
updatedValuesSet.delete(code);
}
});
textInput.value = Array.from(updatedValuesSet).join(',');
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
initMultiselect('CountrySelect_select','AccumCountryCodes_text');
initMultiselect('RBLList_select','RBLList_text');
initMultiselect('SBLList_select','SBLList_text');
initMultiselect('URIBLList_select','URIBLList_text');
// Add change listener to multiselect
document.getElementById('CountrySelect_select')
.addEventListener('click', () => updateTextInput('CountrySelect_select','AccumCountryCodes_text'));
document.getElementById('RBLList_select')
.addEventListener('click', () => updateTextInput('RBLList_select','RBLList_text'));
document.getElementById('SBLList_select')
.addEventListener('click', () => updateTextInput('SBLList_select','SBLList_text'));
document.getElementById('URIBLList_select')
.addEventListener('click', () => updateTextInput('URIBLList_select','URIBLList_text'));
});
/**
* Date Select Manager
*
* This script manages an HTML select element with dates and updates an HTML object
* based on the selected date. It sets the initial value to yesterday's date (or the latest date available),
* updates the object's data parameter, and refreshes the display.
*/
// Wait for DOM to be fully loaded before executing
document.addEventListener('DOMContentLoaded', function() {
// Initialize all date selects and their corresponding objects
initializeSelects();
});
function initializeSelects() {
// Find all select elements with IDs starting with "StatsDate_select"
const selects = document.querySelectorAll('select[id^="StatsDate_select"]');
selects.forEach(function(dateSelect) {
const selectId = dateSelect.id;
// Find the corresponding object element
// If the select has a numeric suffix, look for object with same suffix
const objectId = selectId === 'StatsDate_select' ?
'mailstats_object' :
'mailstats_object' + selectId.replace('StatsDate_select', '');
const mailstatsObject = document.getElementById(objectId);
const mailstatsfull = document.getElementById('mailstats-full-window');
// Check if elements exist before proceeding
if (!dateSelect) {
console.error(`Select element with ID "${selectId}" not found.`);
return; // Skip this pair if select doesn't exist
}
if (!mailstatsObject) {
console.error(`Object element with ID "${objectId}" not found.`);
// Continue anyway, as we can still set the select to the right date
}
// Function to update the object's data parameter and refresh it
function updateMailstatsObject(date) {
if (date && mailstatsObject) {
try {
// Create the full URL including the selected date
const currentDomain = window.location.origin;
const fullUrl = `${currentDomain}/mailstats/mailstats_for_${date}.html`;
// Update the data attribute with the full URL
mailstatsObject.setAttribute('data', fullUrl);
mailstatsfull.setAttribute('href', fullUrl);
// Force a refresh of the object
// [refresh code here]
console.log(`Updated object to show data from: ${fullUrl}`);
} catch (error) {
console.error(`Error updating object:`, error);
}
}
}
// Set the initial value of the select
const yesterdayDate = getYesterdayDate();
let dateFound = false;
// Find and select the option with yesterday's date
for (let i = 0; i < dateSelect.options.length; i++) {
if (dateSelect.options[i].value === yesterdayDate) {
dateSelect.selectedIndex = i;
dateFound = true;
break;
}
}
// If yesterday's date is not found, select the latest date (last option)
if (!dateFound && dateSelect.options.length > 0) {
dateSelect.selectedIndex = dateSelect.options.length - 1;
console.log(`Yesterday's date (${yesterdayDate}) not found in options for ${selectId}. Selected the latest date instead: ${dateSelect.value}`);
}
// Update the object with the initial date
if (dateSelect.value) {
updateMailstatsObject(dateSelect.value);
}
// Add event listener for changes to the select element
dateSelect.addEventListener('change', function() {
const selectedDate = this.value;
updateMailstatsObject(selectedDate);
});
});
}
// Function to get yesterday's date in YYYY-MM-DD format
function getYesterdayDate() {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const year = yesterday.getFullYear();
const month = String(yesterday.getMonth() + 1).padStart(2, '0');
const day = String(yesterday.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
//
// disable enable RBL/SBL/URIBL list selection
//
// Example html formatting to use it.
//<div data-mailstats-group>
//<select id="EnableRHSBL_select" data-mailstats-control data-mailstats-target="RBL_group">
//<option value="disabled">Disabled</option>
//<option value="enabled">Enabled</option>
//</select>
//<div id="RBL_group">
//<!-- Content -->
//</div>
//</div>
// or , 'data-mailstats-control' => undef, 'data-mailstats-target' => "RBL_group" for mojo html helper
//
document.addEventListener('DOMContentLoaded', () => {
// Initialize all control pairs
document.querySelectorAll('[data-mailstats-group]').forEach(group => {
const select = group.querySelector('select[data-mailstats-control]');
const target = document.getElementById(select.dataset.mailstatsTarget);
if (!select || !target) {
console.error('Control group misconfigured', group);
return;
}
// Initial state
updateState(target, select.value === 'enabled');
// Change listener
select.addEventListener('change', () =>
updateState(target, select.value === 'enabled')
);
});
});
function updateState(target, enabled) {
target.style.opacity = enabled ? '1' : '0.5';
target.style.pointerEvents = enabled ? 'auto' : 'none';
target.style.filter = enabled ? 'none' : 'grayscale(50%)';
target.setAttribute('aria-disabled', !enabled);
}

View File

@@ -0,0 +1,60 @@
%#
%# Generated by SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-04-05 11:59:08
%#
% layout 'default', title => "Sme server 2 - Mailstats", share_dir => './';
%# css specific to this panel:
% content_for 'module' => begin
%= stylesheet '/css/mailstats.css'
%= javascript '/js/mailstats.js'
<div id="module" class="module Mailstats-panel">
% if (config->{debug} == 1) {
<pre>
%= dumper $c->current_route
%= dumper $mst_data->{trt}
</pre>
% }
<h1><%=$title%></h1>
% if ( stash('modul')) {
%= $c->render_to_string(inline => stash('modul') );
% }
%if ($c->stash('first')) {
<br><p>
%=$c->render_to_string(inline =>$c->l($c->stash('first')))
</p>
%} elsif ($c->stash('success')) {
<div class='success '>
<p>
%= $c->l($c->stash('success'));
</p>
</div>
<br />
%} elsif ($c->stash('error')) {
<div class='sme-error'>
<p>
%= $c->l($c->stash('error'));
</p>
</div>
<br />
%}
%#Routing to partials according to trt parameter.
%#This ought to be cascading if/then/elsif, but is easier to just stack the if/then's rather like a case statement'
% if ($mst_data->{trt} eq "TABLE") {
%= include 'partials/_mst_TABLE'
%}
% if ($mst_data->{trt} eq "CONFIG") {
%= include 'partials/_mst_CONFIG'
%}
</div>
%end

View File

@@ -0,0 +1,237 @@
%#
%# Generated by SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-04-05 11:17:38
%#
<div id="Mailstats-CONFIG" class="partial Mailstats-CONFIG">
% if (config->{debug} == 1) {
<pre>
%= dumper $mst_data
</pre>
% }
% my $btn = l('mst_APPLY');
%= form_for "mailstatsu" => (method => 'POST') => begin
% param 'trt' => $mst_data->{trt} unless param 'trt';
%= hidden_field 'trt' => $mst_data->{trt}
%# Inputs etc in here.
<p><span class=label>
%=l('mst_Specify_if_you_would_like')
</span><span class=data>
% my @TextorHTML_options = [['HTML' => 'HTML'], ['Text' => 'Text'], ['Both' => 'Both'], ['Neither' => 'Neither']];
% param 'TextorHTML' => $mst_data->{TextorHTML} unless param 'TextorHTML';
%= select_field 'TextorHTML' => @TextorHTML_options, class => 'input', id => 'TextorHTML_select'
<br></span> </p>
<div class=emailwanted>
<h2 class='subh'><%=l('mst_Email_details')%></h2>
<p><span class=label>
%=l('mst_Email_for_stats')
</span><span class=data>
% param 'Email' => $mst_data->{Email} unless param 'Email';
%=email_field 'Email', class => 'emai3'
</span></p>
<p><span class=label>
%=l('mst_Host_name_for_email_server')
</span><span class=data>
% param 'EmailHost' => $mst_data->{EmailHost} unless param 'EmailHost';
%= text_field 'EmailHost', size => '50', class => 'textinput EmailHost' , pattern=>'.*' , placeholder=>'EmailHost', title =>'Pattern regex mismatch'
<br></span></p>
<p><span class='label'>
%=l('mst_Port_number_for_email_server')
</span><span class=data>
% param 'EmailPort' => $mst_data->{EmailPort} unless param 'EmailPort';
%=number_field 'EmailPort', class => 'numb6'
</span></p>
<p><span class=label>
%=l('mst_User_name_for_email_sending')
</span><span class=data>
% param 'EmailUser' => $mst_data->{EmailUser} unless param 'EmailUser';
%= text_field 'EmailUser', size => '50', class => 'textinput EmailUser' , pattern=>'.*' , placeholder=>'EmailUser', title =>'Pattern regex mismatch'
<br></span></p>
<p><span class='label'>
%=l('mst_User_Password_for_email_sending')
</span><span class=data>
% param 'EmailPassword' => $mst_data->{EmailPassword} unless param 'EmailPassword';
%=password_field 'EmailPassword', class => 'pass8 sme-password', autocomplete => 'off'
</span></p>
</div>
<p><span class=label>
%=l('mst_Would_you_like_to_save')
</span><span class=data>
% my @DBSave_options = [['yes' => 'yes'], ['no' => 'no']];
% param 'DBSave' => $mst_data->{DBSave} unless param 'DBSave';
%= select_field 'DBSave' => @DBSave_options, class => 'input', id => 'DBSave_select'
<br></span> </p>
<div class=dbwanted>
<h2 class='subh2'><%=l('mst_Details_for_connection_to_database')%></h2>
<p><span class=label>
%=l('mst_Host_name_for_DB_server')
</span><span class=data>
% param 'DBHost' => $mst_data->{DBHost} unless param 'DBHost';
%= text_field 'DBHost', size => '50', class => 'textinput DBHost' , pattern=>'.*' , placeholder=>'DBHost', title =>'Pattern regex mismatch'
<br></span></p>
<p><span class='label'>
%=l('mst_Port_number_for_DB_server')
</span><span class=data>
% param 'DBPort' => $mst_data->{DBPort} unless param 'DBPort';
%=number_field 'DBPort', class => 'numb11'
</span></p>
<p><span class=label>
%=l('mst_User_name_for_DB_sending')
</span><span class=data>
% param 'DBUser' => $mst_data->{DBUser} unless param 'DBUser';
%= text_field 'DBUser', size => '50', class => 'textinput DBUser' , pattern=>'.*' , placeholder=>'DBUser', title =>'Pattern regex mismatch'
<br></span></p>
<p><span class='label'>
%=l('mst_User_Password_for_DB_sending')
</span><span class=data>
% param 'DBPassword' => $mst_data->{DBPassword} unless param 'DBPassword';
%=password_field 'DBPassword', class => 'pass13 sme-password', autocomplete => 'off'
</span></p>
</div>
<h2 class='subh3'><%=l('mst_Email_filtering_/_exclusion')%></h2>
<p><span class=label>
%=l('mst_Select_the_countries_you_would')
</span><span class=data>
% my @CountrySelect_options = $c->get_CountryCodes();
% param 'CountrySelect' => $mst_data->{CountrySelect} unless param 'CountrySelect';
%= select_field 'CountrySelect' => @CountrySelect_options, class => 'input', id => 'CountrySelect_select', multiple => 'multiple'
<br></span> </p>
<p><span class=label>
%=l('mst_Accumulated_country_codes_(editable)')
</span><span class=data>
% param 'AccumCountryCodes' => $mst_data->{AccumCountryCodes} unless param 'AccumCountryCodes';
%= text_field 'AccumCountryCodes', size => '50', class => 'textinput AccumCountryCodes' , pattern=>'.*' , placeholder=>'AccumCountryCodes', title =>'Pattern regex mismatch', id => 'AccumCountryCodes_text'
<br></span></p>
<div data-mailstats-group>
<p><span class=label>
%=l('mst_Enable_RHSBL_checking')
</span><span class=data>
% my @EnableRHSBL_options = [['yes' => 'enabled'], ['no' => 'disabled']];
% param 'EnableRHSBL' => $mst_data->{EnableRHSBL} unless param 'EnableRHSBL';
%= select_field 'EnableRHSBL' => @EnableRHSBL_options, class => 'input', id => 'EnableRHSBL_select', 'data-mailstats-control' => undef, 'data-mailstats-target' => "RBL_group"
<br></span> </p>
<div id=RBL_group>
<p><span class=label>
%=l('mst_Select_the_RBL_Lists')
</span><span class=data>
% my @RBLSelect_options = $c->get_RBL_lists();
% param 'RBLSelect' => $mst_data->{RBLSelect} unless param 'RBLSelect';
%= select_field 'RBLSelect' => @RBLSelect_options, class => 'input', id => 'RBLList_select', multiple => 'multiple'
<br></span> </p>
<p><span class=label>
%=l('mst_RBL_Servers_to_use')
</span><span class=data>
% param 'RBLList' => $mst_data->{RBLList} unless param 'RBLList';
%= text_field 'RBLList', size => '50', class => 'textinput RBLList' , pattern=>'.*' , placeholder=>'RBLList', title =>'Pattern regex mismatch', id => 'RBLList_text'
<br></span></p>
</div>
</div>
<div data-mailstats-group>
<p><span class=label>
%=l('mst_Enable_DNSBL_checking')
</span><span class=data>
% my @EnableDNSBL_options = [['yes' => 'enabled'], ['no' => 'disabled']];
% param 'EnableDNSBL' => $mst_data->{EnableDNSBL} unless param 'EnableDNSBL';
%= select_field 'EnableDNSBL' => @EnableDNSBL_options, class => 'input', id => 'EnableDNSBL_select' , 'data-mailstats-control' => undef, 'data-mailstats-target' => "SBL_group"
<br></span> </p>
<div id=SBL_group>
<p><span class=label>
%=l('mst_Select_the_SBL_Lists')
</span><span class=data>
% my @SBLSelect_options = $c->get_SBL_lists();
% param 'SBLSelect' => $mst_data->{SBLSelect} unless param 'SBLSelect';
%= select_field 'SBLSelect' => @SBLSelect_options, class => 'input', id => 'SBLList_select', multiple => 'multiple'
<br></span> </p>
<p><span class=label>
%=l('mst_SBL_Servers_to_use')
</span><span class=data>
% param 'SBLList' => $mst_data->{SBLList} unless param 'SBLList';
%= text_field 'SBLList', size => '50', class => 'textinput SBLList' , pattern=>'.*' , placeholder=>'SBLList', title =>'Pattern regex mismatch', id => "SBLList_text"
<br></span></p>
</div>
</div>
<div data-mailstats-group>
<p><span class=label>
%=l('mst_Enable_URIBL_checking')
</span><span class=data>
% my @EnableURIBL_options = [['yes' => 'enabled'], ['no' => 'disabled']];
% param 'EnableURIBL' => $mst_data->{EnableURIBL} unless param 'EnableURIBL';
%= select_field 'EnableURIBL' => @EnableURIBL_options, class => 'input', id => 'EnableURIBL_select' , 'data-mailstats-control' => undef, 'data-mailstats-target' => "URIBL_group"
<br></span> </p>
<div id=URIBL_group>
<p><span class=label>
%=l('mst_Select_the_URIBL_Lists')
</span><span class=data>
% my @URIBLSelect_options = $c->get_URIBL_lists();
% param 'URIBLSelect' => $mst_data->{URIBLSelect} unless param 'URIBLSelect';
%= select_field 'URIBLSelect' => @URIBLSelect_options, class => 'input', id => 'URIBLList_select', multiple => 'multiple'
<br></span> </p>
<p><span class=label>
%=l('mst_URIBL_Servers_to_use')
</span><span class=data>
% param 'URIBLList' => $mst_data->{URIBLList} unless param 'URIBLList';
%= text_field 'URIBLList', size => '50', class => 'textinput URIBLList' , pattern=>'.*' , placeholder=>'URIBLList', title =>'Pattern regex mismatch', id => 'URIBLList_text'
<br></span></p>
</div>
</div>
<h2 class='subh4'><%=l('mst_Spamassassin_scores_-_tag_and')%></h2>
<p><span class='label'>
%=l('mst_Score_to_fully_reject_emmail')
</span><span class=data>
% param 'RejectLevel' => $mst_data->{RejectLevel} unless param 'RejectLevel';
%=number_field 'RejectLevel', class => 'numb21'
</span></p>
<p><span class='label'>
%=l('mst_Score_for_tagging_as_spam,')
</span><span class=data>
% param 'TagLevel' => $mst_data->{TagLevel} unless param 'TagLevel';
%=number_field 'TagLevel', class => 'numb22'
</span></p>
<span class='data'>
%= submit_button l('mst_Save'), class => 'action subm23'
</span>
%# Probably finally by a submit.
%end
</div>

View File

@@ -0,0 +1,46 @@
%#
%# Generated by SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-04-05 11:59:08
%#
<div id="Mailstats-TABLE" class="partial Mailstats-TABLE">
% if (config->{debug} == 1) {
<pre>
%= dumper $mst_data
</pre>
% }
% my $btn = l('mst_APPLY');
%= form_for "mailstatsu" => (method => 'POST') => begin
% param 'trt' => $mst_data->{trt} unless param 'trt';
%= hidden_field 'trt' => $mst_data->{trt}
%# Inputs etc in here.
<br>
<div class=inline-buttons>
<a href='mailstatsd?trt=CONFIG' class='link link1'>
%= l('mst_Configure_Mailstats')
</a>
<a id='mailstats-full-window' href='' class='link link1'>
%= l('mst_Full_Window')
</a>
</div>
<h2 class='subh1'><%=l('mst_Table_of_email_status')%></h2>
<p class='paragraph para1'>
%= $c->render_to_string(inline=>$c->l('mst_Descriptive_paragraph'))
</p>
<p><span class=label>
%=l('mst_Date_for_Stats_display')
</span><span class=data>
% my @StatsDate_options = $c->get_mailstat_dates();
% param 'StatsDate' => $mst_data->{StatsDate} unless param 'StatsDate';
%= select_field 'StatsDate' => @StatsDate_options, class => 'input', id => 'StatsDate_select'
%=l('mst_Use_Cursor_keys')
<br></span> </p>
<object id = 'mailstats_object' data="<%='' %>" title="<%= $c->stash('title') %>" type="text/html" height='600px' width='100%' ><%= $c->stash('title') %> not found</object>
%# Probably finally by a submit.
%end
</div>

View File

@@ -1,17 +1,17 @@
# $Id: smeserver-mailstats.spec,v 1.8 2023/02/15 09:40:09 brianr Exp $
# $Id: smeserver-mailstats.spec,v 1.7 2021/04/02 11:11:44 brianr Exp $
# Authority: brianread
# Name: Brian Read
Summary: Daily mail statistics for SME Server
%define name smeserver-mailstats
Name: %{name}
%define version 1.1
%define release 18
%define version 11.1
%define release 2
Version: %{version}
Release: %{release}%{?dist}
License: GPL
Group: SME/addon
Source: %{name}-%{version}.tar.xz
Source: %{name}-%{version}.tgz
BuildRoot: /var/tmp/%{name}-%{version}-%{release}-buildroot
BuildArchitectures: noarch
@@ -19,18 +19,27 @@ Requires: smeserver-release => 9.0
Requires: qpsmtpd >= 0.96
BuildRequires: e-smith-devtools >= 1.13.1-03
Requires: perl-Switch
BuildRequires: python36
Requires: python36
Requires: html2text
Requires: python3-chameleon
Requires: python3-mysql
Requires: python3-matplotlib
AutoReqProv: no
%description
A script that via cron.d e-mails mail statistics to admin on a daily basis.
See http://www.contribs.org/bugzilla/show_bug.cgi?id=819
%changelog
* Sun Jul 09 2023 cvs2git.sh aka Brian Read <brianr@koozali.org> 1.1-18.sme
- Roll up patches and move to git repo [SME: 12338]
* Sun Apr 06 2025 Brian Read <brianr@koozali.org> 11.2-2.sme
- Add in SM2 panel [SME: ]
* Sun Jul 09 2023 BogusDateBot
- Eliminated rpmbuild "bogus date" warnings due to inconsistent weekday,
by assuming the date is correct and changing the weekday.
* Mon Dec 30 2024 Brian Read <brianr@koozali.org> 11.2-1.sme
- Update mailstats.pl to accomodate change in log format for SME11 [SME: 12841]
* Fri Jun 07 2024 Brian Read <brianr@koozali.org> 1.1-18.sme
- Pull in python re-write from SME11 dev [SME: ]
* Wed Feb 15 2023 Brian Read <brianr@bjsystems.co.uk> 1.1-17.sme
- Add-in-imap-authorisation-log-string [SME: 12327]
@@ -109,8 +118,19 @@ perl createlinks
/bin/rm -rf $RPM_BUILD_ROOT
(cd root ; /usr/bin/find . -depth -print | /bin/cpio -dump $RPM_BUILD_ROOT)
chmod +x $RPM_BUILD_ROOT/usr/bin/runmailstats.sh
# Define the placeholder and generate the current date and time
now=$(date +"%Y-%m-%d %H:%M:%S")
# Replace the placeholder in the Python program located at %{BUILDROOT}/usr/bin
sed -i "s|__BUILD_DATE_TIME__|$now|" $RPM_BUILD_ROOT/usr/bin/mailstats.py
/bin/rm -f %{name}-%{version}-filelist
/sbin/e-smith/genfilelist $RPM_BUILD_ROOT > %{name}-%{version}-filelist
/sbin/e-smith/genfilelist $RPM_BUILD_ROOT | grep -v "\.pyc" | grep -v "\.pyo" > %{name}-%{version}-filelist
%pre
/usr/bin/pip3 install -q pymysql
/usr/bin/pip3 install -q numpy
/usr/bin/pip3 install -q pandas
%clean
/bin/rm -rf $RPM_BUILD_ROOT