diff --git a/.env.example b/.env.example index 86967030..777871b6 100644 --- a/.env.example +++ b/.env.example @@ -9,9 +9,16 @@ # App name displayed in the UI APP_NAME=Bulwark Webmail -# URL of your JMAP-compatible mail server (required) +# URL of your JMAP-compatible mail server (required unless ALLOW_CUSTOM_JMAP_ENDPOINT is set) JMAP_SERVER_URL=https://your-jmap-server.com +# Allow users to specify a custom JMAP server URL on the login form. +# When enabled, a "JMAP Server" field appears on the login page. +# Users can connect to any JMAP-compatible server. +# NOTE: External JMAP servers must include this domain in their CORS +# Access-Control-Allow-Origin header, or browser requests will be blocked. +# ALLOW_CUSTOM_JMAP_ENDPOINT=true + # ============================================================================= # Stalwart Mail Server Integration # ============================================================================= diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx index 8741aefc..7ea19c32 100644 --- a/app/[locale]/login/page.tsx +++ b/app/[locale]/login/page.tsx @@ -32,13 +32,14 @@ export default function LoginPage() { const isAddAccountMode = searchParams.get("mode") === "add-account"; const { login, loginDemo, isLoading, error, clearError, isAuthenticated } = useAuthStore(); const { theme, setTheme, initializeTheme } = useThemeStore(useShallow((s) => ({ theme: s.theme, setTheme: s.setTheme, initializeTheme: s.initializeTheme }))); - const { appName, jmapServerUrl: serverUrl, oauthEnabled, oauthOnly, oauthClientId, oauthIssuerUrl, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode } = useConfig(); + const { appName, jmapServerUrl: serverUrl, oauthEnabled, oauthOnly, oauthClientId, oauthIssuerUrl, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode, allowCustomJmapEndpoint } = useConfig(); const resolvedTheme = useThemeStore((s) => s.resolvedTheme); const [formData, setFormData] = useState({ username: "", password: "", }); + const [jmapEndpoint, setJmapEndpoint] = useState(""); const [totpCode, setTotpCode] = useState(""); const [showTotpField, setShowTotpField] = useState(false); const [rememberMe, setRememberMe] = useState(false); @@ -73,6 +74,12 @@ export default function LoginPage() { } }, [appName, serverUrl]); + useEffect(() => { + if (serverUrl && !jmapEndpoint) { + setJmapEndpoint(serverUrl); + } + }, [serverUrl, jmapEndpoint]); + useEffect(() => { try { if (sessionStorage.getItem('session_expired') === 'true') { @@ -264,7 +271,7 @@ export default function LoginPage() { ); } - if (!serverUrl && !demoMode) { + if (!serverUrl && !demoMode && !allowCustomJmapEndpoint) { return (