diff --git a/.env.example b/.env.example
index 08b7ea09..eaea9c85 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
# =============================================================================
@@ -78,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/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
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/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/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 4811e25c..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: "Bulwark 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;
+ })()}
+