import { ImapFlow } from "imapflow"; import { getMailConnections, deleteOldMailBlocked, getAlreadyBlockedUidSet, insertMailBlocked, updateMailConnectionScanStats, } from "../../db/mail"; import { getBlocklistedDomainsSet } from "../../db/domains"; import { getProfile } from "../../db/profile"; import { getPlanLimits } from "../../utils/plan-features"; // Single-Source-of-Truth (Mo's Finding #4) // @ts-expect-error — .mjs ohne types, GAMBLING_KEYWORDS ist string[] import { GAMBLING_KEYWORDS } from "../../utils/gambling-keywords.mjs"; /** * POST /api/mail/scan * Scannt ALLE Ordner (INBOX, Spam, Papierkorb, All Mail …) nach Gambling-Mails. * Free-User: nur eigene Domains + Keywords. Pro/Legend: globale Blocklist + eigene. */ export default defineEventHandler(async (event) => { const user = await requireUser(event); const connections = await getMailConnections(user.id); if (connections.length === 0) { throw createError({ statusCode: 404, message: "Kein Mail-Konto verbunden", }); } // Plan-aware: Free users get only custom domains, Pro/Legend get global blocklist const profile = await getProfile(user.id); const limits = getPlanLimits(profile?.plan ?? "free"); const includeGlobal = limits.globalBlocklist; await deleteOldMailBlocked(user.id); let totalScanned = 0; let totalBlocked = 0; for (const connection of connections) { let password: string; try { password = decrypt(connection.passwordEncrypted); } catch { continue; } // useStarttls=true → STARTTLS (secure=false + requireTLS=true) // rejectUnauthorized=false → self-signed Certs zulassen (nur Custom-IMAP) const useImplicitTls = !connection.useStarttls; const imap = new ImapFlow({ host: connection.imapHost, port: connection.imapPort, secure: useImplicitTls, ...(connection.useStarttls ? { requireTLS: true } : {}), auth: { user: connection.email, pass: password }, logger: false, tls: { rejectUnauthorized: connection.rejectUnauthorized ?? true }, }); let scanned = 0; let newlyBlocked = 0; try { await imap.connect(); // Scan ALL mailbox folders (not just hardcoded list) const mailboxes = await imap.list(); const scannable = mailboxes.filter( (mb: any) => !mb.flags?.has("\\Noselect"), ); for (const mb of scannable) { let lock: any; try { lock = await imap.getMailboxLock(mb.path); } catch { continue; } try { const SCAN_LIMIT = 200; const status = await imap.status(mb.path, { messages: true }); const msgCount = (status as any).messages ?? 0; if (msgCount === 0) continue; const fetchRange = msgCount > SCAN_LIMIT ? `${msgCount - SCAN_LIMIT + 1}:*` : "1:*"; const allMessages = await imap.fetchAll(fetchRange, { envelope: true, }); scanned += allMessages.length; totalScanned += allMessages.length; const allUids = allMessages.map( (m: any) => `${mb.path}:${String(m.uid ?? m.seq)}`, ); const [blockedDomainSet, alreadyBlockedSet] = await Promise.all([ getBlocklistedDomainsSet( allMessages .map( (m: any) => (m.envelope?.from?.[0]?.address ?? "") .toLowerCase() .split("@")[1] ?? "", ) .filter(Boolean), user.id, includeGlobal, ), getAlreadyBlockedUidSet(allUids, user.id), ]); const toInsert: Parameters[0] = []; const uidsToDelete: string[] = []; for (const msg of allMessages) { const from = msg.envelope?.from?.[0]; const senderEmail = (from?.address ?? "").toLowerCase(); const senderName = from?.name ?? null; const subject = (msg.envelope?.subject ?? "").trim(); const msgDate = msg.envelope?.date ?? new Date(); const uid = `${mb.path}:${String(msg.uid ?? msg.seq)}`; const haystack = `${senderEmail} ${subject}`.toLowerCase(); const isGamblingKeyword = GAMBLING_KEYWORDS.some((kw) => haystack.includes(kw), ); const senderDomain = senderEmail.split("@")[1] ?? ""; const isBlocklisted = senderDomain ? blockedDomainSet.has(senderDomain) : false; if (!isGamblingKeyword && !isBlocklisted) continue; if (alreadyBlockedSet.has(uid)) continue; uidsToDelete.push(String(msg.uid)); toInsert.push({ userId: user.id, connectionId: connection.id, gmailMessageId: uid, senderEmail: senderEmail || "unbekannt", senderName, subject: subject.slice(0, 200) || "(kein Betreff)", receivedAt: msgDate, action: "deleted", }); newlyBlocked++; } // Permanently delete gambling mails from this folder if (uidsToDelete.length > 0) { try { await imap.messageDelete(uidsToDelete.join(","), { uid: true }); } catch { try { for (const uid of uidsToDelete) { await imap .messageFlagsAdd(uid, ["\\Deleted"], { uid: true }) .catch(() => {}); } await imap.expunge(); } catch { /* ignore */ } } } await insertMailBlocked(toInsert); } finally { lock.release(); } } await imap.logout(); } catch { try { await imap.logout(); } catch {} } totalBlocked += newlyBlocked; await updateMailConnectionScanStats( connection.id, scanned, newlyBlocked, connection.emailsBlocked, connection.emailsScanned, connection.scanInterval, ); } return { scanned: totalScanned, blocked: totalBlocked }; });