import type { ConfigData } from '@/hooks/use-config'; import type { SettingsPolicy } from '@/lib/admin/types'; import { DEFAULT_POLICY } from '@/lib/admin/types'; import { httpUrlOrEmpty } from '@/lib/config-validation'; /** * Flags that need the Next.js server (cookies, filesystem, OAuth relay). They * are pinned regardless of what config.json contains. `oauthEnabled` is the * server-relayed flow; Lite on Stalwart signs in through an OpenID provider * on its own, decided per account at runtime (lib/auth/lite-oauth.ts). */ export const LITE_FORCED_FLAGS = { oauthEnabled: false, oauthOnly: false, autoSsoEnabled: false, settingsSyncEnabled: false, stalwartFeaturesEnabled: false, stalwartJmapPassthroughEnabled: false, devMode: false, } as const satisfies Partial; function str(value: unknown, fallback: string): string { return typeof value === 'string' ? value : fallback; } function bool(value: unknown, fallback: boolean): boolean { return typeof value === 'boolean' ? value : fallback; } function trimUrl(value: string): string { return value.trim().replace(/\/+$/, ''); } /** * Turns the (possibly partial, possibly hand-edited) contents of config.json * into the full `ConfigData` shape the app consumes. * * - `jmapServerUrl` is trimmed of trailing slashes. * - `allowCustomJmapEndpoint` defaults to *on* when no server is configured, * so an unedited download still lets people type their server. * - `rememberMeEnabled` defaults to on, and sign-ins are remembered without * asking unless `loginShowRememberMe` shows the box. Servers without * Stalwart's token login are never remembered (see lib/auth/lite-tokens.ts). * - `defaults.jmapServerUrl` (the Stalwart build passes the page origin) fills * an empty server URL, and the server field then stays hidden unless * config.json explicitly allows it. */ export function applyLiteConfig(raw: unknown, defaults?: { jmapServerUrl?: string }): ConfigData { const input = (raw && typeof raw === 'object' ? raw : {}) as Record; const jmapServerUrl = trimUrl(str(input.jmapServerUrl, '')) || trimUrl(defaults?.jmapServerUrl ?? ''); const jmapServers = Array.isArray(input.jmapServers) ? (input.jmapServers as unknown[]) .filter((entry): entry is Record => !!entry && typeof entry === 'object') .filter((entry) => typeof entry.id === 'string' && typeof entry.url === 'string') .map((entry) => ({ id: entry.id as string, label: str(entry.label, entry.id as string), url: trimUrl(entry.url as string), domains: Array.isArray(entry.domains) ? (entry.domains as unknown[]).filter((d): d is string => typeof d === 'string') : [], })) : []; return { appName: str(input.appName, '') || 'Webmail', jmapServerUrl, oauthClientId: '', oauthIssuerUrl: '', oauthScopes: '', rememberMeEnabled: bool(input.rememberMeEnabled, true), faviconUrl: str(input.faviconUrl, '') || '/branding/Bulwark_Favicon.svg', appLogoLightUrl: str(input.appLogoLightUrl, ''), appLogoDarkUrl: str(input.appLogoDarkUrl, ''), loginLogoLightUrl: str(input.loginLogoLightUrl, '') || '/branding/Bulwark_Logo_Color.svg', loginLogoDarkUrl: str(input.loginLogoDarkUrl, '') || '/branding/Bulwark_Logo_White.svg', loginCompanyName: str(input.loginCompanyName, ''), loginImprintUrl: str(input.loginImprintUrl, ''), loginPrivacyPolicyUrl: str(input.loginPrivacyPolicyUrl, ''), loginWebsiteUrl: str(input.loginWebsiteUrl, ''), loginLogoMaxHeight: str(input.loginLogoMaxHeight, ''), loginLogoMaxWidth: str(input.loginLogoMaxWidth, ''), loginShowHeading: bool(input.loginShowHeading, true), loginShowSubtitle: bool(input.loginShowSubtitle, true), loginShowTotp: bool(input.loginShowTotp, true), loginShowTokenLogin: bool(input.loginShowTokenLogin, false), loginShowRememberMe: bool(input.loginShowRememberMe, false), loginShowVersion: bool(input.loginShowVersion, true), demoMode: bool(input.demoMode, false), allowCustomJmapEndpoint: bool(input.allowCustomJmapEndpoint, jmapServerUrl === '' && jmapServers.length === 0), jmapServers, jmapServerAutoPickByDomain: bool(input.jmapServerAutoPickByDomain, false), embeddedMode: bool(input.embeddedMode, false), parentOrigin: str(input.parentOrigin, ''), sourceCodeUrl: httpUrlOrEmpty(input.sourceCodeUrl), ...LITE_FORCED_FLAGS, }; } /** * Zero-config defaults for a bundle served by Stalwart itself: the JMAP * server is the page's own origin (so no CORS), and there is nothing to type. */ export function liteStalwartDefaults(): { jmapServerUrl: string } { return { jmapServerUrl: typeof window === 'undefined' ? '' : window.location.origin }; } /** * Policy for Lite: whatever policy.json says, merged over the defaults, with * the features that need the sandbox routes or the admin server pinned off. */ export function applyLitePolicy(raw: unknown): SettingsPolicy { const input = (raw && typeof raw === 'object' ? raw : {}) as Partial; return { ...DEFAULT_POLICY, ...input, restrictions: { ...(input.restrictions ?? {}) }, defaults: { ...(input.defaults ?? {}) }, themePolicy: { ...DEFAULT_POLICY.themePolicy, ...(input.themePolicy ?? {}) }, features: { ...DEFAULT_POLICY.features, ...(input.features ?? {}), pluginsEnabled: false, pluginsUploadEnabled: false, sidebarAppsEnabled: false, // Kept by this app's server, which Lite does not have. accountArchiveEnabled: false, accountArchivePasswordsEnabled: false, }, forceEnabledPlugins: [], approvedPlugins: [], forceEnabledThemes: [], defaultSidebarApps: [], }; }