Branding and templates: per-domain, tenant and server logos, /logo, operator calendar email templates and RSVP page (BT-1 to BT-26)
Logos resolve domain, then tenant, then server-wide, then the built-in, with subdomains finding their domain. GET /logo serves a data-URL image, redirects to a URL logo without fetching it, sandboxes SVG, and answers 404 when no custom logo applies. Emails embed the first PNG, JPEG or GIF logo. Logo and template writes are checked; stored templates are read at send time, always escaped, and fall back to the built-in with a build warning when they don't parse. The RSVP page is served byte for byte with a CSP and no-referrer. The sign-in and RSVP pages load the logo through an image element. MT-22's session logo follows the chain to the server-wide logo. Acceptance tests 1 to 17; test 18 written as the ignored branding_compat.
This commit is contained in:
@@ -100,6 +100,17 @@ impl GroupwareConfig {
|
||||
let dr = bp.setting_infallible::<DataRetention>().await;
|
||||
let system = bp.setting_infallible::<SystemSettings>().await;
|
||||
|
||||
// inbuxa: BT-19: a stored template that doesn't parse is reported at
|
||||
// start and on each reload; the built-in is used meanwhile
|
||||
inbuxa_features::branding::templates::warn_unusable::<CalendarTemplateVariable>(
|
||||
"CalendarAlarm.template",
|
||||
alarm.template.as_deref(),
|
||||
);
|
||||
inbuxa_features::branding::templates::warn_unusable::<CalendarTemplateVariable>(
|
||||
"CalendarScheduling.emailTemplate",
|
||||
sched.email_template.as_deref(),
|
||||
);
|
||||
|
||||
GroupwareConfig {
|
||||
max_request_size: dav.request_max_size as usize,
|
||||
dead_property_size: dav.dead_property_max_size.map(|v| v as usize),
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
//! Which logo applies to a domain name (branding spec BT-1, BT-2). The rules
|
||||
//! live in `inbuxa_features::branding::logo`; this finds the domain through
|
||||
//! the server's domain cache and reads the three levels from the registry
|
||||
//! each time, so a change shows at once on every node (BT-10).
|
||||
|
||||
use crate::Server;
|
||||
use inbuxa_features::branding::logo::{self, Logo, Source};
|
||||
use registry::schema::structs::{Domain, Enterprise, Tenant};
|
||||
use types::id::Id;
|
||||
|
||||
impl Server {
|
||||
/// The logos that apply to a domain name, most specific first. An unknown
|
||||
/// name gets what a known domain with no logo of its own gets (BT-6).
|
||||
pub async fn logos_for(&self, name: &str) -> trc::Result<Vec<Logo>> {
|
||||
let mut domain = None;
|
||||
for candidate in logo::lookup_names(name) {
|
||||
if let Some(found) = self.domain(&candidate).await? {
|
||||
domain = Some(found);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let registry = self.registry();
|
||||
let domain_logo = match &domain {
|
||||
Some(domain) => registry
|
||||
.object::<Domain>(Id::from(domain.id))
|
||||
.await?
|
||||
.and_then(|d| d.logo),
|
||||
None => None,
|
||||
};
|
||||
let tenant_id = domain.as_ref().and_then(|d| d.id_tenant);
|
||||
let tenant_logo = match tenant_id {
|
||||
Some(tenant_id) => registry
|
||||
.object::<Tenant>(Id::from(tenant_id))
|
||||
.await?
|
||||
.and_then(|t| t.logo),
|
||||
None => None,
|
||||
};
|
||||
let server_logo = registry
|
||||
.object::<Enterprise>(Id::singleton())
|
||||
.await?
|
||||
.and_then(|e| e.logo_url);
|
||||
Ok(logo::chain([
|
||||
(
|
||||
Source::Domain(domain.as_ref().map_or(u32::MAX, |d| d.id)),
|
||||
domain_logo.as_deref(),
|
||||
),
|
||||
(
|
||||
Source::Tenant(tenant_id.unwrap_or(u32::MAX)),
|
||||
tenant_logo.as_deref(),
|
||||
),
|
||||
(Source::Server, server_logo.as_deref()),
|
||||
]))
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use store::{BlobStore, InMemoryStore, RegistryStore, SearchStore, Store};
|
||||
|
||||
pub mod archive;
|
||||
pub mod blob;
|
||||
pub mod branding; // inbuxa: branding BT-1, BT-2
|
||||
pub mod dav;
|
||||
pub mod document;
|
||||
pub mod encryption;
|
||||
@@ -95,11 +96,27 @@ impl Server {
|
||||
self.registry().count_object(ObjectType::Domain).await
|
||||
}
|
||||
|
||||
// inbuxa: BT-9: the first logo mail can carry inline; none leaves the
|
||||
// built-in INBUXA logo
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn logo_resource(
|
||||
&self,
|
||||
_: &str,
|
||||
domain: &str,
|
||||
) -> trc::Result<Option<crate::manager::application::Resource<Vec<u8>>>> {
|
||||
Ok(None)
|
||||
Ok(self
|
||||
.logos_for(domain)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|logo| logo.is_embeddable())
|
||||
.and_then(|logo| match logo {
|
||||
inbuxa_features::branding::logo::Logo::Image {
|
||||
content_type,
|
||||
bytes,
|
||||
} => Some(crate::manager::application::Resource::new(
|
||||
content_type,
|
||||
bytes,
|
||||
)),
|
||||
inbuxa_features::branding::logo::Logo::Url(_) => None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user