Adapter\FluentCrm::class, 'newsletter' => Adapter\Newsletter::class, 'groundhogg' => Adapter\Groundhogg::class, 'mailpoet' => Adapter\MailPoet::class, 'icegram' => Adapter\IcegramExpress::class, 'noptin' => Adapter\Noptin::class, 'acymailing' => Adapter\AcyMailing::class, ]; public static function tabelle(): string { global $wpdb; return $wpdb->prefix . 'sendmails_ereignisse'; } public static function stand(): array { $s = get_option(self::STAND, []); return array_replace(['von' => '', 'vor' => '', 'neuester' => '', 'letzter_lauf' => '', 'fehler' => '', 'neu' => 0], is_array($s) ? $s : []); } /** @return array{ok: bool, neu: int, fehler: string} */ /** * Gespeichertes Ergebnis ("Tool: kennung; ...") fuer die Anzeige uebersetzen. * Kennungen: unchanged, unsubscribed, bounced, complained, error: ...; aeltere Eintraege deutsch. */ public static function ergebnisAnzeige(string $ergebnis): string { $texte = [ 'unchanged' => __('unchanged', 'purehost-send-mails'), 'unverändert' => __('unchanged', 'purehost-send-mails'), 'unsubscribed' => __('unsubscribed', 'purehost-send-mails'), 'abgemeldet' => __('unsubscribed', 'purehost-send-mails'), 'bounced' => __('marked as bounced', 'purehost-send-mails'), 'complained' => __('marked as complaint', 'purehost-send-mails'), ]; $teile = []; foreach (array_filter(explode('; ', $ergebnis), 'strlen') as $teil) { [$tool, $was] = array_pad(explode(': ', $teil, 2), 2, ''); if (isset($texte[$was])) { $was = $texte[$was]; } elseif (preg_match('/^(?:error|Fehler): (.*)$/s', $was, $m)) { /* translators: %s: technical error message */ $was = sprintf(__('error: %s', 'purehost-send-mails'), $m[1]); } $teile[] = $was === '' ? $tool : $tool . ': ' . $was; } return implode('; ', $teile); } public static function laufen(): array { if (Einstellungen::apiSchluessel() === '') { return ['ok' => false, 'neu' => 0, 'fehler' => __('No API key configured.', 'purehost-send-mails')]; } if (get_transient('sendmails_abgleich_sperre')) { return ['ok' => false, 'neu' => 0, 'fehler' => __('A sync is already running.', 'purehost-send-mails')]; } set_transient('sendmails_abgleich_sperre', 1, 10 * MINUTE_IN_SECONDS); try { // Liste der verifizierten Domains fuer den Versandweg auffrischen (dort nie eine API-Abfrage). Api::domains(true); // Annahmestellen fuer den Wechsel im Versand auffrischen (Liste kommt mit dem Kontingent) Api::kontingent(); $erg = self::lauf(); // Zustellstatus fuer das E-Mail-Protokoll, danach Adressen und Inhalte nach Ablauf leeren Protokoll::zustellungAbgleichen(); Protokoll::aufraeumen(); return $erg; } finally { delete_transient('sendmails_abgleich_sperre'); } } /** * Die API liefert neueste Ereignisse zuerst und blaettert rueckwaerts (vor=). * Reichen 20 Seiten nicht, merkt sich der Lauf die Stelle (vor) und setzt im * naechsten Lauf dort fort; erst wenn alles geholt ist, rueckt "von" vor. */ private static function lauf(): array { $stand = self::stand(); $von = $stand['von'] !== '' ? strtotime($stand['von']) - self::UEBERLAPPUNG_S : time() - self::ERSTER_LAUF_TAGE * DAY_IN_SECONDS; $query = ['art' => 'bounce_hart,bounce_weich,beschwerde', 'von' => gmdate('Y-m-d\TH:i:s\Z', $von), 'limit' => 500]; if ($stand['vor'] !== '') { $query['vor'] = $stand['vor']; } $alle = []; $fehler = ''; $weiter = null; for ($seite = 0; $seite < self::SEITEN; $seite++) { $r = Api::anfrage('/v1/ereignisse', $query); if (!$r['ok']) { $fehler = $r['fehler']; break; } foreach ((array) ($r['daten']['eintraege'] ?? []) as $e) { $alle[(int) $e['id']] = $e; } $weiter = $r['daten']['weiter'] ?? null; if (!$weiter) { break; } $query['vor'] = (string) $weiter; } ksort($alle); $neu = 0; $neuester = $stand['neuester']; foreach ($alle as $e) { if (self::verarbeiten($e)) { $neu++; } if ($neuester === '' || strtotime((string) $e['zeit']) > strtotime($neuester)) { $neuester = (string) $e['zeit']; } } if ($fehler !== '') { // Stand nicht verschieben: sonst fehlen aeltere Ereignisse. $naechster = $stand; } elseif ($weiter) { $naechster = ['von' => $stand['von'], 'vor' => (string) $weiter, 'neuester' => $neuester]; } else { $naechster = ['von' => $neuester !== '' ? $neuester : $stand['von'], 'vor' => '', 'neuester' => '']; } update_option(self::STAND, array_replace($naechster, [ 'letzter_lauf' => gmdate('c'), 'fehler' => $fehler, 'neu' => $neu, ]), false); // Erst nach dem Speichern des Stands: bricht ein Ereignis jedes Mal ab, blockiert es den Abruf nicht. self::nachholen(); self::autoantwortenEntfernen(); self::aufraeumen(); return ['ok' => $fehler === '', 'neu' => $neu, 'fehler' => $fehler]; } /** * Abwesenheitsnotizen, die send-mails.de nachtraeglich als "autoantwort" einordnet (vorher weicher Bounce), * aus der eigenen Liste nehmen: sie sollen nicht zur Schwelle fuer weiche Bounces zaehlen. Beim ersten Lauf * 30 Tage zurueck, danach 2 Tage. */ private static function autoantwortenEntfernen(): void { global $wpdb; $erster = !get_option('sendmails_autoantwort_geprueft'); $r = Api::anfrage('/v1/ereignisse', ['art' => 'autoantwort', 'limit' => 500, 'von' => gmdate('Y-m-d\TH:i:s\Z', time() - ($erster ? 30 : 2) * DAY_IN_SECONDS)]); if (!$r['ok']) { return; } $ids = array_filter(array_map(static fn($e) => (int) ($e['id'] ?? 0), (array) ($r['daten']['eintraege'] ?? []))); foreach (array_chunk($ids, 100) as $teil) { $wpdb->query($wpdb->prepare( 'DELETE FROM %i WHERE art = %s AND id IN (' . implode(',', array_fill(0, count($teil), '%d')) . ')', ...array_merge([self::tabelle(), 'bounce_weich'], $teil) )); } if ($erster) { update_option('sendmails_autoantwort_geprueft', gmdate('c'), false); } } /** Ein Ereignis aus der API; false, wenn es schon bekannt war. */ public static function verarbeiten(array $e): bool { global $wpdb; $id = (int) ($e['id'] ?? 0); $email = strtolower(trim((string) ($e['empfaenger'] ?? ''))); $art = (string) ($e['art'] ?? ''); if ($id <= 0 || !in_array($art, ['bounce_hart', 'bounce_weich', 'beschwerde'], true)) { return false; } $zeit = strtotime((string) ($e['zeit'] ?? '')) ?: time(); $grund = mb_substr(self::grund($art, (array) ($e['daten'] ?? [])), 0, 300); $eingefuegt = $wpdb->query($wpdb->prepare( 'INSERT IGNORE INTO %i (id, zeit, art, empfaenger, grund, fertig, verarbeitet) VALUES (%d, %s, %s, %s, %s, 0, %s)', self::tabelle(), $id, gmdate('Y-m-d H:i:s', $zeit), $art, mb_substr($email, 0, 254), $grund, gmdate('Y-m-d H:i:s') )); if (!$eingefuegt) { return false; } self::anwenden($id, $art, $email, $zeit, $grund, $e); return true; } /** Ereignisse, deren Eintrag in die Tools abgebrochen ist (Zeitlimit, Fehler), erneut anwenden, hoechstens dreimal. */ private static function nachholen(): void { global $wpdb; $t = self::tabelle(); $offen = (array) $wpdb->get_results($wpdb->prepare('SELECT id, zeit, art, empfaenger, grund, versuche FROM %i WHERE fertig = 0 ORDER BY id LIMIT 200', $t), ARRAY_A); foreach ($offen as $z) { if ((int) $z['versuche'] >= 3) { $wpdb->update($t, ['fertig' => 1, 'ergebnis' => __('given up after 3 attempts', 'purehost-send-mails')], ['id' => $z['id']]); continue; } // Versuch zuerst zaehlen: stuerzt das Anwenden ab, ist er trotzdem verbraucht. $wpdb->query($wpdb->prepare('UPDATE %i SET versuche = versuche + 1 WHERE id = %d', $t, $z['id'])); $zeit = strtotime($z['zeit'] . ' UTC') ?: time(); self::anwenden((int) $z['id'], (string) $z['art'], (string) $z['empfaenger'], $zeit, (string) $z['grund'], ['id' => (int) $z['id'], 'zeit' => gmdate('c', $zeit), 'art' => $z['art'], 'empfaenger' => $z['empfaenger'], 'daten' => ['grund' => $z['grund']]]); } } /** Wirkung bestimmen, in die Tools eintragen, erst danach als fertig markieren. */ private static function anwenden(int $id, string $art, string $email, int $zeit, string $grund, array $e): void { global $wpdb; $wirkung = self::wirkung($art, $email, $zeit); $ergebnisse = []; if ($wirkung !== '' && is_email($email)) { $aktiv = (array) Einstellungen::wert('adapter'); foreach (self::ADAPTER as $schluessel => $klasse) { if (empty($aktiv[$schluessel]) || !$klasse::verfuegbar()) { continue; } try { $r = $klasse::markieren($email, $wirkung, $grund); } catch (\Throwable $t) { $r = 'error: ' . $t->getMessage(); } if ($r !== '') { $ergebnisse[] = $klasse::name() . ': ' . $r; } } /** * Fuer eigene Erweiterungen und nicht unterstuetzte Tools. * * @param string $wirkung 'bounced' oder 'beschwerde' * @param string $email Empfaengeradresse * @param array $e Ereignis wie von /v1/ereignisse */ do_action('sendmails_ereignis', $wirkung, $email, $e); } $wpdb->update(self::tabelle(), [ 'wirkung' => $wirkung === '' ? null : $wirkung, 'ergebnis' => mb_substr(implode('; ', $ergebnisse), 0, 500), 'fertig' => 1, ], ['id' => $id]); } /** 'bounced', 'beschwerde' oder '' (weicher Bounce unter der Schwelle). */ private static function wirkung(string $art, string $email, int $zeit): string { global $wpdb; if ($art === 'bounce_hart') { return 'bounced'; } if ($art === 'beschwerde') { return 'beschwerde'; } $tage = max(1, (int) Einstellungen::wert('weich_tage')); $schwelle = max(1, (int) Einstellungen::wert('weich_schwelle')); $anzahl = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE empfaenger = %s AND art = 'bounce_weich' AND zeit > %s", self::tabelle(), $email, gmdate('Y-m-d H:i:s', $zeit - $tage * DAY_IN_SECONDS) )); return $anzahl >= $schwelle ? 'bounced' : ''; } private static function grund(string $art, array $d): string { if ($art === 'beschwerde') { return trim(__('Complaint', 'purehost-send-mails') . ' ' . (string) ($d['typ'] ?? '')); } $teile = array_filter([(string) ($d['untertyp'] ?? ''), (string) ($d['status'] ?? ''), (string) ($d['diagnose'] ?? '')]); return ($art === 'bounce_hart' ? __('Hard bounce', 'purehost-send-mails') : __('Soft bounce', 'purehost-send-mails')) . ($teile ? ': ' . implode(', ', $teile) : ''); } public static function letzte(int $max = 20): array { global $wpdb; return (array) $wpdb->get_results($wpdb->prepare( 'SELECT id, zeit, art, empfaenger, grund, wirkung, ergebnis FROM %i ORDER BY zeit DESC, id DESC LIMIT %d', self::tabelle(), $max ), ARRAY_A); } private static function aufraeumen(): void { global $wpdb; $wpdb->query($wpdb->prepare( 'DELETE FROM %i WHERE zeit < %s', self::tabelle(), gmdate('Y-m-d H:i:s', time() - self::AUFBEWAHRUNG_TAGE * DAY_IN_SECONDS) )); } }