From 6680b65863230650edb38f6faa942f6c8d0a75f5 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:58:12 +0100 Subject: [PATCH 1/6] feat: add server listen address configuration to .env and README --- .env.example | 11 +++++++++++ README.md | 10 ++++++++++ docker-compose.yml | 3 +++ 3 files changed, 24 insertions(+) diff --git a/.env.example b/.env.example index 08b7ea09..fea2feea 100644 --- a/.env.example +++ b/.env.example @@ -67,6 +67,17 @@ JMAP_SERVER_URL=https://your-jmap-server.com # For Docker, mount a persistent volume at this path. # SETTINGS_DATA_DIR=./data/settings +# ============================================================================= +# Server Listen Address +# ============================================================================= + +# Hostname the server binds to (default: 0.0.0.0) +# Set to "::" for IPv6 or "[::]" for dual-stack support. +# HOSTNAME=0.0.0.0 + +# Port the server listens on (default: 3000) +# PORT=3000 + # ============================================================================= # Logging # ============================================================================= diff --git a/README.md b/README.md index 7931c4b7..53fc5600 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,16 @@ APP_NAME=My Webmail All variables are **runtime** — Docker deployments can be configured without rebuilding. +
+Server Listen Address + +```env +HOSTNAME=0.0.0.0 # Default; use "::" for IPv6 +PORT=3000 # Default listen port +``` + +
+
OAuth2/OIDC (SSO) diff --git a/docker-compose.yml b/docker-compose.yml index 733c3645..a228a0ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,9 @@ services: image: ghcr.io/bulwarkmail/webmail:latest ports: - "3000:3000" + environment: + - HOSTNAME=0.0.0.0 # Use "::" for IPv6 + - PORT=3000 env_file: - .env.local healthcheck: From ce4ebb3dc254b5e842d73d40860ae1d50e5e97d4 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:00:34 +0100 Subject: [PATCH 2/6] fix: correct formatting of HOSTNAME environment variable in docker-compose.yml --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a228a0ae..2479dc33 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: ports: - "3000:3000" environment: - - HOSTNAME=0.0.0.0 # Use "::" for IPv6 + - HOSTNAME=0.0.0.0 # Use "::" for IPv6 - PORT=3000 env_file: - .env.local From e96b9a72e7ecb02abd38b16f171f8ba730dfd668 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:07:50 +0100 Subject: [PATCH 3/6] fix: update page title to use appName from configuration --- app/[locale]/page.tsx | 14 ++++++++------ app/layout.tsx | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 22f3aef6..28841ac0 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -41,11 +41,13 @@ import { isFilePreviewable } from "@/lib/file-preview"; import { Search, Filter, ChevronDown, X, Paperclip, Star, Mail, MailOpen, RotateCcw, PenSquare, PenLine, CheckSquare, Square } from "lucide-react"; import { ResizeHandle } from "@/components/layout/resize-handle"; import { Button } from "@/components/ui/button"; +import { useConfig } from "@/hooks/use-config"; export default function Home() { const router = useRouter(); const t = useTranslations(); const tCommon = useTranslations('common'); + const { appName } = useConfig(); const [showComposer, setShowComposer] = useState(false); const [composerMode, setComposerMode] = useState<'compose' | 'reply' | 'replyAll' | 'forward'>('compose'); const [composerDraftText, setComposerDraftText] = useState(""); @@ -224,7 +226,7 @@ export default function Home() { // Update page title based on context useEffect(() => { - let title = tCommon('app_title'); + let title = appName; if (showComposer) { // Composing email @@ -234,11 +236,11 @@ export default function Home() { replyAll: t('email_composer.reply_all'), forward: t('email_composer.forward'), }[composerMode] || t('email_composer.new_message'); - title = `${modeText} - ${tCommon('app_title')}`; + title = `${modeText} - ${appName}`; } else if (selectedEmail) { // Reading email const subject = selectedEmail.subject || t('email_viewer.no_subject'); - title = `${subject} - ${tCommon('app_title')}`; + title = `${subject} - ${appName}`; } else if (selectedMailbox && mailboxes.length > 0) { // Mailbox view const mailbox = mailboxes.find(mb => mb.id === selectedMailbox); @@ -246,13 +248,13 @@ export default function Home() { const mailboxName = mailbox.name; const unreadCount = mailbox.unreadEmails || 0; title = unreadCount > 0 - ? `${mailboxName} (${unreadCount}) - ${tCommon('app_title')}` - : `${mailboxName} - ${tCommon('app_title')}`; + ? `${mailboxName} (${unreadCount}) - ${appName}` + : `${mailboxName} - ${appName}`; } } document.title = title; - }, [showComposer, composerMode, selectedEmail, selectedMailbox, mailboxes, t, tCommon]); + }, [showComposer, composerMode, selectedEmail, selectedMailbox, mailboxes, t, appName]); // Check auth on mount useEffect(() => { diff --git a/app/layout.tsx b/app/layout.tsx index 4811e25c..b4e7facb 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -15,7 +15,7 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Bulwark Webmail", + title: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || "Webmail", description: "Minimalist webmail client using JMAP protocol", }; From 71cd826c6f9282d0f574a190fb6d3a9f052a2358 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:12:37 +0100 Subject: [PATCH 4/6] fix: update vendor label in Dockerfile to correct value --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 91b91d4b..a37a48c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ LABEL org.opencontainers.image.description="Modern webmail client built with Nex LABEL org.opencontainers.image.source="https://github.com/bulwarkmail/webmail" LABEL org.opencontainers.image.url="https://github.com/bulwarkmail/webmail" LABEL org.opencontainers.image.licenses="AGPL-3.0-only" -LABEL org.opencontainers.image.vendor="root.cloud" +LABEL org.opencontainers.image.vendor="rbm.systems" WORKDIR /app ENV NODE_ENV=production From 54f4d375950eedae563cf5eb430f88438c724f44 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:14:06 +0100 Subject: [PATCH 5/6] fix: update author information in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 600a5b92..8c3459c6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bulwark-webmail", "version": "1.3.0", "description": "Bulwark Webmail — a modern webmail client built for Stalwart Mail Server", - "author": "Matthieu MALVACHE ", + "author": "Bulwark Webmail ", "license": "AGPL-3.0-only", "repository": { "type": "git", From 7c5785e9e8ce8733572de5ad911d8e53a585119a Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:25:06 +0100 Subject: [PATCH 6/6] feat: enhance branding options with custom favicon and logos in configuration --- .env.example | 21 ++++++++++++-- app/api/config/route.ts | 3 ++ app/layout.tsx | 13 ++++++--- components/layout/sidebar.tsx | 15 ++++++++++ hooks/use-config.ts | 12 ++++++++ lib/__tests__/config-route.test.ts | 18 ++++++++++++ setup.sh | 44 ++++++++++++++++++++++-------- 7 files changed, 109 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index fea2feea..eaea9c85 100644 --- a/.env.example +++ b/.env.example @@ -89,10 +89,27 @@ JMAP_SERVER_URL=https://your-jmap-server.com # LOG_LEVEL=info # ============================================================================= -# Login Page Customization (all optional) +# Branding (all optional) # ============================================================================= -# Custom logo images for the login page (PNG, SVG, etc.) +# Custom favicon for the browser tab. +# Supported formats: SVG (recommended), PNG, ICO. +# Recommended size: 32×32px minimum, 512×512px maximum (or SVG for best scaling). +# Can be an absolute URL or a path relative to the public/ directory. +# Defaults to the Bulwark favicon if not set. +# FAVICON_URL=/branding/my-favicon.svg + +# Custom logos for the sidebar (shown in the main app after login). +# Supported formats: SVG (recommended), PNG, WebP. +# Recommended size: min 24×24px, max 128×128px +# Can be absolute URLs or paths relative to the public/ directory. +# If not set, no logo is shown in the sidebar. +# APP_LOGO_LIGHT_URL=/branding/my-logo-color.svg +# APP_LOGO_DARK_URL=/branding/my-logo-white.svg + +# Custom logo images for the login page. +# Supported formats: SVG (recommended), PNG, WebP. +# Recommended size: min 32×32px, max 512×512px # Can be absolute URLs or paths relative to the public/ directory. # Light mode logo (shown on light backgrounds), defaults to Bulwark logo. LOGIN_LOGO_LIGHT_URL=/branding/Bulwark_Logo_Color.svg diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 92cb4b1d..40f18a04 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -26,6 +26,9 @@ export async function GET() { settingsSyncEnabled: process.env.SETTINGS_SYNC_ENABLED === 'true' && !!process.env.SESSION_SECRET, stalwartFeaturesEnabled: process.env.STALWART_FEATURES !== 'false', devMode: process.env.DEV_MOCK_JMAP === 'true', + faviconUrl: process.env.FAVICON_URL || '/branding/Bulwark_Favicon.svg', + appLogoLightUrl: process.env.APP_LOGO_LIGHT_URL || '', + appLogoDarkUrl: process.env.APP_LOGO_DARK_URL || '', loginLogoLightUrl: process.env.LOGIN_LOGO_LIGHT_URL || '/branding/Bulwark_Logo_Color.svg', loginLogoDarkUrl: process.env.LOGIN_LOGO_DARK_URL || '/branding/Bulwark_Logo_White.svg', loginCompanyName: process.env.LOGIN_COMPANY_NAME || '', diff --git a/app/layout.tsx b/app/layout.tsx index b4e7facb..5ab67392 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -14,10 +14,15 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); -export const metadata: Metadata = { - title: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || "Webmail", - description: "Minimalist webmail client using JMAP protocol", -}; +export async function generateMetadata(): Promise { + const faviconUrl = process.env.FAVICON_URL; + + return { + title: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || "Webmail", + description: "Minimalist webmail client using JMAP protocol", + ...(faviconUrl ? { icons: { icon: faviconUrl } } : {}), + }; +} export default async function RootLayout({ children, diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 73d89de4..b8c7446f 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -37,6 +37,8 @@ import { useSettingsStore, KEYWORD_PALETTE, KeywordDefinition } from "@/stores/s import { useEmailStore } from "@/stores/email-store"; import { toast } from "@/stores/toast-store"; import { debug } from "@/lib/debug"; +import { useConfig } from "@/hooks/use-config"; +import { useThemeStore } from "@/stores/theme-store"; interface SidebarProps { mailboxes: Mailbox[]; @@ -361,6 +363,8 @@ export function Sidebar({ }: SidebarProps) { const { sidebarCollapsed: isCollapsed, toggleSidebarCollapsed } = useUIStore(); const { primaryIdentity } = useAuthStore(); + const { appLogoLightUrl, appLogoDarkUrl } = useConfig(); + const resolvedTheme = useThemeStore((s) => s.resolvedTheme); const [expandedFolders, setExpandedFolders] = useState>(new Set()); const [tagsExpanded, setTagsExpanded] = useState(() => { try { @@ -460,6 +464,17 @@ export function Sidebar({ + {(() => { + const logoUrl = resolvedTheme === 'dark' ? (appLogoDarkUrl || appLogoLightUrl) : (appLogoLightUrl || appLogoDarkUrl); + return logoUrl ? ( + + ) : null; + })()} +