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.
455 lines
27 KiB
Markdown
455 lines
27 KiB
Markdown
# Feature spec: branding and templates
|
|
|
|
Status: draft, 2026-09-18. Feature 4 in SPEC.md §4.
|
|
|
|
## Provenance
|
|
|
|
Written for the clean room (SPEC.md §3). Sources, and nothing else:
|
|
|
|
| Source | License | Used for |
|
|
|---|---|---|
|
|
| Stalwart's registry schema: `crates/registry/src/schema/*.rs` and `resources/schema/schema.json.gz`, as imported into this repository (v0.16.22) | AGPL-3.0-only OR LicenseRef-SEL, taken under the AGPL | The five flagged fields, their types, descriptions and defaults, the related settings, permission prefixes |
|
|
| This repository's shared code: the template syntax (`crates/utils/src/template.rs`), the variable names (`crates/common/src/config/groupware.rs`), the alarm and iMIP senders (`crates/services/src/task_manager/alarm.rs`, `imip.rs`), the RSVP API types (`crates/groupware/src/calendar/itip.rs`), the HTTP routes (`crates/http/src/request.rs`), the logo cache and its invalidation (`crates/common/src/lib.rs`, `cache/invalidate.rs`), and the default pages and templates (`resources/html-templates/`, `resources/branding/`) | AGPL-3.0-only OR LicenseRef-SEL, with Enterprise-only parts already stripped; the pages and logo are the fork's own | What the built-in templates receive, how they're rendered and escaped, what the pages call, where hooks go |
|
|
| Stalwart documentation: "Branding" (`docs/management/webui/branding.md` and the 0.15 `webadmin/branding.md`), "Scheduling" (`docs/collaboration/scheduling.md`, sections HTTP RSVP and Branding and templating), the Enterprise and CalendarAlarm object references | Unlicensed public documentation: facts used, prose not copied | The logo order, hostname-based selection, the RSVP page contract, which templates exist |
|
|
| RFC 2397 (`data:` URLs), RFC 2392 (`cid:` URLs), RFC 5546 (iTIP), RFC 6047 (iMIP), RFC 8620 | IETF | Logo value forms, how the email logo is referenced, the messages the templates dress, `/set` errors |
|
|
|
|
No Enterprise-only file or snippet was used. The author is a fresh session
|
|
that has never seen Enterprise code. No server was probed for this spec. Where
|
|
no public source settles a behavior, this spec makes a decision of its own,
|
|
marked **Decision**, or lists it under "Open questions / to observe". It never
|
|
fills a gap from memory of upstream code.
|
|
|
|
## What it is
|
|
|
|
An operator can put its own logo, and its own layout, on what the server
|
|
shows and sends to people:
|
|
|
|
- **Logos.** One server-wide logo, one per tenant, one per domain. The most
|
|
specific one that applies is used on the server's sign-in page, the RSVP
|
|
page, calendar alarm and invitation emails, and in ihasmail.
|
|
- **Templates.** The HTML of calendar alarm emails, of iMIP invitation emails
|
|
(invitations, updates, cancellations and replies), and the whole HTTP RSVP
|
|
page.
|
|
|
|
Upstream ships this only in its Enterprise Edition. inbuxa-server ships it to
|
|
everybody. INBUXA's own branding is the default: the fork already ships
|
|
rebranded built-in templates, an INBUXA email logo
|
|
(`resources/branding/email-logo.png`), and `INBUXA Calendar` as the alarm
|
|
sender name.
|
|
|
|
Today, in the stripped tree, the five fields read and write normally but do
|
|
nothing: the built-in templates are always used, `logo_resource` always
|
|
answers none, and there's no `/logo` route. The login and RSVP pages already
|
|
ask `/logo` and fall back to their built-in logo when it fails.
|
|
|
|
Tenant logos (`x:Tenant.logo`) and the per-principal logo over JMAP are in
|
|
multi-tenancy MT-22 and MT-23. This spec uses them and doesn't repeat them.
|
|
|
|
## Data model
|
|
|
|
Unchanged from upstream, so existing data opens as it is (SPEC.md §7). All
|
|
five flagged fields are nullable strings. Null means "use the built-in".
|
|
|
|
| Object | Field | Schema type | Meaning |
|
|
|---|---|---|---|
|
|
| `x:Enterprise` (singleton) | `logoUrl` | `Uri?` | The server-wide default logo |
|
|
| `x:Domain` | `logo` | `String?` | The domain's logo: "URL or base64-encoded image" |
|
|
| `x:CalendarAlarm` (singleton) | `template` | `Html?` | Replaces the built-in alarm email |
|
|
| `x:CalendarScheduling` (singleton) | `emailTemplate` | `Html?` | Replaces the built-in iMIP email |
|
|
| `x:CalendarScheduling` (singleton) | `httpRsvpTemplate` | `Html?` | Replaces the built-in RSVP page. "Served verbatim", and responsible for calling `/api/calendar/rsvp` itself |
|
|
|
|
Related, not flagged, and unchanged:
|
|
|
|
- `x:Tenant.logo` (multi-tenancy spec).
|
|
- `x:CalendarAlarm`: `enable`, `fromName` (default `INBUXA Calendar` in the
|
|
fork), `fromEmail`, `allowExternalRcpts`, `minTriggerInterval`.
|
|
- `x:CalendarScheduling`: `enable`, `httpRsvpEnable` (default true),
|
|
`httpRsvpUrl`, `httpRsvpLinkExpiry` (default 90 days), `autoAddInvitations`,
|
|
`itipMaxSize`, `maxRecipients`.
|
|
- `x:Enterprise.licenseKey` and `apiKey`. There's no license (SPEC.md §4).
|
|
**Decision**: they stay readable and writable so existing data round-trips,
|
|
and are ignored.
|
|
- `x:OAuthClient.logo`, shown on the consent page (contract C-9). Not part of
|
|
this feature, but it follows BT-3 and BT-7 too.
|
|
|
|
Permissions, all existing: `sysEnterpriseGet` and `sysEnterpriseUpdate`,
|
|
`sysCalendarAlarmGet` and `sysCalendarAlarmUpdate`,
|
|
`sysCalendarSchedulingGet` and `sysCalendarSchedulingUpdate`, and
|
|
`sysDomainUpdate` for a domain's logo. The names stay as they are, "Enterprise"
|
|
included: they're protocol identifiers (SPEC.md §2.4).
|
|
|
|
## Required behavior
|
|
|
|
Each requirement has an ID, and tests name the IDs they check.
|
|
|
|
### Logos: which one applies
|
|
|
|
- **BT-1.** Logos are resolved for a domain name. For a name D:
|
|
1. the `logo` of the `x:Domain` whose `name` or `aliases` match D;
|
|
2. else the `logo` of that domain's tenant (`memberTenantId`);
|
|
3. else `x:Enterprise.logoUrl`;
|
|
4. else the built-in INBUXA logo.
|
|
This is the order upstream documents. **Decision** on matching: exact match
|
|
first, then D with its leftmost label removed, repeated while at least two
|
|
labels remain, so `mail.example.com` finds `example.com`. Matching is
|
|
case-insensitive.
|
|
- **BT-2.** Where each surface gets its domain:
|
|
- the sign-in page and the RSVP page: the `domain` query parameter of
|
|
`/logo` (BT-5), else the request's `Host` without its port, as upstream
|
|
documents;
|
|
- alarm emails: the domain of the account's primary address;
|
|
- iMIP emails: the domain of the message's `From` address;
|
|
- ihasmail: the signed-in principal's domain (multi-tenancy MT-22).
|
|
**Decision**: MT-22's chain ends at "none". This spec extends it with steps
|
|
3 and 4, so ihasmail and the server show the same logo. The multi-tenancy
|
|
spec should be updated to point here.
|
|
- **BT-3.** A logo value is one of:
|
|
- an `https:` URL;
|
|
- a `data:` URL (RFC 2397), base64, with type `image/png`, `image/jpeg`,
|
|
`image/gif`, `image/webp` or `image/svg+xml`.
|
|
**Decision** on writes: anything else is refused with `invalidProperties`
|
|
naming the field, and so is a data URL whose decoded image is over 256 KiB
|
|
or whose bytes don't match its declared type. `logoUrl` takes the same two
|
|
forms: a data URL is a valid URI.
|
|
- **BT-4.** Stored values that predate the fork are read as they are, never
|
|
rewritten, and never refused on read. **Decision** on odd ones: a bare
|
|
base64 string (the schema says "base64-encoded image") is treated as a data
|
|
URL whose type is sniffed from its first bytes, and an `http:` URL is used
|
|
like an `https:` one. A value that is none of these is skipped as if unset,
|
|
with a `registry.build-warning` event naming the object.
|
|
|
|
### Logos: serving and embedding
|
|
|
|
- **BT-5.** `GET /logo`, anonymous, rate-limited like the other anonymous
|
|
endpoints. It resolves a logo per BT-1 and BT-2 and answers:
|
|
- a data-URL logo: `200` with the decoded bytes and their type;
|
|
- a URL logo: `302` to that URL;
|
|
- no custom logo at any level: `404`, and the page draws its own.
|
|
Every answer carries `Cache-Control: public, max-age=300`,
|
|
`X-Content-Type-Options: nosniff`, and `Access-Control-Allow-Origin: *`.
|
|
**Decision** on the shape (see open question 3).
|
|
- **BT-6.** An unknown domain answers exactly as a known domain with no logo
|
|
of its own would: the server-wide logo or `404`. **Decision**: `/logo` isn't
|
|
a way to test which domains are hosted, beyond what a domain's own logo
|
|
shows.
|
|
- **BT-7.** The server never fetches a logo URL, for any purpose: not to serve
|
|
it, not to check it, not to embed it. Only browsers and mail clients fetch
|
|
URL logos, and ihasmail through its image proxy (MT-23).
|
|
- **BT-8.** An SVG served by `/logo` is sent with
|
|
`Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox`.
|
|
**Decision**, a security requirement: a tenant administrator can set its
|
|
domain's logo, and `/logo` is on the server's origin, so a scripted SVG must
|
|
never run there.
|
|
- **BT-9.** Emails embed the logo as an inline MIME part inside the
|
|
`multipart/related` part, and the template reaches it through `{{logo_cid}}`,
|
|
a `cid:` URL (RFC 2392). This is what the shared code already does. Only a
|
|
PNG, JPEG or GIF data-URL logo is embedded. **Decision**: a URL logo is never
|
|
embedded (BT-7), and SVG and WebP are skipped because many mail clients
|
|
can't show them. Resolution then goes on down BT-1's chain for the first logo
|
|
that can be embedded, ending at the built-in PNG.
|
|
- **BT-10.** A logo change takes effect without a restart, on every node of a
|
|
cluster, through the existing `DomainLogo` and `TenantLogo` cache
|
|
invalidations. A change to `logoUrl` clears the whole logo cache.
|
|
|
|
### Email templates
|
|
|
|
- **BT-11.** A template set in `x:CalendarAlarm.template` replaces the
|
|
built-in alarm email, and one set in `x:CalendarScheduling.emailTemplate`
|
|
replaces the built-in iMIP email. Null restores the built-in. Both are
|
|
server-wide: there's no per-tenant or per-domain template. The logo is what
|
|
varies (BT-9).
|
|
- **BT-12.** The template language is the one the built-in templates already
|
|
use (`crates/utils/src/template.rs`):
|
|
- `{{name}}` inserts a value, HTML-escaped (`& < > " '`);
|
|
- `{{#if name}}…{{/if name}}` keeps its content only when `name` is set;
|
|
- `{{#each name}}…{{/each name}}` repeats its content once per entry of a
|
|
list, and inside it `{{name}}` and `{{#if name}}` refer to the entry.
|
|
A token can't span lines. `#each` can't be nested.
|
|
- **BT-13.** The variables. What the server sets for each template:
|
|
|
|
| Variable | Kind | Alarm | iMIP |
|
|
|---|---|---|---|
|
|
| `page_title` | value | the subject | the subject |
|
|
| `lang`, `dir` | value | recipient's locale and direction | same |
|
|
| `logo_cid` | value | `cid:` of the logo part | same |
|
|
| `header` | value | the alarm heading | on update, cancel and reply: what happened |
|
|
| `color` | value | — | `info`, `warning` or `danger`, with `header` |
|
|
| `event_title`, `event_description` | value | if the event has them | same |
|
|
| `event_details` | list of `key`, `value`, `link`?; iMIP also `changed`?, `old_value`? | start, end, location, conference, organizer | summary, description, when, location, conference; the old value when it changed |
|
|
| `attendees_title` | value | always | when there are attendees |
|
|
| `attendees` | list of `key` (name), `value` (address) | when there are guests | when there are attendees |
|
|
| `action_name`, `action_url` | value | "open" label and the event's webcal link | — |
|
|
| `rsvp` | value | — | "reply as …", when RSVP links are on |
|
|
| `actions` | list of `action_name`, `action_url`, `color` | — | yes, no and maybe, when RSVP links are on |
|
|
| `footer` | alarm: value; iMIP: list of `key` | the footer line | two footer lines |
|
|
|
|
`link` is set only for `https`, `http`, `tel`, `sip`, `sips` and `xmpp`
|
|
values, so a template can't be made to link `javascript:`. The labels are the
|
|
server's own translations for the recipient's locale. A template can't add
|
|
translated text of its own.
|
|
- **BT-14.** **Decision**, a security requirement: values are always escaped
|
|
in operator templates. The shared syntax also has `{{!name}}` for raw output.
|
|
A write that uses it is refused (BT-15). A stored template that uses it
|
|
(from before the fork) is rendered with those values escaped too. Event titles,
|
|
descriptions and attendee names come from whoever sent the invitation, often
|
|
from outside the server, so raw output would let a stranger put HTML into
|
|
mail the server sends under its own name.
|
|
- **BT-15.** A write of `template` or `emailTemplate` is checked, and refused
|
|
with `invalidProperties` naming the field and the first problem, when:
|
|
- it doesn't parse (unbalanced block, block end that doesn't match, token
|
|
across lines, nested `#each`);
|
|
- it names a variable not in BT-13;
|
|
- it uses `{{!…}}`;
|
|
- it's over 256 KiB.
|
|
**Decision** on all four. A variable used outside the scope where it's set
|
|
(e.g. `{{key}}` outside a list) renders empty. That's allowed, not refused.
|
|
- **BT-16.** The server fetches nothing a template references, and doesn't
|
|
rewrite it. A remote image in an operator's template is the recipient's mail
|
|
client's business. The plain-text part of each email is still generated from
|
|
the rendered HTML, as today.
|
|
- **BT-17.** Subjects, sender names and addresses aren't templated. They stay
|
|
as the shared code builds them, with `fromName` and `fromEmail` for alarms.
|
|
- **BT-18.** A template change takes effect for the next email rendered, with
|
|
no restart and no settings reload. **Decision**, matching undelete UD-6a:
|
|
whether upstream needs a reload is open question 4.
|
|
- **BT-19.** A stored template that fails BT-15's parse on load (data written
|
|
before the fork) never stops the server. The built-in is used instead, and a
|
|
`registry.build-warning` event names the field and the error. It's reported
|
|
again whenever settings are reloaded until it's fixed.
|
|
|
|
### The RSVP page
|
|
|
|
- **BT-20.** When `httpRsvpEnable` is true, `GET /calendar/rsvp` serves
|
|
`httpRsvpTemplate` if set, else the built-in page. A custom page is served
|
|
byte for byte: no variables, no substitution. `{{` in it is plain text.
|
|
Setting `httpRsvpEnable` to false turns off both the page and the API, as
|
|
upstream documents.
|
|
- **BT-21.** Whichever page is served, the answer carries:
|
|
- `Content-Type: text/html; charset=utf-8` and `Cache-Control: no-store`;
|
|
- `Referrer-Policy: no-referrer`, because the token is in the query string;
|
|
- `Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'`.
|
|
**Decision**, a security requirement: the page holds a live RSVP token. It
|
|
may show remote images (URL logos, BT-5), but it can't send the token
|
|
anywhere but the server.
|
|
- **BT-22.** A write of `httpRsvpTemplate` is refused with `invalidProperties`
|
|
when it's over 1 MiB or isn't valid UTF-8. **Decision**. Its content isn't
|
|
otherwise checked: it's the operator's own page, and only a server-level
|
|
administrator can set it (BT-24).
|
|
- **BT-23.** The API a page calls is unchanged: `POST /api/calendar/rsvp` with
|
|
`{token}` to load the invitation, and `{token, partstat, comment}` to reply.
|
|
The answer is `invitation`, `recorded` or `error`, with localized `labels`, a
|
|
`language` and a `dir`, as the scheduling documentation describes. This
|
|
spec adds nothing to it.
|
|
|
|
### Who can change what
|
|
|
|
- **BT-24.** The three templates and `logoUrl` live on server-wide singletons.
|
|
Only a principal in no tenant, holding the matching `sys*Update`
|
|
permission, can change them. A principal in a tenant can't, whatever its
|
|
permissions (multi-tenancy MT-2).
|
|
- **BT-25.** A domain's `logo` is changed with `sysDomainUpdate`. Inside a
|
|
tenant, the tenant's administrator can set the logo of its own domains
|
|
(multi-tenancy MT-11). A tenant's `logo` is server-level (MT-12).
|
|
|
|
### Built-in pages
|
|
|
|
- **BT-26.** The built-in sign-in and RSVP pages load the logo with an image
|
|
element pointing at `/logo` (with `?domain=` as they do now), not with
|
|
`fetch`. On an error they keep their built-in logo. **Decision**: `fetch`
|
|
can't follow BT-5's redirect to another origin without that origin's CORS
|
|
headers, and an image element can.
|
|
|
|
## Interfaces
|
|
|
|
- **Existing, unchanged:** `x:Enterprise/get` and `/set`,
|
|
`x:CalendarAlarm/get` and `/set`, `x:CalendarScheduling/get` and `/set`,
|
|
`x:Domain/get` and `/set`, their permissions; `GET /calendar/rsvp`;
|
|
`POST /api/calendar/rsvp`.
|
|
- **New:** `GET /logo` (BT-5), with an optional `domain` parameter. The fork's
|
|
own pages already call it.
|
|
- **Per principal:** the applicable logo over JMAP, as proposed in
|
|
multi-tenancy MT-22, following BT-1's full chain (BT-2).
|
|
- **Errors:** RFC 8620 `invalidProperties`, naming the field, with the parse
|
|
error or limit in `description`, so ihasmail and INBUXA Admin can show it.
|
|
- **Events:** `registry.build-warning` for BT-4 and BT-19.
|
|
|
|
## ihasmail changes
|
|
|
|
These go in the INBUXA fork of ihasmail, ihasmail-inbuxa, never in public
|
|
ihasmail, which stays Stalwart-facing (SPEC.md §5).
|
|
|
|
- **Administration, Domains:** a logo field on each domain. Upload a PNG, JPEG
|
|
or GIF, stored as a data URL, with a preview and the 256 KiB limit checked
|
|
before sending. A note says that SVG, WebP and URL logos show on the web but
|
|
aren't used in email (BT-9). Tenant logos stay where multi-tenancy puts them.
|
|
- **Show the applicable logo** for the signed-in user (MT-22, BT-2). URL logos
|
|
go through the image proxy (MT-23).
|
|
- **Not in ihasmail:** the server-wide logo and the three templates are
|
|
server-level settings. INBUXA Admin's schema-driven forms already cover them
|
|
(SPEC.md §5.4). ihasmail links there, and doesn't grow a template editor.
|
|
- New strings: the logo field's label, its help text, the email note, and the
|
|
size and type errors. That's about six strings, new translation work for
|
|
each of ihasmail's nine languages.
|
|
|
|
INBUXA Admin needs no new screen. It shows the `invalidProperties`
|
|
descriptions from BT-15 and BT-22 as it shows any validation error.
|
|
|
|
## Acceptance tests
|
|
|
|
Every test runs against inbuxa-server built with no Enterprise code. The one
|
|
marked **(compat)** also runs against a copy of INBUXA's data.
|
|
|
|
1. Nothing set: `/logo` answers `404`, and alarm and invite emails carry the
|
|
built-in INBUXA PNG, referenced by `cid:` (BT-1, BT-5, BT-9).
|
|
2. `logoUrl` set to a PNG data URL: `/logo` answers `200` `image/png` with the
|
|
bytes, and emails embed it (BT-1, BT-5, BT-9).
|
|
3. Tenant T with a logo, its domain without one: `/logo?domain=` for the
|
|
domain gives T's logo. With the domain's own logo set, the domain's wins
|
|
(BT-1).
|
|
4. `/logo?domain=mail.example.com` finds `example.com`'s logo. No parameter,
|
|
`Host: mail.example.com`: the same (BT-1, BT-2).
|
|
5. A domain logo that's an `https:` URL: `/logo` answers `302` to it, the
|
|
server makes no outbound request (watched at the network), and emails fall
|
|
back to the next logo that can be embedded (BT-5, BT-7, BT-9).
|
|
6. An unknown domain and a known domain with no logo give identical answers
|
|
(BT-6).
|
|
7. An SVG logo with a `<script>`: `/logo` sends the sandboxing CSP, and the
|
|
script doesn't run when the URL is opened directly (BT-8).
|
|
8. Logo writes: `javascript:alert(1)`, `data:text/html,…`, a 300 KiB PNG, and
|
|
`data:image/png` holding JPEG bytes are each refused `invalidProperties`
|
|
(BT-3).
|
|
9. Changing a domain's logo shows on the next `/logo` request on another
|
|
cluster node, without a restart (BT-10).
|
|
10. A custom alarm template renders with every BT-13 alarm variable filled.
|
|
An event titled `<b>x</b>` shows as text, not bold (BT-12, BT-13, BT-14).
|
|
11. A custom iMIP template for an invitation, an update (with `changed` and
|
|
`old_value`), a cancellation and a reply each render, with RSVP actions
|
|
only when RSVP is on (BT-13).
|
|
12. Template writes: unbalanced `{{#if header}}`, `{{unknown}}`, `{{!header}}`,
|
|
nested `#each`, and 300 KiB are each refused `invalidProperties`, naming
|
|
the field (BT-15).
|
|
13. A template stored directly in the registry with `{{!event_title}}` renders
|
|
the title escaped. One that doesn't parse leaves the server running, uses
|
|
the built-in, and emits `registry.build-warning` (BT-14, BT-19).
|
|
14. A template change is used for the next alarm, with no reload (BT-18).
|
|
15. A custom RSVP page containing `{{page_title}}` is served byte for byte, with
|
|
BT-21's headers. With `httpRsvpEnable` false, the page and the API are
|
|
both gone (BT-20, BT-21).
|
|
16. A tenant administrator can set its domain's logo, but can't change
|
|
`logoUrl`, any template, or its tenant's logo (BT-24, BT-25).
|
|
17. The built-in sign-in and RSVP pages show a URL logo through `/logo`'s
|
|
redirect, and their built-in logo when `/logo` is `404` (BT-26).
|
|
18. **(compat)** Every logo and template INBUXA holds reads back unchanged, and
|
|
each renders or is served as it did before cutover (BT-4, BT-19).
|
|
|
|
## Open questions / to observe
|
|
|
|
To check read-only against INBUXA's live Enterprise server before
|
|
implementation. None blocks the spec. Items 2 and 5 need a write, so they need
|
|
the operator's approval first, as with the other features' probes.
|
|
|
|
1. **What INBUXA holds.** Read `x:Enterprise.logoUrl`, every `x:Domain.logo`
|
|
and `x:Tenant.logo`, and the three template fields. If all are null, test 18
|
|
has nothing to carry over. Note the forms any logos take (URL, data URL,
|
|
bare base64), which BT-4 depends on.
|
|
2. **URL logos in email.** Does upstream embed a URL logo in alarm and invite
|
|
emails (which would mean it fetches it), reference it remotely, or skip it?
|
|
Look at the MIME structure of an invite already in a mailbox before
|
|
sending a new one. BT-7 and BT-9 stand either way. This only says how far
|
|
the fork differs.
|
|
3. **Upstream's `/logo`.** Its exact path and parameters, whether it honors
|
|
`domain=` and `Host`, and its status, content type and caching for data
|
|
URLs, URL logos and no logo. The fork's own pages already call
|
|
`/logo?domain=`. A plain `GET` settles this, and BT-5 is aligned to it where
|
|
nothing more important is at stake.
|
|
4. **Reload.** Does a change to a template or a logo take effect upstream
|
|
without a settings reload (BT-10, BT-18)?
|
|
5. **Invalid templates on write.** Does upstream refuse a template that
|
|
doesn't parse, or store it and fall back? It matters only for how much
|
|
invalid data INBUXA might already hold (BT-19).
|
|
6. **RSVP page headers.** What headers upstream sends with a custom RSVP page,
|
|
so BT-21 is known as a difference or not.
|
|
7. **The stored `fromName`.** Whether INBUXA's `x:CalendarAlarm` stores
|
|
`Stalwart Calendar` as a value or relies on the default. **Decision** until
|
|
known: stored values are never rewritten (SPEC.md §7). If it's stored, the
|
|
operator changes it once, by hand. (The packaged `schema.json.gz` carried
|
|
upstream's default, `Stalwart Calendar`, until 2026-09-18. It now says
|
|
`INBUXA Calendar`, matching the code.)
|
|
8. **INBUXA Admin's logo.** Upstream documents that its web interface picks its
|
|
logo by request hostname. Check in `inbuxa-admin` (an ordinary AGPL fork,
|
|
SPEC.md §5) whether it calls `/logo`, so it keeps working against BT-5.
|
|
|
|
## Implementation status
|
|
|
|
Built 2026-09-18 from this spec, clean-room, under the multi-tenancy hand-off
|
|
brief's rules. The rules live in `crates/features` (`inbuxa-features`, module
|
|
`branding`); the domain lookup in `crates/common/src/storage/branding.rs`;
|
|
`/logo` and the RSVP page's answer in `crates/http/src/branding.rs`; upstream
|
|
files carry hooks marked `inbuxa:`. Acceptance tests 1 to 17 pass as
|
|
`tests/src/system/branding.rs`.
|
|
|
|
- **BT-1 to BT-26:** built.
|
|
- **ihasmail changes** belong to ihasmail-inbuxa and aren't part of this
|
|
repository.
|
|
- **Test 18 (compat)** is written as `branding_compat`, ignored, and unrun
|
|
until a copy of INBUXA's data is provided. INBUXA holds no logos or
|
|
templates (observed 1), so it has nothing to carry today.
|
|
- **Known limits, not requirements of this spec:**
|
|
- Logos are read from the registry on each `/logo` request and each email,
|
|
not cached. That is what makes BT-10 hold on every node with nothing to
|
|
invalidate; the existing logo cache is left unused.
|
|
- Test 9 runs on one node. The cluster half of BT-10 follows from reading
|
|
the registry each time, and isn't exercised by a test.
|
|
- Test 7 checks the sandboxing header. That the script doesn't run is the
|
|
browser honouring it, which no test here drives.
|
|
- Test 17 checks the pages' source, not a browser loading them.
|
|
- `httpRsvpEnable` still needs a settings reload, as upstream (it isn't one
|
|
of this spec's fields); the templates and logos don't (BT-18, BT-10).
|
|
|
|
## Observed
|
|
|
|
Settled on 2026-09-18 against INBUXA's live Enterprise server (Stalwart
|
|
0.16.22), read-only, as a server-level administrator and the throwaway test
|
|
account. No upstream code was read.
|
|
|
|
1. **What INBUXA holds** (open question 1). `x:Enterprise.logoUrl`, all three
|
|
templates and every tenant logo are null. Six of nine domains had a logo:
|
|
four held the same `https:` URL, pointing at a retired website that now
|
|
answers with an HTML page, and two held the literal string `admin`. With
|
|
the operator's approval, the four URL logos were removed the same day (a
|
|
write). The two `admin` values were removed too, also with approval, so
|
|
INBUXA holds no logos at all and compat test 18 has nothing to carry.
|
|
2. **Upstream fetches URL logos itself** (open questions 2 and 3). While the
|
|
URL logos were set, `GET /logo` returned `200 text/html`: the retired
|
|
site's page, served from the mail server's own origin with no
|
|
`X-Content-Type-Options` and no CSP. So upstream fetches the URL on the
|
|
server side and passes on whatever comes back, content type included.
|
|
BT-7 (never fetch) and BT-8 (sandbox what is served) are deliberate
|
|
differences, and this is why.
|
|
3. **`/logo` shape.** `/logo` with no parameter used the request's host, and
|
|
`?domain=` selected a domain. `/logo/<domain>` answered the same as
|
|
`/logo`, so the path segment is ignored. An unknown domain, a domain with
|
|
an unusable value, and (after the removal) every domain answered `404`
|
|
with `application/problem+json`. No caching headers were sent. BT-5 keeps
|
|
`/logo?domain=` and `404`, and adds its own headers.
|
|
4. **Reload** (open question 4). Removing the logos took effect on the very
|
|
next `/logo` request, with no settings reload.
|
|
5. **RSVP page headers** (open question 6). `GET /calendar/rsvp` sends only
|
|
`Content-Type: text/html; charset=utf-8` and
|
|
`Cache-Control: no-store, no-cache, must-revalidate`: no CSP and no
|
|
`Referrer-Policy`. BT-21's headers are a difference.
|
|
6. **The stored `fromName`** (open question 7). INBUXA stores
|
|
`INBUXA Calendar` as a value already. Nothing to change at cutover.
|
|
|
|
7. **INBUXA Admin's logo** (open question 8), checked 2026-09-18 in the
|
|
`inbuxa-admin` source, not on a server. It requests `/logo` with no
|
|
parameter (so the server goes by `Host`), through `fetch()`, and draws its
|
|
built-in logo whenever the answer isn't an `image/*` response. It reads no
|
|
logo fields and never asks the server to fetch a URL. Against BT-5, a
|
|
data-URL logo shows; a URL logo's cross-origin redirect falls back to the
|
|
built-in unless the logo's host sends CORS headers. Moving it to an image
|
|
element, as BT-26 does for the server's own pages, is a change for that
|
|
repository.
|
|
|
|
Not settled: open question 5 (invalid templates on write) needs a write.
|