import { CURRENT_ART9_MAIL_VERSION } from "../../utils/consent-texts"; import { writeConsentGrant, setMailConnectionConsent, } from "../../db/consent"; import { countMailConnections, upsertMailConnection, } from "../../db/mail"; import { getProfile } from "../../db/profile"; import { getPlanLimits } from "../../utils/plan-features"; import { detectImapProviderAsync } from "../../utils/imap-providers"; import { ImapFlow } from "imapflow"; /** * POST /api/mail-connections/:id * * Gateway-Endpoint für das Anlegen einer neuen MailConnection — mit Art. 9-Consent-Check. * * Wenn kein gültiger Consent vorliegt, antwortet der Endpoint mit 412 BEFORE * jede IMAP-Verbindung versucht wird. Das Frontend muss dann: * 1. Das Consent-Modal anzeigen (Art. 9-Text) * 2. User bestätigt → POST /api/mail-connections/consent * 3. Danach diesen Endpoint erneut aufrufen (mit consentVersion im Body) * * Body: * email: string (required) * password: string (required) * consentVersion: string (required — muss CURRENT_ART9_MAIL_VERSION entsprechen) * imapHost?: string * imapPort?: number * useTls?: boolean * rejectUnauthorized?: boolean * * Response: * 200: { connected: true, email, provider, custom } * 412: { error: 'consent_required', consentVersion: string } ← Frontend zeigt Modal * 400: { error: 'invalid_body' } * 401: { error: 'imap_auth_failed' } * 403: { error: 'plan_limit', ... } * * HINWEIS: Dieser Endpoint ersetzt NICHT connect.post.ts — er ist ein paralleler * Pfad mit explizitem Consent-Gate. Der bestehende /api/mail/connect bleibt * vorerst aktiv (Abwärtskompatibilität), sollte aber mittelfristig auf diesen * Endpoint migriert werden. */ export default defineEventHandler(async (event) => { const user = await requireUser(event); const body = await readBody(event).catch(() => null); if (!body) { throw createError({ statusCode: 400, data: { error: "invalid_body" }, }); } const { email, password, consentVersion, imapHost: customImapHost, imapPort: customImapPort, useTls, rejectUnauthorized, } = body as { email?: string; password?: string; consentVersion?: string; imapHost?: string; imapPort?: number; useTls?: boolean; rejectUnauthorized?: boolean; }; if (!email || !password) { throw createError({ statusCode: 400, data: { error: "invalid_body" }, }); } // ── Art. 9-Consent-Gateway ──────────────────────────────────────────────── // Keine Einwilligung → sofort 412, bevor IMAP-Verbindung aufgebaut wird. if (!consentVersion || consentVersion !== CURRENT_ART9_MAIL_VERSION) { throw createError({ statusCode: 412, data: { error: "consent_required", consentVersion: CURRENT_ART9_MAIL_VERSION, }, }); } // ── Plan-Limit prüfen ───────────────────────────────────────────────────── const profile = await getProfile(user.id); const limits = getPlanLimits(profile?.plan ?? "free"); if (limits.mailAgents !== Infinity) { const count = await countMailConnections(user.id); if (count >= limits.mailAgents) { throw createError({ statusCode: 403, data: { error: "plan_limit", resource: "mail_accounts", current: count, limit: limits.mailAgents, }, }); } } // ── IMAP-Provider-Detection ─────────────────────────────────────────────── const provider = await detectImapProviderAsync(email); const resolvedHost = customImapHost?.trim() || provider.host; const resolvedPort = customImapPort ?? provider.port; const useImplicitTls = useTls !== false; const tlsRejectUnauthorized = rejectUnauthorized !== false; const useStarttls = useTls === false; // ── IMAP-Verbindung testen ──────────────────────────────────────────────── const client = new ImapFlow({ host: resolvedHost, port: resolvedPort, secure: useImplicitTls, ...(useStarttls ? { requireTLS: true } : {}), auth: { user: email, pass: password }, logger: false, tls: { rejectUnauthorized: tlsRejectUnauthorized }, }); try { await client.connect(); await client.logout(); } catch (err: any) { throw createError({ statusCode: 401, data: { error: "imap_auth_failed", detail: err.message ?? "connection_failed", }, }); } // ── Consent-Zeitstempel & Audit-Log VOR dem Upsert ─────────────────────── const now = new Date(); const ipAddress = getHeader(event, "x-forwarded-for")?.split(",")[0]?.trim() ?? getHeader(event, "x-real-ip") ?? null; const userAgent = getHeader(event, "user-agent") ?? null; // MailConnection anlegen/updaten const connection = await upsertMailConnection({ userId: user.id, email, provider: "imap", providerName: customImapHost ? resolvedHost : provider.name, imapHost: resolvedHost, imapPort: resolvedPort, passwordEncrypted: encrypt(password), rejectUnauthorized: tlsRejectUnauthorized, useStarttls, }); // consent_at + version auf der Connection setzen await setMailConnectionConsent({ connectionId: connection.id, userId: user.id, consentAt: now, consentVersion, consentIpAddress: ipAddress, }); // Append-only Audit-Log await writeConsentGrant({ userId: user.id, consentType: "art9-mail", consentVersion, consentAt: now, ipAddress, userAgent, mailConnectionId: connection.id, }); return { connected: true, email, provider: customImapHost ? resolvedHost : provider.name, custom: !!customImapHost, }; });