['path' => $tmp . '/uploads', 'max_size_bytes' => 100000, 'allowed_extensions' => ['png', 'pdf', 'txt', 'docx']]])); require_once dirname(__DIR__) . '/includes/bootstrap.php'; require_once dirname(__DIR__) . '/includes/mail_fetch_manual.php'; $failures = []; $check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; }; $pdo = get_db(); $docx = random_bytes(700); $png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='); $html = '

Hallo Team

Zweiter Absatz mit fett

Logo'; $mail = "From: Olaf Andresen \r\nTo: einsendungen@example.org\r\nSubject: Aw: AW: [#923425] Haus Ledwin Teil 11\r\nMessage-ID: \r\nDate: Mon, 03 Nov 2026 00:39:00 +0100\r\nMIME-Version: 1.0\r\n" . "Content-Type: multipart/mixed; boundary=\"BND1\"\r\n\r\n" . "--BND1\r\nContent-Type: multipart/related; boundary=\"BND2\"\r\n\r\n" . "--BND2\r\nContent-Type: multipart/alternative; boundary=\"BND3\"\r\n\r\n" . "--BND3\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\nHallo Team\r\n\r\nZweiter Absatz mit fett\r\n" . "--BND3\r\nContent-Type: text/html; charset=utf-8\r\nContent-Transfer-Encoding: base64\r\n\r\n" . chunk_split(base64_encode($html)) . "--BND3--\r\n" . "--BND2\r\nContent-Type: image/png; name=\"logo.png\"\r\nContent-Disposition: inline; filename=\"logo.png\"\r\nContent-ID: \r\nContent-Transfer-Encoding: base64\r\n\r\n" . chunk_split(base64_encode($png)) . "--BND2--\r\n" . "--BND1\r\nContent-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document; name=\"Teil 12.docx\"\r\nContent-Disposition: attachment; filename=\"Teil 12.docx\"\r\nContent-Transfer-Encoding: base64\r\n\r\n" . chunk_split(base64_encode($docx)) . "--BND1--\r\n"; file_put_contents($tmp . '/mail.eml', $mail); $port = 0; for ($try = 0; $try < 20 && !$port; $try++) { $p = random_int(20000, 60000); $s = @stream_socket_server('tcp://127.0.0.1:' . $p); if ($s) { fclose($s); $port = $p; } } $proc = proc_open([PHP_BINARY, __DIR__ . '/fixtures/fake_imap_server.php', (string) $port, $tmp . '/mail.eml', $tmp . '/imap.log'], [0 => ['file', 'php://stdin', 'r'], 1 => ['file', $tmp . '/srv.out', 'w'], 2 => ['file', $tmp . '/srv.err', 'w']], $pipes); for ($i = 0; $i < 50; $i++) { $c = @fsockopen('127.0.0.1', $port, $en, $es, 0.2); if ($c) { fclose($c); break; } usleep(100000); } try { $deptId = (int) $pdo->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn(); $pdo->prepare("INSERT INTO imap_accounts (label, host, port, encryption, username, password_enc, folder, after_fetch, department_id, priority_id, is_active) VALUES ('Test','127.0.0.1',?,'none','u',?,'INBOX','seen',?,2,1)") ->execute([$port, encrypt_secret('geheim'), $deptId]); $now = date('Y-m-d H:i:s'); // Vorhandenes Ticket mit der alten Nummer, beantwortet (Status "answered") $pdo->prepare("INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES ('923425','Haus Ledwin Teil 11','Olaf','olaf@example.de',?,2,'answered','email',?,?)")->execute([$deptId, $now, $now]); $tid = (int) $pdo->lastInsertId(); $pdo->exec("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES ({$tid},'customer','erste Nachricht',0,'{$now}')"); $before = (int) $pdo->query('SELECT COUNT(*) FROM tickets')->fetchColumn(); $r = mail_fetch_manual($pdo); $check($r['ok'] && $r['processed'] === 1 && $r['replies'] === 1 && $r['new_tickets'] === 0, 'Abruf: 1 Antwort, kein neues Ticket: ' . json_encode($r) . ' | ' . @file_get_contents($tmp . '/srv.err')); $check((int) $pdo->query('SELECT COUNT(*) FROM tickets')->fetchColumn() === $before, 'es ist kein neues Ticket entstanden'); $threads = $pdo->query("SELECT * FROM ticket_threads WHERE ticket_id = {$tid} ORDER BY id")->fetchAll(PDO::FETCH_ASSOC); $check(count($threads) === 2 && $threads[1]['author_type'] === 'customer' && str_contains((string) $threads[1]['message'], 'Hallo Team'), 'Antwort hängt als Kundennachricht am Ticket'); $check(trim((string) ($threads[1]['message_html'] ?? '')) !== '', 'HTML-Fassung der Antwort gespeichert'); $att = $pdo->query('SELECT original_name, is_inline FROM ticket_attachments WHERE thread_id = ' . (int) ($threads[1]['id'] ?? 0) . ' ORDER BY is_inline')->fetchAll(PDO::FETCH_ASSOC); $check(count($att) === 2 && $att[0]['original_name'] === 'Teil 12.docx', 'Anhang und Bild hängen an der Antwort: ' . json_encode($att)); $check((string) $pdo->query("SELECT status FROM tickets WHERE id = {$tid}")->fetchColumn() === 'open', 'das beantwortete Ticket ist wieder offen'); $check(str_contains(implode(" ", array_column($pdo->query("SELECT action FROM activity_log WHERE ticket_id = {$tid}")->fetchAll(PDO::FETCH_ASSOC), 'action')), 'Antwort per E-Mail empfangen'), 'Verlaufseintrag "Antwort per E-Mail empfangen"'); } finally { if (is_resource($proc)) { proc_terminate($proc); proc_close($proc); } } $logTxt = (string) @file_get_contents($tmp . '/imap.log'); $rm = function (string $d) use (&$rm) { foreach (scandir($d) ?: [] as $e) { if ($e === '.' || $e === '..') continue; is_dir("$d/$e") ? $rm("$d/$e") : @unlink("$d/$e"); } @rmdir($d); }; if ($failures) $failures[] = 'IMAP-Protokoll (Ende): ' . substr($logTxt, -1200); $rm($tmp); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "Mail fetch end-to-end checks passed.\n";