Files
kaffeekasse-saas/scripts/check-imap-support.php
T

217 lines
8.6 KiB
PHP

<?php
declare(strict_types=1);
/**
* Diagnose fuer den PayPal-Mailabruf: klaert in einem Lauf, ob dieser Host
* das Postfach ueberhaupt erreichen kann - und auf welchem Weg.
*
* Geprueft wird:
* 1. PHP-Version und ob die imap-Erweiterung geladen ist (seit PHP 8.4 ist
* sie nicht mehr Teil von PHP, sondern nur noch ueber PECL zu haben).
* 2. Ob die PAYPAL_*-Einstellungen in der env.local.php gesetzt sind.
* 3. Ob sich das Postfach ohne die Erweiterung erreichen laesst - ueber
* eine reine TLS-Verbindung, wie sie auch ein Fallback nutzen wuerde.
*
* Der Test ist bewusst nur lesend: EXAMINE statt SELECT, kein Flag wird
* gesetzt, keine Mail veraendert. Das Passwort wird nie ausgegeben.
*
* Aufruf (Plesk: Geplante Aufgabe vom Typ "PHP-Skript"):
* /httpdocs/scripts/check-imap-support.php
*/
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit("Dieses Skript ist nur fuer die Kommandozeile gedacht.\n");
}
require_once __DIR__ . '/../app/bootstrap.php';
// paypal_from_is_paypal() - in der Auflistung nur als Information; die
// Verarbeitung selbst prueft den Absender nicht.
require_once __DIR__ . '/../app/paypal-inbox.php';
function imap_check_line(string $label, string $value): void
{
printf("%-28s %s\n", $label . ':', $value);
}
echo "=== PHP ===\n";
imap_check_line('Version', PHP_VERSION);
imap_check_line('imap-Erweiterung', function_exists('imap_open') ? 'geladen' : 'FEHLT');
imap_check_line('openssl', extension_loaded('openssl') ? 'vorhanden' : 'FEHLT');
imap_check_line('Sockets/Streams', function_exists('stream_socket_client') ? 'vorhanden' : 'FEHLT');
if (!function_exists('imap_open') && version_compare(PHP_VERSION, '8.4', '>=')) {
echo "Hinweis: Ab PHP 8.4 ist imap kein Bestandteil von PHP mehr. Entweder den\n"
. "PHP-Handler dieses Vhosts auf 8.3 stellen oder den Abruf ueber eine reine\n"
. "TLS-Verbindung laufen lassen (siehe Test unten).\n";
}
echo "\n=== Einstellungen (env.local.php) ===\n";
$host = app_env('PAYPAL_IMAP_HOST');
$user = app_env('PAYPAL_IMAP_USER');
$pass = app_env('PAYPAL_IMAP_PASS');
$port = (int) app_env('PAYPAL_IMAP_PORT', '993');
$flags = (string) app_env('PAYPAL_IMAP_FLAGS', '/imap/ssl');
$mailbox = (string) app_env('PAYPAL_IMAP_MAILBOX', 'INBOX');
imap_check_line('PAYPAL_INBOX_BASE', (string) (app_env('PAYPAL_INBOX_BASE') ?? '(nicht gesetzt)'));
imap_check_line('PAYPAL_IMAP_HOST', (string) ($host ?? '(nicht gesetzt)'));
imap_check_line('PAYPAL_IMAP_PORT', (string) $port);
imap_check_line('PAYPAL_IMAP_FLAGS', $flags);
imap_check_line('PAYPAL_IMAP_USER', (string) ($user ?? '(nicht gesetzt)'));
// Das Passwort selbst gehoert nicht in ein Protokoll, das per Mail
// verschickt oder weitergereicht wird - nur die Laenge als Tippfehler-Probe.
imap_check_line('PAYPAL_IMAP_PASS', $pass === null ? '(nicht gesetzt)' : 'gesetzt (' . strlen($pass) . ' Zeichen)');
imap_check_line('PAYPAL_IMAP_MAILBOX', $mailbox);
if ($host === null || $user === null || $pass === null) {
echo "\nOhne Host, Benutzer und Passwort ist kein Verbindungstest moeglich.\n";
exit(1);
}
/**
* Liest die Antwort bis zur Zeile mit dem gesuchten Tag.
*
* @return array{ok: bool, lines: list<string>}
*/
function imap_check_read($sock, string $tag): array
{
$lines = [];
while (($line = fgets($sock)) !== false) {
$line = rtrim($line, "\r\n");
$lines[] = $line;
if (str_starts_with($line, $tag . ' ')) {
return ['ok' => str_starts_with($line, $tag . ' OK'), 'lines' => $lines];
}
}
return ['ok' => false, 'lines' => $lines];
}
function imap_check_send($sock, string $tag, string $command): array
{
fwrite($sock, $tag . ' ' . $command . "\r\n");
return imap_check_read($sock, $tag);
}
echo "\n=== Verbindungstest ohne imap-Erweiterung (nur lesend) ===\n";
// Port 993 = TLS von Anfang an; alles andere (typisch 143) = STARTTLS.
$implizitesTls = $port === 993 || str_contains($flags, '/ssl');
$adresse = ($implizitesTls ? 'ssl://' : 'tcp://') . $host . ':' . $port;
imap_check_line('Verbindung', $adresse . ($implizitesTls ? ' (TLS sofort)' : ' (STARTTLS)'));
$fehlerNr = 0;
$fehlerText = '';
$sock = @stream_socket_client($adresse, $fehlerNr, $fehlerText, 15);
if ($sock === false) {
echo "FEHLER: Verbindung nicht moeglich ({$fehlerNr}: {$fehlerText})\n";
echo "Moegliche Ursachen: Host/Port falsch, ausgehende Verbindungen gesperrt,\n"
. "oder TLS-Zertifikat nicht akzeptiert.\n";
exit(1);
}
stream_set_timeout($sock, 15);
$greeting = rtrim((string) fgets($sock), "\r\n");
imap_check_line('Begruessung', $greeting !== '' ? $greeting : '(keine)');
if (!$implizitesTls) {
$start = imap_check_send($sock, 'a0', 'STARTTLS');
if (!$start['ok'] || !@stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
echo "FEHLER: STARTTLS fehlgeschlagen - ohne Verschluesselung wird nicht angemeldet.\n";
exit(1);
}
imap_check_line('STARTTLS', 'ok');
}
// Anfuehrungszeichen und Backslashes im Passwort maskieren (IMAP-Quoting).
$quote = static fn(string $wert): string => '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $wert) . '"';
$login = imap_check_send($sock, 'a1', 'LOGIN ' . $quote($user) . ' ' . $quote($pass));
if (!$login['ok']) {
echo "FEHLER: Anmeldung abgelehnt.\n";
echo ' Antwort: ' . end($login['lines']) . "\n";
fwrite($sock, "a9 LOGOUT\r\n");
fclose($sock);
exit(1);
}
imap_check_line('Anmeldung', 'ok');
// EXAMINE statt SELECT: oeffnet das Postfach ausdruecklich nur lesend.
$examine = imap_check_send($sock, 'a2', 'EXAMINE ' . $quote($mailbox));
if (!$examine['ok']) {
echo "FEHLER: Postfach '{$mailbox}' nicht zu oeffnen.\n";
echo ' Antwort: ' . end($examine['lines']) . "\n";
fwrite($sock, "a9 LOGOUT\r\n");
fclose($sock);
exit(1);
}
$gesamt = 0;
foreach ($examine['lines'] as $line) {
if (preg_match('/^\* (\d+) EXISTS/', $line, $m)) {
$gesamt = (int) $m[1];
}
}
imap_check_line('Postfach', $mailbox . ' geoeffnet');
imap_check_line('Mails gesamt', (string) $gesamt);
$search = imap_check_send($sock, 'a3', 'SEARCH UNSEEN');
$ungelesen = 0;
foreach ($search['lines'] as $line) {
if (str_starts_with($line, '* SEARCH')) {
$ids = trim(substr($line, strlen('* SEARCH')));
$ungelesen = $ids === '' ? 0 : count(preg_split('/\s+/', $ids) ?: []);
}
}
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 sieht nach PayPal aus: %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 (wird trotzdem verarbeitet)'
);
}
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"
. "laeuft ohne weitere Aenderung. Naechster Schritt: derselbe Task mit --dry-run.\n";
} else {
echo "Die imap-Erweiterung fehlt, das Postfach ist aber ueber die reine\n"
. "TLS-Verbindung erreichbar - ein Abruf ohne Erweiterung ist also moeglich.\n";
}