.
*
* =========================
*
* 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[] = "
Domain: " . htmlspecialchars($this->domain->fqdn()) . '
';
$res[] = 'Range: ' . htmlspecialchars($rdata['range']) . '
';
$res[] = "Summary
";
$res[] = '';
$total = $rdata['summary']['total'];
$a_cnt = $rdata['summary']['aligned'];
$n_cnt = $rdata['summary']['n_aligned'];
$res[] = " Total: | " . $total . ' |
';
$color = $add_green($a_cnt);
$res[] = " DKIM or SPF aligned: | {$a_cnt} |
";
$color = $add_red($n_cnt);
$res[] = " Not aligned: | {$n_cnt} |
";
$res[] = " Organizations: | " .
$rdata['summary']['organizations'] .
' |
';
$res[] = '
';
$rs2 = 'rowspan="2"';
$cs3 = 'colspan="3"';
$s_cnt = count($rdata['sources']);
if ($s_cnt > 0) {
$res[] = "Sources
";
$res[] = "";
$res[] = " Total records: {$s_cnt}";
$res[] = ' ';
$style = "style=\"{$d3s}{$d5s}\"";
$res[] = " IP address | Email volume | " .
"SPF | DKIM |
";
$style = "style=\"{$d2s}{$d3s}{$d5s}\"";
$res[] = " pass | fail | rate | " .
"pass | fail | rate |
";
$res[] = ' ';
$res[] = ' ';
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 = " {$ip} | {$total} | ";
$row_str .= "{$spf_a} | ";
$row_str .= "{$spf_n} | ";
$row_str .= "{$spf_p} | ";
$row_str .= "{$dkim_a} | ";
$row_str .= "{$dkim_n} | ";
$row_str .= "{$dkim_p} | ";
$res[] = $row_str . '
';
}
unset($row);
$res[] = ' ';
$res[] = '
';
}
$o_cnt = count($rdata['organizations']);
if ($o_cnt) {
$res[] = "Organizations
";
$res[] = "";
$res[] = " Total records: {$o_cnt}";
$res[] = ' ';
$style = "style=\"{$d3s}{$d5s}\"";
$res[] = " Name | Emails | Reports |
";
$res[] = ' ';
$res[] = ' ';
foreach ($rdata['organizations'] as &$row) {
$name = htmlspecialchars($row['name']);
$style2 = "style=\"{$d3s}{$d4s}{$d5s}\"";
$res[] = " {$name} | " .
"{$row['emails']} | " .
"{$row['reports']} |
";
}
unset($row);
$res[] = ' ';
$res[] = '
';
}
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;
}
}