'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 '

' . esc_html__('The send-mails.de statistics require an API key.', 'purehost-send-mails') . ' ' . esc_html__('Set up', 'purehost-send-mails') . '

'; } 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 '
'; echo '' . esc_html__('Refresh', 'purehost-send-mails') . ' '; echo '' . esc_html__('CSV send-mails.de', 'purehost-send-mails') . ' '; echo '' . esc_html__('CSV of this website', '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 ''; foreach ($a['quellen'] as $q) { $anteil = $a['gesamt'] > 0 ? $q['mails'] / $a['gesamt'] : 0; Ansicht::html('' . '' . ''); } 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') . '
' . 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'])) . '
'; } $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 '
' . esc_html__('Block an address', 'purehost-send-mails') . '' . '
'; wp_nonce_field('sendmails_sperrliste'); echo '' . '' . ' ' . '
' . '' . esc_html__('Bounces and complaints as CSV', 'purehost-send-mails') . '
'; 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 '' . ($mitHerkunft ? '' : '') . ''; 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 = '' . wp_nonce_field('sendmails_sperrliste', '_wpnonce', true, false) . '' . ''; } 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 '' . ($mitHerkunft ? '' : '') . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $aktion aus maskierten Teilen } echo '
' . esc_html__('Address', 'purehost-send-mails') . '' . esc_html__('Reason', 'purehost-send-mails') . '' . esc_html__('Triggered by', 'purehost-send-mails') . '' . esc_html__('Blocked since', 'purehost-send-mails') . '' . esc_html__('Action', 'purehost-send-mails') . '
' . esc_html(Testpostfach::anzeige($adresse)) . '' . $grund . '' . self::sperrHerkunft($e) . '' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- aus maskierten Teilen . esc_html(Ansicht::zeit((string) ($e['erstellt'] ?? ''))) . '' . $aktion . '
'; $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; } }