Compare commits
132 Commits
1.1-18.el8
...
11_1-7_el8
Author | SHA1 | Date | |
---|---|---|---|
55cb7a6f05 | |||
1b757b1336 | |||
52b33e166a | |||
88bc38adf3 | |||
b070554fdd | |||
2dd3d234df | |||
d94bf8e033 | |||
5deb31cd92 | |||
f86021b8c9 | |||
a77cb094df | |||
d81543187f | |||
76ca0f528c | |||
1858edc41c | |||
056992272d | |||
3de794d90d | |||
34fd81cd51 | |||
1258b41ad8 | |||
9453031df3 | |||
756e0f94c3 | |||
f885ab684e | |||
7504f32ce7 | |||
9dc7d3d7a1 | |||
89475c0aa3 | |||
b513dfc9be | |||
32d91c4a24 | |||
2d1824553b | |||
e610abd351 | |||
8d8c97d1fa | |||
a58191f667 | |||
7fcdfccfa6 | |||
cd4a0b1725 | |||
3b0b574171 | |||
da71021889 | |||
4d29da7f3d | |||
736315d47e | |||
40daa827c4 | |||
2d54c4f7f5 | |||
fce93e1dcd | |||
eff56815da | |||
d1ddf5d04c | |||
f2f4078bb8 | |||
9bfaa754e6 | |||
9739f78b19 | |||
d4961059b6 | |||
93b5eb22ab | |||
f57b0c6e43 | |||
dce1df37db | |||
e1250779de | |||
372d2b45dd | |||
9be485a1a9 | |||
9ebe02b80e | |||
8be2103dec | |||
20a8d3b4ef | |||
51dd523249 | |||
a20b570e11 | |||
382489ecac | |||
c1c4251361 | |||
1fc294e8f6 | |||
076e844898 | |||
136a9416ec | |||
f2f570c87e | |||
1132efb828 | |||
edb71ad684 | |||
8a506beb0f | |||
c335e93def | |||
471cb25ad1 | |||
b661e436fb | |||
3c1dc868aa | |||
54ceac1ee8 | |||
7eb3ff0048 | |||
51912e5525 | |||
a1c9a698ee | |||
c7cf518477 | |||
7ad144e3b0 | |||
aa0ebc76e9 | |||
a9be56deae | |||
e014d91060 | |||
ddcde8fa07 | |||
b0c63e61fd | |||
4997bbffa2 | |||
ae0e133918 | |||
ce1db0a31b | |||
f4f4c8173e | |||
61f9872e66 | |||
ebff1f3d78 | |||
e94c96cb26 | |||
fcc2a6fce8 | |||
c5a708a382 | |||
6cb877d358 | |||
55575811e7 | |||
906448378f | |||
68928375d8 | |||
1ef07f3acc | |||
85dc97aa05 | |||
0947689c0f | |||
44b811d09e | |||
6e81bf1600 | |||
1adf1b83db | |||
49095c3830 | |||
9c6aae8ea7 | |||
9db68b263d | |||
3d7f2407b6 | |||
d5c387d12e | |||
767ade0e0d | |||
5e77fd4c82 | |||
1917053811 | |||
cfe5d57656 | |||
765bcb5896 | |||
528bebf7a7 | |||
b5970977e6 | |||
4a22b47580 | |||
389175c392 | |||
52c1bfba48 | |||
0bc452c38a | |||
c0cc10f417 | |||
997de8ca9f | |||
6de00553c2 | |||
5f737912e4 | |||
4a0c17e1c0 | |||
1dd11f04f1 | |||
c647574cb0 | |||
5db9ddf82f | |||
b2440be6d0 | |||
ad1962753b | |||
f64ff2feea | |||
a5a38bae43 | |||
5768306bc8 | |||
d7ae6e9106 | |||
67f4621dd7 | |||
731233cce1 | |||
02deabb6af | |||
eefac0a502 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,3 +2,7 @@
|
||||
*.log
|
||||
*spec-20*
|
||||
*.tgz
|
||||
*.xz
|
||||
*.html
|
||||
*.txt
|
||||
*el8*
|
||||
|
49
README.md
49
README.md
@@ -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.
|
||||
|
BIN
additional/journalwrap
Executable file
BIN
additional/journalwrap
Executable file
Binary file not shown.
12
createlinks
12
createlinks
@@ -6,8 +6,10 @@ $event = 'smeserver-mailstats-update';
|
||||
#see the /etc/systemd/system-preset/49-koozali.preset should be present for systemd integration on all you yum update event
|
||||
|
||||
foreach my $file (qw(
|
||||
/etc/systemd/system-preset/49-koozali.preset
|
||||
/etc/e-smith/sql/init/99smeserver-mailstats.sql
|
||||
/etc/systemd/system-preset/49-koozali.preset
|
||||
/etc/mailstats/db.php
|
||||
/etc/e-smith/sql/init/99mailstats
|
||||
/etc/httpd/conf/httpd.conf
|
||||
))
|
||||
{
|
||||
templates2events( $file, $event );
|
||||
@@ -18,8 +20,8 @@ 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");
|
||||
safe_symlink("restart", "root/etc/e-smith/events/$event/services2adjust/mysql.init");;
|
||||
#and Server Mmanager panel link
|
||||
#panel_link('somefunction', 'manager');
|
||||
|
||||
templates2events("/etc/e-smith/sql/init/99smeserver-mailstats.sql", "post-upgrade");
|
||||
#templates2events("/etc/e-smith/sql/init/99smeserver-mailstats.sql", "post-upgrade");
|
179
journalwrap.c
Normal file
179
journalwrap.c
Normal file
@@ -0,0 +1,179 @@
|
||||
#include <systemd/sd-journal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifndef MAX_OUTPUT_BYTES
|
||||
#define MAX_OUTPUT_BYTES (2 * 1000 * 1000) // 2 MB
|
||||
#endif
|
||||
|
||||
static int append_bytes(char **buf, size_t *len, size_t *cap, const char *src, size_t n) {
|
||||
if (*len + n + 1 > *cap) {
|
||||
size_t newcap = (*cap == 0) ? 8192 : *cap;
|
||||
while (*len + n + 1 > newcap) {
|
||||
newcap *= 2;
|
||||
if (newcap > (size_t)(MAX_OUTPUT_BYTES + 65536)) {
|
||||
newcap = (size_t)(MAX_OUTPUT_BYTES + 65536);
|
||||
break;
|
||||
}
|
||||
}
|
||||
char *nbuf = realloc(*buf, newcap);
|
||||
if (!nbuf) return -1;
|
||||
*buf = nbuf; *cap = newcap;
|
||||
}
|
||||
memcpy(*buf + *len, src, n);
|
||||
*len += n;
|
||||
(*buf)[*len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int append_cstr(char **buf, size_t *len, size_t *cap, const char *s) {
|
||||
return append_bytes(buf, len, cap, s, strlen(s));
|
||||
}
|
||||
|
||||
static size_t min_size(size_t a, size_t b) { return a < b ? a : b; }
|
||||
|
||||
static void sanitize_text(char *s, size_t n) {
|
||||
for (size_t i = 0; i < n; i++) if (s[i] == '\0') s[i] = ' ';
|
||||
}
|
||||
|
||||
static void format_ts(char *out, size_t outsz, uint64_t usec) {
|
||||
time_t sec = (time_t)(usec / 1000000ULL);
|
||||
struct tm tm;
|
||||
localtime_r(&sec, &tm);
|
||||
strftime(out, outsz, "%Y-%m-%d %H:%M:%S", &tm);
|
||||
}
|
||||
|
||||
static const char* field_value(const void *data, size_t len, const char *key, size_t *vlen) {
|
||||
size_t klen = strlen(key);
|
||||
if (len < klen + 1) return NULL;
|
||||
const char *p = (const char *)data;
|
||||
if (memcmp(p, key, klen) != 0 || p[klen] != '=') return NULL;
|
||||
*vlen = len - (klen + 1);
|
||||
return p + klen + 1;
|
||||
}
|
||||
|
||||
static int append_entry_line(sd_journal *j, char **buf, size_t *len, size_t *cap) {
|
||||
uint64_t usec = 0;
|
||||
(void)sd_journal_get_realtime_usec(j, &usec);
|
||||
char ts[32];
|
||||
format_ts(ts, sizeof(ts), usec);
|
||||
|
||||
const void *data = NULL;
|
||||
size_t dlen = 0;
|
||||
const char *message = NULL;
|
||||
size_t mlen = 0;
|
||||
|
||||
int r = sd_journal_get_data(j, "MESSAGE", &data, &dlen);
|
||||
if (r >= 0) message = field_value(data, dlen, "MESSAGE", &mlen);
|
||||
|
||||
const char *ident = NULL;
|
||||
size_t ilen = 0;
|
||||
r = sd_journal_get_data(j, "SYSLOG_IDENTIFIER", &data, &dlen);
|
||||
if (r >= 0) {
|
||||
ident = field_value(data, dlen, "SYSLOG_IDENTIFIER", &ilen);
|
||||
} else if (sd_journal_get_data(j, "_COMM", &data, &dlen) >= 0) {
|
||||
ident = field_value(data, dlen, "_COMM", &ilen);
|
||||
}
|
||||
|
||||
if (append_cstr(buf, len, cap, "[") < 0) return -1;
|
||||
if (append_cstr(buf, len, cap, ts) < 0) return -1;
|
||||
if (append_cstr(buf, len, cap, "] ") < 0) return -1;
|
||||
if (ident && ilen > 0) {
|
||||
if (append_bytes(buf, len, cap, ident, ilen) < 0) return -1;
|
||||
if (append_cstr(buf, len, cap, ": ") < 0) return -1;
|
||||
}
|
||||
|
||||
if (message && mlen > 0) {
|
||||
char *tmp = malloc(mlen);
|
||||
if (!tmp) return -1;
|
||||
memcpy(tmp, message, mlen);
|
||||
sanitize_text(tmp, mlen);
|
||||
size_t to_copy = min_size(mlen, (size_t)(MAX_OUTPUT_BYTES > *len ? MAX_OUTPUT_BYTES - *len : 0));
|
||||
int ok = append_bytes(buf, len, cap, tmp, to_copy);
|
||||
free(tmp);
|
||||
if (ok < 0) return -1;
|
||||
} else {
|
||||
const char *keys[] = {"PRIORITY","SYSLOG_IDENTIFIER","_COMM","_EXE","_CMDLINE","MESSAGE"};
|
||||
for (size_t i = 0; i < sizeof(keys)/sizeof(keys[0]); i++) {
|
||||
if (sd_journal_get_data(j, keys[i], &data, &dlen) < 0) continue;
|
||||
if (append_cstr(buf, len, cap, (i == 0 ? "" : " ")) < 0) return -1;
|
||||
if (append_bytes(buf, len, cap, (const char*)data, min_size(dlen, (size_t)(MAX_OUTPUT_BYTES - *len))) < 0) return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (*len < MAX_OUTPUT_BYTES) {
|
||||
if (append_cstr(buf, len, cap, "\n") < 0) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char* journal_get_by_pid_impl(int pid) {
|
||||
if (pid <= 0) { char *z = malloc(1); if (z) z[0] = '\0'; return z; }
|
||||
|
||||
sd_journal *j = NULL;
|
||||
if (sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY) < 0) {
|
||||
char *z = malloc(1); if (z) z[0] = '\0'; return z;
|
||||
}
|
||||
|
||||
char match[64];
|
||||
snprintf(match, sizeof(match), "_PID=%d", pid);
|
||||
if (sd_journal_add_match(j, match, 0) < 0) {
|
||||
sd_journal_close(j);
|
||||
char *z = malloc(1); if (z) z[0] = '\0'; return z;
|
||||
}
|
||||
|
||||
sd_journal_seek_head(j);
|
||||
|
||||
char *buf = NULL; size_t len = 0, cap = 0;
|
||||
int r;
|
||||
while ((r = sd_journal_next(j)) > 0) {
|
||||
if (len >= MAX_OUTPUT_BYTES) break;
|
||||
if (append_entry_line(j, &buf, &len, &cap) < 0) {
|
||||
free(buf); sd_journal_close(j); return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (len >= MAX_OUTPUT_BYTES) {
|
||||
const char *trunc = "[output truncated]\n";
|
||||
(void)append_bytes(&buf, &len, &cap, trunc, strlen(trunc));
|
||||
}
|
||||
|
||||
if (!buf) { buf = malloc(1); if (!buf) { sd_journal_close(j); return NULL; } buf[0] = '\0'; }
|
||||
sd_journal_close(j);
|
||||
return buf;
|
||||
}
|
||||
|
||||
#ifdef __GNUC__
|
||||
__attribute__((visibility("default")))
|
||||
#endif
|
||||
char* journal_get_by_pid(int pid) { return journal_get_by_pid_impl(pid); }
|
||||
|
||||
#ifdef __GNUC__
|
||||
__attribute__((visibility("default")))
|
||||
#endif
|
||||
void journal_free(char* p) { free(p); }
|
||||
|
||||
#ifdef BUILD_CLI
|
||||
static int parse_pid(const char *s, int *out) {
|
||||
if (!s || !*s) return -1;
|
||||
char *end = NULL;
|
||||
errno = 0;
|
||||
long v = strtol(s, &end, 10);
|
||||
if (errno != 0 || end == s || *end != '\0' || v <= 0 || v > 0x7fffffffL) return -1;
|
||||
*out = (int)v; return 0;
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 2) { fprintf(stderr, "Usage: %s <pid>\n", argv[0]); return 2; }
|
||||
int pid = 0;
|
||||
if (parse_pid(argv[1], &pid) != 0) { fprintf(stderr, "Invalid pid\n"); return 2; }
|
||||
char *out = journal_get_by_pid_impl(pid);
|
||||
if (!out) { fprintf(stderr, "Out of memory or error\n"); return 1; }
|
||||
fputs(out, stdout);
|
||||
free(out);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
@@ -0,0 +1 @@
|
||||
enabled
|
@@ -0,0 +1 @@
|
||||
report
|
16
root/etc/e-smith/db/configuration/migrate/80DBPass
Normal file
16
root/etc/e-smith/db/configuration/migrate/80DBPass
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
use MIME::Base64 qw(encode_base64);
|
||||
|
||||
my $rec = $DB->get('mailstats') || $DB->new_record('mailstats', {type => 'report'});
|
||||
|
||||
my $pw = $rec->prop('DBPass');
|
||||
return "" if $pw;
|
||||
|
||||
my $length = shift || 16;
|
||||
|
||||
my @chars = ('A'..'Z', 'a'..'z', 0..9, qw(! @ $ % ^ & * ? _ - + =));
|
||||
$pw = '';
|
||||
$pw .= $chars[rand @chars] for 1..$length;
|
||||
$rec->set_prop('DBPass', $pw);
|
||||
return ""
|
||||
}
|
24
root/etc/e-smith/templates/etc/e-smith/sql/init/99mailstats
Normal file
24
root/etc/e-smith/templates/etc/e-smith/sql/init/99mailstats
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
my $db = $mailstats{DBName} || 'mailstats';
|
||||
my $user = $mailstats{DBUser} || 'mailstats_rw';
|
||||
my $pass = $mailstats{DBPass} || 'changeme';
|
||||
$OUT .= <<END
|
||||
#! /bin/sh
|
||||
if [ -d /var/lib/mysql/mailstats ]; then
|
||||
exit
|
||||
fi
|
||||
/usr/bin/mariadb <<EOF
|
||||
CREATE DATABASE $db DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
|
||||
USE $db;
|
||||
CREATE TABLE IF NOT EXISTS SummaryLogs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
Date DATE,
|
||||
Hour INT,
|
||||
logData TEXT
|
||||
);
|
||||
CREATE USER $user@localhost IDENTIFIED BY '$pass';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON $db.* TO $user@localhost;
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
END
|
||||
}
|
@@ -1,94 +0,0 @@
|
||||
CREATE DATABASE IF NOT EXISTS `mailstats`;
|
||||
|
||||
USE `mailstats`;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ColumnStats` (
|
||||
`ColumnStatsid` int(11) NOT NULL auto_increment,
|
||||
`dateid` int(11) NOT NULL default '0',
|
||||
`timeid` int(11) NOT NULL default '0',
|
||||
`descr` varchar(20) NOT NULL default '',
|
||||
`count` bigint(20) NOT NULL default '0',
|
||||
`servername` varchar(30) NOT NULL default '',
|
||||
PRIMARY KEY (`ColumnStatsid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `JunkMailStats` (
|
||||
`JunkMailstatsid` int(11) NOT NULL auto_increment,
|
||||
`dateid` int(11) NOT NULL default '0',
|
||||
`user` varchar(12) NOT NULL default '',
|
||||
`count` bigint(20) NOT NULL default '0',
|
||||
`servername` varchar(30) default NULL,
|
||||
PRIMARY KEY (`JunkMailstatsid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `SARules` (
|
||||
`SARulesid` int(11) NOT NULL auto_increment,
|
||||
`dateid` int(11) NOT NULL default '0',
|
||||
`rule` varchar(50) NOT NULL default '',
|
||||
`count` bigint(20) NOT NULL default '0',
|
||||
`totalhits` bigint(20) NOT NULL default '0',
|
||||
`servername` varchar(30) NOT NULL default '',
|
||||
PRIMARY KEY (`SARulesid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `SAscores` (
|
||||
`SAscoresid` int(11) NOT NULL auto_increment,
|
||||
`dateid` int(11) NOT NULL default '0',
|
||||
`acceptedcount` bigint(20) NOT NULL default '0',
|
||||
`rejectedcount` bigint(20) NOT NULL default '0',
|
||||
`hamcount` bigint(20) NOT NULL default '0',
|
||||
`acceptedscore` decimal(20,2) NOT NULL default '0.00',
|
||||
`rejectedscore` decimal(20,2) NOT NULL default '0.00',
|
||||
`hamscore` decimal(20,2) NOT NULL default '0.00',
|
||||
`totalsmtp` bigint(20) NOT NULL default '0',
|
||||
`totalrecip` bigint(20) NOT NULL default '0',
|
||||
`servername` varchar(30) NOT NULL default '',
|
||||
PRIMARY KEY (`SAscoresid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `VirusStats` (
|
||||
`VirusStatsid` int(11) NOT NULL auto_increment,
|
||||
`dateid` int(11) NOT NULL default '0',
|
||||
`descr` varchar(40) NOT NULL default '',
|
||||
`count` bigint(20) NOT NULL default '0',
|
||||
`servername` varchar(30) NOT NULL default '',
|
||||
PRIMARY KEY (`VirusStatsid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `date` (
|
||||
`dateid` int(11) NOT NULL auto_increment,
|
||||
`date` date NOT NULL default '0000-00-00',
|
||||
PRIMARY KEY (`dateid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `domains` (
|
||||
`domainsid` int(11) NOT NULL auto_increment,
|
||||
`dateid` int(11) NOT NULL default '0',
|
||||
`domain` varchar(40) NOT NULL default '',
|
||||
`type` varchar(10) NOT NULL default '',
|
||||
`total` bigint(20) NOT NULL default '0',
|
||||
`denied` bigint(20) NOT NULL default '0',
|
||||
`xfererr` bigint(20) NOT NULL default '0',
|
||||
`accept` bigint(20) NOT NULL default '0',
|
||||
`servername` varchar(30) NOT NULL default '',
|
||||
PRIMARY KEY (`domainsid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `qpsmtpdcodes` (
|
||||
`qpsmtpdcodesid` int(11) NOT NULL auto_increment,
|
||||
`dateid` int(11) NOT NULL default '0',
|
||||
`reason` varchar(40) NOT NULL default '',
|
||||
`count` bigint(20) NOT NULL default '0',
|
||||
`servername` varchar(30) NOT NULL default '',
|
||||
PRIMARY KEY (`qpsmtpdcodesid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `time` (
|
||||
`timeid` int(11) NOT NULL auto_increment,
|
||||
`time` time NOT NULL default '00:00:00',
|
||||
PRIMARY KEY (`timeid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
|
||||
|
||||
|
||||
grant all privileges on mailstats.* to 'mailstats'@'localhost' identified by 'mailstats';
|
@@ -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";
|
||||
}
|
||||
}
|
24
root/etc/e-smith/templates/etc/mailstats/db.php/10DBDetails
Normal file
24
root/etc/e-smith/templates/etc/mailstats/db.php/10DBDetails
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
# Load SME::ConfigDB to read values from DB
|
||||
my $cdb = esmith::ConfigDB->open() || die "Cannot open configuration DB\n";
|
||||
|
||||
# Get the fragment (report database definition)
|
||||
my $report = $cdb->get('mailstats');
|
||||
|
||||
my $dbhost = $report->prop('DBHost') || 'localhost';
|
||||
my $dbport = $report->prop('DBPort') || '3306';
|
||||
my $dbuser = $report->prop('DBUser') || 'mailstats_rw';
|
||||
# Assume password is stored in a property 'DBPass'
|
||||
my $dbpass = $report->prop('DBPass') || 'changeme';
|
||||
my $dbname = $report->key || 'mailstats';
|
||||
|
||||
$OUT = <<"END";
|
||||
<?php
|
||||
return [
|
||||
'host' => '$dbhost',
|
||||
'user' => '$dbuser',
|
||||
'pass' => '$dbpass',
|
||||
'name' => '$dbname',
|
||||
];
|
||||
END
|
||||
}
|
7
root/etc/mailstats/db.php
Normal file
7
root/etc/mailstats/db.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
return [
|
||||
'host' => 'localhost',
|
||||
'user' => 'mailstats', //Should be mailstat-ro
|
||||
'pass' => 'mailstats', //Will be randon strong password
|
||||
'name' => 'mailstats',
|
||||
];
|
347
root/opt/mailstats/css/mailstats.css
Normal file
347
root/opt/mailstats/css/mailstats.css
Normal file
@@ -0,0 +1,347 @@
|
||||
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;}
|
||||
|
||||
/* ==============================================
|
||||
Summary Logs Section (scoped under .mailstats-summary)
|
||||
============================================== */
|
||||
.mailstats-summary .summary-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
font-size: 0.85vw;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.mailstats-summary .summary-table {
|
||||
border-collapse: collapse;
|
||||
width: 98%;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.mailstats-summary .summary-table th {
|
||||
text-align: left;
|
||||
padding: 0.5em;
|
||||
border-bottom: 2px solid #ddd;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.mailstats-summary .summary-table td {
|
||||
padding: 0.5em;
|
||||
border-bottom: 1px solid #ddd;
|
||||
word-break: break-word; /* Allows breaking long words at arbitrary points */
|
||||
overflow-wrap: break-word; /* Modern standard for breaking long words */
|
||||
hyphens: auto; /* Optionally adds hyphenation if supported */
|
||||
}
|
||||
|
||||
/* Zebra striping */
|
||||
.mailstats-summary .summary-table tbody tr:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.mailstats-summary .pagination {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.mailstats-summary .pagination a {
|
||||
text-decoration: none;
|
||||
color: #0066cc;
|
||||
padding: 0.3em 0.6em;
|
||||
}
|
||||
|
||||
.mailstats-summary .pagination a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.mailstats-summary table.stripes {
|
||||
border-collapse: collapse;
|
||||
width: 95%;
|
||||
overflow-x: auto;
|
||||
margin: 0.6% auto;
|
||||
}
|
||||
|
||||
/* Optional zebra striping */
|
||||
.mailstats-summary table.stripes tbody tr:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
Log Detail Page (scoped under .mailstats-detail)
|
||||
============================================== */
|
||||
.mailstats-detail .detail-container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 1em auto;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
/* Preformatted log box */
|
||||
.mailstats-detail .log {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background: #111;
|
||||
color: #eee;
|
||||
padding: 1em;
|
||||
border-radius: 6px;
|
||||
font-family: monospace, monospace;
|
||||
font-size: 0.75em;
|
||||
line-height: 1.4;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Back link styling */
|
||||
.mailstats-detail a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mailstats-detail a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
Status header at top of table (scoped under emailstatus)
|
||||
============================================== */
|
||||
.emailstatus-wrapper {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
.emailstatus-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.emailstatus-tablecontainer {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.emailstatus-table {
|
||||
border-collapse: collapse;
|
||||
min-width: 300px;
|
||||
flex: 1 1 45%;
|
||||
}
|
||||
.emailstatus-table th {
|
||||
background-color: #a9a9a9;
|
||||
color: black;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
}
|
||||
.emailstatus-table td {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.emailstatus-table tr:nth-child(even) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.emailstatus-tablecontainer {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
244
root/opt/mailstats/html/ShowDetailedLogs.php
Normal file
244
root/opt/mailstats/html/ShowDetailedLogs.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
// Security headers
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; object-src 'none'; frame-ancestors 'none'");
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
||||
function e($s) {
|
||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
// Configuration: env first, then fallback to optional file
|
||||
$servername = getenv('MAILSTATS_DB_HOST') ?: 'localhost';
|
||||
$username = getenv('MAILSTATS_DB_USER') ?: '';
|
||||
$password = getenv('MAILSTATS_DB_PASS') ?: '';
|
||||
$dbname = getenv('MAILSTATS_DB_NAME') ?: '';
|
||||
|
||||
if ($username === '' || $password === '' || $dbname === '') {
|
||||
$cfgPath = '/etc/mailstats/db.php'; // optional fallback config file
|
||||
if (is_readable($cfgPath)) {
|
||||
ob_start();
|
||||
$cfg = include $cfgPath;
|
||||
ob_end_clean();
|
||||
$servername = $cfg['host'] ?? $servername;
|
||||
$username = $cfg['user'] ?? $username;
|
||||
$password = $cfg['pass'] ?? $password;
|
||||
$dbname = $cfg['name'] ?? $dbname;
|
||||
}
|
||||
}
|
||||
|
||||
if ($username === '' || $password === '' || $dbname === '') {
|
||||
error_log('DB credentials missing (env and config file).');
|
||||
http_response_code(500);
|
||||
exit('Service temporarily unavailable.');
|
||||
}
|
||||
|
||||
// Input validation: id
|
||||
$id = isset($_GET['id']) ? filter_var($_GET['id'], FILTER_VALIDATE_INT) : null;
|
||||
if ($id === false || $id === null || $id < 1) {
|
||||
http_response_code(400);
|
||||
exit('Invalid id');
|
||||
}
|
||||
|
||||
// DB connect with exceptions
|
||||
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||
try {
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
$conn->set_charset('utf8mb4');
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
error_log('DB connect failed: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
exit('Service temporarily unavailable.');
|
||||
}
|
||||
|
||||
// Fetch the record and extract PID from JSON logData
|
||||
try {
|
||||
$stmt = $conn->prepare('SELECT id, logData FROM SummaryLogs WHERE id = ?');
|
||||
$stmt->bind_param('i', $id);
|
||||
$stmt->execute();
|
||||
$res = $stmt->get_result();
|
||||
$row = $res->fetch_assoc();
|
||||
$stmt->close();
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
error_log('Query failed: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
exit('Service temporarily unavailable.');
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
http_response_code(404);
|
||||
exit('Record not found');
|
||||
}
|
||||
|
||||
$logData = $row['logData'];
|
||||
$pid = null;
|
||||
$data = json_decode($logData, true, 512, JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
if (is_array($data)) {
|
||||
foreach (['id','pid', 'PID', 'Pid', 'process_id', 'ProcessId'] as $k) {
|
||||
if (isset($data[$k]) && (is_int($data[$k]) || ctype_digit((string)$data[$k]))) {
|
||||
$pid = (int)$data[$k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$pid || $pid < 1) {
|
||||
http_response_code(422);
|
||||
exit('PID not found in this record');
|
||||
}
|
||||
|
||||
// Journal retrieval using C wrapper
|
||||
define('FFI_LIB', 'libjournalwrap.so'); // adjust if needed
|
||||
define('WRAPPER_BIN', '/usr/bin/journalwrap'); // fallback executable path
|
||||
define('MAX_OUTPUT_BYTES', 2_000_000); // 2MB safety cap
|
||||
|
||||
function getJournalByPidViaFFI(int $pid): ?string {
|
||||
if (!extension_loaded('FFI')) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// Adjust the function signatures to match your wrapper
|
||||
$ffi = FFI::cdef("
|
||||
char* journal_get_by_pid(int pid);
|
||||
void journal_free(char* p);
|
||||
", FFI_LIB);
|
||||
$cstr = $ffi->journal_get_by_pid($pid);
|
||||
if ($cstr === null) {
|
||||
return '';
|
||||
}
|
||||
$out = FFI::string($cstr);
|
||||
$ffi->journal_free($cstr);
|
||||
return $out;
|
||||
} catch (Throwable $e) {
|
||||
error_log('FFI journal wrapper failed: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getJournalByPidViaExec(int $pid): ?string {
|
||||
// Fallback to an external wrapper binary (must be safe and not use shell)
|
||||
$cmd = WRAPPER_BIN . ' ' . (string)$pid;
|
||||
|
||||
$descriptorspec = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
$pipes = [];
|
||||
$proc = proc_open($cmd, $descriptorspec, $pipes, null, null, ['bypass_shell' => true]);
|
||||
|
||||
if (!\is_resource($proc)) {
|
||||
error_log('Failed to start journal wrapper binary');
|
||||
return null;
|
||||
}
|
||||
|
||||
fclose($pipes[0]); // no stdin
|
||||
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$start = microtime(true);
|
||||
$timeout = 10.0; // seconds
|
||||
$readChunk = 65536;
|
||||
|
||||
while (true) {
|
||||
$status = proc_get_status($proc);
|
||||
$running = $status['running'];
|
||||
|
||||
$read = [$pipes[1], $pipes[2]];
|
||||
$write = null;
|
||||
$except = null;
|
||||
$tv_sec = 0;
|
||||
$tv_usec = 300000; // 300ms
|
||||
stream_select($read, $write, $except, $tv_sec, $tv_usec);
|
||||
|
||||
foreach ($read as $r) {
|
||||
if ($r === $pipes[1]) {
|
||||
$chunk = fread($pipes[1], $readChunk);
|
||||
if ($chunk !== false && $chunk !== '') {
|
||||
$stdout .= $chunk;
|
||||
}
|
||||
} elseif ($r === $pipes[2]) {
|
||||
$chunk = fread($pipes[2], $readChunk);
|
||||
if ($chunk !== false && $chunk !== '') {
|
||||
$stderr .= $chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$running) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((microtime(true) - $start) > $timeout) {
|
||||
proc_terminate($proc);
|
||||
$stderr .= "\n[terminated due to timeout]";
|
||||
break;
|
||||
}
|
||||
|
||||
if (strlen($stdout) + strlen($stderr) > MAX_OUTPUT_BYTES) {
|
||||
proc_terminate($proc);
|
||||
$stderr .= "\n[terminated due to output size limit]";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pipes as $p) {
|
||||
if (is_resource($p)) {
|
||||
fclose($p);
|
||||
}
|
||||
}
|
||||
$exitCode = proc_close($proc);
|
||||
|
||||
if ($exitCode !== 0 && $stderr !== '') {
|
||||
error_log('journal wrapper stderr: ' . $stderr);
|
||||
}
|
||||
|
||||
return $stdout;
|
||||
}
|
||||
|
||||
$logs = getJournalByPidViaFFI($pid);
|
||||
if ($logs === null) {
|
||||
$logs = getJournalByPidViaExec($pid);
|
||||
}
|
||||
if ($logs === null) {
|
||||
http_response_code(500);
|
||||
exit('Unable to read journal for this PID');
|
||||
}
|
||||
|
||||
// Safety cap to avoid rendering gigantic outputs
|
||||
if (strlen($logs) > MAX_OUTPUT_BYTES) {
|
||||
$logs = substr($logs, 0, MAX_OUTPUT_BYTES) . "\n[output truncated]";
|
||||
}
|
||||
|
||||
// Done with DB
|
||||
$conn->close();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Log details for PID <?= e($pid) ?> (record <?= e($id) ?>)</title>
|
||||
<link rel="stylesheet" type="text/css" href="css/mailstats.css" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="mailstats-detail">
|
||||
<div class="detail-container">
|
||||
<h1>Log details for PID <?= e($pid) ?> (record <?= e($id) ?>)</h1>
|
||||
<p><a href="javascript:history.back()">Back</a></p>
|
||||
<pre class="log"><?= e($logs) ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
root/opt/mailstats/html/favicon.ico
Normal file
BIN
root/opt/mailstats/html/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
284
root/opt/mailstats/html/showSummaryLogs.php
Normal file
284
root/opt/mailstats/html/showSummaryLogs.php
Normal file
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
// Set security headers (must be sent before output)
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
header("Content-Security-Policy: default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; base-uri 'none'; object-src 'none'; frame-ancestors 'none'");
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
||||
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
||||
// Helper for safe HTML encoding
|
||||
function e($s) {
|
||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
// Configuration: read DB credentials from environment
|
||||
$servername = getenv('MAILSTATS_DB_HOST') ?: '';
|
||||
$username = getenv('MAILSTATS_DB_USER') ?: '';
|
||||
$password = getenv('MAILSTATS_DB_PASS') ?: '';
|
||||
$dbname = getenv('MAILSTATS_DB_NAME') ?: '';
|
||||
|
||||
// Otherwise try config in /etc/mailstats
|
||||
if ($username === '' || $password === '' || $dbname === '') {
|
||||
$cfgPath = '/etc/mailstats/db.php';
|
||||
if (is_readable($cfgPath)) {
|
||||
ob_start();
|
||||
$cfg = include $cfgPath;
|
||||
ob_end_clean();
|
||||
$servername = $cfg['host'] ?? $servername ?: 'localhost';
|
||||
$username = $cfg['user'] ?? $username;
|
||||
$password = $cfg['pass'] ?? $password;
|
||||
$dbname = $cfg['name'] ?? $dbname;
|
||||
}
|
||||
}
|
||||
|
||||
// Fail fast if credentials are not provided via environment
|
||||
if ($username === '' || $password === '' || $dbname === '') {
|
||||
error_log('Configuration error: DB credentials not set via environment.');
|
||||
http_response_code(500);
|
||||
exit('Service temporarily unavailable.');
|
||||
}
|
||||
|
||||
// Robust input handling
|
||||
$defaultDate = date('Y-m-d', strtotime('-1 day'));
|
||||
$date = isset($_GET['date']) ? $_GET['date'] : $defaultDate;
|
||||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
http_response_code(400);
|
||||
exit('Invalid date');
|
||||
}
|
||||
|
||||
// hour: allow 0–23 or special 99 meaning “all hours”
|
||||
$hour = isset($_GET['hour']) ? filter_var($_GET['hour'], FILTER_VALIDATE_INT) : 99;
|
||||
if ($hour === false || ($hour !== 99 && ($hour < 0 || $hour > 23))) {
|
||||
http_response_code(400);
|
||||
exit('Invalid hour');
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$page = isset($_GET['page']) ? filter_var($_GET['page'], FILTER_VALIDATE_INT) : 1;
|
||||
if ($page === false || $page < 1) { $page = 1; }
|
||||
$pageSize = isset($_GET['page_size']) ? filter_var($_GET['page_size'], FILTER_VALIDATE_INT) : 50;
|
||||
if ($pageSize === false) { $pageSize = 50; }
|
||||
// Bound page size to prevent huge result sets
|
||||
if ($pageSize < 1) { $pageSize = 1; }
|
||||
if ($pageSize > 100) { $pageSize = 100; }
|
||||
$limit = $pageSize;
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
// Use mysqli with exceptions and UTF-8
|
||||
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||
try {
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
$conn->set_charset('utf8mb4');
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
error_log('DB connect failed: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
exit('Service temporarily unavailable.');
|
||||
}
|
||||
|
||||
// Build WHERE clause and bind parameters safely
|
||||
$where = 'Date = ?';
|
||||
$bindTypesCount = 's';
|
||||
$bindValuesCount = [$date];
|
||||
|
||||
if ($hour !== 99) {
|
||||
$where .= ' AND Hour = ?';
|
||||
$bindTypesCount .= 'i';
|
||||
$bindValuesCount[] = $hour;
|
||||
}
|
||||
|
||||
// Count query for total rows (for display/pagination info)
|
||||
try {
|
||||
$sqlCount = "SELECT COUNT(*) AS total FROM SummaryLogs WHERE $where";
|
||||
$stmtCount = $conn->prepare($sqlCount);
|
||||
$stmtCount->bind_param($bindTypesCount, ...$bindValuesCount);
|
||||
$stmtCount->execute();
|
||||
$resultCount = $stmtCount->get_result();
|
||||
$rowCount = $resultCount->fetch_assoc();
|
||||
$totalRows = (int)$rowCount['total'];
|
||||
$stmtCount->close();
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
error_log('Count query failed: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
exit('Service temporarily unavailable.');
|
||||
}
|
||||
|
||||
// Data query with ORDER and LIMIT/OFFSET
|
||||
try {
|
||||
$sql = "SELECT id, logData FROM SummaryLogs WHERE $where ORDER BY id DESC LIMIT ? OFFSET ?";
|
||||
// Bind types: existing where types + limit (i) + offset (i)
|
||||
$bindTypesData = $bindTypesCount . 'ii';
|
||||
$bindValuesData = $bindValuesCount;
|
||||
$bindValuesData[] = $limit;
|
||||
$bindValuesData[] = $offset;
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param($bindTypesData, ...$bindValuesData);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
error_log('Data query failed: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
exit('Service temporarily unavailable.');
|
||||
}
|
||||
|
||||
function generateLogDataTable($logData) {
|
||||
// Defensive decode with substitution for invalid UTF-8
|
||||
$data = json_decode($logData, true, 512, JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
|
||||
if (!is_array($data)) {
|
||||
return '<em>Invalid JSON data</em>';
|
||||
}
|
||||
|
||||
// Remove entries with key 'logterse' and entries with empty values
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === 'logterse' || $value === '' || $value === null) {
|
||||
unset($data[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge adjacent duplicates by value
|
||||
$mergedData = [];
|
||||
$previousValue = null;
|
||||
foreach ($data as $key => $value) {
|
||||
// Normalize non-scalar values for display
|
||||
if (is_array($value) || is_object($value)) {
|
||||
$value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
$valueStr = (string)$value;
|
||||
|
||||
if ($valueStr === $previousValue) {
|
||||
end($mergedData);
|
||||
$lastKey = key($mergedData);
|
||||
$newKey = $lastKey . '/' . $key;
|
||||
$mergedData[$newKey] = $valueStr;
|
||||
unset($mergedData[$lastKey]);
|
||||
} else {
|
||||
$mergedData[$key] = $valueStr;
|
||||
}
|
||||
$previousValue = $valueStr;
|
||||
}
|
||||
|
||||
// Optional truncation to keep rendering safe
|
||||
$maxValueLen = 500;
|
||||
foreach ($mergedData as $k => $v) {
|
||||
if (mb_strlen($v, 'UTF-8') > $maxValueLen) {
|
||||
$mergedData[$k] = mb_substr($v, 0, $maxValueLen, 'UTF-8') . '…';
|
||||
}
|
||||
}
|
||||
|
||||
$keys = array_keys($mergedData);
|
||||
$values = array_values($mergedData);
|
||||
|
||||
$output = '<table class="mailstats-summary stripes"><tbody>';
|
||||
|
||||
// Divide keys and values into sets of 6
|
||||
$chunks = array_chunk($keys, 6);
|
||||
foreach ($chunks as $chunkIndex => $chunk) {
|
||||
$output .= '<tr>';
|
||||
foreach ($chunk as $key) {
|
||||
$output .= '<th>' . e($key) . '</th>';
|
||||
}
|
||||
$output .= '</tr><tr>';
|
||||
foreach ($chunk as $i => $key) {
|
||||
$val = $values[$chunkIndex * 6 + $i];
|
||||
$output .= '<td>' . e($val) . '</td>';
|
||||
}
|
||||
$output .= '</tr>';
|
||||
}
|
||||
|
||||
$output .= '</tbody></table>';
|
||||
return $output;
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Summary Logs</title>
|
||||
<link rel="stylesheet" type="text/css" href="css/mailstats.css" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<div class="mailstats-summary">
|
||||
<div class="summary-container">
|
||||
<h1>
|
||||
Summary Logs for Date: <?= e($date) ?>
|
||||
<?= $hour === 99 ? ' (All Hours)' : ' at Hour: ' . e($hour) ?>
|
||||
</h1>
|
||||
<?php
|
||||
$startRow = $totalRows > 0 ? ($offset + 1) : 0;
|
||||
$endRow = min($offset + $limit, $totalRows);
|
||||
?>
|
||||
<h3>Found <?= e($totalRows) ?> records. Showing <?= e($startRow) ?>–<?= e($endRow) ?>.</h3>
|
||||
|
||||
<table class="summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Details</th>
|
||||
<th>Log Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($result && $result->num_rows > 0): ?>
|
||||
<?php while ($row = $result->fetch_assoc()): ?>
|
||||
<?php
|
||||
$id = (int)$row['id'];
|
||||
$detailUrl = './ShowDetailedLogs.php?id=' . rawurlencode((string)$id);
|
||||
?>
|
||||
<tr>
|
||||
<td><?= e($id) ?></td>
|
||||
<td><a href="<?= e($detailUrl) ?>">View details</a></td>
|
||||
<td><?= generateLogDataTable($row['logData']) ?></td>
|
||||
</tr>
|
||||
<?php endwhile; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="3">No records found for the specified date and hour.</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php
|
||||
// Pagination
|
||||
$baseParams = [
|
||||
'date' => $date,
|
||||
'hour' => $hour,
|
||||
'page_size' => $pageSize
|
||||
];
|
||||
$prevPage = $page > 1 ? $page - 1 : null;
|
||||
$nextPage = ($offset + $limit) < $totalRows ? $page + 1 : null;
|
||||
?>
|
||||
<div class="pagination">
|
||||
<?php if ($prevPage !== null): ?>
|
||||
<?php
|
||||
$paramsPrev = $baseParams; $paramsPrev['page'] = $prevPage;
|
||||
$urlPrev = '?' . http_build_query($paramsPrev, '', '&', PHP_QUERY_RFC3986);
|
||||
?>
|
||||
<a href="<?= e($urlPrev) ?>">« Previous</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($nextPage !== null): ?>
|
||||
<?php
|
||||
$paramsNext = $baseParams; $paramsNext['page'] = $nextPage;
|
||||
$urlNext = '?' . http_build_query($paramsNext, '', '&', PHP_QUERY_RFC3986);
|
||||
?>
|
||||
<?php if ($prevPage !== null): ?> | <?php endif; ?>
|
||||
<a href="<?= e($urlNext) ?>">Next »</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
if (isset($stmt) && $stmt instanceof mysqli_stmt) { $stmt->close(); }
|
||||
if (isset($conn) && $conn instanceof mysqli) { $conn->close(); }
|
||||
?>
|
||||
</body>
|
||||
</html>
|
98
root/opt/mailstats/js/mailstats.js
Normal file
98
root/opt/mailstats/js/mailstats.js
Normal 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
0
root/opt/mailstats/logs/.gitignore
vendored
Normal file
22
root/opt/mailstats/templates/mailstats-sub-table.html.pt
Normal file
22
root/opt/mailstats/templates/mailstats-sub-table.html.pt
Normal file
@@ -0,0 +1,22 @@
|
||||
<div class="${classname}">
|
||||
<h2>${title}</h2>
|
||||
<tal:block condition="threshold != 0">
|
||||
<span class='greyed-out'>${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>
|
144
root/opt/mailstats/templates/mailstats.html.pt
Normal file
144
root/opt/mailstats/templates/mailstats.html.pt
Normal file
@@ -0,0 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html><head>
|
||||
<meta charset="utf-8">
|
||||
<title>SMEServer Mailstats</title>
|
||||
<link rel='stylesheet' type='text/css' href='css/mailstats.css' />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<!-- 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="emailstatus-wrapper">
|
||||
<h2 class="emailstatus-header">Email System Status</h2>
|
||||
<div class="emailstatus-tablecontainer">
|
||||
<!-- Table 1 -->
|
||||
<table class="emailstatus-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Security & Filtering</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!---Add in table1 information here -->
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="emailstatus-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Mail Traffic Statistics</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!---Add in table2 information here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
97
root/usr/bin/mailstats-convert-log-sme10-to-sme11.py
Normal file
97
root/usr/bin/mailstats-convert-log-sme10-to-sme11.py
Normal 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}")
|
@@ -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
|
||||
|
||||
|
||||
#If date specified, only process lines matching date
|
||||
next LINE if ( $tai lt $starttai );
|
||||
next LINE if ( $tai gt $endtai );
|
||||
# Convert datetime to epoch
|
||||
my $current_epoch = convert_to_epoch($datetime);
|
||||
|
||||
# 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,12 +1955,34 @@ 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;
|
||||
return $ip->is_rfc1918();
|
||||
}
|
||||
}
|
2223
root/usr/bin/mailstats.py
Normal file
2223
root/usr/bin/mailstats.py
Normal file
File diff suppressed because it is too large
Load Diff
15
root/usr/bin/runallmailstats.sh
Executable file
15
root/usr/bin/runallmailstats.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Extract the earliest date from the journalctl header for qpsmtpd service
|
||||
earliest_date=$(journalctl -u qpsmtpd | head -n 1 | sed -n 's/.*Logs begin at [A-Za-z]* \([0-9-]*\).*/\1/p')
|
||||
|
||||
# Get yesterday's date
|
||||
yesterday=$(date -d 'yesterday' +%F)
|
||||
|
||||
current_date="$earliest_date"
|
||||
|
||||
# Loop from earliest date to yesterday
|
||||
while [[ "$current_date" < "$yesterday" || "$current_date" == "$yesterday" ]]; do
|
||||
runmailstats.sh "$current_date"
|
||||
current_date=$(date -I -d "$current_date + 1 day")
|
||||
done
|
22
root/usr/bin/runmailstats.sh
Normal file → Executable file
22
root/usr/bin/runmailstats.sh
Normal file → Executable 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"
|
@@ -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;
|
315
root/usr/share/smanager/lib/SrvMngr/Controller/Mailstats.pm
Normal file
315
root/usr/share/smanager/lib/SrvMngr/Controller/Mailstats.pm
Normal 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 : 7000 150
|
||||
#
|
||||
# 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;
|
@@ -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)'
|
@@ -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 {}
|
224
root/usr/share/smanager/themes/default/public/js/mailstats.js
Normal file
224
root/usr/share/smanager/themes/default/public/js/mailstats.js
Normal 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);
|
||||
}
|
@@ -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
|
@@ -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>
|
@@ -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>
|
@@ -1,17 +1,20 @@
|
||||
# $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 7
|
||||
Version: %{version}
|
||||
Release: %{release}%{?dist}
|
||||
License: GPL
|
||||
Group: SME/addon
|
||||
Source: %{name}-%{version}.tar.xz
|
||||
Source: %{name}-%{version}.tgz
|
||||
|
||||
%global _binaries_in_noarch_packages_terminate_build 0
|
||||
%global debug_package %{nil}
|
||||
|
||||
BuildRoot: /var/tmp/%{name}-%{version}-%{release}-buildroot
|
||||
BuildArchitectures: noarch
|
||||
@@ -19,18 +22,109 @@ Requires: smeserver-release => 9.0
|
||||
Requires: qpsmtpd >= 0.96
|
||||
BuildRequires: e-smith-devtools >= 1.13.1-03
|
||||
Requires: perl-Switch
|
||||
BuildRequires: python36
|
||||
Requires: python36
|
||||
# These need the EPEL repo enabled
|
||||
# So install as: dnf install smeserver-mailstats --enablerepo=epel,smecontribs
|
||||
Requires: html2text
|
||||
Requires: python3-chameleon
|
||||
Requires: python3-mysql
|
||||
Requires: python3-matplotlib
|
||||
Requires: python3-pip
|
||||
Requires: systemd-libs
|
||||
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
|
||||
See https://wiki.koozali.org/mailstats
|
||||
|
||||
%prep
|
||||
%setup
|
||||
|
||||
%build
|
||||
perl createlinks
|
||||
|
||||
%install
|
||||
/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
|
||||
|
||||
#chmod 0640 $RPM_BUILD_ROOT/etc/mailstats/db.php
|
||||
#ls -l /builddir/build/BUILDROOT/smeserver-mailstats-11.1-5.el8.sme.x86_64/etc/mailstats/
|
||||
#chown root:102 $RPM_BUILD_ROOT/etc/mailstats/db.php
|
||||
|
||||
# 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 --file '/etc/mailstats/db.php' 'attr(0640, root, apache)' $RPM_BUILD_ROOT | grep -v "\.pyc" | grep -v "\.pyo" > %{name}-%{version}-filelist
|
||||
|
||||
install -Dpm 0755 journalwrap %{buildroot}%{_bindir}/journalwrap
|
||||
#install -Dpm 0644 libjournalwrap.so %{buildroot}%{_libdir}/libjournalwrap.so
|
||||
|
||||
|
||||
%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
|
||||
|
||||
%files -f %{name}-%{version}-filelist
|
||||
%defattr(-,root,root)
|
||||
#%attr(0640, root, apache) %config(noreplace) /etc/mailstats/db.php
|
||||
%{_bindir}/journalwrap
|
||||
|
||||
#%{_libdir}/libjournalwrap.so
|
||||
|
||||
|
||||
%post
|
||||
/sbin/ldconfig
|
||||
usermod -aG systemd-journal www
|
||||
|
||||
%postun
|
||||
/sbin/ldconfig
|
||||
|
||||
%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]
|
||||
* Fri Sep 12 2025 Brian Read <brianr@koozali.org> 11.1-7.sme
|
||||
- Truncate Geoip table and add other category [SME: 13121]
|
||||
- Cope with blank data in action1 [SME: 13121]
|
||||
|
||||
* Sun Jul 09 2023 BogusDateBot
|
||||
- Eliminated rpmbuild "bogus date" warnings due to inconsistent weekday,
|
||||
by assuming the date is correct and changing the weekday.
|
||||
* Thu Sep 04 2025 Brian Read <brianr@koozali.org> 11.1-6.sme
|
||||
- Add favicon to mailstats table, summary and detailed pages [SME: 13121]
|
||||
- Bring DB config reading for mailstats itself inline with php summary and detailed logs - using /etc/mailstats/db.php [SME: 13121]
|
||||
- Remove DB config fields from the SM2 config panel {sme: 13121]
|
||||
- Arrange for password to be generated and mailstats user to be set with limited permissions [SME: 13121]
|
||||
|
||||
* Tue Sep 02 2025 Brian Read <brianr@koozali.org> 11.1-5.sme
|
||||
- Speed up Journal access [SME: 13121]
|
||||
- Fix missing blacklist URL [SME: 13121]
|
||||
- Add extra security to php show summary page [SME: 13121]
|
||||
- Fix up CSS for Summary Page [SME: 13121]
|
||||
- Get Detail logs page working and prettyfy [SME: 13121]
|
||||
- Add in C wrapper source code to interrogate journal [SME: 13121]
|
||||
- Get permission and ownership right for /etc/mailstats/db.php [SME: 13121]
|
||||
- Refactor main table header into two tables side by side [SME: 13121]
|
||||
|
||||
* Mon Sep 01 2025 Brian Read <brianr@koozali.org> 11.1-4.sme
|
||||
- More fixes for Journal bytes instead of characters [SME: 13117]
|
||||
|
||||
* Mon Sep 01 2025 Brian Read <brianr@koozali.org> 11.1-3.sme
|
||||
- Sort out ASCII escape codes in return from journalctl API [SME: 13117]
|
||||
- Add in Status enabled t default for mailstats DB [SME: 13118]
|
||||
|
||||
* Sun Apr 06 2025 Brian Read <brianr@koozali.org> 11.1-2.sme
|
||||
- First build on Koji - and Add in SM2 panel [SME: 13116]
|
||||
|
||||
* Mon Dec 30 2024 Brian Read <brianr@koozali.org> 11.1-1.sme
|
||||
- Update mailstats.py 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]
|
||||
@@ -98,22 +192,3 @@ See http://www.contribs.org/bugzilla/show_bug.cgi?id=819
|
||||
|
||||
* Sat May 26 2012 Brian J read <brianr@bjsystems.co.uk> 1.0-1.sme
|
||||
- Initial version
|
||||
|
||||
%prep
|
||||
%setup
|
||||
|
||||
%build
|
||||
perl createlinks
|
||||
|
||||
%install
|
||||
/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
|
||||
/bin/rm -f %{name}-%{version}-filelist
|
||||
/sbin/e-smith/genfilelist $RPM_BUILD_ROOT > %{name}-%{version}-filelist
|
||||
|
||||
%clean
|
||||
/bin/rm -rf $RPM_BUILD_ROOT
|
||||
|
||||
%files -f %{name}-%{version}-filelist
|
||||
%defattr(-,root,root)
|
||||
|
Reference in New Issue
Block a user