feat(bimi): logotipo do dominio validado, alojado e publicado no DNS

A foto de perfil nao viaja: nem o Gmail nem o Outlook leem uma imagem por
utilizador do servidor de quem envia, e nao existe protocolo que o permita. O
avatar que esses mostram vem dos contactos de quem recebe ou da conta Google
ou Microsoft do remetente.

O que eles leem e BIMI, que e por dominio. Fica implementado: validacao,
alojamento e o registo.

O validador reporta todas as violacoes de uma vez, e nao a primeira. Um
receptor que recusa a marca nao explica nada -- simplesmente nao desenha -- por
isso descobrir as regras uma viagem de cada vez seria a pior maneira possivel
de aprender este formato. Sao verificados o baseProfile e a versao que os
receptores procuram, o viewBox quadrado (uma marca nao quadrada e cortada de
forma diferente por cada cliente), exactamente um title, e a ausencia de tudo
o que seja activo ou externo: script, handlers de evento, links, imagens
rasterizadas, animacoes e referencias para fora do documento.

O registo so e publicado quando ha mesmo uma marca guardada. Um registo que
aponta para um logotipo inexistente e pior do que nenhum, porque o receptor
para de procurar assim que encontra um.

O logotipo e servido aberto, porque quem o vai buscar sao os fornecedores de
correio a avaliar uma mensagem e esses nunca terao sessao aqui.

Fica escrito no README que o Gmail e o Outlook so desenham a marca com um VMC,
e o painel avisa quando nao ha nenhum configurado.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Opus 5 committed 2026-09-07 08:12:11 -03:00
1 parent fcfee6a3f9
commit 6a602ab88f
16 files changed
+955 -5

No files matched your search

@@ -230,11 +230,15 @@ func run(cfg config) {
// list stops disagreeing with the page next to it.
router.SetDnsStatusWriter(postgres.NewDomainRepository(pool))
router.SetAdminDnsAPI(
&dnsSpecSource{cfg: cfg, dkim: dkimRepo},
&dnsSpecSource{cfg: cfg, dkim: dkimRepo, bimi: postgres.NewBimiRepository(pool)},
netdns.NewPublicVerifier(),
cfg.JwtSecret,
)
}
// The BIMI mark: served to receivers over the well-known URL, managed from
// the domains panel.
router.SetBimiAPI(bimiStore{postgres.NewBimiRepository(pool)}, cfg.JwtSecret, cfg.domain())
// The TLS panel reads the live watcher instead of the constants the admin
// service used to return, which reported a healthy certificate whatever
// the process actually held.
@@ -698,6 +702,7 @@ func runHealthcheck() {
type dnsSpecSource struct {
cfg config
dkim *postgres.DkimRepository
bimi *postgres.BimiRepository
}
func (s *dnsSpecSource) ExpectedRecords(ctx context.Context, domain string) ([]entity.DnsRecord, error) {
@@ -710,6 +715,15 @@ func (s *dnsSpecSource) ExpectedRecords(ctx context.Context, domain string) ([]e
RelaySpfInclude: relayConfig(s.cfg).SpfInclude(),
}
// The BIMI record is published only when a mark is actually stored, so
// the panel never advertises a logo that would 404.
if s.bimi != nil {
if logo, err := s.bimi.Load(ctx, domain); err == nil && logo != nil {
spec.BimiLogoURL = httppresentation.BimiLogoURL(s.cfg.PrimaryMailHost)
spec.BimiVmcURL = logo.VmcURL
}
}
// Read rather than provisioned: verifying must not create keys as a side
// effect of somebody opening a screen.
if s.dkim != nil {