Mail-Page-Refactor — Privacy-friendly + DiGA-tauglich:
- Custom title pro mail-connection (z.B. "Privat-Gmail" statt voller E-Mail).
Memory-Pattern: Anonymität via Nickname jetzt auch für Mail-Adressen
sichtbar, Datenminimierung. Title nullable, Fallback auf Email-Domain.
- Schema-Migration mail_connection_title (additiv, NULL default für Bestand)
- Endpoint PATCH /api/mail-connections/:id mit title-Validation (max 60,
trim, leerer String → NULL)
- "Passwort ändern"-Collapsible → vollwertige "Einstellungen"-Sektion:
Title editieren · Email read-only · Passwort neu setzen · Verbindung
trennen (mit Confirm-Dialog)
- EditMailTitleSheet als FormSheet-Pattern für Title-Edit
- mailConnectDraft-Store kriegt Title-Feld für Pre-Fill bei Re-Open
Zwei neue Stats-Charts auf der Mail-Page:
- MailBlockedByDayChart — 30-Tage-Bar-Chart, Plain-View-Bars (Pattern wie
Sparkline-Profile), Empty-State bei 0 Cooldowns
· Backend: GET /api/mail/stats/blocked-by-day?days=30
- MailDistributionChart — Half-Donut via react-native-svg, Top-5 Connections
+ "Sonstige", rendert nicht bei ≤1 Connection
· Backend: GET /api/mail/stats/blocked-by-connection
Activity-Log mit Provider-Filter:
- Filter-Chips Mo Gmail/Outlook/iCloud/etc. über bestehendem Activity-Log
- GET /api/mail/results?provider=X (war vorher hardcoded all)
- Endpoint-Naming-Fix in useMailResults (war /api/mail/blocked, jetzt
korrekt /api/mail/results — UI-Agent hatte falschen Path geraten)
Backend-Side-Effects:
- imap-providers util resolveProviderMeta(host) — gibt {provider, label,
isCustomDomain} zurück, von 3 Endpoints konsumiert
- /api/mail/status erweitert: title, provider, providerLabel,
isCustomDomain im Account-Shape
- /api/mail/results erweitert: connection-Sub-Objekt pro Entry +
provider-Filter-Query
Open follow-ups (TODOs):
- deleteOldMailBlocked-Cron löscht <24h → Bar-Chart-Daten weg. Retention
auf 90 Tage hochsetzen oder Cron stoppen.
- POST /api/mail/connect könnte die neue connection.id im Response
mitliefern → Title-PATCH direkt ohne Extra-GET (UI-Agent-Empfehlung).
- /api/mail/status zeigt nur active Connections — paused mit Title wären
unsichtbar. Entscheiden.
18 neue i18n-Keys (mail.title_*, mail.settings_*, mail.row_*,
mail.disconnect_confirm_*, mail.stats.*, mail.filter.all) in DE + EN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
7.1 KiB
TypeScript
172 lines
7.1 KiB
TypeScript
import dns from "node:dns/promises";
|
|
|
|
/**
|
|
* IMAP-Provider Konfigurationen
|
|
* Automatisch erkennen anhand der Email-Domain
|
|
*/
|
|
export interface ImapConfig {
|
|
host: string;
|
|
port: number;
|
|
name: string;
|
|
}
|
|
|
|
const PROVIDER_MAP: Record<string, ImapConfig> = {
|
|
// Google
|
|
"gmail.com": { host: "imap.gmail.com", port: 993, name: "Gmail" },
|
|
"googlemail.com": { host: "imap.gmail.com", port: 993, name: "Gmail" },
|
|
// Apple
|
|
"icloud.com": { host: "imap.mail.me.com", port: 993, name: "iCloud" },
|
|
"me.com": { host: "imap.mail.me.com", port: 993, name: "iCloud" },
|
|
"mac.com": { host: "imap.mail.me.com", port: 993, name: "iCloud" },
|
|
// Microsoft
|
|
"outlook.com": { host: "outlook.office365.com", port: 993, name: "Outlook" },
|
|
"hotmail.com": { host: "outlook.office365.com", port: 993, name: "Hotmail" },
|
|
"hotmail.de": { host: "outlook.office365.com", port: 993, name: "Hotmail" },
|
|
"live.com": { host: "outlook.office365.com", port: 993, name: "Live" },
|
|
"live.de": { host: "outlook.office365.com", port: 993, name: "Live" },
|
|
"msn.com": { host: "outlook.office365.com", port: 993, name: "Outlook" },
|
|
// Yahoo
|
|
"yahoo.com": { host: "imap.mail.yahoo.com", port: 993, name: "Yahoo" },
|
|
"yahoo.de": { host: "imap.mail.yahoo.com", port: 993, name: "Yahoo" },
|
|
"yahoo.co.uk": { host: "imap.mail.yahoo.com", port: 993, name: "Yahoo" },
|
|
"ymail.com": { host: "imap.mail.yahoo.com", port: 993, name: "Yahoo" },
|
|
// GMX / Web.de (United Internet)
|
|
"gmx.de": { host: "imap.gmx.net", port: 993, name: "GMX" },
|
|
"gmx.net": { host: "imap.gmx.net", port: 993, name: "GMX" },
|
|
"gmx.at": { host: "imap.gmx.net", port: 993, name: "GMX" },
|
|
"gmx.ch": { host: "imap.gmx.net", port: 993, name: "GMX" },
|
|
"web.de": { host: "imap.web.de", port: 993, name: "Web.de" },
|
|
// Telekom
|
|
"t-online.de": { host: "secureimap.t-online.de", port: 993, name: "T-Online" },
|
|
"magenta.de": { host: "secureimap.t-online.de", port: 993, name: "T-Online" },
|
|
// Freenet
|
|
"freenet.de": { host: "mx.freenet.de", port: 993, name: "Freenet" },
|
|
// Posteo
|
|
"posteo.de": { host: "posteo.de", port: 993, name: "Posteo" },
|
|
// Tutanota / Tuta
|
|
"tutanota.com": { host: "mail.tutanota.com", port: 993, name: "Tutanota" },
|
|
"tuta.io": { host: "mail.tutanota.com", port: 993, name: "Tutanota" },
|
|
// IONOS / 1&1 (direkte Consumer-Domains)
|
|
"ionos.de": { host: "imap.ionos.de", port: 993, name: "IONOS" },
|
|
"1und1.de": { host: "imap.ionos.de", port: 993, name: "IONOS" },
|
|
"1and1.com": { host: "imap.ionos.de", port: 993, name: "IONOS" },
|
|
"1and1.de": { host: "imap.ionos.de", port: 993, name: "IONOS" },
|
|
"1blu.de": { host: "imap.1blu.de", port: 993, name: "1blu" },
|
|
};
|
|
|
|
/**
|
|
* MX-Record-Patterns die IONOS-Hosting-Infrastruktur identifizieren.
|
|
* IONOS-Kunden mit eigener Domain nutzen mx00.ionos.de oder kundenserver.de.
|
|
* Quelle: IONOS Hilfe-Seite + eigene DNS-Lookups (verifiziert 2026-05).
|
|
*/
|
|
const MX_IONOS_PATTERNS = [
|
|
"ionos.de",
|
|
"kundenserver.de",
|
|
"perfora.net",
|
|
"ui-dns.de",
|
|
"ui-dns.org",
|
|
"ui-dns.biz",
|
|
"ui-dns.com",
|
|
];
|
|
|
|
/**
|
|
* Versucht via MX-Record-Lookup den Provider einer Custom-Domain zu erkennen.
|
|
* Wird nur aufgerufen wenn PROVIDER_MAP keinen direkten Treffer hat.
|
|
* Timeout: 3s (schnell genug für Connect-Flow, kein User-Impact).
|
|
*/
|
|
async function detectImapProviderByMx(domain: string): Promise<ImapConfig | null> {
|
|
try {
|
|
const mxRecords = await Promise.race([
|
|
dns.resolveMx(domain),
|
|
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("timeout")), 3000)),
|
|
]);
|
|
|
|
for (const mx of mxRecords) {
|
|
const exchange = mx.exchange.toLowerCase();
|
|
if (MX_IONOS_PATTERNS.some((pattern) => exchange.endsWith(pattern))) {
|
|
return { host: "imap.ionos.de", port: 993, name: "IONOS" };
|
|
}
|
|
}
|
|
} catch {
|
|
// DNS-Lookup fehlgeschlagen oder Timeout — kein Problem, Fallback greift
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function detectImapProvider(email: string): ImapConfig {
|
|
const domain = email.split("@")[1]?.toLowerCase() ?? "";
|
|
return (
|
|
PROVIDER_MAP[domain] ?? {
|
|
host: `imap.${domain}`,
|
|
port: 993,
|
|
name: domain,
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Async-Variante mit MX-Lookup-Fallback für Custom-Domains.
|
|
* Nutzen wenn detectImapProvider keinen bekannten Provider zurückgibt
|
|
* (erkennbar daran dass name === domain, also kein Friendly-Name).
|
|
*/
|
|
export async function detectImapProviderAsync(email: string): Promise<ImapConfig> {
|
|
const domain = email.split("@")[1]?.toLowerCase() ?? "";
|
|
const direct = PROVIDER_MAP[domain];
|
|
if (direct) return direct;
|
|
|
|
const byMx = await detectImapProviderByMx(domain);
|
|
if (byMx) return byMx;
|
|
|
|
// Letzter Fallback: imap.<domain> — Standard-Konvention
|
|
return { host: `imap.${domain}`, port: 993, name: domain };
|
|
}
|
|
|
|
const SMTP_MAP: Record<string, { host: string; port: number }> = {
|
|
"imap.gmail.com": { host: "smtp.gmail.com", port: 587 },
|
|
"imap.mail.me.com": { host: "smtp.mail.me.com", port: 587 },
|
|
"imap.gmx.net": { host: "mail.gmx.net", port: 587 },
|
|
"imap.web.de": { host: "smtp.web.de", port: 587 },
|
|
"outlook.office365.com": { host: "smtp-mail.outlook.com", port: 587 },
|
|
"imap.mail.yahoo.com": { host: "smtp.mail.yahoo.com", port: 587 },
|
|
"secureimap.t-online.de": { host: "securesmtp.t-online.de", port: 587 },
|
|
"mx.freenet.de": { host: "mx.freenet.de", port: 587 },
|
|
"posteo.de": { host: "posteo.de", port: 587 },
|
|
};
|
|
|
|
export function detectSmtpProvider(imapHost: string): { host: string; port: number } {
|
|
return SMTP_MAP[imapHost] ?? { host: imapHost.replace("imap.", "smtp."), port: 587 };
|
|
}
|
|
|
|
/**
|
|
* Leitet einen normalisierten Provider-Key + Label aus dem gespeicherten
|
|
* providerName/imapHost einer MailConnection ab.
|
|
* Wird in List-Endpoints + Stats-Endpoints für Filterung und Donut-Chart genutzt.
|
|
*
|
|
* Gibt { provider, providerLabel, isCustomDomain } zurück:
|
|
* provider = slug (z.B. "gmail", "outlook", "icloud", "custom")
|
|
* providerLabel = human-readable (z.B. "Gmail", "Outlook", "iCloud Mail")
|
|
* isCustomDomain = true wenn kein bekannter großer Provider
|
|
*/
|
|
const HOST_TO_PROVIDER: Record<string, { provider: string; providerLabel: string }> = {
|
|
"imap.gmail.com": { provider: "gmail", providerLabel: "Gmail" },
|
|
"outlook.office365.com": { provider: "outlook", providerLabel: "Outlook" },
|
|
"imap.mail.me.com": { provider: "icloud", providerLabel: "iCloud Mail" },
|
|
"imap.gmx.net": { provider: "gmx", providerLabel: "GMX" },
|
|
"imap.web.de": { provider: "webde", providerLabel: "Web.de" },
|
|
"imap.mail.yahoo.com": { provider: "yahoo", providerLabel: "Yahoo" },
|
|
"secureimap.t-online.de": { provider: "tonline", providerLabel: "T-Online" },
|
|
"mx.freenet.de": { provider: "freenet", providerLabel: "Freenet" },
|
|
"posteo.de": { provider: "posteo", providerLabel: "Posteo" },
|
|
"imap.ionos.de": { provider: "ionos", providerLabel: "IONOS" },
|
|
};
|
|
|
|
export function resolveProviderMeta(imapHost: string): {
|
|
provider: string;
|
|
providerLabel: string;
|
|
isCustomDomain: boolean;
|
|
} {
|
|
const match = HOST_TO_PROVIDER[imapHost];
|
|
if (match) return { ...match, isCustomDomain: false };
|
|
return { provider: "custom", providerLabel: imapHost, isCustomDomain: true };
|
|
}
|