Issue Description
Custom per-domain logos don’t show up.
On the Enterprise edition you can give each domain (or tenant) its own logo, so that when your users open the login / account page they see your branding instead of the default Stalwart one.
The problem: setting a logo on a domain has no effect. You add the logo, save it, and the login page still shows the default logo, for every domain. It doesn’t matter whether the logo is set correctly; it’s simply never used.
Expected Behavior
With a logo set on a domain (or tenant) principal, GET /logo on that domain’s hostname serves that logo, per the documented hierarchy (domain → tenant → enterprise.logo-url → built-in).
Actual Behavior
The domain/tenant logo is silently ignored; /logo always falls back to enterprise.logo-url or the built-in logo. No error is logged
Reproduction Steps
- Enterprise edition, valid
- Set logo on a domain principal (e.g. example.org) to any reachable image URL.
- GET https://mail.example.org/logo
- domain’s logo is never returned
Stalwart Version
v0.16.x
Installation Method
Docker
Database Backend
PostgreSQL
Blob Storage
S3-compatible
Search Engine
PostgreSQL
Directory Backend
LDAP
Additional Context
Proposed Patch:
Patch 1: URL Images
crates/common/src/enterprise/mod.rs
@@ logo_resource()
if let Some((d_id, t_id)) = self.domain(domain).await?.map(|d| (d.id, d.id_tenant))
- && let Some(domain_record) = self.registry().object::<Domain>(domain_id.into()).await?
+ && let Some(domain_record) = self.registry().object::<Domain>(d_id.into()).await?
{
logo_url = domain_record.logo;
domain_id = d_id;
Patch 2: base64-encoded Images
-use mail_parser::DateTime;
+use mail_parser::{DateTime, decoders::base64::base64_decode};
@@ logo_resource()
let mut logo = None;
+
+ if let Some((meta, data)) = logo_url
+ .as_deref()
+ .and_then(|url| url.strip_prefix("data:"))
+ .and_then(|uri| uri.split_once(','))
+ {
+ let contents = base64_decode(data.as_bytes()).ok_or_else(|| {
+ trc::ResourceEvent::DownloadExternal.into_err().details("Failed to decode logo")
+ })?;
+ if contents.len() > MAX_IMAGE_SIZE {
+ return Err(trc::ResourceEvent::DownloadExternal
+ .into_err().details("Download exceeded maximum size"));
+ }
+ let content_type = meta
+ .split(';').next().filter(|ct| !ct.is_empty()).unwrap_or("image/svg+xml");
+ logo = Resource::new(content_type.to_string(), contents).into();
+ logo_url = None;
+ }
+
if let Some(logo_url) = logo_url {
Same symptom was reported and closed in #1856, #2572, #2074 without identifying the cause; it’s still broken in v0.16.12
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
on