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; + })()} +