From 518510c0884fe9a885d72bf6ac419f9bcb19ef77 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Mon, 11 May 2026 05:15:29 +0200 Subject: [PATCH] feat(mail): IONOS-Detection + MX-Lookup-Fallback + humanisierte Error-Messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - imap-providers: IONOS/1&1/1blu, msn.com, magenta.de, yahoo.co.uk, ymail.com, tutanota hinzugefügt - detectImapProviderAsync: MX-Lookup-Fallback für Custom-Domains (IONOS kundenserver.de/ionos.de Pattern) - connect.post.ts: nutzt jetzt detectImapProviderAsync statt sync-Variante - ConnectMailSheet: rohe Server-Errors werden via humanizeMailError() + t() übersetzt - useMailConnect: IONOS/t-online/freenet Domains in Client-Side-Detection ergänzt - Locale de/en: provider_other, app_password_guide_other, host_unreachable, unknown Text präzisiert Co-Authored-By: Claude Sonnet 4.6 --- .../components/mail/ConnectMailSheet.tsx | 11 ++- apps/rebreak-native/hooks/useMailConnect.ts | 16 ++++ apps/rebreak-native/locales/de.json | 29 +++++-- apps/rebreak-native/locales/en.json | 29 +++++-- backend/server/api/mail/connect.post.ts | 5 +- backend/server/utils/imap-providers.ts | 85 +++++++++++++++++-- 6 files changed, 152 insertions(+), 23 deletions(-) diff --git a/apps/rebreak-native/components/mail/ConnectMailSheet.tsx b/apps/rebreak-native/components/mail/ConnectMailSheet.tsx index ecb0004..664b399 100644 --- a/apps/rebreak-native/components/mail/ConnectMailSheet.tsx +++ b/apps/rebreak-native/components/mail/ConnectMailSheet.tsx @@ -12,6 +12,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useMailConnect, detectProvider, type MailProvider } from '../../hooks/useMailConnect'; +import { humanizeMailError } from '../../lib/mailErrors'; import { useColors } from '../../lib/theme'; import { KeyboardAwareSheet } from '../KeyboardAwareSheet'; @@ -134,16 +135,14 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) { const body: Parameters[0] = { email: email.trim(), password }; - // Für "other" Provider: User muss imapHost selbst eingeben — aktuell nicht - // unterstützt in dieser Sheet-Version. Custom-IMAP bleibt TODO für Phase 11. - // Provider-Detection passiert server-seitig via Email-Domain. - const result = await connect(body); if (result.ok) { handleClose(); onSuccess(); } else { - setFormError(result.error ?? t('mail.connect_failed')); + // Rohen Server-Error durch den humanen Mapper leiten. + // humanizeMailError klassifiziert IMAP-Fehlertexte → i18n-Key → t() → lesbarer Satz. + setFormError(t(humanizeMailError(result.error))); } } @@ -206,7 +205,7 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) { onPasswordChange={(v) => { setPassword(v); setFormError(null); }} passwordVisible={passwordVisible} onTogglePasswordVisible={() => setPasswordVisible((p) => !p)} - error={formError ?? connectError} + error={formError ?? (connectError ? t(humanizeMailError(connectError)) : null)} connecting={connecting} onConnect={handleConnect} insets={insets} diff --git a/apps/rebreak-native/hooks/useMailConnect.ts b/apps/rebreak-native/hooks/useMailConnect.ts index cfa4968..2cb143d 100644 --- a/apps/rebreak-native/hooks/useMailConnect.ts +++ b/apps/rebreak-native/hooks/useMailConnect.ts @@ -37,24 +37,40 @@ export type UseMailConnectReturn = { }; const PROVIDER_DOMAIN_MAP: Record = { + // Google 'gmail.com': 'gmail', 'googlemail.com': 'gmail', + // Apple 'icloud.com': 'icloud', 'me.com': 'icloud', 'mac.com': 'icloud', + // Microsoft 'outlook.com': 'outlook', 'hotmail.com': 'outlook', + 'hotmail.de': 'outlook', 'live.com': 'outlook', + 'live.de': 'outlook', 'msn.com': 'outlook', + // Yahoo 'yahoo.com': 'yahoo', 'yahoo.de': 'yahoo', 'yahoo.co.uk': 'yahoo', 'ymail.com': 'yahoo', + // GMX / Web.de 'gmx.de': 'gmx', 'gmx.net': 'gmx', 'gmx.at': 'gmx', 'gmx.ch': 'gmx', 'web.de': 'gmx', + // IONOS / 1&1 → als 'other' behandeln (Server-seitig korrekt erkannt) + 'ionos.de': 'other', + '1und1.de': 'other', + '1and1.com': 'other', + '1and1.de': 'other', + '1blu.de': 'other', + // Telekom + Freenet → 'other' (kein eigenes Tile, aber Server kennt sie) + 't-online.de': 'other', + 'freenet.de': 'other', }; export function detectProvider(email: string): MailProvider { diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index 60320d4..db1ccf0 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -311,7 +311,7 @@ "privacy_2": "Kein Zugriff auf Mail-Inhalte", "privacy_3": "DSGVO-konform, Server in DE", "providers_title": "Unterstützte Anbieter", - "provider_other": "Andere", + "provider_other": "Weitere Anbieter", "empty_title": "Noch keine Mails blockiert", "empty_subtitle": "Verbinde dein Postfach, damit Rebreak automatisch schützt.", "connect_sheet_title": "Postfach verbinden", @@ -327,7 +327,7 @@ "app_password_guide_outlook": "Outlook mit Microsoft-Konto: Aktiviere 2FA und erstelle ein App-Passwort unter account.microsoft.com/security.", "app_password_guide_yahoo": "Yahoo erfordert ein App-Passwort. Aktiviere 2FA und erstelle es unter login.yahoo.com/account/security.", "app_password_guide_gmx": "GMX / Web.de: Aktiviere IMAP in den Einstellungen und verwende dein normales Passwort oder ein App-Passwort falls 2FA aktiv.", - "app_password_guide_other": "Gib die IMAP-Zugangsdaten deines E-Mail-Anbieters ein. App-Passwort empfohlen wenn vorhanden.", + "app_password_guide_other": "Gib deine E-Mail-Adresse und dein App-Passwort ein. Rebreak erkennt deinen Anbieter automatisch anhand der Domain (z.B. IONOS, Strato, 1&1). App-Passwort empfohlen falls vorhanden.", "app_password_open_link": "Jetzt App-Passwort erstellen", "form_email_label": "E-Mail-Adresse", "form_email_placeholder": "deine@email.de", @@ -402,10 +402,10 @@ "auth_failed": "Das App-Passwort ist nicht korrekt. Bitte erneuere es bei deinem Mail-Anbieter und trage es hier ein.", "app_password_required": "Dein Mail-Anbieter verlangt ein App-spezifisches Passwort. Erstelle eines in den Account-Einstellungen.", "connection_failed": "Verbindung zum Mail-Server fehlgeschlagen. Bitte später erneut versuchen.", - "host_unreachable": "Mail-Server ist gerade nicht erreichbar. Internet-Verbindung prüfen oder später erneut versuchen.", + "host_unreachable": "Mail-Server nicht erreichbar. Internet-Verbindung prüfen — oder dein Anbieter wird noch nicht unterstützt. Schreib uns: support@rebreak.org", "tls_error": "Sichere Verbindung zum Mail-Server konnte nicht hergestellt werden. Provider kontaktieren.", "rate_limited": "Zu viele Verbindungsversuche. Bitte ein paar Minuten warten und erneut versuchen.", - "unknown": "Unbekannter Fehler beim Verbinden. Bitte App-Passwort prüfen oder erneut versuchen." + "unknown": "Verbindung fehlgeschlagen. Prüfe das App-Passwort oder schreib uns an support@rebreak.org — wir fügen deinen Anbieter gerne hinzu." } }, "settings": { @@ -746,7 +746,26 @@ "success_title": "Mac geschützt!", "success_body": "Du kannst weitere Geräte hinzufügen wenn du willst.", "remove_warning_title": "Profile manuell entfernen", - "remove_warning_body": "Wir können das Profile nicht aus der Ferne löschen. Auf dem Mac: Systemeinstellungen → Profile → ReBreak → Entfernen (Admin-Passwort nötig)." + "remove_warning_body": "Wir können das Profile nicht aus der Ferne löschen. Auf dem Mac: Systemeinstellungen → Profile → ReBreak → Entfernen (Admin-Passwort nötig).", + "add_windows_enabled": "Windows-PC hinzufügen", + "windows_label_question": "Wie soll der Windows-PC heißen?", + "windows_label_default": "Windows-PC", + "windows_label_placeholder": "z.B. Gaming-PC", + "windows_lyra_intro": "Fünf kurze Schritte. Ich begleite dich — wenn was nicht klappt, klick auf Hilfe.", + "windows_step_1_title": "Datei herunterladen", + "windows_step_1_body": "Klick den Button unten — auf deinem Windows-PC. Die .reg-Datei wird gedownloadet.", + "windows_step_2_title": ".reg ausführen", + "windows_step_2_body": "Doppelklick auf die heruntergeladene .reg-Datei. Windows fragt: 'Möchten Sie der Registrierung Schlüssel hinzufügen?' → Klick 'Ja'.", + "windows_step_3_title": "UAC bestätigen", + "windows_step_3_body": "Wenn ein blauer UAC-Prompt erscheint: 'Ja' klicken. Das ist die Admin-Bestätigung.", + "windows_step_4_title": "DNS aktivieren", + "windows_step_4_body": "Öffne Windows-Einstellungen → Netzwerk & Internet → klick auf dein WLAN → DNS-Server-Zuweisung → 'Bearbeiten' → wähle 'Verschlüsselt nur (DNS over HTTPS)' → wähle 'rebreak-...' aus der Liste.", + "windows_step_5_title": "Fertig", + "windows_step_5_body": "Sobald du die DNS-Auswahl gespeichert hast, klick 'Ich hab's installiert' und ich zähl deinen PC als geschütztes Gerät.", + "windows_download_button": "Datei auf Windows-PC herunterladen", + "windows_success_title": "Windows-PC geschützt!", + "windows_success_body": "Du kannst weitere Geräte hinzufügen wenn du willst.", + "windows_remove_warning_body": "Wir können die Registrierung nicht aus der Ferne löschen. Auf dem PC: Regedit → HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\DoHSvc → Schlüssel entfernen." }, "gameOver": { "title": "Spiel beendet", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index f5e23b7..d2bf721 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -311,7 +311,7 @@ "privacy_2": "No access to email content", "privacy_3": "GDPR-compliant, servers in Germany", "providers_title": "Supported providers", - "provider_other": "Other", + "provider_other": "Other providers", "empty_title": "No emails blocked yet", "empty_subtitle": "Connect your mailbox so Rebreak can protect you automatically.", "connect_sheet_title": "Connect mailbox", @@ -327,7 +327,7 @@ "app_password_guide_outlook": "Outlook with Microsoft account: Enable 2FA and create an app password at account.microsoft.com/security.", "app_password_guide_yahoo": "Yahoo requires an app password. Enable 2FA and create it at login.yahoo.com/account/security.", "app_password_guide_gmx": "GMX / Web.de: Enable IMAP in settings and use your regular password or an app password if 2FA is active.", - "app_password_guide_other": "Enter the IMAP credentials of your email provider. An app password is recommended if available.", + "app_password_guide_other": "Enter your email address and app password. Rebreak detects your provider automatically from the domain (e.g. IONOS, Strato, 1&1). An app password is recommended if available.", "app_password_open_link": "Create app password now", "form_email_label": "Email address", "form_email_placeholder": "your@email.com", @@ -402,10 +402,10 @@ "auth_failed": "The app password is incorrect. Please regenerate it at your mail provider and enter it here.", "app_password_required": "Your mail provider requires an app-specific password. Create one in your account settings.", "connection_failed": "Could not connect to the mail server. Please try again later.", - "host_unreachable": "Mail server is currently unreachable. Check your internet connection or try again later.", + "host_unreachable": "Mail server unreachable. Check your internet connection — or your provider may not be supported yet. Write to: support@rebreak.org", "tls_error": "Secure connection to the mail server failed. Please contact your provider.", "rate_limited": "Too many connection attempts. Please wait a few minutes and try again.", - "unknown": "Unknown error while connecting. Please check the app password and try again." + "unknown": "Connection failed. Check your app password or write us at support@rebreak.org — we'll add your provider." } }, "settings": { @@ -746,7 +746,26 @@ "success_title": "Mac protected!", "success_body": "You can add more devices whenever you like.", "remove_warning_title": "Remove profile manually", - "remove_warning_body": "We can't delete the profile remotely. On the Mac: System Settings → Profiles → ReBreak → Remove (admin password required)." + "remove_warning_body": "We can't delete the profile remotely. On the Mac: System Settings → Profiles → ReBreak → Remove (admin password required).", + "add_windows_enabled": "Add Windows PC", + "windows_label_question": "What should this Windows PC be called?", + "windows_label_default": "Windows PC", + "windows_label_placeholder": "e.g. Gaming PC", + "windows_lyra_intro": "Five quick steps. I'll walk you through each one — if something goes wrong, tap Help.", + "windows_step_1_title": "Download the file", + "windows_step_1_body": "Tap the button below — on your Windows PC. The .reg file will be downloaded.", + "windows_step_2_title": "Run the .reg file", + "windows_step_2_body": "Double-click the downloaded .reg file. Windows will ask: 'Do you want to add keys to the registry?' → Click 'Yes'.", + "windows_step_3_title": "Confirm UAC prompt", + "windows_step_3_body": "If a blue UAC prompt appears: click 'Yes'. This is the admin confirmation.", + "windows_step_4_title": "Activate DNS", + "windows_step_4_body": "Open Windows Settings → Network & Internet → click your Wi-Fi → DNS server assignment → 'Edit' → choose 'Encrypted only (DNS over HTTPS)' → select 'rebreak-...' from the list.", + "windows_step_5_title": "Done", + "windows_step_5_body": "Once you've saved the DNS selection, tap 'I've installed it' and I'll count your PC as a protected device.", + "windows_download_button": "Download file to Windows PC", + "windows_success_title": "Windows PC protected!", + "windows_success_body": "You can add more devices whenever you like.", + "windows_remove_warning_body": "We can't delete the registry entry remotely. On the PC: Regedit → HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\DoHSvc → remove the key." }, "gameOver": { "title": "Game over", diff --git a/backend/server/api/mail/connect.post.ts b/backend/server/api/mail/connect.post.ts index 5f0a57f..e311931 100644 --- a/backend/server/api/mail/connect.post.ts +++ b/backend/server/api/mail/connect.post.ts @@ -2,6 +2,7 @@ import { ImapFlow } from "imapflow"; import { getProfile } from "../../db/profile"; import { getPlanLimits } from "../../utils/plan-features"; import { countMailConnections, upsertMailConnection } from "../../db/mail"; +import { detectImapProviderAsync } from "../../utils/imap-providers"; /** * POST /api/mail/connect @@ -42,8 +43,8 @@ export default defineEventHandler(async (event) => { } // Custom-IMAP: wenn imapHost explizit gesetzt → Provider-Detection überspringen. - // Sonst: automatisch via Email-Domain erkennen. - const provider = detectImapProvider(email); + // Sonst: automatisch via Email-Domain erkennen (inkl. MX-Lookup-Fallback für Custom-Domains). + const provider = await detectImapProviderAsync(email); const resolvedHost = customImapHost?.trim() || provider.host; const resolvedPort = customImapPort ?? provider.port; diff --git a/backend/server/utils/imap-providers.ts b/backend/server/utils/imap-providers.ts index 82ed6db..ce7301e 100644 --- a/backend/server/utils/imap-providers.ts +++ b/backend/server/utils/imap-providers.ts @@ -1,3 +1,5 @@ +import dns from "node:dns/promises"; + /** * IMAP-Provider Konfigurationen * Automatisch erkennen anhand der Email-Domain @@ -9,32 +11,88 @@ export interface ImapConfig { } const PROVIDER_MAP: Record = { + // 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" }, - "t-online.de": { - host: "secureimap.t-online.de", - port: 993, - name: "T-Online", - }, + // 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 { + try { + const mxRecords = await Promise.race([ + dns.resolveMx(domain), + new Promise((_, 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 ( @@ -46,6 +104,23 @@ export function detectImapProvider(email: string): ImapConfig { ); } +/** + * 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 { + 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. — Standard-Konvention + return { host: `imap.${domain}`, port: 993, name: domain }; +} + const SMTP_MAP: Record = { "imap.gmail.com": { host: "smtp.gmail.com", port: 587 }, "imap.mail.me.com": { host: "smtp.mail.me.com", port: 587 },