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.org on the blocklist, support@example.org is 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 default record may carry an lps tag listing the local-parts that act as selector candidates, for example v=BIMI1; l=…; a=…; lps=support,order.
  • When the UI requests a logo it passes the sender's localPart alongside the domain. The service resolves the effective selector in this order:
    1. an explicit selector query override (mainly for the selector-specific logo endpoint),
    2. the localPart, if it appears in the domain's published lps list,
    3. otherwise the default selector.
  • If the resolved selector has no record (or no valid logo), the service transparently falls back to the default selector so a mis-published role never hides an otherwise valid brand logo.
  • The list of published selectors lives only on the default record; per-selector records and logos are cached independently. Cache and database entries are therefore keyed by domain + 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, with includeLogo=true, the sanitized logo). Accepts optional localPart and selector query parameters. The response always includes the published selectors and the resolvedSelector that was used.
  • GET /ui/v1/bimi/:domain/:selector/logo — fetch the sanitized SVG for a specific selector directly, returning a 404 with a reason when none is available. This endpoint is exact-match: the selector is treated as an explicit override, so it does not fall back to the default selector.

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 → default precedence and the default fallback apply unchanged.
  • Authoritative: a preconfigured record wins over a DNS record for the same domain + selector and short-circuits the DNS lookup, logo download, and certificate verification entirely.
  • Trust level: these logos are operator-asserted, not certificate-verified. They report certificateType: cmc by default (the UI shows the "confirmed as provided by them" mark); set type: vmc only 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 file or svg. file is 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:

  1. Lookup table (metadata)

    • Stores the domain and selector, DNS result, timestamps, the published lps list (on the default row), and whether BIMI is available.
    • Rows are keyed by domain + selector; in practice almost every domain only has a default row, 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.
  2. 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.