105 lines
5.4 KiB
PHP
105 lines
5.4 KiB
PHP
<?php
|
|
/**
|
|
* includes/mail_threading.php
|
|
*
|
|
* Ordnet eine eingehende Mail einem bestehenden Ticket zu (Antwort statt neues Ticket):
|
|
* 1. Ticketnummer im Betreff "[#260101-0001]" (so wie bisher, ohne weitere Prüfung);
|
|
* 2. andere Nummern im Betreff "[#923425]" (zum Beispiel aus einem früheren Ticketsystem), wenn es ein Ticket mit genau dieser Nummer gibt;
|
|
* 3. die Kopfzeilen In-Reply-To und References: DeskHub schreibt in jede Mail zu einem Ticket eine Message-ID mit der Ticketnummer
|
|
* (<deskhub.260101-0001.…@host>), 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 (<deskhub.NUMMER.zufall@host>) 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 '<deskhub.' . $m[1] . '.' . bin2hex(random_bytes(8)) . '@' . $host . '>';
|
|
}
|
|
|
|
/** 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;
|
|
}
|