fix(mail): Junk-Folder IDLE-Gap + Layer 2.6 global Display-Name-Patterns
Task 1 — Junk-Folder Fix: - noopTimer (alle 2min) ruft jetzt triggerScan(conn) fire-and-forget auf - Outlook/Hotmail-Mails die direkt in "Junk Email" landen werden damit innerhalb von max. 2min erfasst (IDLE hört nur INBOX, kein exists-Event) - Consent-Guard analog exists-Event: nur wenn conn.consentAt gesetzt Task 2 — Layer 2.6 global Display-Name-Patterns: - getMailDisplayNamePatterns(userId) neu in db/domains.ts: lädt aus global_mail_display_names (admin-curated, pending Migration) + user_custom_domains type=mail_display_name (backward-compat) mit try/catch-Fallback bis Schema-Migration deployed ist - getCustomMailDisplayNames() als @deprecated markiert (bleibt für Übergangszeitraum) - scan-internal.post.ts: Import auf getMailDisplayNamePatterns umgestellt - mail-classifier.ts: Layer 2.6 Kommentar von "dead code" auf "live v1.1" aktualisiert Schema-Migration (global_mail_display_names) ist Aufgabe von rebreak-backend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4a9601aadb
commit
807847381f
@ -638,10 +638,20 @@ async function runSession(conn) {
|
|||||||
}, IDLE_RENEW_INTERVAL_MS);
|
}, IDLE_RENEW_INTERVAL_MS);
|
||||||
|
|
||||||
// NOOP-heartbeat alle 2min: silent-drop early-detection (GMX-pattern).
|
// NOOP-heartbeat alle 2min: silent-drop early-detection (GMX-pattern).
|
||||||
|
// Zusätzlich: fire-and-forget scan-internal bei jedem Tick (Junk-Folder-Fix).
|
||||||
|
// Outlook/Hotmail liefert kein INBOX-exists-Event für Mails die direkt in
|
||||||
|
// "Junk Email" landen — IDLE hört nur INBOX. Der 2min-Tick stellt sicher dass
|
||||||
|
// auch Junk-Ordner-Mails innerhalb von max. 2min erfasst werden.
|
||||||
|
// Consent-Gate sitzt in scan-internal → kein doppeltes Check hier nötig.
|
||||||
const noopTimer = setInterval(async () => {
|
const noopTimer = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
await imap.noop();
|
await imap.noop();
|
||||||
await updateIdleHeartbeat(conn.id).catch(() => {});
|
await updateIdleHeartbeat(conn.id).catch(() => {});
|
||||||
|
// Junk-Folder-Sweep: scan-internal scannt alle Ordner inkl. Junk.
|
||||||
|
// Nur auslösen wenn Consent erteilt (analog exists-Event-Guard).
|
||||||
|
if (conn.consentAt) {
|
||||||
|
triggerScan(conn).catch(() => {});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError(conn.email, "noop failed — connection dead, force reconnect", err);
|
logError(conn.email, "noop failed — connection dead, force reconnect", err);
|
||||||
imap.close();
|
imap.close();
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
updateMailConnectionScanStats,
|
updateMailConnectionScanStats,
|
||||||
insertMailClassificationSample,
|
insertMailClassificationSample,
|
||||||
} from "../../db/mail";
|
} from "../../db/mail";
|
||||||
import { getBlocklistedDomainsSet, getCustomMailDisplayNames } from "../../db/domains";
|
import { getBlocklistedDomainsSet, getMailDisplayNamePatterns } from "../../db/domains";
|
||||||
import { getProfile } from "../../db/profile";
|
import { getProfile } from "../../db/profile";
|
||||||
import { getPlanLimits } from "../../utils/plan-features";
|
import { getPlanLimits } from "../../utils/plan-features";
|
||||||
import { resolveProviderMeta } from "../../utils/imap-providers";
|
import { resolveProviderMeta } from "../../utils/imap-providers";
|
||||||
@ -149,7 +149,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const [blockedDomainSet, alreadyBlockedSet, customDisplayNames] = await Promise.all([
|
const [blockedDomainSet, alreadyBlockedSet, customDisplayNames] = await Promise.all([
|
||||||
getBlocklistedDomainsSet(senderDomains, userId, includeGlobal),
|
getBlocklistedDomainsSet(senderDomains, userId, includeGlobal),
|
||||||
getAlreadyBlockedUidSet(allUids, userId),
|
getAlreadyBlockedUidSet(allUids, userId),
|
||||||
getCustomMailDisplayNames(userId),
|
getMailDisplayNamePatterns(userId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const toInsert: Parameters<typeof insertMailBlocked>[0] = [];
|
const toInsert: Parameters<typeof insertMailBlocked>[0] = [];
|
||||||
|
|||||||
@ -97,10 +97,12 @@ export async function addUserCustomDomain(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt alle Display-Name-Patterns eines Users zurück.
|
* Gibt alle Display-Name-Patterns eines Users zurück (user-scope, Legacy).
|
||||||
* Wird vor jedem Mail-Scan geladen und an classifyMail() übergeben (Layer 2.6).
|
* Wird vor jedem Mail-Scan geladen und an classifyMail() übergeben (Layer 2.6).
|
||||||
*
|
*
|
||||||
* DSGVO: keine PII — User-eigene Heuristik-Patterns (z.B. "EXTRASPIN").
|
* DSGVO: keine PII — User-eigene Heuristik-Patterns (z.B. "EXTRASPIN").
|
||||||
|
*
|
||||||
|
* @deprecated Verwende getMailDisplayNamePatterns() — liefert global + user in einem Aufruf.
|
||||||
*/
|
*/
|
||||||
export async function getCustomMailDisplayNames(userId: string): Promise<string[]> {
|
export async function getCustomMailDisplayNames(userId: string): Promise<string[]> {
|
||||||
const db = usePrisma();
|
const db = usePrisma();
|
||||||
@ -111,6 +113,59 @@ export async function getCustomMailDisplayNames(userId: string): Promise<string[
|
|||||||
return rows.map((r) => r.domain);
|
return rows.map((r) => r.domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt alle aktiven Display-Name-Patterns zurück (global + user-scope, union).
|
||||||
|
*
|
||||||
|
* Globale Patterns kommen aus `global_mail_display_names` (admin-curated,
|
||||||
|
* analog BlocklistDomain). User-Patterns aus `user_custom_domains` mit
|
||||||
|
* type='mail_display_name' werden dazugemergt (backward-compat, falls manuell befüllt).
|
||||||
|
*
|
||||||
|
* Caching: caller-seitig (scan-internal lädt einmal pro Mailbox-Iteration).
|
||||||
|
*
|
||||||
|
* SCHEMA-ABHÄNGIGKEIT: Setzt `global_mail_display_names`-Tabelle voraus
|
||||||
|
* (Migration via rebreak-backend). Bis die Migration live ist, fällt die
|
||||||
|
* Funktion auf user-scope-only zurück (try/catch um Prisma-unknown-model-Error).
|
||||||
|
*
|
||||||
|
* DSGVO: keine Mail-Inhalte — reine Heuristik-Patterns (Art. 5 DSGVO).
|
||||||
|
*/
|
||||||
|
export async function getMailDisplayNamePatterns(userId: string): Promise<string[]> {
|
||||||
|
const db = usePrisma();
|
||||||
|
|
||||||
|
// User-scope (backward-compat): immer laden, Schema existiert bereits.
|
||||||
|
const userRows = await db.userCustomDomain.findMany({
|
||||||
|
where: { userId, type: "mail_display_name" },
|
||||||
|
select: { domain: true },
|
||||||
|
});
|
||||||
|
const userPatterns = userRows.map((r) => r.domain);
|
||||||
|
|
||||||
|
// Global-scope: aus global_mail_display_names.
|
||||||
|
// Try/catch: wenn Migration noch nicht deployed → graceful fallback auf user-only.
|
||||||
|
let globalPatterns: string[] = [];
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const globalRows = await (db as any).globalMailDisplayName.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
select: { pattern: true },
|
||||||
|
});
|
||||||
|
globalPatterns = globalRows.map((r: { pattern: string }) => r.pattern);
|
||||||
|
} catch {
|
||||||
|
// Tabelle existiert noch nicht (Migration pending) — user-only reicht als Übergangslösung.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplizieren (case-insensitive): wenn ein User-Pattern identisch zu einem
|
||||||
|
// globalen ist → brauchen wir es nur einmal im Array.
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const result: string[] = [];
|
||||||
|
for (const p of [...globalPatterns, ...userPatterns]) {
|
||||||
|
const key = p.toLowerCase();
|
||||||
|
if (!seen.has(key)) {
|
||||||
|
seen.add(key);
|
||||||
|
result.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteUserCustomDomain(id: string, userId: string) {
|
export async function deleteUserCustomDomain(id: string, userId: string) {
|
||||||
const db = usePrisma();
|
const db = usePrisma();
|
||||||
// Cannot delete submitted/approved domains (protect integrity)
|
// Cannot delete submitted/approved domains (protect integrity)
|
||||||
|
|||||||
@ -316,11 +316,11 @@ export interface ClassifyMailParams {
|
|||||||
/** Menge der geblockten Domains (aus getBlocklistedDomainsSet) */
|
/** Menge der geblockten Domains (aus getBlocklistedDomainsSet) */
|
||||||
blockedDomainSet: Set<string>;
|
blockedDomainSet: Set<string>;
|
||||||
/**
|
/**
|
||||||
* User-spezifische Display-Name-Patterns (aus getCustomMailDisplayNames).
|
* Display-Name-Patterns (global-curated + optional user-scope) aus getMailDisplayNamePatterns().
|
||||||
* Layer 2.6: case-insensitive Substring-Match gegen senderName.
|
* Layer 2.6: case-insensitive Substring-Match gegen senderName.
|
||||||
* Leer-Array wenn User keine Display-Name-Patterns gesetzt hat.
|
* Leer-Array solange keine Patterns geladen wurden.
|
||||||
*
|
*
|
||||||
* DSGVO: keine PII — reine Heuristik-Muster (z.B. ["EXTRASPIN"]).
|
* DSGVO: keine PII — reine Heuristik-Muster (z.B. ["Tipico", "Bet365"]).
|
||||||
*/
|
*/
|
||||||
customDisplayNames?: string[];
|
customDisplayNames?: string[];
|
||||||
}
|
}
|
||||||
@ -440,16 +440,15 @@ export async function classifyMail(params: ClassifyMailParams): Promise<Classifi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Layer 2.6: User-Custom-Display-Name-Hard-Block ──────────────────────────
|
// ── Layer 2.6: Display-Name-Hard-Block (global-curated + user-scope) ────────
|
||||||
// Display-name patterns disabled in v1.0 — re-enable when display-name input UX ships (v1.1).
|
// Patterns kommen aus getMailDisplayNamePatterns() — admin-curated globale
|
||||||
// getCustomMailDisplayNames() returns [] until mail_display_name rows exist,
|
// Gambling-Brand-Liste (z.B. "Tipico", "Bet365") plus optionale user-scope Patterns.
|
||||||
// so this block is dead code in practice. Keep logic intact for trivial re-activation.
|
|
||||||
//
|
//
|
||||||
// User-eigene Patterns (z.B. "EXTRASPIN") matchen case-insensitiv als Substring
|
// v1.1 (2026-05-28): von dead-code zu live — global_mail_display_names-Tabelle
|
||||||
// gegen den Sender-Display-Name. Kein Score — direkter Hard-Block wenn Match.
|
// als Datenquelle. Keine User-UI nötig; Admin pflegt die Liste manuell.
|
||||||
//
|
//
|
||||||
// Substring-Match (nicht exact) damit "EXTRASPIN Casino" und "ExtraSpin Bonus"
|
// Substring-Match (nicht exact) damit "Tipico Casino" und "TIPICO Bonus"
|
||||||
// beide von Pattern "EXTRASPIN" erfasst werden.
|
// beide von Pattern "Tipico" erfasst werden.
|
||||||
//
|
//
|
||||||
// Gambling-Brands rotieren aktiv Capitalization → case-insensitive ist Pflicht.
|
// Gambling-Brands rotieren aktiv Capitalization → case-insensitive ist Pflicht.
|
||||||
if (customDisplayNames && customDisplayNames.length > 0 && senderName) {
|
if (customDisplayNames && customDisplayNames.length > 0 && senderName) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user