@…). * 2. Absender gegen paypal.de/.com pruefen. * 3. Body parsen, Netto ermitteln, per Transaktionscode deduplizieren. * 4. Eindeutiger Namens-Match -> automatisch als Einzahlung buchen, * sonst in die Warteschlange legen. * * Konfiguration ausschliesslich ueber Server-/Env-Einstellungen (Betreiber): * PAYPAL_INBOX_BASE z. B. zahlungen@kaffeeliste.de * PAYPAL_IMAP_HOST Mailserver-Host * PAYPAL_IMAP_PORT (Standard 993) * PAYPAL_IMAP_FLAGS (Standard "/imap/ssl") * PAYPAL_IMAP_USER Postfach-Benutzer * PAYPAL_IMAP_PASS Postfach-Passwort * PAYPAL_IMAP_MAILBOX (Standard "INBOX") * * Testmodus ohne IMAP (eine Rohmail aus einer Datei verarbeiten): * php scripts/fetch-paypal-payments.php --file=mail.html \ * --recipient=zahlungen+abc123@kaffeeliste.de --from=service@paypal.de */ if (PHP_SAPI !== 'cli') { http_response_code(403); exit("Nur fuer die Kommandozeile.\n"); } require_once __DIR__ . '/../app/paypal-inbox.php'; $pdo = app_db_pdo(); $opts = getopt('', ['file:', 'recipient:', 'from:', 'dry-run']); // ---- Testmodus: eine Datei verarbeiten (ohne IMAP) -------------------------- if (isset($opts['file'])) { $body = @file_get_contents($opts['file']); if ($body === false) { fwrite(STDERR, "Datei nicht lesbar: {$opts['file']}\n"); exit(1); } $recipient = (string) ($opts['recipient'] ?? ''); $from = (string) ($opts['from'] ?? 'service@paypal.de'); $result = paypal_process_raw($pdo, $recipient, $from, $body); echo 'Ergebnis: ' . json_encode($result, JSON_UNESCAPED_UNICODE) . "\n"; exit(0); } // ---- IMAP-Modus ------------------------------------------------------------- if (!function_exists('imap_open')) { fwrite(STDERR, "Die PHP-IMAP-Erweiterung ist nicht installiert. Auf dem Server 'php-imap' aktivieren.\n"); exit(1); } $host = app_env('PAYPAL_IMAP_HOST'); $user = app_env('PAYPAL_IMAP_USER'); $pass = app_env('PAYPAL_IMAP_PASS'); if ($host === null || $user === null || $pass === null) { fwrite(STDERR, "PAYPAL_IMAP_HOST/USER/PASS sind nicht gesetzt.\n"); exit(1); } $port = (int) (app_env('PAYPAL_IMAP_PORT', '993')); $flags = app_env('PAYPAL_IMAP_FLAGS', '/imap/ssl'); $mailboxName = app_env('PAYPAL_IMAP_MAILBOX', 'INBOX'); $dryRun = isset($opts['dry-run']); $ref = '{' . $host . ':' . $port . $flags . '}'; $mbox = @imap_open($ref . $mailboxName, $user, $pass); if ($mbox === false) { fwrite(STDERR, 'IMAP-Verbindung fehlgeschlagen: ' . imap_last_error() . "\n"); exit(1); } /** * Liefert den bevorzugten Textkoerper (HTML, sonst Plain) einer Mail als * dekodierten String. */ function paypal_imap_fetch_body($mbox, int $msgno): string { $structure = imap_fetchstructure($mbox, $msgno); // Einfache Mail ohne Parts. if (!isset($structure->parts) || !is_array($structure->parts)) { $raw = imap_body($mbox, $msgno); return paypal_imap_decode_part($raw, $structure->encoding ?? 0); } $htmlBody = null; $plainBody = null; foreach ($structure->parts as $i => $part) { $partNo = (string) ($i + 1); $data = imap_fetchbody($mbox, $msgno, $partNo); $decoded = paypal_imap_decode_part($data, $part->encoding ?? 0); $subtype = strtoupper((string) ($part->subtype ?? '')); if ($subtype === 'HTML') { $htmlBody = $decoded; } elseif ($subtype === 'PLAIN') { $plainBody = $decoded; } } return $htmlBody ?? $plainBody ?? ''; } function paypal_imap_decode_part(string $data, int $encoding): string { // 3 = BASE64, 4 = QUOTED-PRINTABLE (imap-Konstanten) if ($encoding === 3) { return (string) base64_decode($data, false); } if ($encoding === 4) { return quoted_printable_decode($data); } return $data; } $messageCount = imap_num_msg($mbox); $processed = 0; $booked = 0; $queued = 0; $skipped = 0; for ($msgno = 1; $msgno <= $messageCount; $msgno++) { $overview = imap_fetch_overview($mbox, (string) $msgno, 0)[0] ?? null; if ($overview === null || (int) ($overview->seen ?? 0) === 1) { continue; // nur ungelesene } $processed++; // Empfaengeradressen (inkl. Weiterleitungs-Header) nach dem Plus-Token // durchsuchen. $rawHeader = imap_fetchheader($mbox, $msgno); $recipient = ''; if (preg_match('/^(?:Delivered-To|X-Original-To|To|X-Forwarded-To):\s*(.+)$/im', $rawHeader, $mm)) { $recipient = trim($mm[1]); } // Falls mehrere Empfaengerzeilen: alle zusammenfuehren, damit der Token // sicher gefunden wird. if (preg_match_all('/^(?:Delivered-To|X-Original-To|To|X-Forwarded-To):\s*(.+)$/im', $rawHeader, $all)) { $recipient = implode(' ', $all[1]); } $from = (string) ($overview->from ?? ''); $body = paypal_imap_fetch_body($mbox, $msgno); $result = paypal_process_raw($pdo, $recipient, $from, $body); switch ($result['status']) { case 'booked': $booked++; break; case 'unmatched': $queued++; break; default: $skipped++; } echo "Mail #{$msgno}: {$result['status']}" . (isset($result['tenant_id']) ? " (Mandant {$result['tenant_id']})" : '') . "\n"; // Als gelesen markieren, damit sie nicht erneut verarbeitet wird. if (!$dryRun) { imap_setflag_full($mbox, (string) $msgno, '\\Seen'); } } imap_close($mbox); echo "Fertig: {$processed} verarbeitet, {$booked} gebucht, {$queued} in Warteschlange, {$skipped} uebersprungen" . ($dryRun ? ' (dry-run, nichts als gelesen markiert)' : '') . ".\n";