diff --git a/app/paypal-inbox.php b/app/paypal-inbox.php index a649805..de86126 100644 --- a/app/paypal-inbox.php +++ b/app/paypal-inbox.php @@ -152,6 +152,31 @@ function paypal_match_participant(PDO $pdo, int $tenantId, string $payerName, ?s return null; } +/** + * Stammt die Mail wirklich von PayPal? Geprueft wird die Domain der + * Absenderadresse - inklusive Landesvarianten (paypal.at, paypal.co.uk) und + * Versand-Subdomains (e.paypal.de), die PayPal tatsaechlich benutzt. + * + * Der Vergleich haengt bewusst am Ende der Domain: "paypal.de.beispiel.com" + * ist NICHT PayPal, wuerde bei einer simplen Textsuche nach "@paypal.de" + * aber durchrutschen - und damit koennte jemand mit Kenntnis der + * Eingangsadresse Zahlungen erfinden. + */ +function paypal_from_is_paypal(string $fromHeader): bool +{ + if (!preg_match_all('/[\w.+-]+@([\w-]+(?:\.[\w-]+)+)/u', strtolower($fromHeader), $treffer)) { + return false; + } + + foreach ($treffer[1] as $domain) { + if (preg_match('/(^|\.)paypal\.[a-z]{2,}(\.[a-z]{2,})?$/', $domain)) { + return true; + } + } + + return false; +} + /** * Gehoeren zwei Adressen zum selben Postfach? Plus-Adressierung wird dabei * ignoriert, "zahlungen+ab12cd@host" ist also dasselbe Postfach wie @@ -366,7 +391,7 @@ function paypal_preview(PDO $pdo, int $tenantId, array $parsed): array */ function paypal_process_raw(PDO $pdo, string $recipient, string $fromHeader, string $rawBody, bool $previewOnly = false): array { - if (!preg_match('/@paypal\.(de|com)/i', $fromHeader)) { + if (!paypal_from_is_paypal($fromHeader)) { return ['status' => 'not_from_paypal']; } $token = paypal_inbox_extract_token($recipient); diff --git a/scripts/check-imap-support.php b/scripts/check-imap-support.php index 53e811c..02d0dff 100644 --- a/scripts/check-imap-support.php +++ b/scripts/check-imap-support.php @@ -26,6 +26,8 @@ if (PHP_SAPI !== 'cli') { } require_once __DIR__ . '/../app/bootstrap.php'; +// paypal_from_is_paypal() fuer die Absenderpruefung in der Auflistung. +require_once __DIR__ . '/../app/paypal-inbox.php'; function imap_check_line(string $label, string $value): void { @@ -166,6 +168,43 @@ imap_check_line('davon ungelesen', (string) $ungelesen); fwrite($sock, "a9 LOGOUT\r\n"); fclose($sock); +// Mit der Erweiterung laesst sich zusaetzlich auflisten, was im Postfach +// liegt - die haeufigste Frage bei einer nicht verarbeiteten Mail ist "wer +// war der Absender und an welche Adresse ging sie?". Rein lesend: +// imap_fetch_overview() und imap_fetchheader() setzen kein \Seen. +if (function_exists('imap_open')) { + echo "\n=== Postfachinhalt (nur lesend, letzte 15) ===\n"; + $mbox = @imap_open('{' . $host . ':' . $port . $flags . '}' . $mailbox, $user, $pass, OP_READONLY); + if ($mbox === false) { + echo 'Zugriff ueber die imap-Erweiterung fehlgeschlagen: ' . imap_last_error() . "\n"; + } else { + $anzahl = imap_num_msg($mbox); + $von = max(1, $anzahl - 14); + for ($msgno = $von; $msgno <= $anzahl; $msgno++) { + $overview = imap_fetch_overview($mbox, (string) $msgno, 0)[0] ?? null; + if ($overview === null) { + continue; + } + $kopf = imap_fetchheader($mbox, $msgno); + $empfaenger = []; + if (preg_match_all('/^(?:Delivered-To|X-Original-To|To|X-Forwarded-To):\s*(.+)$/im', $kopf, $alle)) { + $empfaenger = array_map('trim', $alle[1]); + } + $from = (string) ($overview->from ?? ''); + printf( + "#%-3d %s Von: %s\n An: %s\n Betreff: %s\n Absender gilt als PayPal: %s\n", + $msgno, + (int) ($overview->seen ?? 0) === 1 ? 'gelesen ' : 'UNGELESEN', + $from, + implode(' | ', $empfaenger) !== '' ? implode(' | ', $empfaenger) : '(keine Empfaengerzeile gefunden)', + (string) ($overview->subject ?? ''), + paypal_from_is_paypal($from) ? 'ja' : 'NEIN - deshalb not_from_paypal' + ); + } + imap_close($mbox); + } +} + echo "\nErgebnis: Postfach erreichbar und Zugangsdaten gueltig.\n"; if (function_exists('imap_open')) { echo "Die imap-Erweiterung ist vorhanden - scripts/fetch-paypal-payments.php\n" diff --git a/scripts/check-paypal-inbox-flow.php b/scripts/check-paypal-inbox-flow.php index f774491..2eea57a 100644 --- a/scripts/check-paypal-inbox-flow.php +++ b/scripts/check-paypal-inbox-flow.php @@ -234,6 +234,27 @@ try { $fremd = paypal_process_raw($pdo, $empfaengerA, 'no-reply@beispiel.de', $mailA); paypal_flow_assert('Mail von fremdem Absender wird abgelehnt', ($fremd['status'] ?? '') === 'not_from_paypal', $failures, $passes); + // Absenderpruefung: PayPal verschickt aus mehreren Domains, eine + // Lookalike-Domain darf aber nie durchgehen. + foreach ([ + '"PayPal" ' => true, + 'service@paypal.com' => true, + 'PayPal ' => true, + 'noreply@paypal.at' => true, + 'service@paypal.co.uk' => true, + 'betrug@paypal.de.beispiel.com' => false, + 'paypal@beispiel.de' => false, + 'clemens@beispiel.de' => false, + '' => false, + ] as $absender => $erwartet) { + paypal_flow_assert( + 'Absender "' . ($absender === '' ? '(leer)' : $absender) . '" gilt ' . ($erwartet ? 'als' : 'nicht als') . ' PayPal', + paypal_from_is_paypal($absender) === $erwartet, + $failures, + $passes + ); + } + $ohneToken = paypal_process_raw($pdo, 'zahlungen@kaffeeliste.de', 'service@paypal.de', $mailA); paypal_flow_assert('Adresse ohne Token wird abgelehnt', ($ohneToken['status'] ?? '') === 'no_token', $failures, $passes);