From 99d5276cfa3c18357864995b4ac2b42202307d62 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:47:35 +0100 Subject: [PATCH] feat: add support for custom JMAP server endpoints in login and settings --- .env.example | 9 ++++++++- app/[locale]/login/page.tsx | 37 +++++++++++++++++++++++++++++++++---- app/admin/settings/page.tsx | 8 ++++++++ app/api/config/route.ts | 1 + hooks/use-config.ts | 4 ++++ lib/admin/types.ts | 1 + locales/en/common.json | 3 +++ 7 files changed, 58 insertions(+), 5 deletions(-) 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 (