import { refreshAndSaveTokens } from "../db/mail"; import { decrypt } from "./crypto"; /** * MailConnection-Shape: nur die Felder die für Auth-Resolution nötig sind. * Beide Scan-Endpoints bekommen das volle Prisma-Objekt — dieses Interface * dient als explizites Subset damit der Helper nicht vom vollen Typ abhängt. */ export interface MailConnectionAuthFields { id: string; email: string; authMethod: string; passwordEncrypted: string; oauthAccessToken?: string | null; oauthTokenExpiry?: Date | null; } export type ImapAuth = | { user: string; accessToken: string } | { user: string; pass: string }; export interface OauthClientIds { /** Azure App Registration Client ID (MS_OAUTH_CLIENT_ID). */ msClientId: string; /** Google Cloud OAuth Client ID (GOOGLE_OAUTH_CLIENT_ID). */ googleClientId: string; } /** * Gibt das korrekte `auth`-Objekt für ImapFlow zurück. * * - oauth2_microsoft: Access-Token decrypten, bei Ablauf via MS-Endpoint refreshen. * Nutzt refreshAndSaveTokens() aus db/mail (Race-Condition-sicher, Prisma-basiert). * - oauth2_google: Analog zu oauth2_microsoft. Google rotiert refresh_tokens nicht, * aber der 5-Minuten-Puffer-Refresh gilt trotzdem (access_token ist 1h gültig). * - Alle anderen authMethods (app_password, default): passwordEncrypted decrypten. * * Wirft wenn: * - App-Password leer oder decrypt fehlschlägt * - OAuth-Token fehlt und kein Refresh möglich * - refreshAndSaveTokens() wirft (revoked refresh_token, Provider-Endpoint-Fehler) * * @param connection MailConnection-Felder (Subset) * @param clientIds Beide OAuth Client-IDs; die richtige wird anhand authMethod gewählt. */ export async function resolveImapAuth( connection: MailConnectionAuthFields, clientIds: OauthClientIds, ): Promise { if ( connection.authMethod === "oauth2_microsoft" || connection.authMethod === "oauth2_google" ) { // Wähle den richtigen Client-ID anhand des authMethod-Diskriminators. const clientId = connection.authMethod === "oauth2_google" ? clientIds.googleClientId : clientIds.msClientId; // Token-Expiry-Check: 5-Minuten-Puffer damit der Scan nicht // mitten in einem großen Mailbox-Durchlauf mit abgelaufenem Token stirbt. const fiveMinFromNow = Date.now() + 5 * 60 * 1000; const isExpiredOrMissing = !connection.oauthTokenExpiry || connection.oauthTokenExpiry.getTime() < fiveMinFromNow; let accessToken: string; if (isExpiredOrMissing) { // Wirft wenn Refresh-Token fehlt oder Provider-Endpoint antwortet mit Fehler. // Caller (scan.post / scan-internal.post) soll per try/catch continue-n. accessToken = await refreshAndSaveTokens(connection.id, clientId); } else { if (!connection.oauthAccessToken) { throw new Error( `${connection.authMethod} connection ${connection.id} has no oauthAccessToken stored`, ); } accessToken = decrypt(connection.oauthAccessToken); } return { user: connection.email, accessToken }; } // App-Password-Pfad (gmail app-password, icloud, gmx, yahoo, custom) if (!connection.passwordEncrypted) { throw new Error( `Connection ${connection.id} has no passwordEncrypted (authMethod=${connection.authMethod})`, ); } const pass = decrypt(connection.passwordEncrypted); return { user: connection.email, pass }; }