), die Mailprogramme in der Antwort mitschicken. Außerdem zählen die Message-IDs früher * abgerufener Mails des Tickets. * Bei 2 und 3 (die nicht schon durch die DeskHub-Nummer im Betreff gesichert sind) muss der Absender zum Ticket gehören * (Auftraggeber oder früherer Verfasser), damit niemand mit einer geratenen Nummer fremden Tickets Nachrichten unterschieben kann. */ /** Message-IDs aus In-Reply-To und References, klein und ohne spitze Klammern. @return string[] */ function mail_reference_ids(string $rawHeader): array { $ids = []; // Kopfzeilen können gefaltet sein (Fortsetzungszeilen beginnen mit Leerraum) $unfolded = preg_replace('/\r?\n[ \t]+/', ' ', $rawHeader) ?? $rawHeader; foreach (['In-Reply-To', 'References'] as $name) { if (preg_match_all('/^' . $name . ':(.*)$/mi', $unfolded, $m)) { foreach ($m[1] as $value) { if (preg_match_all('/<([^<>\s]+)>/', $value, $mm)) foreach ($mm[1] as $id) $ids[] = strtolower(trim($id)); } } } return array_values(array_unique($ids)); } /** Ticketnummer aus einer von DeskHub erzeugten Message-ID () oder null. */ function mail_ticket_number_from_message_id(string $id): ?string { return preg_match('/^deskhub\.([a-z0-9-]{4,24})\.[a-z0-9]+@/i', $id, $m) ? $m[1] : null; } /** Message-ID für eine ausgehende Mail zu einem Ticket (Nummer steht im Betreff als [#NUMMER]); null, wenn der Betreff keine Nummer enthält. */ function mail_ticket_message_id(string $subject, string $host): ?string { if (!preg_match('/\[#([A-Za-z0-9-]{4,24})\]/', $subject, $m)) return null; $host = preg_replace('/[^A-Za-z0-9.-]/', '', $host) ?: 'localhost'; return ''; } /** Gehört der Absender zum Ticket (Auftraggeber oder früherer Verfasser einer Kundennachricht)? */ function mail_sender_belongs_to_ticket(PDO $pdo, array $ticket, string $fromEmail): bool { $from = strtolower(trim($fromEmail)); if ($from === '') return false; if (strtolower(trim((string) ($ticket['requester_email'] ?? ''))) === $from) return true; try { $stmt = $pdo->prepare("SELECT 1 FROM ticket_threads tt JOIN users u ON u.id = tt.user_id WHERE tt.ticket_id = ? AND tt.author_type = 'customer' AND LOWER(u.email) = ? LIMIT 1"); $stmt->execute([(int) $ticket['id'], $from]); return (bool) $stmt->fetchColumn(); } catch (Throwable $e) { return false; } } /** * Sucht das Ticket zu einer eingehenden Mail. * * @return array{ticket:array,via:string}|null via: subject (DeskHub-Nummer), legacy (andere Nummer im Betreff), header (In-Reply-To/References) */ function mail_find_ticket_for_reply(PDO $pdo, string $subject, string $rawHeader, string $fromEmail): ?array { $byNumber = $pdo->prepare('SELECT * FROM tickets WHERE LOWER(ticket_number) = LOWER(?)'); // 1. DeskHub-Nummer im Betreff wie bisher if (preg_match('/\[#([0-9]{6}-[0-9]{4})\]/', $subject, $m)) { $byNumber->execute([$m[1]]); if ($t = $byNumber->fetch()) return ['ticket' => $t, 'via' => 'subject']; } // 2. Andere Nummer im Betreff (genau diese Nummer muss es als Ticket geben) if (preg_match_all('/\[#([A-Za-z0-9-]{4,24})\]/', $subject, $all)) { foreach ($all[1] as $num) { $byNumber->execute([$num]); if (($t = $byNumber->fetch()) && mail_sender_belongs_to_ticket($pdo, $t, $fromEmail)) return ['ticket' => $t, 'via' => 'legacy']; } } // 3. Kopfzeilen $ids = mail_reference_ids($rawHeader); foreach ($ids as $id) { $num = mail_ticket_number_from_message_id($id); if ($num !== null) { $byNumber->execute([$num]); if (($t = $byNumber->fetch()) && mail_sender_belongs_to_ticket($pdo, $t, $fromEmail)) return ['ticket' => $t, 'via' => 'header']; } } if ($ids) { // Message-IDs früher abgerufener Mails des Tickets (gespeichert mit oder ohne spitze Klammern): grob in SQL vorfiltern, genau in PHP prüfen $ids = array_slice($ids, 0, 15); $likes = implode(' OR ', array_fill(0, count($ids), 'LOWER(message_id) LIKE ?')); $stmt = $pdo->prepare("SELECT ticket_id, message_id FROM ticket_email_sources WHERE message_id IS NOT NULL AND ({$likes}) ORDER BY id DESC LIMIT 50"); $stmt->execute(array_map(fn($id) => '%' . str_replace(['%', '_'], ['\%', '\_'], $id) . '%', $ids)); foreach ($stmt->fetchAll() as $row) { if (!in_array(strtolower(trim((string) $row['message_id'], '<> ')), $ids, true)) continue; $byId = $pdo->prepare('SELECT * FROM tickets WHERE id = ?'); $byId->execute([(int) $row['ticket_id']]); if (($t = $byId->fetch()) && mail_sender_belongs_to_ticket($pdo, $t, $fromEmail)) return ['ticket' => $t, 'via' => 'header']; } } return null; }