USP-Confirmed: Outlook-OAuth Casino-Bonus-Mail wurde end-to-end gefiltert (User-verifiziert). Mit dieser Welle ist der Daemon plus alle Scan-Pfade OAuth-aware. Backend — Mail-Stack (mo): - backend/server/utils/mail-auth.ts NEU: zentraler resolveImapAuth-Helper kapselt OAuth-vs-AppPassword-Entscheidung. 5-min-Token-Expiry-Puffer, race-condition-sicheres Refresh via refreshAndSaveTokens. - scan.post.ts + scan-internal.post.ts nutzen jetzt resolveImapAuth statt decrypt(passwordEncrypted). Vorher: Outlook-Connections wurden still übersprungen weil passwordEncrypted='' → decrypt failed. Cron + manueller Scan-Button funktionieren jetzt für OAuth-Connections. - imap-idle: Initial-Sweep via triggerScan(conn) direkt nach Connect-Success. Neue Outlook-Connections kriegen sofort einen Full-Folder-Scan statt bis zu 30 Min Cron-Lag zu warten. scan-internal scannt ohnehin schon alle Folders via imap.list() (Junk, Spam, Archive, Custom) — Multi-Folder- Anforderung ist damit erfüllt. Frontend — Mail-Page Polish v4 (rebreak-native-ui): - MailDistributionChart: Donut zurück auf 200px (240 wuchs auch in der Breite und quetschte die Legend), "Live"-Pill-Header komplett raus (paddingTop von 16 auf 13 reduziert für tighteres Layout) - mail.tsx Page-Hierarchie: "Mehr Infos"-Collapsible wandert von unter der Postfach-Liste direkt unter den Hero-Donut. Sub-Beschreibung "Blockiert — letzte 30 Tage" entfernt — Title reicht. - Account-Card Expanded: adaptive Bar-Chart über Connection-Age (too-new <24h zeigt Empty-State, 1-14d Day-Buckets via Backend ?connectionId=, 15-90d client-Week-Aggregation, >90d Month) - Account-Card Expanded: Scan-Button "Jetzt scannen" mit Refresh-Icon (Memory: kein Pen-Icon, refresh ok). Spinner während Scan, Feedback mit Blocked-Count nach Success. Eskalations-Hinweis (nicht in dieser Welle): - POST /api/mail/scan akzeptiert noch keinen connectionId-Filter → Scan-Button-Tap scannt aktuell alle Connections statt nur die angeklickte. Kleiner Folge-Patch, nicht blocking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.7 KiB
TypeScript
75 lines
2.7 KiB
TypeScript
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 };
|
|
|
|
/**
|
|
* 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).
|
|
* - 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, MS-Endpoint-Fehler)
|
|
*
|
|
* @param connection MailConnection-Felder (Subset)
|
|
* @param clientId MS Azure App Registration Client-ID (nur für OAuth-Pfad)
|
|
*/
|
|
export async function resolveImapAuth(
|
|
connection: MailConnectionAuthFields,
|
|
clientId: string,
|
|
): Promise<ImapAuth> {
|
|
if (connection.authMethod === "oauth2_microsoft") {
|
|
// 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 MS-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(
|
|
`oauth2_microsoft connection ${connection.id} has no oauthAccessToken stored`,
|
|
);
|
|
}
|
|
accessToken = decrypt(connection.oauthAccessToken);
|
|
}
|
|
|
|
return { user: connection.email, accessToken };
|
|
}
|
|
|
|
// App-Password-Pfad (gmail, 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 };
|
|
}
|