['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_reload.php'; $failures = []; $check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; }; $pdo = get_db(); // Mail wie von Outlook: Text und HTML (alternative) + eingebettetes Logo (related) + Word-Datei (mixed) $docx = random_bytes(900); $png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='); $html = '

und de nächste Teil

Absatz zwei fett

Logox'; $mail = "From: Olaf Andresen \r\nTo: einsendungen@example.org\r\nSubject: Aw: [#260101-0001] 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: quoted-printable\r\n\r\nund de n=C3=A4chste Teil\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=\"Haus Ledwin .docx\"\r\nContent-Disposition: attachment; filename=\"Haus Ledwin .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); // Server starten $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; } } $logFile = $tmp . '/imap.log'; $proc = proc_open([PHP_BINARY, __DIR__ . '/fixtures/fake_imap_server.php', (string) $port, $tmp . '/mail.eml', $logFile], [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 { $pdo->prepare("INSERT INTO imap_accounts (label, host, port, encryption, username, password_enc, folder) VALUES ('Test','127.0.0.1',?, 'none','u',?, 'INBOX')") ->execute([$port, encrypt_secret('geheim')]); $accountId = (int) $pdo->lastInsertId(); $now = date('Y-m-d H:i:s'); $pdo->exec("INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES ('260101-0001','Haus Ledwin','Olaf','olaf@example.de',1,2,'open','email','{$now}','{$now}')"); $ticketId = (int) $pdo->lastInsertId(); $early = date('Y-m-d H:i:s', time() - 3600); $pdo->exec("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES ({$ticketId},'customer','erste Nachricht',0,'{$early}')"); $firstThread = (int) $pdo->lastInsertId(); $pdo->exec("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES ({$ticketId},'customer','und de nächste Teil',0,'{$now}')"); $replyThread = (int) $pdo->lastInsertId(); $pdo->exec("INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at) VALUES ({$ticketId},'staff','Antwort',0,'{$now}')"); $pdo->prepare('INSERT INTO ticket_email_sources (ticket_id, imap_account_id, message_uid, message_id, current_folder, created_at) VALUES (?,?,?,?,?,?)') ->execute([$ticketId, $accountId, 7, '', 'INBOX', $now]); $ticket = $pdo->query("SELECT * FROM tickets WHERE id = {$ticketId}")->fetch(PDO::FETCH_ASSOC); // Zuordnung zur Nachricht $threads = $pdo->query("SELECT id, created_at FROM ticket_threads WHERE ticket_id = {$ticketId} AND author_type = 'customer' ORDER BY id")->fetchAll(PDO::FETCH_ASSOC); $check(mail_reload_pick_thread($threads, $now) === $replyThread, 'Mail wird der Nachricht zugeordnet, die zur Zeit des Abrufs entstand'); $check(mail_reload_pick_thread($threads, $early) === $firstThread, 'frühe Mail gehört zur ersten Nachricht'); $r = reload_ticket_mail_attachments($pdo, $ticket, 1); $check($r['error'] === null && $r['mails'] === 1 && $r['added'] === 1 && $r['inline'] === 1 && $r['html'] === 1 && $r['failed'] === [], 'Nachladen: 1 Anhang übernommen: ' . json_encode($r) . ' | Server: ' . @file_get_contents($tmp . '/srv.err')); $rows = $pdo->query("SELECT * FROM ticket_attachments WHERE thread_id = {$replyThread}")->fetchAll(PDO::FETCH_ASSOC); $files = array_values(array_filter($rows, fn($r) => (int) $r['is_inline'] === 0)); $inl = array_values(array_filter($rows, fn($r) => (int) $r['is_inline'] === 1)); $check(count($files) === 1 && $files[0]['original_name'] === 'Haus Ledwin .docx', 'Anhang hängt an der richtigen Nachricht: ' . json_encode($rows)); $check(count($inl) === 1 && $inl[0]['content_id'] === 'logo1@example.de', 'das Logo ist als eingebettetes Bild gespeichert (nicht in der Anhangliste)'); $check($inl && file_get_contents($tmp . '/uploads/' . $inl[0]['stored_name']) === $png, 'Inhalt des eingebetteten Bildes stimmt'); $mh = (string) $pdo->query("SELECT message_html FROM ticket_threads WHERE id = {$replyThread}")->fetchColumn(); $check(str_contains($mh, '

und de nächste Teil

') && str_contains($mh, 'fett') && str_contains($mh, 'cid:logo1@example.de'), 'HTML-Fassung der Nachricht ist gespeichert: ' . substr($mh, 0, 200)); $check(!str_contains($mh, ' 'placeholder', 'cid_map' => ['logo1@example.de' => '/download.php?id=' . (int) $inl[0]['id'] . '&preview=1']]); $check(str_contains($disp, 'src="/download.php?id=' . (int) $inl[0]['id'] . '&preview=1"') && !str_contains($disp, 'tracker.example.com') && str_contains($disp, 'data-blocked-remote'), 'Anzeige: eingebettetes Bild wird gezeigt, fremdes Bild nicht geladen: ' . $disp); $check((int) $pdo->query("SELECT COUNT(*) FROM ticket_attachments WHERE thread_id = {$firstThread}")->fetchColumn() === 0, 'die erste Nachricht bekommt nichts'); $check((int) $pdo->query("SELECT COUNT(*) FROM activity_log WHERE ticket_id = {$ticketId} AND action LIKE 'Anhänge und Bilder aus der E-Mail nachgeladen%'")->fetchColumn() === 1, 'Verlaufseintrag "nachgeladen"'); // Zweiter Durchlauf: nichts doppelt $r2 = reload_ticket_mail_attachments($pdo, $ticket, 1); $check($r2['added'] === 0 && $r2['inline'] === 0 && $r2['html'] === 0 && $r2['existing'] === 2, 'zweiter Durchlauf speichert nichts doppelt: ' . json_encode($r2)); $check((int) $pdo->query("SELECT COUNT(*) FROM ticket_attachments WHERE thread_id = {$replyThread}")->fetchColumn() === 2, 'weiterhin genau ein Anhang und ein eingebettetes Bild'); // UID stimmt nicht mehr (Mail wurde verschoben): Suche über die Message-ID $pdo->exec("UPDATE ticket_email_sources SET message_uid = 999 WHERE ticket_id = {$ticketId}"); $pdo->exec("DELETE FROM ticket_attachments WHERE thread_id = {$replyThread}"); $r3 = reload_ticket_mail_attachments($pdo, $ticket, 1); $check($r3['added'] === 1 && $r3['inline'] === 1, 'Mail wird über die Message-ID gefunden, wenn die UID nicht mehr passt: ' . json_encode($r3)); // Mail nicht auffindbar (andere Message-ID, andere UID) $pdo->exec("UPDATE ticket_email_sources SET message_uid = 999, message_id = '' WHERE ticket_id = {$ticketId}"); $r4 = reload_ticket_mail_attachments($pdo, $ticket, 1); $check($r4['added'] === 0 && count($r4['failed']) === 1 && str_contains($r4['failed'][0], 'nicht gefunden'), 'nicht auffindbare Mail wird gemeldet: ' . json_encode($r4)); // Kein Ticket aus E-Mail $pdo->exec("DELETE FROM ticket_email_sources WHERE ticket_id = {$ticketId}"); $r5 = reload_ticket_mail_attachments($pdo, $ticket, 1); $check($r5['error'] !== null && str_contains($r5['error'], 'keine Original-E-Mail'), 'Ticket ohne gespeicherte Mail: verständliche Meldung'); } finally { $pdo->exec('DELETE FROM imap_accounts'); if (is_resource($proc)) { proc_terminate($proc); proc_close($proc); } } $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); }; $logTxt = (string) @file_get_contents($logFile); if ($failures) $failures[] = 'IMAP-Protokoll (Ende): ' . substr($logTxt, -1500); $rm($tmp); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "Mail reload checks passed.\n";