generated from smedev/Template-for-SMEServer-Contribs-Package
Add in software files and templates
This commit is contained in:
165
root/opt/dmarc-srg/classes/Report/Report.php
Normal file
165
root/opt/dmarc-srg/classes/Report/Report.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
|
||||
* Copyright (C) 2020 Aleksey Andreev (liuch)
|
||||
*
|
||||
* Available at:
|
||||
* https://github.com/liuch/dmarc-srg
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Liuch\DmarcSrg\Report;
|
||||
|
||||
use Liuch\DmarcSrg\Core;
|
||||
use Liuch\DmarcSrg\Exception\SoftException;
|
||||
use Liuch\DmarcSrg\Exception\DatabaseNotFoundException;
|
||||
|
||||
class Report
|
||||
{
|
||||
private $db = null;
|
||||
private $data = null;
|
||||
|
||||
public function __construct($data, $db = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->db = $db ?? Core::instance()->database();
|
||||
}
|
||||
|
||||
public static function fromXmlFile($fd)
|
||||
{
|
||||
$data = ReportData::fromXmlFile($fd);
|
||||
if (!self::checkData($data)) {
|
||||
throw new SoftException('Incorrect or incomplete report data');
|
||||
}
|
||||
return new Report($data);
|
||||
}
|
||||
|
||||
public function fetch()
|
||||
{
|
||||
$domain = $this->data['domain'];
|
||||
$report_id = $this->data['report_id'];
|
||||
if (empty($domain) || empty($report_id)) {
|
||||
throw new SoftException('Not specified report\'s domain or id');
|
||||
}
|
||||
$this->data = [ 'domain' => $domain, 'report_id' => $report_id ];
|
||||
try {
|
||||
$this->db->getMapper('report')->fetch($this->data);
|
||||
} catch (DatabaseNotFoundException $e) {
|
||||
throw new SoftException('The report is not found');
|
||||
}
|
||||
}
|
||||
|
||||
public function save(string $real_fname)
|
||||
{
|
||||
$b_ts = $this->data['begin_time'];
|
||||
$e_ts = $this->data['end_time'];
|
||||
if (!$b_ts->getTimestamp() || !$e_ts->getTimestamp() || $b_ts > $e_ts) {
|
||||
throw new SoftException('Failed to add an incoming report: wrong date range');
|
||||
}
|
||||
|
||||
$this->db->getMapper('report')->save($this->data);
|
||||
return [ 'message' => 'The report is loaded successfully' ];
|
||||
}
|
||||
|
||||
public function get()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function set($name, $value)
|
||||
{
|
||||
if (empty($this->data['domain']) || empty($this->data['report_id'])) {
|
||||
throw new SoftException('Not specified report\'s domain or id');
|
||||
}
|
||||
|
||||
$this->db->getMapper('report')->setProperty($this->data, $name, $value);
|
||||
return [ 'message' => 'Ok' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks report data for correctness and completeness
|
||||
*
|
||||
* @param array $data Report data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function checkData(array $data): bool
|
||||
{
|
||||
static $fields = [
|
||||
'domain' => [ 'required' => true, 'type' => 'string' ],
|
||||
'begin_time' => [ 'required' => true, 'type' => 'object' ],
|
||||
'end_time' => [ 'required' => true, 'type' => 'object' ],
|
||||
'org' => [ 'required' => true, 'type' => 'string' ],
|
||||
'external_id' => [ 'required' => true, 'type' => 'string' ],
|
||||
'email' => [ 'required' => false, 'type' => 'string' ],
|
||||
'extra_contact_info' => [ 'required' => false, 'type' => 'string' ],
|
||||
'error_string' => [ 'required' => false, 'type' => 'array' ],
|
||||
'policy_adkim' => [ 'required' => false, 'type' => 'string' ],
|
||||
'policy_aspf' => [ 'required' => false, 'type' => 'string' ],
|
||||
'policy_p' => [ 'required' => false, 'type' => 'string' ],
|
||||
'policy_sp' => [ 'required' => false, 'type' => 'string' ],
|
||||
'policy_np' => [ 'required' => false, 'type' => 'string' ],
|
||||
'policy_pct' => [ 'required' => false, 'type' => 'string' ],
|
||||
'policy_fo' => [ 'required' => false, 'type' => 'string' ],
|
||||
'records' => [ 'required' => true, 'type' => 'array' ]
|
||||
];
|
||||
if (!self::checkRow($data, $fields) || count($data['records']) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static $rfields = [
|
||||
'ip' => [ 'required' => true, 'type' => 'string' ],
|
||||
'rcount' => [ 'required' => true, 'type' => 'integer' ],
|
||||
'disposition' => [ 'required' => true, 'type' => 'string' ],
|
||||
'reason' => [ 'required' => false, 'type' => 'array' ],
|
||||
'dkim_auth' => [ 'required' => false, 'type' => 'array' ],
|
||||
'spf_auth' => [ 'required' => false, 'type' => 'array' ],
|
||||
'dkim_align' => [ 'required' => true, 'type' => 'string' ],
|
||||
'spf_align' => [ 'required' => true, 'type' => 'string' ],
|
||||
'envelope_to' => [ 'required' => false, 'type' => 'string' ],
|
||||
'envelope_from' => [ 'required' => false, 'type' => 'string' ],
|
||||
'header_from' => [ 'required' => false, 'type' => 'string' ]
|
||||
];
|
||||
foreach ($data['records'] as &$rec) {
|
||||
if (gettype($rec) !== 'array' || !self::checkRow($rec, $rfields)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks one row of report data
|
||||
*
|
||||
* @param array $row Data row
|
||||
* @param array $def Row definition
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function checkRow(array &$row, array &$def): bool
|
||||
{
|
||||
foreach ($def as $key => &$dd) {
|
||||
if (isset($row[$key])) {
|
||||
if (gettype($row[$key]) !== $dd['type']) {
|
||||
return false;
|
||||
}
|
||||
} elseif ($dd['required']) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
370
root/opt/dmarc-srg/classes/Report/ReportData.php
Normal file
370
root/opt/dmarc-srg/classes/Report/ReportData.php
Normal file
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
|
||||
* Copyright (C) 2020 Aleksey Andreev (liuch)
|
||||
*
|
||||
* Available at:
|
||||
* https://github.com/liuch/dmarc-srg
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace Liuch\DmarcSrg\Report;
|
||||
|
||||
use Liuch\DmarcSrg\DateTime;
|
||||
use Liuch\DmarcSrg\Exception\RuntimeException;
|
||||
|
||||
class ReportData
|
||||
{
|
||||
public static $rep_data = null;
|
||||
public static $tag_id = null;
|
||||
|
||||
public static function fromXmlFile($fd)
|
||||
{
|
||||
self::$tag_id = '<root>';
|
||||
self::$rep_data = [ 'records' => [] ];
|
||||
|
||||
$parser = xml_parser_create();
|
||||
xml_set_element_handler(
|
||||
$parser,
|
||||
'Liuch\DmarcSrg\Report\ReportData::xmlStartTag',
|
||||
'Liuch\DmarcSrg\Report\ReportData::xmlEndTag'
|
||||
);
|
||||
xml_set_character_data_handler($parser, 'Liuch\DmarcSrg\Report\ReportData::xmlTagData');
|
||||
xml_set_external_entity_ref_handler($parser, function () {
|
||||
throw new RuntimeException('The XML document has an external entity!');
|
||||
});
|
||||
try {
|
||||
while ($file_data = fread($fd, 4096)) {
|
||||
if (!xml_parse($parser, $file_data, feof($fd))) {
|
||||
throw new RuntimeException('XML error!');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
xml_parser_free($parser);
|
||||
unset($parser);
|
||||
}
|
||||
return self::$rep_data;
|
||||
}
|
||||
|
||||
public static function xmlStartTag($parser, $name, $attrs)
|
||||
{
|
||||
self::xmlEnterTag($name);
|
||||
|
||||
switch (self::$tag_id) {
|
||||
case 'rec':
|
||||
self::$rep_data['records'][] = [];
|
||||
break;
|
||||
case 'error_string':
|
||||
if (!isset(self::$rep_data['error_string'])) {
|
||||
self::$rep_data['error_string'] = [];
|
||||
}
|
||||
break;
|
||||
case 'reason':
|
||||
$idx = count(self::$rep_data['records']) - 1;
|
||||
if (!isset(self::$rep_data['records'][$idx]['reason'])) {
|
||||
self::$rep_data['records'][$idx]['reason'] = [];
|
||||
}
|
||||
self::$report_tags['reason']['tmp_data'] = [];
|
||||
break;
|
||||
case 'dkim_auth':
|
||||
$idx = count(self::$rep_data['records']) - 1;
|
||||
if (!isset(self::$rep_data['records'][$idx]['dkim_auth'])) {
|
||||
self::$rep_data['records'][$idx]['dkim_auth'] = [];
|
||||
}
|
||||
self::$report_tags['dkim_auth']['tmp_data'] = [];
|
||||
break;
|
||||
case 'spf_auth':
|
||||
$idx = count(self::$rep_data['records']) - 1;
|
||||
if (!isset(self::$rep_data['records'][$idx]['spf_auth'])) {
|
||||
self::$rep_data['records'][$idx]['spf_auth'] = [];
|
||||
}
|
||||
self::$report_tags['spf_auth']['tmp_data'] = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static function xmlEndTag($parser, $name)
|
||||
{
|
||||
switch (self::$tag_id) {
|
||||
case 'reason':
|
||||
$idx = count(self::$rep_data['records']) - 1;
|
||||
self::$rep_data['records'][$idx]['reason'][] = self::$report_tags['reason']['tmp_data'];
|
||||
unset(self::$report_tags['reason']['tmp_data']);
|
||||
break;
|
||||
case 'dkim_auth':
|
||||
$idx = count(self::$rep_data['records']) - 1;
|
||||
self::$rep_data['records'][$idx]['dkim_auth'][] = self::$report_tags['dkim_auth']['tmp_data'];
|
||||
unset(self::$report_tags['dkim_auth']['tmp_data']);
|
||||
break;
|
||||
case 'spf_auth':
|
||||
$idx = count(self::$rep_data['records']) - 1;
|
||||
self::$rep_data['records'][$idx]['spf_auth'][] = self::$report_tags['spf_auth']['tmp_data'];
|
||||
unset(self::$report_tags['spf_auth']['tmp_data']);
|
||||
break;
|
||||
case 'feedback':
|
||||
// Set the default value if it's necessary and there is no data
|
||||
foreach (self::$report_tags as $tag_id => &$tag_data) {
|
||||
if (array_key_exists('default', $tag_data)) { // not isset() because of null values
|
||||
if (isset($tag_data['header']) && $tag_data['header']) {
|
||||
if (!isset(self::$rep_data[$tag_id])) {
|
||||
self::$rep_data[$tag_id] = $tag_data['default'];
|
||||
}
|
||||
} else {
|
||||
foreach (self::$rep_data['records'] as $idx => &$rec_val) {
|
||||
if (!isset($rec_val[$tag_id])) {
|
||||
$rec_val[$tag_id] = $tag_data['default'];
|
||||
}
|
||||
}
|
||||
unset($rec_val);
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($tag_data);
|
||||
$b_ts = intval(self::$rep_data['begin_time']);
|
||||
$e_ts = intval(self::$rep_data['end_time']);
|
||||
self::$rep_data['begin_time'] = new DateTime('@' . ($b_ts < 0 ? 0 : $b_ts));
|
||||
self::$rep_data['end_time'] = new DateTime('@' . ($e_ts < 0 ? 0 : $e_ts));
|
||||
foreach (self::$rep_data['records'] as &$rec_data) {
|
||||
$rec_data['rcount'] = intval($rec_data['rcount']);
|
||||
}
|
||||
unset($rec_data);
|
||||
break;
|
||||
}
|
||||
self::xmlLeaveTag();
|
||||
}
|
||||
|
||||
public static function xmlTagData($parser, $data)
|
||||
{
|
||||
switch (self::$tag_id) {
|
||||
case 'error_string':
|
||||
if (self::$tag_id === 'error_string') {
|
||||
self::$rep_data['error_string'][] = $data;
|
||||
}
|
||||
break;
|
||||
case 'reason_type':
|
||||
self::$report_tags['reason']['tmp_data']['type'] = $data;
|
||||
break;
|
||||
case 'reason_comment':
|
||||
self::$report_tags['reason']['tmp_data']['comment'] = $data;
|
||||
break;
|
||||
case 'dkim_domain':
|
||||
self::$report_tags['dkim_auth']['tmp_data']['domain'] = $data;
|
||||
break;
|
||||
case 'dkim_selector':
|
||||
self::$report_tags['dkim_auth']['tmp_data']['selector'] = $data;
|
||||
break;
|
||||
case 'dkim_result':
|
||||
self::$report_tags['dkim_auth']['tmp_data']['result'] = $data;
|
||||
break;
|
||||
case 'dkim_human_result':
|
||||
self::$report_tags['dkim_auth']['tmp_data']['human_result'] = $data;
|
||||
break;
|
||||
case 'spf_domain':
|
||||
self::$report_tags['spf_auth']['tmp_data']['domain'] = $data;
|
||||
break;
|
||||
case 'spf_scope':
|
||||
self::$report_tags['spf_auth']['tmp_data']['scope'] = $data;
|
||||
break;
|
||||
case 'spf_result':
|
||||
self::$report_tags['spf_auth']['tmp_data']['result'] = $data;
|
||||
break;
|
||||
default:
|
||||
if (!isset(self::$report_tags[self::$tag_id]['children'])) {
|
||||
if (isset(self::$report_tags[self::$tag_id]['header']) &&
|
||||
self::$report_tags[self::$tag_id]['header']
|
||||
) {
|
||||
if (!isset(self::$rep_data[self::$tag_id])) {
|
||||
self::$rep_data[self::$tag_id] = $data;
|
||||
} else {
|
||||
self::$rep_data[self::$tag_id] .= $data;
|
||||
}
|
||||
} else {
|
||||
$last_idx = count(self::$rep_data['records']) - 1;
|
||||
$last_rec =& self::$rep_data['records'][$last_idx];
|
||||
if (!isset($last_rec[self::$tag_id])) {
|
||||
$last_rec[self::$tag_id] = $data;
|
||||
} else {
|
||||
$last_rec[self::$tag_id] .= $data;
|
||||
}
|
||||
unset($last_rec);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function xmlEnterTag($name)
|
||||
{
|
||||
if (!isset(self::$report_tags[self::$tag_id]['children']) ||
|
||||
!isset(self::$report_tags[self::$tag_id]['children'][$name])
|
||||
) {
|
||||
throw new RuntimeException("Unknown tag: {$name}");
|
||||
}
|
||||
|
||||
self::$tag_id = self::$report_tags[self::$tag_id]['children'][$name];
|
||||
}
|
||||
|
||||
public static function xmlLeaveTag()
|
||||
{
|
||||
self::$tag_id = self::$report_tags[self::$tag_id]['parent'];
|
||||
}
|
||||
|
||||
public static $report_tags = [
|
||||
'<root>' => [
|
||||
'children' => [
|
||||
'FEEDBACK' => 'feedback'
|
||||
]
|
||||
],
|
||||
'feedback' => [
|
||||
'parent' => '<root>',
|
||||
'children' => [
|
||||
'VERSION' => 'ver',
|
||||
'REPORT_METADATA' => 'rmd',
|
||||
'POLICY_PUBLISHED' => 'p_p',
|
||||
'RECORD' => 'rec'
|
||||
]
|
||||
],
|
||||
'ver' => [ 'parent' => 'feedback', 'header' => true, 'default' => null ],
|
||||
'rmd' => [
|
||||
'parent' => 'feedback',
|
||||
'children' => [
|
||||
'ORG_NAME' => 'org',
|
||||
'EMAIL' => 'email',
|
||||
'EXTRA_CONTACT_INFO' => 'extra_contact_info',
|
||||
'REPORT_ID' => 'external_id',
|
||||
'DATE_RANGE' => 'd_range',
|
||||
'ERROR' => 'error_string'
|
||||
]
|
||||
],
|
||||
'p_p' => [
|
||||
'parent' => 'feedback',
|
||||
'children' => [
|
||||
'DOMAIN' => 'domain',
|
||||
'ADKIM' => 'policy_adkim',
|
||||
'ASPF' => 'policy_aspf',
|
||||
'P' => 'policy_p',
|
||||
'SP' => 'policy_sp',
|
||||
'NP' => 'policy_np',
|
||||
'PCT' => 'policy_pct',
|
||||
'FO' => 'policy_fo'
|
||||
]
|
||||
],
|
||||
'rec' => [
|
||||
'parent' => 'feedback',
|
||||
'children' => [
|
||||
'ROW' => 'row',
|
||||
'IDENTIFIERS' => 'ident',
|
||||
'AUTH_RESULTS' => 'au_res'
|
||||
]
|
||||
],
|
||||
'org' => [ 'parent' => 'rmd', 'header' => true ],
|
||||
'email' => [ 'parent' => 'rmd', 'header' => true, 'default' => null ],
|
||||
'extra_contact_info' => [ 'parent' => 'rmd', 'header' => true, 'default' => null ],
|
||||
'external_id' => [ 'parent' => 'rmd', 'header' => true ],
|
||||
'd_range' => [
|
||||
'parent' => 'rmd',
|
||||
'children' => [
|
||||
'BEGIN' => 'begin_time',
|
||||
'END' => 'end_time'
|
||||
]
|
||||
],
|
||||
'error_string' => [ 'parent' => 'rmd', 'header' => true, 'default' => null ],
|
||||
'begin_time' => [ 'parent' => 'd_range', 'header' => true ],
|
||||
'end_time' => [ 'parent' => 'd_range', 'header' => true ],
|
||||
'domain' => [ 'parent' => 'p_p', 'header' => true ],
|
||||
'policy_adkim' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
|
||||
'policy_aspf' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
|
||||
'policy_p' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
|
||||
'policy_sp' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
|
||||
'policy_np' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
|
||||
'policy_pct' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
|
||||
'policy_fo' => [ 'parent' => 'p_p', 'header' => true, 'default' => null ],
|
||||
'row' => [
|
||||
'parent' => 'rec',
|
||||
'children' => [
|
||||
'SOURCE_IP' => 'ip',
|
||||
'COUNT' => 'rcount',
|
||||
'POLICY_EVALUATED' => 'p_e'
|
||||
]
|
||||
],
|
||||
'ident' => [
|
||||
'parent' => 'rec',
|
||||
'children' => [
|
||||
'ENVELOPE_TO' => 'envelope_to',
|
||||
'ENVELOPE_FROM' => 'envelope_from',
|
||||
'HEADER_FROM' => 'header_from'
|
||||
]
|
||||
],
|
||||
'au_res' => [
|
||||
'parent' => 'rec',
|
||||
'children' => [
|
||||
'DKIM' => 'dkim_auth',
|
||||
'SPF' => 'spf_auth'
|
||||
]
|
||||
],
|
||||
'ip' => [ 'parent' => 'row' ],
|
||||
'rcount' => [ 'parent' => 'row' ],
|
||||
'p_e' => [
|
||||
'parent' => 'row',
|
||||
'children' => [
|
||||
'DISPOSITION' => 'disposition',
|
||||
'DKIM' => 'dkim_align',
|
||||
'SPF' => 'spf_align',
|
||||
'REASON' => 'reason'
|
||||
]
|
||||
],
|
||||
'disposition' => [ 'parent' => 'p_e' ],
|
||||
'dkim_align' => [ 'parent' => 'p_e' ],
|
||||
'spf_align' => [ 'parent' => 'p_e' ],
|
||||
'reason' => [
|
||||
'parent' => 'p_e',
|
||||
'default' => null,
|
||||
'children' => [
|
||||
'TYPE' => 'reason_type',
|
||||
'COMMENT' => 'reason_comment'
|
||||
]
|
||||
],
|
||||
'envelope_to' => [ 'parent' => 'ident', 'default' => null ],
|
||||
'envelope_from' => [ 'parent' => 'ident', 'default' => null ],
|
||||
'header_from' => [ 'parent' => 'ident', 'default' => null ],
|
||||
'dkim_auth' => [
|
||||
'parent' => 'au_res',
|
||||
'default' => null,
|
||||
'children' => [
|
||||
'DOMAIN' => 'dkim_domain',
|
||||
'SELECTOR' => 'dkim_selector',
|
||||
'RESULT' => 'dkim_result',
|
||||
'HUMAN_RESULT' => 'dkim_human_result'
|
||||
]
|
||||
],
|
||||
'spf_auth' => [
|
||||
'parent' => 'au_res',
|
||||
'default' => null,
|
||||
'children' => [
|
||||
'DOMAIN' => 'spf_domain',
|
||||
'SCOPE' => 'spf_scope',
|
||||
'RESULT' => 'spf_result'
|
||||
]
|
||||
],
|
||||
'reason_type' => [ 'parent' => 'reason' ],
|
||||
'reason_comment' => [ 'parent' => 'reason' ],
|
||||
'dkim_domain' => [ 'parent' => 'dkim_auth' ],
|
||||
'dkim_selector' => [ 'parent' => 'dkim_auth' ],
|
||||
'dkim_result' => [ 'parent' => 'dkim_auth' ],
|
||||
'dkim_human_result' => [ 'parent' => 'dkim_auth' ],
|
||||
'spf_domain' => [ 'parent' => 'spf_auth' ],
|
||||
'spf_scope' => [ 'parent' => 'spf_auth' ],
|
||||
'spf_result' => [ 'parent' => 'spf_auth' ]
|
||||
];
|
||||
}
|
242
root/opt/dmarc-srg/classes/Report/ReportFetcher.php
Normal file
242
root/opt/dmarc-srg/classes/Report/ReportFetcher.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
|
||||
* Copyright (C) 2020 Aleksey Andreev (liuch)
|
||||
*
|
||||
* Available at:
|
||||
* https://github.com/liuch/dmarc-srg
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* =========================
|
||||
*
|
||||
* This file contains the class ReportFetcher
|
||||
*
|
||||
* @category API
|
||||
* @package DmarcSrg
|
||||
* @author Aleksey Andreev (liuch)
|
||||
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
|
||||
*/
|
||||
|
||||
namespace Liuch\DmarcSrg\Report;
|
||||
|
||||
use Liuch\DmarcSrg\Core;
|
||||
use Liuch\DmarcSrg\ErrorHandler;
|
||||
use Liuch\DmarcSrg\Report\Report;
|
||||
use Liuch\DmarcSrg\Sources\Source;
|
||||
use Liuch\DmarcSrg\ReportLog\ReportLogItem;
|
||||
use Liuch\DmarcSrg\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* This class is designed to fetch report files from report sources and store them to the database.
|
||||
*/
|
||||
class ReportFetcher
|
||||
{
|
||||
private $source = null;
|
||||
|
||||
/**
|
||||
* It's the constructor of the class.
|
||||
*
|
||||
* @param Source $sou Source for fetching report files.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($sou)
|
||||
{
|
||||
$this->source = $sou;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves report files from the source and stores them in the database
|
||||
* taking into account the limits from the configuration file.
|
||||
*
|
||||
* @return array Array of results.
|
||||
*/
|
||||
public function fetch(): array
|
||||
{
|
||||
try {
|
||||
$this->source->rewind();
|
||||
} catch (RuntimeException $e) {
|
||||
return [[ 'source_error' => $e->getMessage() ]];
|
||||
}
|
||||
|
||||
$core = Core::instance();
|
||||
$limit = 0;
|
||||
$stype = $this->source->type();
|
||||
switch ($stype) {
|
||||
case Source::SOURCE_MAILBOX:
|
||||
$s_act = $core->config('fetcher/mailboxes/when_done', '');
|
||||
$f_act = $core->config('fetcher/mailboxes/when_failed', '');
|
||||
$limit = $core->config('fetcher/mailboxes/messages_maximum', 0);
|
||||
break;
|
||||
case Source::SOURCE_DIRECTORY:
|
||||
$s_act = $core->config('fetcher/directories/when_done', '');
|
||||
$f_act = $core->config('fetcher/directories/when_failed', '');
|
||||
$limit = $core->config('fetcher/directories/files_maximum', 0);
|
||||
break;
|
||||
}
|
||||
$limit = intval($limit);
|
||||
if ($stype === Source::SOURCE_MAILBOX || $stype === Source::SOURCE_DIRECTORY) {
|
||||
$this->source->setParams([
|
||||
'when_done' => $s_act,
|
||||
'when_failed' => $f_act
|
||||
]);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
while ($this->source->valid()) {
|
||||
$result = null;
|
||||
$fname = null;
|
||||
$report = null;
|
||||
$success = false;
|
||||
$err_msg = null;
|
||||
|
||||
// Extracting and saving reports
|
||||
try {
|
||||
$rfile = $this->source->current();
|
||||
$fname = $rfile->filename();
|
||||
$report = Report::fromXmlFile($rfile->datastream());
|
||||
$result = $report->save($fname);
|
||||
$success = true;
|
||||
} catch (RuntimeException $e) {
|
||||
$err_msg = $e->getMessage();
|
||||
$result = ErrorHandler::exceptionResult($e);
|
||||
}
|
||||
unset($rfile);
|
||||
|
||||
// Post processing
|
||||
try {
|
||||
if ($success) {
|
||||
$this->source->accepted();
|
||||
} else {
|
||||
$this->source->rejected();
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
$err_msg = $e->getMessage();
|
||||
$result['post_processing_message'] = $err_msg;
|
||||
}
|
||||
|
||||
// Adding a record to the log.
|
||||
if (!$err_msg) {
|
||||
$log = ReportLogItem::success($stype, $report, $fname, null)->save();
|
||||
} else {
|
||||
$log = ReportLogItem::failed($stype, $report, $fname, $err_msg)->save();
|
||||
if ($this->source->type() === Source::SOURCE_MAILBOX) {
|
||||
$msg = $this->source->mailMessage();
|
||||
$ov = $msg->overview();
|
||||
if ($ov) {
|
||||
if (property_exists($ov, 'from')) {
|
||||
$result['emailed_from'] = $ov->from;
|
||||
}
|
||||
if (property_exists($ov, 'date')) {
|
||||
$result['emailed_date'] = $ov->date;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($report) {
|
||||
$rd = $report->get();
|
||||
if (isset($rd['external_id'])) {
|
||||
$result['report_id'] = $rd['external_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($report);
|
||||
|
||||
// Adding result to the results array.
|
||||
$results[] = $result;
|
||||
|
||||
// Checking the fetcher limits
|
||||
if ($limit > 0) {
|
||||
if (--$limit === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->source->next();
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the final result based on the results of loading individual report files.
|
||||
*
|
||||
* @param array $results Array with results of loading report files.
|
||||
*
|
||||
* @return array Array of the final result to be sent to the client.
|
||||
*/
|
||||
public static function makeSummaryResult(array $results): array
|
||||
{
|
||||
$reps = [];
|
||||
$others = [];
|
||||
$r_count = 0;
|
||||
$loaded = 0;
|
||||
foreach ($results as &$r) {
|
||||
if (isset($r['source_error'])) {
|
||||
$others[] = $r['source_error'];
|
||||
} else {
|
||||
$reps[] = $r;
|
||||
++$r_count;
|
||||
if (!isset($r['error_code']) || $r['error_code'] === 0) {
|
||||
++$loaded;
|
||||
}
|
||||
if (isset($r['post_processing_message'])) {
|
||||
$others[] = $r['post_processing_message'];
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($r);
|
||||
|
||||
$result = null;
|
||||
$o_count = count($others);
|
||||
if ($r_count + $o_count === 1) {
|
||||
if ($r_count === 1) {
|
||||
$result = $reps[0];
|
||||
} else {
|
||||
$result = [
|
||||
'error_code' => -1,
|
||||
'message' => $others[0]
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$err_code = null;
|
||||
$message = null;
|
||||
if ($loaded === $r_count) {
|
||||
$err_code = 0;
|
||||
if ($r_count > 0) {
|
||||
$message = strval($r_count) . ' report files have been loaded successfully';
|
||||
} elseif ($o_count === 0) {
|
||||
$message = 'There are no report files to load';
|
||||
} else {
|
||||
$err_code = -1;
|
||||
}
|
||||
} else {
|
||||
$err_code = -1;
|
||||
if ($loaded > 0) {
|
||||
$message = "Only {$loaded} of the {$r_count} report files have been loaded";
|
||||
} else {
|
||||
$message = "None of the {$r_count} report files has been loaded";
|
||||
}
|
||||
}
|
||||
$result['error_code'] = $err_code;
|
||||
$result['message'] = $message;
|
||||
if ($r_count > 0) {
|
||||
$result['results'] = $reps;
|
||||
}
|
||||
if ($o_count > 0) {
|
||||
$result['other_errors'] = $others;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
212
root/opt/dmarc-srg/classes/Report/ReportList.php
Normal file
212
root/opt/dmarc-srg/classes/Report/ReportList.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
|
||||
* Copyright (C) 2020 Aleksey Andreev (liuch)
|
||||
*
|
||||
* Available at:
|
||||
* https://github.com/liuch/dmarc-srg
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* =========================
|
||||
*
|
||||
* This file contains ReportList class
|
||||
*
|
||||
* @category API
|
||||
* @package DmarcSrg
|
||||
* @author Aleksey Andreev (liuch)
|
||||
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
|
||||
*/
|
||||
|
||||
namespace Liuch\DmarcSrg\Report;
|
||||
|
||||
use Liuch\DmarcSrg\Core;
|
||||
|
||||
/**
|
||||
* It's the main class for working with the incoming reports, such as:
|
||||
* - getting a list of reports
|
||||
* - deleting several reports at once
|
||||
* - getting the number of reports stored in the database
|
||||
*/
|
||||
class ReportList
|
||||
{
|
||||
public const ORDER_NONE = 0;
|
||||
public const ORDER_BEGIN_TIME = 1;
|
||||
public const ORDER_ASCENT = 2;
|
||||
public const ORDER_DESCENT = 3;
|
||||
|
||||
private $db = null;
|
||||
private $limit = 0;
|
||||
private $filter = [];
|
||||
private $order = null;
|
||||
|
||||
/**
|
||||
* The constructor
|
||||
*
|
||||
* @param DatabaseController $db The database controller
|
||||
*/
|
||||
public function __construct($db = null)
|
||||
{
|
||||
$this->db = $db ?? Core::instance()->database();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of reports with specified parameters from position $pos
|
||||
*
|
||||
* This method returns a list of reports that depends on the filter and order.
|
||||
* The filter, order, and limit for the list can be set using the setFilter, setOrder and setMaxCount methods.
|
||||
*
|
||||
* @param int $pos The starting position from which the list will be returned
|
||||
*
|
||||
* @return array An array with keys `reports` and `more`.
|
||||
* `reports` is an array of incoming reports which contains maximum 25 records by default.
|
||||
* Another value of the number of records can be specified by calling
|
||||
* the method setMaxCount.
|
||||
* `more` is true if there are more records in the database, false otherwise.
|
||||
*/
|
||||
public function getList(int $pos): array
|
||||
{
|
||||
$order = [
|
||||
'field' => 'begin_time',
|
||||
'direction' => ($this->order ?? self::ORDER_DESCENT) === self::ORDER_ASCENT ? 'ascent' : 'descent'
|
||||
];
|
||||
|
||||
$max_rec = $this->limit > 0 ? $this->limit : 25;
|
||||
$limit = [
|
||||
'offset' => $pos,
|
||||
'count' => $max_rec + 1
|
||||
];
|
||||
|
||||
$list = $this->db->getMapper('report')->list($this->filter, $order, $limit);
|
||||
if (count($list) > $max_rec) {
|
||||
$more = true;
|
||||
unset($list[$max_rec]);
|
||||
} else {
|
||||
$more = false;
|
||||
}
|
||||
return [
|
||||
'reports' => $list,
|
||||
'more' => $more
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sort order for the list and for deleting several reports at once
|
||||
*
|
||||
* @param int $field The field to sort by. Currently only ORDER_BEGIN_TIME is available.
|
||||
* @param int $direction The sorting direction. ORDER_ASCENT or ORDER_DESCENT must be used here.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setOrder(int $field, int $direction): void
|
||||
{
|
||||
$this->order = null;
|
||||
if ($field > self::ORDER_NONE && $field < self::ORDER_ASCENT) {
|
||||
if ($direction !== self::ORDER_ASCENT) {
|
||||
$direction = self::ORDER_DESCENT;
|
||||
}
|
||||
$this->order = [
|
||||
'field' => $field,
|
||||
'direction' => $direction
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets maximum numbers of records in the list and for deleting reports
|
||||
*
|
||||
* @param int $num Maximum number of records in the list
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setMaxCount(int $num): void
|
||||
{
|
||||
if ($num > 0) {
|
||||
$this->limit = $num;
|
||||
} else {
|
||||
$this->limit = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets filter values for the list and for deleting reports
|
||||
*
|
||||
* @param array $filter Key-value array:
|
||||
* 'before_time' => DateTime, timestamp
|
||||
* 'dkim' => string, 'fail' or 'pass'
|
||||
* 'domain' => string or instance of Domain class
|
||||
* 'month' => string, yyyy-mm format
|
||||
* 'organization' => string
|
||||
* 'spf' => string, 'fail' or 'pass'
|
||||
* 'status' => string, 'read' or 'unread'
|
||||
* Note! 'dkim' and 'spf' do not affect the delete and count methods
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setFilter(array $filter): void
|
||||
{
|
||||
$this->filter = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of reports in the database
|
||||
*
|
||||
* It returns the number of reports in the database.
|
||||
* The limit and some filter items (`dkim`, `spf`) do not affect this.
|
||||
*
|
||||
* @return int The number of reports in the database
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
$limit = [ 'offset' => 0, 'count' => $this->limit ];
|
||||
return $this->db->getMapper('report')->count($this->filter, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes reports from the database
|
||||
*
|
||||
* It deletes repors form the database. The filter items `dkim` and `spf` do not affect this.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete(): void
|
||||
{
|
||||
$order = [
|
||||
'field' => 'begin_time',
|
||||
'direction' => ($this->order ?? self::ORDER_DESCENT) === self::ORDER_ASCENT ? 'ascent' : 'descent'
|
||||
];
|
||||
$limit = [ 'offset' => 0, 'count' => $this->limit ];
|
||||
$this->db->getMapper('report')->delete($this->filter, $order, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of values for each filter item except for `before_time`
|
||||
*
|
||||
* @return array An key-value array, where the key is the filter item name
|
||||
* and the value is an array of possible values for the item
|
||||
*/
|
||||
public function getFilterList(): array
|
||||
{
|
||||
$domainMapper = $this->db->getMapper('domain');
|
||||
$reportMapper = $this->db->getMapper('report');
|
||||
return [
|
||||
'domain' => $domainMapper->names(),
|
||||
'month' => $reportMapper->months(),
|
||||
'organization' => $reportMapper->organizations(),
|
||||
'dkim' => [ 'pass', 'fail' ],
|
||||
'spf' => [ 'pass', 'fail' ],
|
||||
'status' => [ 'read', 'unread' ]
|
||||
];
|
||||
}
|
||||
}
|
400
root/opt/dmarc-srg/classes/Report/SummaryReport.php
Normal file
400
root/opt/dmarc-srg/classes/Report/SummaryReport.php
Normal file
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
|
||||
* Copyright (C) 2020 Aleksey Andreev (liuch)
|
||||
*
|
||||
* Available at:
|
||||
* https://github.com/liuch/dmarc-srg
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, either version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* =========================
|
||||
*
|
||||
* This file contains SummaryReport class
|
||||
*
|
||||
* @category API
|
||||
* @package DmarcSrg
|
||||
* @author Aleksey Andreev (liuch)
|
||||
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU/GPLv3
|
||||
*/
|
||||
|
||||
namespace Liuch\DmarcSrg\Report;
|
||||
|
||||
use Liuch\DmarcSrg\Statistics;
|
||||
use Liuch\DmarcSrg\Exception\SoftException;
|
||||
use Liuch\DmarcSrg\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* This class is for generating summary data for the specified period and domain
|
||||
*/
|
||||
class SummaryReport
|
||||
{
|
||||
private const LAST_WEEK = -1;
|
||||
private const LAST_MONTH = -2;
|
||||
|
||||
private $period = 0;
|
||||
private $domain = null;
|
||||
private $stat = null;
|
||||
private $subject = '';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $period The period for which the report is created
|
||||
* Must me one of the following values: `lastweek`, `lastmonth`, and `lastndays:N`
|
||||
* where N is the number of days the report is created for
|
||||
*/
|
||||
public function __construct(string $period)
|
||||
{
|
||||
switch ($period) {
|
||||
case 'lastweek':
|
||||
$period = self::LAST_WEEK;
|
||||
$subject = ' weekly';
|
||||
break;
|
||||
case 'lastmonth':
|
||||
$period = self::LAST_MONTH;
|
||||
$subject = ' monthly';
|
||||
break;
|
||||
default:
|
||||
$ndays = 0;
|
||||
$av = explode(':', $period);
|
||||
if (count($av) === 2 && $av[0] === 'lastndays') {
|
||||
$ndays = intval($av[1]);
|
||||
if ($ndays <= 0) {
|
||||
throw new SoftException('The parameter "days" has an incorrect value');
|
||||
}
|
||||
$subject = sprintf(' %d day%s', $ndays, ($ndays > 1 ? 's' : ''));
|
||||
}
|
||||
$period = $ndays;
|
||||
break;
|
||||
}
|
||||
if (empty($subject)) {
|
||||
throw new SoftException('The parameter "period" has an incorrect value');
|
||||
}
|
||||
$this->period = $period;
|
||||
$this->subject = "DMARC{$subject} digest";
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a domain to the report
|
||||
*
|
||||
* @param Domain $domain The domain for which the report is created
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setDomain($domain)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
$this->stat = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the report data as an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$this->ensureData();
|
||||
|
||||
$res = [];
|
||||
$stat = $this->stat;
|
||||
$range = $stat->range();
|
||||
$res['date_range'] = [ 'begin' => $range[0], 'end' => $range[1] ];
|
||||
$res['summary'] = $stat->summary();
|
||||
$res['sources'] = $stat->ips();
|
||||
$res['organizations'] = $stat->organizations();
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subject string. It is used in email messages.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function subject(): string
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the report as an array of text strings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function text(): array
|
||||
{
|
||||
$rdata = $this->reportData();
|
||||
|
||||
$res = [];
|
||||
$res[] = '# Domain: ' . $this->domain->fqdn();
|
||||
|
||||
$res[] = ' Range: ' . $rdata['range'];
|
||||
$res[] = '';
|
||||
|
||||
$res[] = '## Summary';
|
||||
$total = $rdata['summary']['total'];
|
||||
$res[] = sprintf(' Total: %d', $total);
|
||||
$res[] = sprintf(' DKIM or SPF aligned: %s', self::num2percent($rdata['summary']['aligned'], $total));
|
||||
$res[] = sprintf(' Not aligned: %s', self::num2percent($rdata['summary']['n_aligned'], $total));
|
||||
$res[] = sprintf(' Organizations: %d', $rdata['summary']['organizations']);
|
||||
$res[] = '';
|
||||
|
||||
if (count($rdata['sources']) > 0) {
|
||||
$res[] = '## Sources';
|
||||
$res[] = sprintf(
|
||||
' %-25s %13s %13s %13s',
|
||||
'',
|
||||
'Total',
|
||||
'SPF aligned',
|
||||
'DKIM aligned'
|
||||
);
|
||||
foreach ($rdata['sources'] as &$it) {
|
||||
$total = $it['emails'];
|
||||
$spf_a = $it['spf_aligned'];
|
||||
$dkim_a = $it['dkim_aligned'];
|
||||
$spf_str = self::num2percent($spf_a, $total);
|
||||
$dkim_str = self::num2percent($dkim_a, $total);
|
||||
$res[] = sprintf(
|
||||
' %-25s %13d %13s %13s',
|
||||
$it['ip'],
|
||||
$total,
|
||||
$spf_str,
|
||||
$dkim_str
|
||||
);
|
||||
}
|
||||
unset($it);
|
||||
$res[] = '';
|
||||
}
|
||||
|
||||
if (count($rdata['organizations']) > 0) {
|
||||
$res[] = '## Organizations';
|
||||
|
||||
$org_len = 15;
|
||||
foreach ($rdata['organizations'] as &$org) {
|
||||
$org_len = max($org_len, mb_strlen($org['name']));
|
||||
}
|
||||
unset($org);
|
||||
|
||||
$org_len = min($org_len, 55);
|
||||
$res[] = sprintf(" %-{$org_len}s %8s %8s", '', 'emails', 'reports');
|
||||
$frm_str = " %-{$org_len}s %8d %8d";
|
||||
foreach ($rdata['organizations'] as &$org) {
|
||||
$res[] = sprintf(
|
||||
$frm_str,
|
||||
trim($org['name']),
|
||||
$org['emails'],
|
||||
$org['reports']
|
||||
);
|
||||
}
|
||||
unset($org);
|
||||
$res[] = '';
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the report as an array of html strings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function html(): array
|
||||
{
|
||||
$h2a = 'style="margin:15px 0 5px;"';
|
||||
$t2a = 'style="border-collapse:collapse;border-spacing:0;"';
|
||||
$c1a = 'style="font-style:italic;"';
|
||||
$d1s = 'padding-left:1em;';
|
||||
$d2s = 'min-width:4em;';
|
||||
$d3s = 'border:1px solid #888;';
|
||||
$d4s = 'text-align:right;';
|
||||
$d5s = 'padding:.3em;';
|
||||
|
||||
$add_red = function (int $num) {
|
||||
return $num > 0 ? 'color:#f00;' : '';
|
||||
};
|
||||
$add_green = function (int $num) {
|
||||
return $num > 0 ? 'color:#080;' : '';
|
||||
};
|
||||
|
||||
$rdata = $this->reportData();
|
||||
$res = [];
|
||||
$res[] = "<h2 {$h2a}>Domain: " . htmlspecialchars($this->domain->fqdn()) . '</h2>';
|
||||
$res[] = '<p style="margin:0;">Range: ' . htmlspecialchars($rdata['range']) . '</p>';
|
||||
|
||||
$res[] = "<h3 {$h2a}>Summary</h3>";
|
||||
$res[] = '<table>';
|
||||
$total = $rdata['summary']['total'];
|
||||
$a_cnt = $rdata['summary']['aligned'];
|
||||
$n_cnt = $rdata['summary']['n_aligned'];
|
||||
$res[] = " <tr><td>Total: </td><td style=\"{$d1s}\">" . $total . '</td></tr>';
|
||||
$color = $add_green($a_cnt);
|
||||
$res[] = " <tr><td>DKIM or SPF aligned: </td><td style=\"{$d1s}{$color}\">{$a_cnt}</td></tr>";
|
||||
$color = $add_red($n_cnt);
|
||||
$res[] = " <tr><td>Not aligned: </td><td style=\"{$d1s}{$color}\">{$n_cnt}</td></tr>";
|
||||
$res[] = " <tr><td>Organizations: </td><td style=\"{$d1s}\">" .
|
||||
$rdata['summary']['organizations'] .
|
||||
'</td></tr>';
|
||||
$res[] = '</table>';
|
||||
|
||||
$rs2 = 'rowspan="2"';
|
||||
$cs3 = 'colspan="3"';
|
||||
$s_cnt = count($rdata['sources']);
|
||||
if ($s_cnt > 0) {
|
||||
$res[] = "<h3 {$h2a}>Sources</h3>";
|
||||
$res[] = "<table {$t2a}>";
|
||||
$res[] = " <caption {$c1a}>Total records: {$s_cnt}</caption>";
|
||||
$res[] = ' <thead>';
|
||||
$style = "style=\"{$d3s}{$d5s}\"";
|
||||
$res[] = " <tr><th {$rs2} {$style}>IP address</th><th {$rs2} {$style}>Email volume</th>" .
|
||||
"<th {$cs3} {$style}>SPF</th><th {$cs3} {$style}>DKIM</th></tr>";
|
||||
$style = "style=\"{$d2s}{$d3s}{$d5s}\"";
|
||||
$res[] = " <tr><th {$style}>pass</th><th {$style}>fail</th><th {$style}>rate</th>" .
|
||||
"<th {$style}>pass</th><th {$style}>fail</th><th {$style}>rate</th></tr>";
|
||||
$res[] = ' </thead>';
|
||||
$res[] = ' <tbody>';
|
||||
foreach ($rdata['sources'] as &$row) {
|
||||
$ip = htmlspecialchars($row['ip']);
|
||||
$total = $row['emails'];
|
||||
$spf_a = $row['spf_aligned'];
|
||||
$spf_n = $total - $spf_a;
|
||||
$spf_p = sprintf('%.0f%%', $spf_a / $total * 100);
|
||||
$dkim_a = $row['dkim_aligned'];
|
||||
$dkim_n = $total - $dkim_a;
|
||||
$dkim_p = sprintf('%.0f%%', $dkim_a / $total * 100);
|
||||
$style = "style=\"{$d3s}{$d5s}";
|
||||
|
||||
$row_str = " <tr><td {$style}\">{$ip}</td><td {$style}{$d4s}\">{$total}</td>";
|
||||
$row_str .= "<td {$style}{$d4s}{$add_green($spf_a)}\">{$spf_a}</td>";
|
||||
$row_str .= "<td {$style}{$d4s}{$add_red($spf_n)}\">{$spf_n}</td>";
|
||||
$row_str .= "<td {$style}{$d4s}\">{$spf_p}</td>";
|
||||
$row_str .= "<td {$style}{$d4s}{$add_green($dkim_a)}\">{$dkim_a}</td>";
|
||||
$row_str .= "<td {$style}{$d4s}{$add_red($dkim_n)}\">{$dkim_n}</td>";
|
||||
$row_str .= "<td {$style}{$d4s}\">{$dkim_p}</td>";
|
||||
$res[] = $row_str . '</tr>';
|
||||
}
|
||||
unset($row);
|
||||
$res[] = ' </tbody>';
|
||||
$res[] = '</table>';
|
||||
}
|
||||
|
||||
$o_cnt = count($rdata['organizations']);
|
||||
if ($o_cnt) {
|
||||
$res[] = "<h3 {$h2a}>Organizations</h3>";
|
||||
$res[] = "<table {$t2a}>";
|
||||
$res[] = " <caption {$c1a}>Total records: {$o_cnt}</caption>";
|
||||
$res[] = ' <thead>';
|
||||
$style = "style=\"{$d3s}{$d5s}\"";
|
||||
$res[] = " <tr><th {$style}>Name</th><th {$style}>Emails</th><th {$style}>Reports</th></tr>";
|
||||
$res[] = ' </thead>';
|
||||
$res[] = ' <tbody>';
|
||||
foreach ($rdata['organizations'] as &$row) {
|
||||
$name = htmlspecialchars($row['name']);
|
||||
$style2 = "style=\"{$d3s}{$d4s}{$d5s}\"";
|
||||
$res[] = " <tr><td {$style}>{$name}</td>" .
|
||||
"<td {$style2}>{$row['emails']}</td>" .
|
||||
"<td {$style2}>{$row['reports']}</td></tr>";
|
||||
}
|
||||
unset($row);
|
||||
$res[] = ' </tbody>';
|
||||
$res[] = '</table>';
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the percentage with the original number. If $per is 0 then '0' is returned.
|
||||
*
|
||||
* @param int $per Value
|
||||
* @param int $cent Divisor for percentage calculation
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function num2percent(int $per, int $cent): string
|
||||
{
|
||||
if (!$per) {
|
||||
return '0';
|
||||
}
|
||||
return sprintf('%.0f%%(%d)', $per / $cent * 100, $per);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the report if it has not already been done
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function ensureData(): void
|
||||
{
|
||||
if (!$this->domain) {
|
||||
throw new LogicException('No one domain was specified');
|
||||
}
|
||||
|
||||
if (!$this->stat) {
|
||||
switch ($this->period) {
|
||||
case self::LAST_WEEK:
|
||||
$this->stat = Statistics::lastWeek($this->domain);
|
||||
break;
|
||||
case self::LAST_MONTH:
|
||||
$this->stat = Statistics::lastMonth($this->domain);
|
||||
break;
|
||||
default:
|
||||
$this->stat = Statistics::lastNDays($this->domain, $this->period);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns prepared data for the report
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function reportData(): array
|
||||
{
|
||||
$this->ensureData();
|
||||
|
||||
$stat = $this->stat;
|
||||
$rdata = [];
|
||||
$range = $stat->range();
|
||||
$cyear = (new \Datetime())->format('Y');
|
||||
$dform = ($range[0]->format('Y') !== $cyear || $range[1]->format('Y') !== $cyear) ? 'M d Y' : 'M d';
|
||||
$rdata['range'] = $range[0]->format($dform) . ' - ' . $range[1]->format($dform);
|
||||
|
||||
$summ = $stat->summary();
|
||||
$total = $summ['emails']['total'];
|
||||
$aligned = $summ['emails']['dkim_spf_aligned'] +
|
||||
$summ['emails']['dkim_aligned'] +
|
||||
$summ['emails']['spf_aligned'];
|
||||
$n_aligned = $total - $aligned;
|
||||
$rdata['summary'] = [
|
||||
'total' => $total,
|
||||
'organizations' => $summ['organizations']
|
||||
];
|
||||
if ($total > 0) {
|
||||
$rdata['summary']['aligned'] = $aligned;
|
||||
$rdata['summary']['n_aligned'] = $n_aligned;
|
||||
} else {
|
||||
$rdata['summary']['aligned'] = $aligned;
|
||||
$rdata['summary']['n_aligned'] = $aligned;
|
||||
}
|
||||
|
||||
$rdata['sources'] = $stat->ips();
|
||||
|
||||
$rdata['organizations'] = $stat->organizations();
|
||||
|
||||
return $rdata;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user