prepare("INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at) VALUES (?,?,?,?,1,2,'open','email',?,?)") ->execute([$number, 'Test ' . $number, 'Kunde', $email, $now, $now]); $id = (int) $pdo->lastInsertId(); return $pdo->query("SELECT * FROM tickets WHERE id = {$id}")->fetch(PDO::FETCH_ASSOC); }; $a = $mk('260101-0001', 'olaf@example.de'); $legacy = $mk('923425', 'olaf@example.de'); $b = $mk('260101-0002', 'anna@example.de'); $pdo->prepare('INSERT INTO ticket_email_sources (ticket_id, imap_account_id, message_uid, message_id, current_folder, created_at) VALUES (?,?,?,?,?,?)') ->execute([(int) $b['id'], 1, 5, '', 'INBOX', $now]); // Kopfzeilen lesen $raw = "From: x\r\nIn-Reply-To: \r\nReferences: \r\n \r\n\t\r\nSubject: s\r\n"; $ids = mail_reference_ids($raw); $check($ids === ['abc@x.de', 'r1@x.de', 'r2@x.de', 'r3@x.de'], 'In-Reply-To und gefaltete References werden gelesen: ' . json_encode($ids)); $check(mail_reference_ids("Subject: nur\r\n") === [], 'ohne Kopfzeilen keine IDs'); // Message-ID mit Ticketnummer: erzeugen und wieder lesen $mid = mail_ticket_message_id('Neue Antwort [#260101-0001] Ihr Ticket', 'support.example.de'); $check($mid !== null && preg_match('/^$/', $mid) === 1, 'Message-ID mit Nummer: ' . $mid); $check(mail_ticket_number_from_message_id(trim((string) $mid, '<>')) === '260101-0001', 'Nummer wird aus der Message-ID gelesen'); $check(mail_ticket_message_id('Ohne Nummer', 'x.de') === null, 'Betreff ohne Nummer: keine Message-ID'); $check(mail_ticket_number_from_message_id('irgendwas@mail.example.de') === null, 'fremde Message-ID enthält keine Nummer'); // 1. Nummer im Betreff (DeskHub-Format): wie bisher ohne Absenderprüfung $r = mail_find_ticket_for_reply($pdo, 'AW: [#260101-0001] Frage', '', 'jemand-anders@example.org'); $check($r && (int) $r['ticket']['id'] === (int) $a['id'] && $r['via'] === 'subject', 'DeskHub-Nummer im Betreff ordnet zu, wie bisher'); // 2. Andere Nummer im Betreff $r = mail_find_ticket_for_reply($pdo, 'Aw: AW: [#923425] Haus Ledwin Teil 11', '', 'Olaf@Example.de'); $check($r && (int) $r['ticket']['id'] === (int) $legacy['id'] && $r['via'] === 'legacy', 'fremde Nummer (923425) ordnet zu, wenn der Absender der Auftraggeber ist: ' . json_encode($r['via'] ?? null)); $check(mail_find_ticket_for_reply($pdo, 'Aw: [#923425] x', '', 'fremder@example.org') === null, 'fremde Nummer von einem fremden Absender wird nicht zugeordnet'); $check(mail_find_ticket_for_reply($pdo, 'Aw: [#99999999] x', '', 'olaf@example.de') === null, 'Nummer, die es nicht gibt: neues Ticket'); // 3. Kopfzeilen $r = mail_find_ticket_for_reply($pdo, 'Betreff geändert', "References: \r\n", 'olaf@example.de'); $check($r && (int) $r['ticket']['id'] === (int) $a['id'] && $r['via'] === 'header', 'DeskHub-Message-ID in References ordnet zu, auch bei geändertem Betreff'); $check(mail_find_ticket_for_reply($pdo, 'Betreff geändert', "In-Reply-To: \r\n", 'fremder@example.org') === null, 'DeskHub-Message-ID von fremdem Absender wird nicht zugeordnet'); $r = mail_find_ticket_for_reply($pdo, 'Neuer Betreff', "In-Reply-To: \r\n", 'ANNA@example.de'); $check($r && (int) $r['ticket']['id'] === (int) $b['id'] && $r['via'] === 'header', 'Message-ID einer früher abgerufenen Mail ordnet zu (Groß-/Kleinschreibung egal)'); $check(mail_find_ticket_for_reply($pdo, 'Neuer Betreff', "In-Reply-To: \r\n", 'olaf@example.de') === null, 'Mail-ID eines fremden Tickets: Absender passt nicht'); $check(mail_find_ticket_for_reply($pdo, 'Hallo', '', 'olaf@example.de') === null, 'ohne Nummer und ohne Kopfzeilen: neues Ticket'); $check(mail_find_ticket_for_reply($pdo, 'Hallo', "In-Reply-To: \r\n", 'olaf@example.de') === null, 'unbekannte Message-ID: neues Ticket'); // Früherer Verfasser darf antworten $pdo->exec("INSERT INTO users (name, email, password_hash, created_at) VALUES ('Kollegin','kollegin@example.de','x','{$now}')"); $uid = (int) $pdo->lastInsertId(); $pdo->exec("INSERT INTO ticket_threads (ticket_id, author_type, user_id, message, is_internal_note, created_at) VALUES ({$a['id']},'customer',{$uid},'x',0,'{$now}')"); $check(mail_sender_belongs_to_ticket($pdo, $a, 'kollegin@example.de'), 'früherer Verfasser einer Kundennachricht gehört zum Ticket'); $check(!mail_sender_belongs_to_ticket($pdo, $a, ''), 'leerer Absender gehört nirgends hin'); // Ausgehende Mail: Header mit Message-ID (nur wenn der Betreff eine Nummer hat) require_once dirname(__DIR__) . '/includes/smtp_mailer.php'; $mailer = new SmtpMailer('localhost', 25, 'none', '', ''); $m = new ReflectionMethod($mailer, 'buildHeaders'); $m->setAccessible(true); $h1 = (string) $m->invoke($mailer, 'kunde@example.de', 'Support ', 'Antwort auf Ihr Ticket [#260101-0001]'); $h2 = (string) $m->invoke($mailer, 'kunde@example.de', 'Support ', 'Willkommen'); $check(preg_match('/^Message-ID: ?$/m', $h1) === 1, 'ausgehende Ticketmail trägt Message-ID mit Nummer und Domain des Absenders'); $check(!str_contains($h2, 'Message-ID'), 'Mail ohne Ticketnummer bekommt keine eigene Message-ID'); if ($failures) { fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "Mail threading checks passed.\n";