. * * ========================= * * 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[] = " '; $color = $add_green($a_cnt); $res[] = " "; $color = $add_red($n_cnt); $res[] = " "; $res[] = " '; $res[] = '
Total: " . $total . '
DKIM or SPF aligned: {$a_cnt}
Not aligned: {$n_cnt}
Organizations: " . $rdata['summary']['organizations'] . '
'; $rs2 = 'rowspan="2"'; $cs3 = 'colspan="3"'; $s_cnt = count($rdata['sources']); if ($s_cnt > 0) { $res[] = "

Sources

"; $res[] = ""; $res[] = " "; $res[] = ' '; $style = "style=\"{$d3s}{$d5s}\""; $res[] = " " . ""; $style = "style=\"{$d2s}{$d3s}{$d5s}\""; $res[] = " " . ""; $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 = " "; $row_str .= ""; $row_str .= ""; $row_str .= ""; $row_str .= ""; $row_str .= ""; $row_str .= ""; $res[] = $row_str . ''; } unset($row); $res[] = ' '; $res[] = '
Total records: {$s_cnt}
IP addressEmail volumeSPFDKIM
passfailratepassfailrate
{$ip}{$total}{$spf_a}{$spf_n}{$spf_p}{$dkim_a}{$dkim_n}{$dkim_p}
'; } $o_cnt = count($rdata['organizations']); if ($o_cnt) { $res[] = "

Organizations

"; $res[] = ""; $res[] = " "; $res[] = ' '; $style = "style=\"{$d3s}{$d5s}\""; $res[] = " "; $res[] = ' '; $res[] = ' '; foreach ($rdata['organizations'] as &$row) { $name = htmlspecialchars($row['name']); $style2 = "style=\"{$d3s}{$d4s}{$d5s}\""; $res[] = " " . "" . ""; } unset($row); $res[] = ' '; $res[] = '
Total records: {$o_cnt}
NameEmailsReports
{$name}{$row['emails']}{$row['reports']}
'; } 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; } }