gezogen, weil der * dort am zuverlaessigsten steht. */ /** * Wandelt den (ggf. UTF-16-kodierten) Mail-Body in normalisierten UTF-8-Text um: * Skript/Style raus, Tags durch Leerzeichen ersetzt, Entities dekodiert, NBSP * und Mehrfach-Whitespace zusammengefasst. */ function paypal_mail_to_text(string $body): string { // UTF-16-BOM erkennen und nach UTF-8 wandeln (weitergeleitete .htm-Exporte). if (str_starts_with($body, "\xFF\xFE") || str_starts_with($body, "\xFE\xFF")) { $converted = @iconv('UTF-16', 'UTF-8//IGNORE', $body); if ($converted !== false) { $body = $converted; } } $body = preg_replace('#<(script|style)[^>]*>.*?#is', ' ', $body) ?? $body; $text = preg_replace('#<[^>]+>#', ' ', $body) ?? $body; $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = str_replace(["\xC2\xA0", "\xEF\xBB\xBF"], ' ', $text); // NBSP, BOM $text = preg_replace('/\s+/u', ' ', $text) ?? $text; return trim($text); } /** * Deutscher Betrag ("1.234,56") -> Cent. Null bei ungueltigem Format. */ function paypal_amount_to_cents(string $raw): ?int { $raw = trim($raw); $raw = str_replace('.', '', $raw); // Tausenderpunkte $raw = str_replace(',', '.', $raw); if (!preg_match('/^\d+(\.\d{1,2})?$/', $raw)) { return null; } return (int) round(((float) $raw) * 100); } /** * Deutsches Datum ("7. Juli 2026") -> "Y-m-d". Null, wenn nicht erkennbar. */ function paypal_german_date_to_iso(string $raw): ?string { $months = [ 'januar' => 1, 'februar' => 2, 'märz' => 3, 'maerz' => 3, 'april' => 4, 'mai' => 5, 'juni' => 6, 'juli' => 7, 'august' => 8, 'september' => 9, 'oktober' => 10, 'november' => 11, 'dezember' => 12, ]; if (!preg_match('/(\d{1,2})\.\s*([\p{L}]+)\s+(\d{4})/u', $raw, $m)) { return null; } $day = (int) $m[1]; $monthName = mb_strtolower_compat($m[2]); $year = (int) $m[3]; if (!isset($months[$monthName])) { return null; } return sprintf('%04d-%02d-%02d', $year, $months[$monthName], $day); } /** * Kleinschreibung ohne mbstring-Abhaengigkeit (nur fuer deutsche Monatsnamen * inkl. Umlaut noetig). */ function mb_strtolower_compat(string $s): string { if (function_exists('mb_strtolower')) { return mb_strtolower($s, 'UTF-8'); } $s = strtr($s, ['Ä' => 'ä', 'Ö' => 'ö', 'Ü' => 'ü']); return strtolower($s); } /** * Betrag in Cent aus dem Text nach einem Label ("Erhaltener Betrag" etc.). * Waehrungssymbol zwischen Zahl und "EUR" wird ignoriert. */ function paypal_extract_labeled_amount(string $text, string $labelRegex): ?int { if (preg_match('/' . $labelRegex . '\s*([\d.]+,\d{2})\s*\S*\s*EUR/u', $text, $m)) { return paypal_amount_to_cents($m[1]); } return null; } /** * Parst eine PayPal-Zahlungseingangs-Mail. * * @return array{ * payer_name: string, * transaction_code: ?string, * gross_cents: int, * fee_cents: ?int, * net_cents: int, * date: ?string, * note: ?string * }|null Null, wenn es keine erkennbare Zahlungseingangs-Mail ist. */ function paypal_parse_notification(string $body): ?array { $text = paypal_mail_to_text($body); // Kernmarker: " hat dir … EUR gesendet". Fehlt er, ist es // keine Zahlungseingangs-Mail (z. B. Werbung, Sicherheitsmail). if (!preg_match('/([\p{L}\p{M}.\'\- ]{2,60}?)\s+hat dir\s+([\d.]+,\d{2})\s*\S*\s*EUR\s+gesendet/u', $text, $m)) { return null; } $payer = trim($m[1]); $grossCents = paypal_amount_to_cents($m[2]) ?? 0; // Transaktionscode primaer aus der Detail-URL (steht dort am zuverlaessigsten), // sonst nach dem Label. $code = null; if (preg_match('#/activities/details/([A-Z0-9]{12,25})#', $body, $mc)) { $code = $mc[1]; } elseif (preg_match('/Transaktionscode\s+([A-Z0-9]{12,25})/u', $text, $mc)) { $code = $mc[1]; } $receivedCents = paypal_extract_labeled_amount($text, 'Erhaltener Betrag'); $feeCents = paypal_extract_labeled_amount($text, 'Geb(?:ü|ue)hr'); $summeCents = paypal_extract_labeled_amount($text, 'Summe'); // Netto = tatsaechlich angekommener Betrag: bei Waren & Dienstleistungen die // "Summe" (nach Abzug der Gebuehr), bei Freunde & Familie der erhaltene // Betrag (keine Gebuehr, keine Summe-Zeile), im Zweifel der Sende-Betrag. $netCents = $summeCents ?? $receivedCents ?? $grossCents; $date = null; if (preg_match('/Transaktionsdatum\s+(\d{1,2}\.\s*[\p{L}]+\s+\d{4})/u', $text, $md)) { $date = paypal_german_date_to_iso($md[1]); } // Mitteilung des Zahlers ("Mitteilung von ") bis zum naechsten // bekannten Abschnitt. Der Zahlername dient als Anker, da er Leerzeichen // enthalten kann. $note = null; $noteRegex = '/Mitteilung von\s+' . preg_quote($payer, '/') . '\s+(.+?)\s*(?:Geb(?:ü|ue)hr|Summe|Erhaltener Betrag|Transaktions|Lieferadresse|Du siehst|Bist du|Copyright|$)/u'; if (preg_match($noteRegex, $text, $mn)) { $note = trim($mn[1]); } return [ 'payer_name' => $payer, 'transaction_code' => $code, 'gross_cents' => $grossCents, 'fee_cents' => $feeCents, 'net_cents' => $netCents, 'date' => $date, 'note' => $note, ]; }