BIMI deprecated
Brand Indicators for Message Identification (BIMI)
BIMI lets mailbox providers display a verified company logo next to incoming messages.
In App Suite UI, the BIMI logo is resolved and rendered locally so we can show it next to authenticated senders in the mail list and detail view without involving the middleware.
How it works
At a high level, the BIMI flow looks like this:
- BIMI lookups are only performed for messages that have successfully passed DMARC verification. This ensures that brand logos are displayed exclusively for authenticated senders.
- Before issuing any BIMI request, the UI checks a small blocklist of major mailbox providers (for example
gmail.com,icloud.com,outlook.com,yahoo.com). Addresses from these domains are skipped to avoid unnecessary BIMI lookups for providers that typically do not publish BIMI records. - An optional allowlist overrides the blocklist. Its entries may be a domain or a full email address, and an allowlisted entry is always looked up. This keeps a blocklisted provider domain hidden for ordinary mailboxes while still showing a logo for a specific role address — for example, with
example.orgon the blocklist,support@example.orgis still allowed. - A dedicated Fastify-based service exposes
/ui/v1/bimi/:domain. When queried, it resolves the domain’s BIMI DNS records, fetches the referenced SVG logo, validates it, and prepares a sanitized result. - Results are stored both in an in-memory LRU cache — avoiding repeated work for frequently requested domains — and in a persistent database cache, which minimizes DNS traffic and keeps lookup latency predictable.
- All fetched SVG logos are sanitized server-side (no scripts, no external references) before being persisted or returned. This guarantees that the UI never loads untrusted resources directly from third-party domains.
- Sanitized SVGs are retained to speed up repeated rendering across the message list and message detail view.
Selectors and local-part resolution (LPS)
BIMI records are published under a selector, looked up in DNS as <selector>._bimi.<domain>. The vast majority of domains only use the reserved default selector, but a domain can offer different logos per mailbox role (for example support@ vs. order@) by publishing a Local-part Selector (LPS) list.
- The
defaultrecord may carry anlpstag listing the local-parts that act as selector candidates, for examplev=BIMI1; l=…; a=…; lps=support,order. - When the UI requests a logo it passes the sender's
localPartalongside the domain. The service resolves the effective selector in this order:- an explicit
selectorquery override (mainly for the selector-specific logo endpoint), - the
localPart, if it appears in the domain's publishedlpslist, - otherwise the
defaultselector.
- an explicit
- If the resolved selector has no record (or no valid logo), the service transparently falls back to the
defaultselector so a mis-published role never hides an otherwise valid brand logo. - The list of published selectors lives only on the
defaultrecord; per-selector records and logos are cached independently. Cache and database entries are therefore keyed bydomain + selector.
LPS is still a draft of the BIMI standard, and brands rarely publish per-role records or certificates yet. To serve the common request — "show our own logo for
support@" — ahead of the standard, the service also accepts preconfigured records from deployment configuration (see Preconfigured records). They reuse the same selector resolution and the same sanitize/serve pipeline, but the logo comes from local config instead of DNS.
Endpoints
GET /ui/v1/bimi/:domain— resolve a record (and, withincludeLogo=true, the sanitized logo). Accepts optionallocalPartandselectorquery parameters. The response always includes the publishedselectorsand theresolvedSelectorthat was used.GET /ui/v1/bimi/:domain/:selector/logo— fetch the sanitized SVG for a specific selector directly, returning a404with areasonwhen none is available. This endpoint is exact-match: the selector is treated as an explicit override, so it does not fall back to thedefaultselector.
Prerequisites
Before enabling BIMI, make sure the following conditions are met:
- DMARC results available: The mail backend must perform DMARC verification and pass the result to the UI so it can decide when BIMI lookups are allowed.
- DNS and HTTPS connectivity: The BIMI service needs outbound access to DNS and HTTPS to resolve BIMI TXT records and retrieve SVG logos and (optional) VMC certificates.
- Database: A reachable MariaDB/MySQL database instance with enough capacity for the lookup and logo tables.
- Authentication: BIMI requests are authenticated with a JWT. The core UI obtains and provides this token; the BIMI service verifies it (see
JWKS_ENABLED/ the configured public key).
Configuration
Once the prerequisites are in place, enable BIMI with the following configuration:
# set feature toggle
config:
features:
bimi: true
# register a database for caching purposes
databases:
# the key must exist for BIMI
bimi:
host: database-host # example
port: 3306 # default
name: bimi # example / can be any name
user: bimi # example / can be any user
password: "<secret>"
connections: 10 # default
You also need to enable the feature for App Suite UI by adding this line to the App Suite Middleware configuration: yaml io.ox/core//features/bimi: true
You might also want to exclude your own domain, for example, if the domain provides a BIMI record but it is also used for employee addresses: yaml io.ox/core//bimi/blocklist: "[\"yourdomain.org\"]"
Conversely, you can allow individual senders on an otherwise blocked provider domain. Allowlist entries win over the blocklist and may be a domain or a full email address. This pairs naturally with Preconfigured records — allow the role address in the UI, configure its logo on the service: ```yaml
keeps a blocklisted provider domain hidden for normal users, but allows one role address
io.ox/core//bimi/allowlist: "[\"support@example.org\"]" ```
Preconfigured records
Preconfigured records let the operator publish a logo for a specific domain + selector (the selector is the sender local-part, e.g. support or order) directly from the BIMI service configuration, without a DNS record or certificate. They are intended for the role-address use case while the LPS standard and the matching certificates are not yet broadly available.
config:
features:
bimi: true
bimi:
records:
# logo for support@<domain> (and any organizational sub-domain)
- domain: example.org
selector: support # defaults to "default" if omitted
file: /etc/bimi/support.svg # path to an SVG; mount via ConfigMap/Secret
# an inline logo, asserted at the higher "vmc" trust level
- domain: example.org
selector: order
type: vmc # vmc | cmc; defaults to cmc
svg: "<svg viewBox=\"0 0 64 64\">…</svg>"
Behaviour and constraints:
- Resolution: a preconfigured selector joins the domain's selector list, so the normal
selector override → local-part → defaultprecedence and the default fallback apply unchanged. - Authoritative: a preconfigured record wins over a DNS record for the same
domain + selectorand short-circuits the DNS lookup, logo download, and certificate verification entirely. - Trust level: these logos are operator-asserted, not certificate-verified. They report
certificateType: cmcby default (the UI shows the "confirmed as provided by them" mark); settype: vmconly if the operator wants to vouch for them at the verified-mark level. - Validation: each logo is still run through the same SVG validation and sanitization as DNS logos. Invalid or unreadable entries are logged and skipped — they never crash the service or fall back to a raw, unsanitized SVG.
- Each record needs exactly one of
fileorsvg.fileis re-read when the configuration is reloaded; preconfigured logos are not written to the database or LRU caches.
Database sizing
The BIMI feature stores its data in two tables (within the configured BIMI database) with very different growth patterns:
Lookup table (metadata)
- Stores the domain and selector, DNS result, timestamps, the published
lpslist (on thedefaultrow), and whether BIMI is available. - Rows are keyed by
domain + selector; in practice almost every domain only has adefaultrow, so selector support adds rows only for domains that actually publish LPS. - Primarily acts as the negative cache for domains without BIMI so we do not hammer external DNS resolvers.
- Expect this table to reach millions of rows on large consumer deployments because it tracks both hits and true negatives.
- Stores the domain and selector, DNS result, timestamps, the published
Logo table (sanitized SVG blobs)
- Only populated for domains that successfully publish BIMI.
- Each logo is limited to 32 KB (typically around 4 KB), so even tens of thousands of entries consume well below a gigabyte.
- Disk usage is therefore driven by how many BIMI-enabled senders appear in user inboxes rather than overall mail volume.
When planning storage, reserve capacity for millions of metadata rows plus overhead for indexes, then add a modest buffer for logo blobs to support future growth in BIMI adoption. All data is used purely as a cache, so replication and backup are usually not required — a fast, inexpensive database is sufficient.
When setting up a fresh schema, the following privileges are needed: ```sql CREATE DATABASE bimi;
CREATE USER 'bimi'@'%' IDENTIFIED BY '...';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, CREATE ROUTINE, ALTER ROUTINE, EXECUTE ON bimi.* TO 'bimi'@'%'; ```
Metrics and Monitoring
Prometheus metrics are exposed on the standard /metrics endpoint (port controlled by the METRICS_PORT environment variable; default 9090). The table below summarizes the BIMI-specific data points:
| Metric | Type | Description |
|---|---|---|
bimi_requests_total | Counter | Total number of BIMI requests across all domains. |
bimi_default_selector_requests_total | Counter | Requests that resolved to the default selector. |
bimi_non_default_selector_requests_total | Counter | Requests that resolved to a non-default (local-part) selector. |
bimi_preconfigured_logo_requests_total | Counter | Logo lookups served from a preconfigured (operator-provided) record. |
bimi_request_duration_seconds | Histogram | Overall request duration (Fastify handler latency). |
bimi_record_cache_hits_total | Counter | In-memory cache hits for record metadata. |
bimi_record_cache_misses_total | Counter | In-memory cache misses that triggered DB lookups. |
bimi_record_db_hits_total | Counter | DB hits for records after a cache miss. |
bimi_record_db_misses_total | Counter | DB lookups that did not find a record. |
bimi_logo_cache_hits_total | Counter | In-memory cache hits for sanitized SVG logos. |
bimi_logo_cache_misses_total | Counter | Cache misses that required DB fetches for logos. |
bimi_logo_db_hits_total | Counter | Logo rows found in the database after cache misses. |
bimi_logo_db_misses_total | Counter | Logo DB lookups that missed (forces remote fetch). |
bimi_dns_lookup_duration_seconds | Histogram | DNS TXT lookup latency (focus on negative caching). |
bimi_logo_fetch_duration_seconds | Histogram | Time spent downloading remote SVGs. |
bimi_certificate_fetch_duration_seconds | Histogram | Latency for fetching VMC certificates, if present. |
The repository contains a ready-to-use Grafana dashboard (server/grafana/dashboards/dashboard.json).
Limitations
- Best-effort enrichment: BIMI is an optional visual enhancement. If lookups fail, DNS is slow, or the BIMI service is down, mail delivery is not affected; the UI simply omits the logo.
- Sender coverage: Only senders with valid DMARC and BIMI records will ever show a logo. Many large mailbox providers are blocklisted on purpose and therefore never trigger BIMI lookups.
- Caching behaviour: Negative cache entries (no BIMI available) can delay the display of newly published BIMI records until the cache expires. Negative entries expire after 30 days, entries with valid logos are kept for 180 days (because they rarely change).
- Scope in the UI: Logos currently appear only in the mail list and detail view; other modules are not affected by this feature.