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

@@ -35,6 +35,15 @@ type DnsRecordSpec struct {
// this domain. Without it every message the relay forwards fails SPF
// (PLAN.md section 10.4).
RelaySpfInclude string
// BimiLogoURL is where the published mark is served from. Empty when the
// domain has no logo, and then no BIMI record is published at all: an
// advertised record pointing at nothing is worse than none, because a
// receiver stops looking once it finds one.
BimiLogoURL string
// BimiVmcURL is the Verified Mark Certificate. Gmail and Outlook render
// the logo only when the record carries one; without it the record is
// still valid and other clients may use it.
BimiVmcURL string
}
// BuildDnsRecordSpecs generates the DNS records of PLAN.md section 7: the 13
@@ -196,6 +205,21 @@ func BuildDnsRecordSpecs(spec DnsRecordSpec) []DnsRecord {
})
}
// BIMI, published only when there is a logo to point at.
if spec.BimiLogoURL != "" {
value := fmt.Sprintf("v=BIMI1; l=%s;", spec.BimiLogoURL)
if spec.BimiVmcURL != "" {
value = fmt.Sprintf("v=BIMI1; l=%s; a=%s;", spec.BimiLogoURL, spec.BimiVmcURL)
}
records = append(records, DnsRecord{
Type: "TXT",
Name: fmt.Sprintf("default._bimi.%s", domainName),
Value: value,
TTL: 1,
Comment: "LambdaMail BIMI Record",
})
}
// 2. Conditional AAAA record if IPv6 is supplied
if ownsMailHost && serverIPv6 != "" {
records = append(records, DnsRecord{
@@ -195,3 +195,72 @@ func TestBuildDnsRecordSpecs_KeepsMailHostAddressesInsideTheDomain(t *testing.T)
t.Errorf("lost the mail host addresses (A=%v AAAA=%v)", a, aaaa)
}
}
// BIMI is published only when there is a mark to point at.
//
// A record that advertises a logo which is not there is worse than no record:
// a receiver stops looking once it finds one, so an empty pointer buys a
// failed fetch instead of the fallback it would otherwise have used.
func TestBuildDnsRecordSpecs_BimiOnlyWhenThereIsALogo(t *testing.T) {
without := BuildDnsRecordSpecs(DnsRecordSpec{
DomainName: "example.test",
MailHost: "mail.example.test",
ServerIPv4: "192.0.2.1",
})
for _, r := range without {
if strings.Contains(r.Name, "_bimi") {
t.Errorf("published %s with no logo configured", r.Name)
}
}
with := BuildDnsRecordSpecs(DnsRecordSpec{
DomainName: "example.test",
MailHost: "mail.example.test",
ServerIPv4: "192.0.2.1",
BimiLogoURL: "https://mail.example.test/.well-known/bimi/default.svg",
})
var found *DnsRecord
for i, r := range with {
if r.Name == "default._bimi.example.test" {
found = &with[i]
}
}
if found == nil {
t.Fatal("no BIMI record was published for a domain with a logo")
}
if found.Type != "TXT" {
t.Errorf("BIMI record type is %q, want TXT", found.Type)
}
if !strings.HasPrefix(found.Value, "v=BIMI1;") {
t.Errorf("value %q does not start with the version tag", found.Value)
}
// No certificate configured, so no a= tag: an empty one would advertise a
// certificate that cannot be fetched.
if strings.Contains(found.Value, "a=") {
t.Errorf("value %q carries an empty certificate tag", found.Value)
}
}
// Gmail and Outlook render the mark only when the record points at a Verified
// Mark Certificate, so the tag has to be carried when there is one.
func TestBuildDnsRecordSpecs_BimiCarriesTheCertificateWhenConfigured(t *testing.T) {
specs := BuildDnsRecordSpecs(DnsRecordSpec{
DomainName: "example.test",
MailHost: "mail.example.test",
ServerIPv4: "192.0.2.1",
BimiLogoURL: "https://mail.example.test/.well-known/bimi/default.svg",
BimiVmcURL: "https://mail.example.test/.well-known/bimi/default.pem",
})
for _, r := range specs {
if r.Name != "default._bimi.example.test" {
continue
}
if !strings.Contains(r.Value, "a=https://mail.example.test/.well-known/bimi/default.pem") {
t.Errorf("value %q does not carry the certificate", r.Value)
}
return
}
t.Fatal("no BIMI record was published")
}