'/imap/ssl', 'tls' => '/imap/tls', default => '/imap/notls', }; $mailbox = '{' . $account['host'] . ':' . $account['port'] . $sslFlag . '}' . $folder; return @imap_open($mailbox, $account['username'], decrypt_secret($account['password_enc']), 0, 1); } function imap_normalize_message_id(?string $value): string { return trim(strtolower((string) $value), " \t\r\n<>"); } /** * Verschiebt EINE bereits als Ticket importierte Mail in einen Ziel-Ordner * (Spam beim Markieren, ursprünglicher Posteingang beim Rückgängigmachen). * Aktualisiert bei Erfolg current_folder in ticket_email_sources, damit * nachfolgende Aufrufe (erneutes Markieren, Rückgängig machen) wissen, wo die * Mail gerade tatsächlich liegt. */ function move_ticket_email_source(PDO $pdo, array $source, array $account, string $targetFolder): array { if (!function_exists('imap_open')) { return ['ok' => false, 'message' => 'PHP-IMAP ist nicht installiert.']; } $sourceFolder = trim((string) ($source['current_folder'] ?? '')); $targetFolder = trim($targetFolder); if ($sourceFolder === '' || $targetFolder === '') { return ['ok' => false, 'message' => 'IMAP-Quell- oder Zielordner fehlt.']; } if (strcasecmp($sourceFolder, $targetFolder) === 0) { return ['ok' => true, 'message' => 'Die E-Mail befindet sich bereits in „' . $targetFolder . '“.']; } $mbox = imap_connect_account($account, $sourceFolder); if (!$mbox) { return ['ok' => false, 'message' => 'IMAP-Quellordner konnte nicht geöffnet werden: ' . (imap_last_error() ?: 'unbekannter Fehler')]; } $emailNum = null; $uid = (int) ($source['message_uid'] ?? 0); if ($uid > 0) { $candidate = @imap_msgno($mbox, $uid); if ($candidate > 0) $emailNum = $candidate; } // Nach einem vorherigen Verschieben kann sich die UID geändert haben. Dann über Message-ID suchen. if (!$emailNum && !empty($source['message_id'])) { $targetId = imap_normalize_message_id($source['message_id']); $hits = @imap_search($mbox, 'ALL'); if ($hits) { foreach ($hits as $num) { $h = @imap_headerinfo($mbox, $num); $mid = imap_normalize_message_id($h->message_id ?? ''); if ($mid !== '' && $mid === $targetId) { $emailNum = (int) $num; break; } } } } if (!$emailNum) { imap_close($mbox); return ['ok' => false, 'message' => 'Original-E-Mail wurde im IMAP-Ordner nicht gefunden.']; } if (!@imap_mail_move($mbox, (string) $emailNum, $targetFolder)) { $err = imap_last_error() ?: 'unbekannter IMAP-Fehler'; imap_close($mbox); return ['ok' => false, 'message' => 'Verschieben nach „' . $targetFolder . '“ fehlgeschlagen: ' . $err]; } @imap_expunge($mbox); imap_close($mbox); if (!empty($source['id'])) { $pdo->prepare('UPDATE ticket_email_sources SET current_folder = ? WHERE id = ?') ->execute([$targetFolder, (int) $source['id']]); } return ['ok' => true, 'message' => 'E-Mail nach „' . $targetFolder . '“ verschoben.']; } /** * Markiert ein E-Mail-Ticket als Spam, oder macht das (mit $restore = true) * wieder rückgängig. Gemeinsam genutzt von Web und App. * * @return array{error: ?string, moved: int, already: int, failed: string[]} */ function set_ticket_spam(PDO $pdo, array $ticket, int $staffId, bool $restore = false): array { if (($ticket['source'] ?? '') !== 'email') { return ['error' => 'Nur per E-Mail eingegangene Tickets können als Spam markiert werden.', 'moved' => 0, 'already' => 0, 'failed' => []]; } if ($restore && empty($ticket['is_spam'])) { return ['error' => 'Dieses Ticket ist nicht als Spam markiert.', 'moved' => 0, 'already' => 0, 'failed' => []]; } if (!$restore && !empty($ticket['is_spam'])) { return ['error' => 'Dieses Ticket ist bereits als Spam markiert.', 'moved' => 0, 'already' => 0, 'failed' => []]; } $srcStmt = $pdo->prepare( 'SELECT tes.*, ia.label, ia.spam_folder, ia.folder AS inbox_folder, ia.host, ia.port, ia.encryption, ia.username, ia.password_enc FROM ticket_email_sources tes JOIN imap_accounts ia ON ia.id = tes.imap_account_id WHERE tes.ticket_id = ? ORDER BY tes.id' ); $srcStmt->execute([(int) $ticket['id']]); $sources = $srcStmt->fetchAll(); $moved = 0; $already = 0; $failed = []; foreach ($sources as $source) { $targetFolder = $restore ? ($source['inbox_folder'] ?: 'INBOX') : ($source['spam_folder'] ?: 'Junk'); $result = move_ticket_email_source($pdo, $source, $source, $targetFolder); if ($result['ok']) { if (str_contains($result['message'], 'befindet sich bereits')) { $already++; } else { $moved++; } } else { $failed[] = ($source['label'] ?: 'Postfach') . ': ' . $result['message']; } } $now = db_now(); if ($restore) { $pdo->prepare('UPDATE tickets SET is_spam = 0, updated_at = ? WHERE id = ?')->execute([$now, $ticket['id']]); log_activity($pdo, $ticket['id'], $staffId, 'Spam-Markierung aufgehoben'); } else { $pdo->prepare( "UPDATE tickets SET is_spam=1, status='closed', resolution_code='spam', resolution_note='Als Spam markiert', closed_at=COALESCE(closed_at, ?), updated_at=?, locked_by=NULL, locked_at=NULL WHERE id=?" )->execute([$now, $now, $ticket['id']]); ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], 'closed', $staffId); finalize_work_session($pdo, (int) $ticket['id'], $staffId); apply_tag_to_ticket($pdo, (int) $ticket['id'], 'Spam'); log_activity($pdo, $ticket['id'], $staffId, 'Ticket als Spam markiert'); } return ['error' => null, 'moved' => $moved, 'already' => $already, 'failed' => $failed]; }