From 913f673f73154d7176f464383423967b4cebf84d Mon Sep 17 00:00:00 2001 From: Matthieu MALVACHE Date: Mon, 23 Feb 2026 18:21:23 +0100 Subject: [PATCH] feat: detect CORS errors and show actionable guidance on login Distinguish CORS-blocked requests from genuine network failures using a no-cors probe, so users deploying via Docker see a specific message pointing to their JMAP server's CORS settings instead of a misleading "unable to reach the server" error. --- README.md | 1 + ROADMAP.md | 2 +- lib/jmap/client.ts | 11 ++++++++++- locales/de/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/fr/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/nl/common.json | 1 + locales/pt/common.json | 1 + stores/auth-store.ts | 4 +++- 12 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9423f6b7..8ad557ca 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ This webmail client is designed to work seamlessly with [**Stalwart Mail Server* - SPF/DKIM/DMARC status indicators - No password storage (session-based auth) - TOTP two-factor authentication support +- CORS misconfiguration detection with actionable error messages - Shared folder support with proper permissions - Newsletter unsubscribe support (RFC 2369) - CSP headers and security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy) diff --git a/ROADMAP.md b/ROADMAP.md index 1976aa8d..d094a1cf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -22,6 +22,7 @@ This document tracks the development status and planned features for JMAP Webmai ### JMAP Server Connection - [x] Session establishment and keep-alive - [x] Connection error handling and retries +- [x] CORS error detection with actionable user guidance - [x] Storage quota display - [x] Server capability detection - [x] Shared folders support (multi-account access) @@ -250,7 +251,6 @@ This document tracks the development status and planned features for JMAP Webmai ### Security Enhancements - [ ] Rate limiting -- [ ] CORS configuration ## Known Issues diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 4440753c..9dd34c0c 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -133,7 +133,16 @@ export class JMAPClient { // Start keep-alive mechanism this.startKeepAlive(); } catch (error) { - console.error('Connection failed:', error); + if (error instanceof TypeError && (error.message === 'Failed to fetch' || error.message.includes('NetworkError'))) { + let serverReachable = false; + try { + await fetch(sessionUrl, { mode: 'no-cors' }); + serverReachable = true; + } catch { /* genuinely unreachable */ } + if (serverReachable) { + throw new Error('CORS_ERROR'); + } + } throw error; } } diff --git a/locales/de/common.json b/locales/de/common.json index a361331c..1df9f539 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -11,6 +11,7 @@ "error": { "invalid_credentials": "Ungültige E-Mail-Adresse oder Passwort", "connection_failed": "Verbindung zum Server fehlgeschlagen", + "cors_blocked": "Der Server ist erreichbar, blockiert aber Cross-Origin-Anfragen. Überprüfen Sie die CORS-Einstellungen Ihres JMAP-Servers und erlauben Sie diese Domain.", "generic": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.", "totp_invalid": "Ungültiger Authentifizierungscode. Überprüfen Sie Ihre Authenticator-App.", "server_error": "Der Server ist vorübergehend nicht erreichbar. Bitte versuchen Sie es später erneut." diff --git a/locales/en/common.json b/locales/en/common.json index 7a11fa04..c43677a6 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -11,6 +11,7 @@ "error": { "invalid_credentials": "Invalid email or password. Please check your credentials and try again.", "connection_failed": "Unable to reach the server. Check your internet connection and try again.", + "cors_blocked": "The server is reachable but is blocking cross-origin requests. Check your JMAP server's CORS settings and allow this domain.", "server_error": "The server is temporarily unavailable. Please try again later.", "generic": "An unexpected error occurred. If this persists, contact your administrator.", "totp_invalid": "Invalid authentication code. Please check your authenticator app and try again." diff --git a/locales/es/common.json b/locales/es/common.json index 5668cdb5..c0ad0fc1 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -11,6 +11,7 @@ "error": { "invalid_credentials": "Correo electrónico o contraseña inválidos", "connection_failed": "No se pudo conectar con el servidor", + "cors_blocked": "El servidor es accesible pero está bloqueando las solicitudes de origen cruzado. Verifique la configuración CORS de su servidor JMAP y permita este dominio.", "generic": "Ocurrió un error. Por favor, inténtelo de nuevo.", "totp_invalid": "Código de autenticación inválido. Verifica tu aplicación de autenticación.", "server_error": "El servidor no está disponible temporalmente. Inténtalo más tarde." diff --git a/locales/fr/common.json b/locales/fr/common.json index 9a23f180..a719bbcf 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -11,6 +11,7 @@ "error": { "invalid_credentials": "Email ou mot de passe invalide", "connection_failed": "Échec de la connexion au serveur", + "cors_blocked": "Le serveur est joignable mais bloque les requêtes cross-origin. Vérifiez la configuration CORS de votre serveur JMAP et autorisez ce domaine.", "generic": "Une erreur s'est produite. Veuillez réessayer.", "totp_invalid": "Code d'authentification invalide. Vérifiez votre application d'authentification.", "server_error": "Le serveur est temporairement indisponible. Veuillez réessayer plus tard." diff --git a/locales/it/common.json b/locales/it/common.json index 303ade68..a35b262b 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -11,6 +11,7 @@ "error": { "invalid_credentials": "Email o password non valida", "connection_failed": "Impossibile connettersi al server", + "cors_blocked": "Il server è raggiungibile ma sta bloccando le richieste cross-origin. Controlla le impostazioni CORS del tuo server JMAP e consenti questo dominio.", "generic": "Si è verificato un errore. Riprova.", "totp_invalid": "Codice di autenticazione non valido. Controlla la tua app di autenticazione.", "server_error": "Il server non è temporaneamente disponibile. Riprova più tardi." diff --git a/locales/ja/common.json b/locales/ja/common.json index 985beb3d..b54be060 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -11,6 +11,7 @@ "error": { "invalid_credentials": "メールアドレスまたはパスワードが無効です", "connection_failed": "サーバーへの接続に失敗しました", + "cors_blocked": "サーバーには到達できますが、クロスオリジンリクエストがブロックされています。JMAPサーバーのCORS設定を確認し、このドメインを許可してください。", "generic": "エラーが発生しました。もう一度お試しください。", "totp_invalid": "認証コードが無効です。認証アプリを確認してください。", "server_error": "サーバーが一時的に利用できません。後でもう一度お試しください。" diff --git a/locales/nl/common.json b/locales/nl/common.json index 5d842e3a..879bdcf6 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -11,6 +11,7 @@ "error": { "invalid_credentials": "Ongeldig e-mailadres of wachtwoord", "connection_failed": "Kan geen verbinding maken met de server", + "cors_blocked": "De server is bereikbaar maar blokkeert cross-origin verzoeken. Controleer de CORS-instellingen van uw JMAP-server en sta dit domein toe.", "generic": "Er is een fout opgetreden. Probeer het opnieuw.", "totp_invalid": "Ongeldige authenticatiecode. Controleer uw authenticator-app.", "server_error": "De server is tijdelijk niet beschikbaar. Probeer het later opnieuw." diff --git a/locales/pt/common.json b/locales/pt/common.json index 133375fd..eda7b891 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -11,6 +11,7 @@ "error": { "invalid_credentials": "E-mail ou senha inválidos", "connection_failed": "Falha ao conectar com o servidor", + "cors_blocked": "O servidor está acessível mas está bloqueando requisições de origem cruzada. Verifique as configurações de CORS do seu servidor JMAP e permita este domínio.", "generic": "Ocorreu um erro. Por favor, tente novamente.", "totp_invalid": "Código de autenticação inválido. Verifique seu aplicativo de autenticação.", "server_error": "O servidor está temporariamente indisponível. Tente novamente mais tarde." diff --git a/stores/auth-store.ts b/stores/auth-store.ts index ad2ad0c3..ca7903dc 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -98,7 +98,9 @@ export const useAuthStore = create()( let errorKey = 'generic'; if (error instanceof Error) { - if (error.message.includes('Invalid username or password') || + if (error.message === 'CORS_ERROR') { + errorKey = 'cors_blocked'; + } else if (error.message.includes('Invalid username or password') || error.message.includes('401') || error.message.includes('Unauthorized')) { errorKey = 'invalid_credentials';