'Gmail', 'gmx_webde' => 'GMX / WEB.DE', 'tonline' => 'T-Online', 'outlook' => 'Outlook / Hotmail', 'sonstige' => 'Sonstige'];
public static function registrieren(): void
{
add_action('admin_menu', [self::class, 'menue'], 11);
add_action('admin_post_sendmails_csv', [self::class, 'csv']);
add_action('admin_post_sendmails_sperrliste', [self::class, 'sperrlisteAendern']);
}
public static function menue(): void
{
add_submenu_page(Seite::SLUG, __('Overview', 'purehost-send-mails'), __('Overview', 'purehost-send-mails'), 'manage_options', Seite::SLUG, [Seite::class, 'anzeigen']);
add_submenu_page(Seite::SLUG, __('Statistics', 'purehost-send-mails'), __('Statistics', 'purehost-send-mails'), 'manage_options', self::SLUG, [self::class, 'anzeigen']);
add_submenu_page(Seite::SLUG, __('Email log', 'purehost-send-mails'), __('Email log', 'purehost-send-mails'), 'manage_options', Mailprotokoll::SLUG, [Mailprotokoll::class, 'anzeigen']);
add_submenu_page(Seite::SLUG, __('Settings', 'purehost-send-mails'), __('Settings', 'purehost-send-mails'), 'manage_options', Seite::SLUG_EINSTELLUNGEN, [Seite::class, 'einstellungenSeite']);
}
public static function url(string $zeitraum = ''): string
{
return admin_url('admin.php?page=' . self::SLUG . ($zeitraum !== '' ? '&zeitraum=' . rawurlencode($zeitraum) : ''));
}
private static function zeitraum(): string
{
// Nur Auswahl der Ansicht (feste Liste), keine Aenderung: ohne Nonce.
$z = sanitize_key(wp_unslash($_GET['zeitraum'] ?? '30t')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return in_array($z, Api::ZEITRAEUME, true) ? $z : '30t';
}
private static function tage(string $zeitraum, array $st): int
{
return max(1, (int) ($st['zeitraum']['tage'] ?? match ($zeitraum) { '7t' => 7, default => 30 }));
}
public static function anzeigen(): void
{
ob_start();
try {
self::anzeigenInhalt();
} finally {
echo Ansicht::satzweise((string) ob_get_clean()); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Seite aus maskierten Teilen, nur
zwischen Saetzen ergaenzt
}
}
private static function anzeigenInhalt(): void
{
if (!current_user_can('manage_options')) {
return;
}
$zeitraum = self::zeitraum();
$neu = !empty($_GET['neu']) && check_admin_referer('sendmails_neu');
echo '
';
Seite::kopfFuer('statistik');
self::kopf($zeitraum);
$st = Einstellungen::apiSchluessel() !== '' ? Api::statistik($neu, $zeitraum) : null;
$k = Einstellungen::apiSchluessel() !== '' ? Api::kontingent($neu) : null;
if ($st === null) {
echo '
';
} elseif (!$st['ok']) {
echo '
' . esc_html($st['fehler']) . '
';
} else {
$d = $st['daten'];
if (!empty($d['zeitraum']['von']) && !empty($d['zeitraum']['bis'])) {
echo '
' . esc_html(Ansicht::tagLang((string) $d['zeitraum']['von']) . ' – ' . Ansicht::tagLang((string) $d['zeitraum']['bis'])) . '
';
}
self::kennzahlen($d);
echo '
';
self::verlauf($d);
self::quoten($d);
self::kontingent($d, $k && $k['ok'] ? $k['daten'] : []);
self::senderate($k && $k['ok'] ? $k['daten'] : []);
self::bounces($d);
self::heatmapKachel(__('Accepted by weekday and hour', 'purehost-send-mails'), (array) ($d['heatmap'] ?? []), __('Time zone Europe/Berlin, emails accepted by send-mails.de.', 'purehost-send-mails'));
self::laufzeit($d);
self::verteilung($d);
self::sperrliste($neu, $zeitraum);
echo '
';
}
$tage = self::tage($zeitraum, $st['daten'] ?? []);
echo '
' . esc_html__('This website', 'purehost-send-mails') . '
';
echo '
';
self::quellen($tage);
self::newsletterTools($tage);
echo '
';
echo '
';
}
private static function kopf(string $zeitraum): void
{
$namen = ['7t' => __('7 days', 'purehost-send-mails'), '30t' => __('30 days', 'purehost-send-mails'), 'monat' => __('This month', 'purehost-send-mails'), 'vormonat' => __('Last month', 'purehost-send-mails')];
echo '';
}
private static function kennzahlen(array $d): void
{
$s = (array) ($d['summe'] ?? []);
$v = (array) ($d['vergleich']['summe'] ?? []);
$quote = static fn(array $x) => ($x['uebergeben'] ?? 0) > 0 ? (float) ($x['zugestellt'] ?? 0) / (float) $x['uebergeben'] : null;
$bq = static fn(array $x) => ($x['uebergeben'] ?? 0) > 0 ? (float) ($x['bounce_hart'] ?? 0) / (float) $x['uebergeben'] : null;
$zq = $quote($s);
$reihe = array_values(array_filter((array) ($d['reihe'] ?? []), static fn($t) => empty($t['zukunft'])));
$spark = static fn(string $feld, string $art) => Ansicht::spark(array_map(static fn($t) => (float) ($t[$feld] ?? 0), $reihe), $art);
// Erste Reihe Menge (angenommen, uebergeben, zugestellt, Daten), zweite Reihe Probleme
echo '';
Ansicht::html(Ansicht::kennzahl(__('Accepted', 'purehost-send-mails'), Ansicht::zahl($s['angenommen'] ?? 0), Ansicht::trend((float) ($s['angenommen'] ?? 0), (float) ($v['angenommen'] ?? 0)), '', $spark('angenommen', 'ok')));
Ansicht::html(Ansicht::kennzahl(__('Handed over for delivery', 'purehost-send-mails'), Ansicht::zahl($s['uebergeben'] ?? 0), Ansicht::trend((float) ($s['uebergeben'] ?? 0), (float) ($v['uebergeben'] ?? 0)), '', $spark('uebergeben', 'ok')));
Ansicht::html(Ansicht::kennzahl(__('Delivered', 'purehost-send-mails'), Ansicht::zahl($s['zugestellt'] ?? 0), Ansicht::trend((float) ($s['zugestellt'] ?? 0), (float) ($v['zugestellt'] ?? 0)),
/* translators: %s: percentage */
$zq === null ? '' : sprintf(__('Delivery rate %s', 'purehost-send-mails'), Ansicht::prozent($zq, 1)), $spark('zugestellt', 'ok')));
Ansicht::html(Ansicht::kennzahl(__('Data volume', 'purehost-send-mails'), size_format((int) ($s['bytes'] ?? 0), 1) ?: '0 B', Ansicht::trend((float) ($s['bytes'] ?? 0), (float) ($v['bytes'] ?? 0)), '', $spark('bytes', 'ok')));
Ansicht::html(Ansicht::kennzahl(__('Hard bounces', 'purehost-send-mails'), Ansicht::zahl($s['bounce_hart'] ?? 0), Ansicht::trend((float) ($s['bounce_hart'] ?? 0), (float) ($v['bounce_hart'] ?? 0), 'runter'),
/* translators: %s: percentage */
$bq($s) === null ? '' : sprintf(__('%s of those handed over', 'purehost-send-mails'), Ansicht::prozent($bq($s))), $spark('bounce_hart', 'fehler')));
Ansicht::html(Ansicht::kennzahl(__('Soft bounces', 'purehost-send-mails'), Ansicht::zahl($s['bounce_weich'] ?? 0), Ansicht::trend((float) ($s['bounce_weich'] ?? 0), (float) ($v['bounce_weich'] ?? 0), 'runter'), '', $spark('bounce_weich', 'warn')));
Ansicht::html(Ansicht::kennzahl(__('Complaints', 'purehost-send-mails'), Ansicht::zahl($s['beschwerden'] ?? 0), Ansicht::trend((float) ($s['beschwerden'] ?? 0), (float) ($v['beschwerden'] ?? 0), 'runter'), '', $spark('beschwerden', 'fehler')));
Ansicht::html(Ansicht::kennzahl(__('Rejected / expired', 'purehost-send-mails'), Ansicht::zahl((int) ($s['abgelehnt'] ?? 0) + (int) ($s['abgelaufen'] ?? 0)),
Ansicht::trend((float) ($s['abgelehnt'] ?? 0) + (float) ($s['abgelaufen'] ?? 0), (float) ($v['abgelehnt'] ?? 0) + (float) ($v['abgelaufen'] ?? 0), 'runter'), '',
Ansicht::spark(array_map(static fn($t) => (float) ($t['abgelehnt'] ?? 0) + (float) ($t['abgelaufen'] ?? 0), $reihe), 'fehler')));
echo '
';
}
private static function verlauf(array $d): void
{
echo '' . esc_html__('Sending per day', 'purehost-send-mails') . '
';
Ansicht::html(Ansicht::gestapelt((array) ($d['reihe'] ?? []), [
'zugestellt' => [__('delivered', 'purehost-send-mails'), 'sm-f-ok'],
'in_queue' => [__('in transit', 'purehost-send-mails'), 'sm-f-blau'],
'bounce_weich' => [__('soft bounce', 'purehost-send-mails'), 'sm-f-warn'],
'bounce_hart' => [__('hard bounce', 'purehost-send-mails'), 'sm-f-fehler'],
'beschwerden' => [__('complaint', 'purehost-send-mails'), 'sm-f-lila'],
'abgelehnt' => [__('rejected', 'purehost-send-mails'), 'sm-f-grau'],
], __('Sending per day', 'purehost-send-mails')));
echo '';
}
private static function quoten(array $d): void
{
$q = (array) ($d['quoten'] ?? []);
$kurve = (array) ($q['kurve'] ?? []);
/* translators: %d: days */
$fenster = sprintf(__('rolling over %d days', 'purehost-send-mails'), (int) ($q['fenster_tage'] ?? 7));
echo '' . esc_html__('Bounce rate', 'purehost-send-mails') . ' ' . esc_html($fenster) . '
';
Ansicht::html(Ansicht::quote(__('current', 'purehost-send-mails'), isset($q['bounce']) ? (float) $q['bounce'] : null, (float) ($q['bounce_grenze'] ?? 0.05), !empty($q['bounce_warnung'])));
Ansicht::html(Ansicht::quotenkurve($kurve, 'bounce', (float) ($q['bounce_grenze'] ?? 0.05), __('Bounce rate per day', 'purehost-send-mails')));
self::quotenStand($q, 'bounce', __('bounce rate', 'purehost-send-mails'));
echo '';
echo '' . esc_html__('Complaint rate', 'purehost-send-mails') . ' ' . esc_html($fenster) . '
';
Ansicht::html(Ansicht::quote(__('current', 'purehost-send-mails'), isset($q['beschwerde']) ? (float) $q['beschwerde'] : null, (float) ($q['beschwerde_grenze'] ?? 0.001), !empty($q['beschwerde_warnung'])));
Ansicht::html(Ansicht::quotenkurve($kurve, 'beschwerde', (float) ($q['beschwerde_grenze'] ?? 0.001), __('Complaint rate per day', 'purehost-send-mails')));
self::quotenStand($q, 'beschwerde', __('complaint rate', 'purehost-send-mails'));
echo '';
}
/** Warnung nahe der Sperrgrenze und aktueller Stand (oder Mindestmenge) unter der Kurve. */
private static function quotenStand(array $q, string $feld, string $name): void
{
$wert = isset($q[$feld]) ? (float) $q[$feld] : null;
$grenze = (float) ($q[$feld . '_grenze'] ?? 0);
$tage = (int) ($q['fenster_tage'] ?? 7);
if (!empty($q[$feld . '_warnung']) && $wert !== null) {
/* translators: 1: rate name, 2: current rate, 3: suspension limit */
echo '' . esc_html(sprintf(__('Caution: your %1$s is %2$s and approaching the suspension limit of %3$s. At the limit, sending is suspended automatically. Please check your recipient lists.', 'purehost-send-mails'),
$name, Ansicht::prozent($wert), Ansicht::prozent($grenze, 1))) . '
';
}
if (!empty($q['gewertet'])) {
if ($wert !== null) {
/* translators: 1: current rate, 2: days */
echo '' . esc_html(sprintf(__('Currently %1$s over %2$d days.', 'purehost-send-mails'), Ansicht::prozent($wert), $tage)) . '
';
}
return;
}
/* translators: 1: minimum volume, 2: days, 3: current volume */
$satz = sprintf(__('The rate only counts from %1$s emails in the window (%2$d days); currently %3$s.', 'purehost-send-mails'), Ansicht::zahl($q['mindestmenge'] ?? 200), $tage, Ansicht::zahl($q['uebergeben'] ?? 0));
if ($wert !== null) {
/* translators: %s: rate */
$satz .= ' ' . sprintf(__('Value so far: %s.', 'purehost-send-mails'), Ansicht::prozent($wert));
}
echo '' . esc_html($satz) . '
';
}
/** Senderate: Limits des Tarifs, was gerade ankommt, und ob Mails im Takt warten. */
private static function senderate(array $kd): void
{
$r = (array) ($kd['rate'] ?? []);
$l = (array) ($kd['limits'] ?? []);
if (!$r && !$l) {
return;
}
$proStunde = (int) ($r['pro_stunde'] ?? $l['pro_stunde'] ?? 0);
$proSekunde = (float) ($r['pro_sekunde'] ?? $l['pro_sekunde'] ?? 0);
$stunde = (int) ($r['letzte_stunde'] ?? 0);
$warten = (int) ($kd['queue']['mails'] ?? 0);
$prozent = $proStunde > 0 ? min(100, (int) floor($stunde * 100 / $proStunde)) : 0;
echo '' . esc_html__('Sending rate', 'purehost-send-mails') . '
';
if ($proStunde > 0) {
/* translators: 1: emails in the last hour, 2: limit per hour */
echo '
' . esc_html(sprintf(__('%1$s of %2$s emails in the last hour', 'purehost-send-mails'), Ansicht::zahl($stunde), Ansicht::zahl($proStunde))) . '
';
echo '
';
}
echo '
';
$zeile = static function (string $t, string $w): void {
echo '- ' . esc_html($t) . '
- ' . esc_html($w) . '
';
};
if ($proSekunde > 0) {
$zeile(__('Limit per second', 'purehost-send-mails'), number_format_i18n($proSekunde, $proSekunde < 10 && floor($proSekunde) != $proSekunde ? 1 : 0));
}
if ($proStunde > 0) {
$zeile(__('Limit per hour', 'purehost-send-mails'), Ansicht::zahl($proStunde));
}
$zeile(__('Last minute', 'purehost-send-mails'), Ansicht::zahl($r['letzte_minute'] ?? 0));
$zeile(__('Waiting at send-mails.de', 'purehost-send-mails'), Ansicht::zahl($warten));
echo '
';
if (!empty($r['gedrosselt'])) {
echo '
' . esc_html__('The monthly quota is used up. Sending continues throttled at the rates above.', 'purehost-send-mails') . '
';
} elseif ($prozent >= 90) {
echo '
' . esc_html__('Close to the hourly limit. Further emails wait briefly and go out at the pace of your plan; nothing is lost. Higher limits are available on request.', 'purehost-send-mails') . '
';
} elseif ($warten > 0) {
/* translators: %s: count */
echo '
' . esc_html(sprintf(_n('%s email is waiting at send-mails.de and goes out at the pace of your plan.', '%s emails are waiting at send-mails.de and go out at the pace of your plan.', $warten, 'purehost-send-mails'), Ansicht::zahl($warten))) . '
';
}
echo '
';
}
private static function kontingent(array $d, array $kd): void
{
$h = (array) ($d['kontingent'] ?? []);
if (!$h) {
return;
}
echo '' . esc_html__('Quota and costs', 'purehost-send-mails') . '
';
Ansicht::html(Ansicht::halbkreis((int) ($h['prozent'] ?? 0), Ansicht::zahl($h['verbraucht'] ?? 0),
/* translators: %s: count */
sprintf(__('of %s emails', 'purehost-send-mails'), Ansicht::zahl($h['mails_pro_monat'] ?? 0)), (int) ($h['hochrechnung_prozent'] ?? 0)));
echo '
' . esc_html__('used', 'purehost-send-mails') . '' . esc_html__('forecast', 'purehost-send-mails') . '
';
echo '
';
$zeile = static function (string $t, string $w): void {
echo '- ' . esc_html($t) . '
- ' . esc_html($w) . '
';
};
$zeile(__('Remaining', 'purehost-send-mails'), Ansicht::zahl($h['verbleibend'] ?? 0));
$zeile(__('Average per day', 'purehost-send-mails'), Ansicht::zahl($h['tagesschnitt'] ?? 0));
$zeile(__('Forecast for month end', 'purehost-send-mails'), Ansicht::zahl($h['hochrechnung'] ?? 0) . ' (' . (int) ($h['hochrechnung_prozent'] ?? 0) . ' %)');
if (isset($h['grundpreis_cent'])) {
$zeile(__('Base price', 'purehost-send-mails'), Ansicht::euro((int) $h['grundpreis_cent']));
}
if (in_array($h['verhalten'] ?? '', ['nachberechnen', 'drosseln'], true) && isset($h['mehrmenge_hochgerechnet'])) {
$zeile(__('Overage (forecast)', 'purehost-send-mails'), Ansicht::zahl($h['mehrmenge_hochgerechnet']));
}
if (isset($h['kosten_bisher_cent'])) {
$zeile(__('Costs so far (gross)', 'purehost-send-mails'), Ansicht::euro((int) $h['kosten_bisher_cent']));
$zeile(__('Projected costs', 'purehost-send-mails'), Ansicht::euro((int) ($h['kosten_hochgerechnet_cent'] ?? 0)));
}
if (!empty($h['kostendeckel_cent'])) {
$zeile(__('Cost cap', 'purehost-send-mails'), Ansicht::euro((int) $h['kostendeckel_cent']));
}
$verhalten = ['sperren' => __('Sending stops', 'purehost-send-mails'), 'drosseln' => __('Sending slows down, overage is charged', 'purehost-send-mails'), 'nachberechnen' => __('Overage is charged', 'purehost-send-mails')];
if (!empty($h['verhalten'])) {
$zeile(__('Above the quota', 'purehost-send-mails'), $verhalten[$h['verhalten']] ?? (string) $h['verhalten']);
}
echo '
';
echo '
' . esc_html__('Choose what happens when the quota is used up', 'purehost-send-mails') . ' →
';
if (!empty($h['reicht_nicht'])) {
$text = match ((string) ($h['verhalten'] ?? '')) {
/* translators: 1: price per 1,000 emails, 2: cost cap */
'nachberechnen' => sprintf(__('According to the forecast you will exceed the quota. Overage: %1$s incl. 19 %% VAT per started 1,000 emails, at most up to the cost cap of %2$s.', 'purehost-send-mails'), Ansicht::euro((int) ($h['preis_cent_pro_1000'] ?? 100)), Ansicht::euro((int) ($h['kostendeckel_cent'] ?? 0))),
/* translators: 1: price per 1,000 emails, 2: cost cap */
'drosseln' => sprintf(__('According to the forecast the quota will not last. After that, sending continues throttled. Overage: %1$s incl. 19 %% VAT per started 1,000 emails, at most up to the cost cap of %2$s.', 'purehost-send-mails'), Ansicht::euro((int) ($h['preis_cent_pro_1000'] ?? 100)), Ansicht::euro((int) ($h['kostendeckel_cent'] ?? 0))),
'sperren' => __('According to the forecast the quota will not last. After that, no emails are accepted until the end of the month.', 'purehost-send-mails'),
default => __('At the same sending volume, the quota will not last until the end of the month.', 'purehost-send-mails'),
};
echo '
' . esc_html($text) . '
';
}
echo '
';
}
private static function bounces(array $d): void
{
$b = (array) ($d['bounces'] ?? []);
$felder = ['hart' => [__('hard', 'purehost-send-mails'), 'sm-f-fehler'], 'weich' => [__('soft', 'purehost-send-mails'), 'sm-f-warn']];
$prov = [];
foreach (self::PROVIDER as $schluessel => $name) {
$prov[$name] = (array) ($b['provider'][$schluessel] ?? []);
}
echo '' . esc_html__('Bounces by mailbox provider', 'purehost-send-mails') . '
';
Ansicht::html(Ansicht::balkenliste($prov, $felder) . '');
$gruende = ['unbekannt' => __('Address unknown', 'purehost-send-mails'), 'postfach_voll' => __('Mailbox full', 'purehost-send-mails'), 'abgelehnt' => __('Rejected by recipient', 'purehost-send-mails'), 'sonstige' => __('Other', 'purehost-send-mails')];
$gr = [];
foreach ($gruende as $schluessel => $name) {
$gr[$name] = (array) ($b['grund'][$schluessel] ?? []);
}
echo '' . esc_html__('Bounces by reason', 'purehost-send-mails') . '
';
Ansicht::html(Ansicht::balkenliste($gr, $felder) . '');
}
private static function heatmapKachel(string $titel, array $werte, string $hinweis): void
{
Ansicht::html('' . esc_html($titel) . '
' . Ansicht::heatmap($werte) . '
' . esc_html($hinweis) . '
');
}
private static function laufzeit(array $d): void
{
$l = (array) ($d['laufzeit'] ?? []);
echo '' . esc_html__('Time until handover', 'purehost-send-mails') . '
';
Ansicht::html(Ansicht::kennzahl(__('Median', 'purehost-send-mails'), Ansicht::dauer(isset($l['median_s']) ? (float) $l['median_s'] : null)));
Ansicht::html(Ansicht::kennzahl(__('95th percentile', 'purehost-send-mails'), Ansicht::dauer(isset($l['p95_s']) ? (float) $l['p95_s'] : null)));
Ansicht::html(Ansicht::kennzahl(__('Measured', 'purehost-send-mails'), Ansicht::zahl($l['anzahl'] ?? 0)));
echo '
' . esc_html__('Time from acceptance at send-mails.de until handover for delivery.', 'purehost-send-mails') . '
';
}
/** Welche Plugins und Themes ueber send-mails.de senden, aus dem lokalen Protokoll. */
private static function quellen(int $tage): void
{
$a = Protokoll::auswertung($tage);
$s = $a['summe'];
echo '';
Ansicht::html(Ansicht::kennzahl(__('Emails from this website', 'purehost-send-mails'), Ansicht::zahl($a['gesamt'])));
Ansicht::html(Ansicht::kennzahl(__('Accepted directly', 'purehost-send-mails'), Ansicht::zahl($s['gesendet'])));
Ansicht::html(Ansicht::kennzahl(__('Via the queue', 'purehost-send-mails'), Ansicht::zahl($s['warteschlange']), '', sprintf(
/* translators: %s: count */
__('%s of them sent later', 'purehost-send-mails'),
Ansicht::zahl($s['nachgeholt'])
)));
Ansicht::html(Ansicht::kennzahl(__('Rejected / given up', 'purehost-send-mails'), Ansicht::zahl($s['abgelehnt'] + $s['aufgegeben'])));
Ansicht::html(Ansicht::kennzahl(__('Currently in the queue', 'purehost-send-mails'), Ansicht::zahl(Warteschlange::anzahl('wartet'))));
echo '
';
echo '' . esc_html__('Who sends emails?', 'purehost-send-mails') . '
';
if (!$a['quellen']) {
echo '
' . esc_html__('No emails via send-mails.de in this period yet.', 'purehost-send-mails') . '
';
} else {
echo '| ' . esc_html__('Source', 'purehost-send-mails') . ' | ' . esc_html__('Emails', 'purehost-send-mails') . ' | ' . esc_html__('Recipient', 'purehost-send-mails') . ' | ' . esc_html__('Share', 'purehost-send-mails') . ' | ' . esc_html__('Queue', 'purehost-send-mails') . ' | ' . esc_html__('Rejected', 'purehost-send-mails') . ' |
';
foreach ($a['quellen'] as $q) {
$anteil = $a['gesamt'] > 0 ? $q['mails'] / $a['gesamt'] : 0;
Ansicht::html('| ' . esc_html($q['name']) . ' | ' . esc_html(Ansicht::zahl($q['mails'])) . ' | ' . esc_html(Ansicht::zahl($q['empfaenger'])) . ' | '
. ' ' . esc_html(Ansicht::prozent($anteil, 0)) . ' | '
. '' . esc_html(Ansicht::zahl($q['warteschlange'])) . ' | ' . esc_html(Ansicht::zahl($q['abgelehnt'] + $q['aufgegeben'])) . ' |
');
}
echo '
';
}
$reihe = [];
foreach ($a['reihe'] as $tag => $werte) {
$reihe[] = ['tag' => $tag] + $werte;
}
echo '' . esc_html__('Emails from this website per day', 'purehost-send-mails') . '
';
Ansicht::html(Ansicht::gestapelt($reihe, [
'gesendet' => [__('accepted directly', 'purehost-send-mails'), 'sm-f-ok'],
'nachgeholt' => [__('sent later', 'purehost-send-mails'), 'sm-f-blau'],
'abgelehnt' => [__('rejected', 'purehost-send-mails'), 'sm-f-fehler'],
'aufgegeben' => [__('given up', 'purehost-send-mails'), 'sm-f-grau'],
], __('Emails from this website per day', 'purehost-send-mails')));
echo '';
self::heatmapKachel(__('Sending from this website by weekday and hour', 'purehost-send-mails'), $a['heatmap'], __('Time zone of the website.', 'purehost-send-mails'));
echo '' . esc_html__('Most frequent recipient domains', 'purehost-send-mails') . '
';
if (!$a['domains']) {
echo '
' . esc_html__('None yet.', 'purehost-send-mails') . '
';
} else {
$zeilen = [];
foreach ($a['domains'] as $dz) {
$zeilen[(string) $dz['domain']] = ['ok' => (int) $dz['mails'] - (int) $dz['fehler'], 'fehler' => (int) $dz['fehler']];
}
Ansicht::html(Ansicht::balkenliste($zeilen, ['ok' => [__('accepted', 'purehost-send-mails'), 'sm-f-ok'], 'fehler' => [__('rejected', 'purehost-send-mails'), 'sm-f-fehler']]));
}
echo '
' . esc_html__('Only the domain of the first recipient, no addresses.', 'purehost-send-mails') . '
';
}
/** Was der Abgleich in den Newsletter-Tools eingetragen hat. */
private static function newsletterTools(int $tage): void
{
global $wpdb;
$zeilen = (array) $wpdb->get_results($wpdb->prepare(
'SELECT art, wirkung, ergebnis FROM %i WHERE zeit >= %s',
Abgleich::tabelle(),
gmdate('Y-m-d H:i:s', time() - $tage * DAY_IN_SECONDS)
), ARRAY_A);
$arten = ['bounce_hart' => 0, 'bounce_weich' => 0, 'beschwerde' => 0];
$tools = [];
foreach ($zeilen as $z) {
$arten[$z['art']] = ($arten[$z['art']] ?? 0) + 1;
foreach (explode('; ', (string) $z['ergebnis']) as $teil) {
if (!str_contains($teil, ': ')) {
continue;
}
[$tool, $was] = explode(': ', $teil, 2);
$tools[$tool] ??= ['bounced' => 0, 'beschwerde' => 0, 'unveraendert' => 0];
if ($was === 'unchanged' || $was === 'unverändert') {
$tools[$tool]['unveraendert']++;
} elseif ($z['wirkung'] === 'beschwerde') {
$tools[$tool]['beschwerde']++;
} else {
$tools[$tool]['bounced']++;
}
}
}
echo '' . esc_html__('Sync with the newsletter tools', 'purehost-send-mails') . '
';
echo '
';
Ansicht::html(Ansicht::kennzahl(__('Hard bounces', 'purehost-send-mails'), Ansicht::zahl($arten['bounce_hart'])));
Ansicht::html(Ansicht::kennzahl(__('Soft bounces', 'purehost-send-mails'), Ansicht::zahl($arten['bounce_weich'])));
Ansicht::html(Ansicht::kennzahl(__('Complaints', 'purehost-send-mails'), Ansicht::zahl($arten['beschwerde'])));
echo '
';
if ($tools) {
Ansicht::html(Ansicht::balkenliste($tools, [
'bounced' => [__('marked as bounced', 'purehost-send-mails'), 'sm-f-fehler'],
'beschwerde' => [__('unsubscribed as complaint', 'purehost-send-mails'), 'sm-f-lila'],
'unveraendert' => [__('was already marked', 'purehost-send-mails'), 'sm-f-grau'],
]));
} else {
echo '
' . esc_html__('Nothing was recorded in any tool during this period.', 'purehost-send-mails') . '
';
}
$stand = Abgleich::stand();
/* translators: %s: date and time */
echo '
' . esc_html(sprintf(__('Last sync %s.', 'purehost-send-mails'), Ansicht::zeit((string) $stand['letzter_lauf']))) . '
';
}
/** Empfaenger-Domains mit Rueckläufern, Mails je Zugang und Absenderadressen (alle Websites des Kontos, wie im Portal). */
private static function verteilung(array $d): void
{
$v = (array) ($d['verteilung'] ?? []);
if (!$v) {
return;
}
$tabelle = static function (array $kopf, array $zeilen): string {
if (!$zeilen) {
return '' . esc_html__('No emails in this period.', 'purehost-send-mails') . '
';
}
$h = '';
foreach ($kopf as $i => $k) {
$h .= '| ' . esc_html($k) . ' | ';
}
$h .= '
';
foreach ($zeilen as $z) {
$h .= '';
foreach ($z as $i => $w) {
$h .= '| ' . esc_html((string) $w) . ' | ';
}
$h .= '
';
}
return $h . '
';
};
$domains = (array) ($v['empfaenger_domains'] ?? []);
$gesamt = array_sum(array_map(static fn($x) => (int) ($x['mails'] ?? 0), $domains));
$zeilen = array_map(static fn($x) => [(string) ($x['domain'] ?? ''), Ansicht::zahl($x['mails'] ?? 0), $gesamt ? Ansicht::prozent((int) ($x['mails'] ?? 0) / $gesamt, 0) : '–',
Ansicht::zahl($x['bounce_hart'] ?? 0), Ansicht::zahl($x['bounce_weich'] ?? 0), Ansicht::zahl($x['beschwerden'] ?? 0)], $domains);
$hinweis = '';
if (!empty($v['daten_ab']) && !empty($d['zeitraum']['von']) && strtotime((string) $v['daten_ab']) > strtotime((string) $d['zeitraum']['von'])) {
$hinweis = '' . esc_html__('Individual emails are kept for 30 days; older days of this period are missing from these lists.', 'purehost-send-mails') . '
';
}
Ansicht::html('' . esc_html__('Recipient domains', 'purehost-send-mails') . ' ' . esc_html__('all websites of your account', 'purehost-send-mails') . '
'
. $tabelle([__('Domain', 'purehost-send-mails'), __('Emails', 'purehost-send-mails'), __('Share', 'purehost-send-mails'), __('Hard bounces', 'purehost-send-mails'), __('Soft bounces', 'purehost-send-mails'), __('Complaints', 'purehost-send-mails')], $zeilen) . $hinweis . '');
$eigen = strtolower((string) Einstellungen::smtpBenutzer());
$herkunft = array_map(static function ($x) use ($eigen): array {
$name = ($x['art'] ?? '') === 'api' ? __('HTTP API', 'purehost-send-mails') : (string) ($x['name'] ?? $x['benutzername'] ?? __('SMTP access (deleted)', 'purehost-send-mails'));
if ($eigen !== '' && strtolower((string) ($x['benutzername'] ?? '')) === $eigen) {
/* translators: %s: name of the SMTP access */
$name = sprintf(__('%s (this website)', 'purehost-send-mails'), $name);
}
return [$name, Ansicht::zahl($x['mails'] ?? 0), Ansicht::zahl($x['abgelehnt'] ?? 0)];
}, (array) ($v['herkunft'] ?? []));
Ansicht::html('' . esc_html__('Who sends', 'purehost-send-mails') . '
' . $tabelle([__('Access', 'purehost-send-mails'), __('Emails', 'purehost-send-mails'), __('Rejected', 'purehost-send-mails')], $herkunft) . '');
$absender = array_map(static fn($x) => [(string) ($x['adresse'] ?? ''), Ansicht::zahl($x['mails'] ?? 0)], (array) ($v['absender'] ?? []));
Ansicht::html('' . esc_html__('Sender addresses', 'purehost-send-mails') . '
' . $tabelle([__('Sender', 'purehost-send-mails'), __('Emails', 'purehost-send-mails')], $absender) . '');
}
/** Adressen, an die send-mails.de nicht mehr sendet, mit Suche und Weiterblaettern. */
private static function sperrliste(bool $neu, string $zeitraum): void
{
// Nur Auswahl der Ansicht, keine Aenderung: ohne Nonce.
$suche = mb_substr(sanitize_text_field(wp_unslash($_GET['suche'] ?? '')), 0, 100); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$vor = sanitize_key(wp_unslash($_GET['vor'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$r = Api::sperrliste($neu, $suche, $vor);
echo '' . esc_html__('Blocked addresses', 'purehost-send-mails') . '
';
echo '
' . esc_html__('send-mails.de no longer sends to these addresses, so they do not harm your bounce and complaint rates. You can remove a hard bounce yourself once the mailbox exists again. After 3 bounces, after a renewed bounce shortly after a release, and for complaints, only our support can release the address.', 'purehost-send-mails') . '
';
$meldung = get_transient(self::meldungSchluessel());
if (is_array($meldung)) {
delete_transient(self::meldungSchluessel());
echo '
' . esc_html((string) $meldung[1]) . '
';
}
$ziel = esc_url(admin_url('admin-post.php'));
echo '
';
echo '
';
if (!$r['ok']) {
echo '
' . esc_html($r['fehler']) . '
';
return;
}
$liste = (array) ($r['daten']['eintraege'] ?? []);
if (!$liste) {
echo '' . esc_html($suche !== '' ? __('No blocked address matches the search.', 'purehost-send-mails') : __('No blocked addresses.', 'purehost-send-mails')) . '
';
return;
}
$gruende = [
'bounce_hart' => __('Hard bounce', 'purehost-send-mails'),
'beschwerde' => __('Complaint', 'purehost-send-mails'),
'manuell' => __('Added manually', 'purehost-send-mails'),
];
// Spalte "Ausgeloest durch" nur, wenn auf dieser Seite etwas darin stuende (Kennung oder Betreff der Mail)
$mitHerkunft = (bool) array_filter($liste, static fn($e) => !empty(((array) $e)['ref']) || !empty(((array) $e)['betreff']));
echo '| ' . esc_html__('Address', 'purehost-send-mails') . ' | ' . esc_html__('Reason', 'purehost-send-mails') . ' | '
. ($mitHerkunft ? '' . esc_html__('Triggered by', 'purehost-send-mails') . ' | ' : '') . '' . esc_html__('Blocked since', 'purehost-send-mails') . ' | ' . esc_html__('Action', 'purehost-send-mails') . ' |
';
foreach ($liste as $e) {
$e = (array) $e;
$adresse = (string) ($e['adresse'] ?? '');
$aktion = '';
// entfernbar liefert send-mails.de nach seinen Regeln; aeltere Antworten kennen nur vom_kunden
if (!empty($e['entfernbar'] ?? $e['vom_kunden'] ?? false)) {
$frage = ($e['grund'] ?? '') === 'bounce_hart'
/* translators: %s: email address */
? sprintf(__('Allow %s again? Only useful if the mailbox exists again. If the address bounces again, it is blocked again and only our support can release it.', 'purehost-send-mails'), $adresse)
/* translators: %s: email address */
: sprintf(__('Allow %s again?', 'purehost-send-mails'), $adresse);
$aktion = '';
} elseif (!empty($e['sperre'])) {
$sperren = ['beschwerde' => __('only support (complaint)', 'purehost-send-mails'), 'dreimal' => __('only support (3 bounces)', 'purehost-send-mails'), 'wiederholt' => __('only support (bounced again)', 'purehost-send-mails')];
$aktion = '' . esc_html($sperren[$e['sperre']] ?? '') . '';
}
$grund = esc_html($gruende[$e['grund'] ?? ''] ?? (string) ($e['grund'] ?? ''));
[$klar, $roh] = self::sperrGrund($e);
if ($klar !== '') {
$grund .= '
' . esc_html($klar) . '';
}
if ($roh !== '') {
$grund .= '
' . esc_html($roh) . '';
}
echo '| ' . esc_html(Testpostfach::anzeige($adresse)) . ' | ' . $grund . ' | ' . ($mitHerkunft ? '' . self::sperrHerkunft($e) . ' | ' : '') . '' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- aus maskierten Teilen
. esc_html(Ansicht::zeit((string) ($e['erstellt'] ?? ''))) . ' | ' . $aktion . ' |
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $aktion aus maskierten Teilen
}
echo '
';
$weiter = (string) ($r['daten']['weiter'] ?? '');
if ($weiter !== '' || $vor !== '') {
echo '';
if ($vor !== '') {
echo '' . esc_html__('Back to the newest', 'purehost-send-mails') . ' ';
}
if ($weiter !== '') {
echo '' . esc_html__('Older entries', 'purehost-send-mails') . '';
}
echo '
';
}
echo '';
}
/**
* Grund in Klartext und die Originalantwort des Empfaengerservers.
*
* @return array{0: string, 1: string}
*/
public static function sperrGrund(array $e): array
{
if (($e['grund'] ?? '') === 'beschwerde') {
return [__('The recipient marked an email as spam.', 'purehost-send-mails'), ''];
}
if (($e['diagnose_art'] ?? '') === 'unterdrueckt') {
return [__('The address was already invalid in earlier delivery attempts.', 'purehost-send-mails'), ''];
}
$d = (string) ($e['diagnose'] ?? '');
if ($d === '') {
return [(string) ($e['hinweis'] ?? ''), ''];
}
$t = strtolower($d);
$klar = match (true) {
(bool) preg_match('/5\.1\.[0-3]|does not exist|no such|not found|unknown|user unknown|invalid recipient|nicht vorhanden|unbekannt/', $t) => __('The mailbox does not exist.', 'purehost-send-mails'),
(bool) preg_match('/5\.2\.2|mailbox full|quota|over quota|voll/', $t) => __('The mailbox is full.', 'purehost-send-mails'),
(bool) preg_match('/5\.7\.|spam|policy|blocked|rejected|denied|blacklist/', $t) => __('The recipient server rejected the email.', 'purehost-send-mails'),
default => __('Delivery failed permanently.', 'purehost-send-mails'),
};
return [$klar, preg_replace('/^smtp;\s*/i', '', $d)];
}
/** Welche Mail die Sperre ausgeloest hat: Plugin dieser Website (aus der Kennung) und Betreff. Rueckgabe ist maskiertes HTML. */
private static function sperrHerkunft(array $e): string
{
$teile = [];
$ref = (string) ($e['ref'] ?? '');
if ($ref !== '') {
$quelle = Protokoll::refQuelle($ref);
$teile[] = '' . esc_html($quelle === null ? __('another website or application of your account', 'purehost-send-mails')
: ($quelle === 'test' ? __('Test email from this plugin', 'purehost-send-mails') : Protokoll::name($quelle))) . '';
}
if (!empty($e['betreff'])) {
/* translators: %s: email subject */
$teile[] = esc_html(sprintf(__('Subject: %s', 'purehost-send-mails'), (string) $e['betreff']));
}
return $teile ? implode('
', $teile) : '–';
}
private static function meldungSchluessel(): string
{
return 'sendmails_sperr_meldung_' . get_current_user_id();
}
/** Adresse sperren oder eigene Sperre aufheben (Formulare der Sperrlisten-Kachel). */
public static function sperrlisteAendern(): void
{
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Permission denied.', 'purehost-send-mails'), 403);
}
check_admin_referer('sendmails_sperrliste');
$was = sanitize_key(wp_unslash($_POST['was'] ?? ''));
$adresse = strtolower(trim(sanitize_email(wp_unslash($_POST['adresse'] ?? ''))));
$zeitraum = sanitize_key(wp_unslash($_POST['zeitraum'] ?? '30t'));
if ($adresse === '' || !is_email($adresse)) {
$meldung = ['fehler', __('Please enter a valid email address.', 'purehost-send-mails')];
} elseif ($was === 'entfernen') {
$r = Api::entsperren($adresse);
/* translators: %s: email address */
$meldung = $r['ok'] ? ['ok', sprintf(__('%s is allowed again.', 'purehost-send-mails'), $adresse)] : ['fehler', $r['fehler']];
} else {
$r = Api::sperren($adresse);
$meldung = !$r['ok'] ? ['fehler', $r['fehler']]
/* translators: %s: email address */
: (!empty($r['daten']['neu']) ? ['ok', sprintf(__('%s is blocked.', 'purehost-send-mails'), $adresse)] : ['ok', sprintf(__('%s was already on the list.', 'purehost-send-mails'), $adresse)]);
}
set_transient(self::meldungSchluessel(), $meldung, MINUTE_IN_SECONDS);
wp_safe_redirect(self::url(in_array($zeitraum, Api::ZEITRAEUME, true) ? $zeitraum : '30t') . '#sperrliste');
exit;
}
/** Eine CSV-Zeile mit Semikolon, Felder mit Trennzeichen, Anfuehrungszeichen oder Umbruch in Anfuehrungszeichen. */
private static function csvZeile(array $felder): string
{
return implode(';', array_map(static function ($w): string {
$w = (string) $w;
return preg_match('/[;"\r\n]/', $w) ? '"' . str_replace('"', '""', $w) . '"' : $w;
}, $felder)) . "\n";
}
/** CSV-Export: send-mails.de-Statistik durchgereicht oder das lokale Protokoll. */
public static function csv(): void
{
if (!current_user_can('manage_options')) {
wp_die(esc_html__('Permission denied.', 'purehost-send-mails'), 403);
}
check_admin_referer('sendmails_csv');
$zeitraum = self::zeitraum();
$art = sanitize_key(wp_unslash($_GET['art'] ?? 'website'));
if ($art === 'ereignisse') {
$csv = Api::ereignisseCsv();
if ($csv === null) {
wp_die(esc_html__('The send-mails.de statistics are currently unavailable.', 'purehost-send-mails'));
}
$name = 'send-mails-bounces-beschwerden.csv';
} elseif ($art === 'sendmails') {
$csv = Api::statistikCsv($zeitraum);
if ($csv === null) {
wp_die(esc_html__('The send-mails.de statistics are currently unavailable.', 'purehost-send-mails'));
}
$name = 'send-mails-statistik-' . $zeitraum . '.csv';
} else {
$tage = $zeitraum === '7t' ? 7 : 31;
$zeilen = Protokoll::zeilen($tage);
$csv = "\u{FEFF}" . self::csvZeile(['zeit_utc', 'herkunft', 'empfaenger_domain', 'empfaenger', 'status', 'fehler']);
foreach ($zeilen as $z) {
// Formel-Injektion in Tabellenprogrammen verhindern.
$z = array_map(static fn($w) => preg_match('/^[=+\-@\t\r]/', (string) $w) ? "'" . $w : $w, $z);
$csv .= self::csvZeile(array_values($z));
}
$name = 'send-mails-website-' . $zeitraum . '.csv';
}
nocache_headers();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $name . '"');
echo $csv; // phpcs:ignore WordPress.Security.EscapeOutput -- CSV-Download
exit;
}
}