feat(bimi): reconcile the BIMI record to Cloudflare and keep it in step with the logo

The reconciler never wrote the BIMI record: only the console's verification
knew about it, so a stored logo was never published to the zone.

- SyncDnsRecordsInput carries the BIMI logo and certificate URLs; the record is
  created, updated (a= added or dropped) and, when the logo is removed,
  deleted. Only a record carrying this reconciler's comment is retired: one
  made by hand is left alone
- bimi.Publish decides the record in one place for the timer sweep, the
  console's button and the panel's verification. A held certificate is
  advertised only while it is valid for the logo being served: replacing the
  logo ends the certificate, and a record that kept naming it makes every
  receiver reject BIMI, including those that need no certificate
- saving or removing the logo or the certificate triggers the reconciliation in
  the background; the panel shows the record as the server writes it, says when
  a certificate is held back and why, and has a publish-now button
- a read error is not 'no logo': the sync leaves the record alone instead of
  deleting it over a database hiccup

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-16 18:17:07 -03:00
1 parent 0ff9028927
commit 6e49ca00e2
17 files changed
+641 -59

No files matched your search

@@ -0,0 +1,122 @@
package usecase
import (
"context"
"strings"
"testing"
"lambdamail/protocols/internal/domain/entity"
)
func bimiInput(logo, cert string) SyncDnsRecordsInput {
return SyncDnsRecordsInput{
DomainName: "example.test",
MailHost: "mail.example.test",
ServerIPv4: "192.0.2.1",
BimiLogoURL: logo,
BimiVmcURL: cert,
}
}
const (
bimiName = "default._bimi.example.test"
bimiLogo = "https://mail.example.test/.well-known/bimi/default.svg"
bimiCert = "https://mail.example.test/.well-known/bimi/vmc.pem"
)
func bimiRecords(dns *fakeDnsProvider) []entity.DnsRecord { return dns.find("TXT", bimiName) }
// The gap this closes: the console knew the BIMI record, the reconciler did
// not, so storing a logo never put anything in the zone.
func TestSyncPublishesTheBimiRecord(t *testing.T) {
dns := newFakeDnsProvider()
uc := NewSyncDnsRecordsUseCase(dns, &fakeSystemAliasRepository{})
out, err := uc.Execute(context.Background(), bimiInput(bimiLogo, ""))
if err != nil {
t.Fatal(err)
}
got := bimiRecords(dns)
if len(got) != 1 || got[0].Value != "v=BIMI1; l="+bimiLogo+";" {
t.Fatalf("records = %+v", got)
}
if !strings.HasPrefix(got[0].Comment, "LambdaMail") {
t.Errorf("the record must carry the ownership comment, got %q", got[0].Comment)
}
if out.CreatedCount == 0 {
t.Error("nothing reported as created")
}
// Idempotent
out2, _ := uc.Execute(context.Background(), bimiInput(bimiLogo, ""))
if out2.CreatedCount != 0 || out2.UpdatedCount != 0 || len(bimiRecords(dns)) != 1 {
t.Errorf("second run changed something: created=%d updated=%d", out2.CreatedCount, out2.UpdatedCount)
}
}
func TestSyncUpdatesTheBimiRecordWhenACertificateIsAdded(t *testing.T) {
dns := newFakeDnsProvider()
uc := NewSyncDnsRecordsUseCase(dns, &fakeSystemAliasRepository{})
_, _ = uc.Execute(context.Background(), bimiInput(bimiLogo, ""))
out, err := uc.Execute(context.Background(), bimiInput(bimiLogo, bimiCert))
if err != nil {
t.Fatal(err)
}
got := bimiRecords(dns)
if len(got) != 1 || got[0].Value != "v=BIMI1; l="+bimiLogo+"; a="+bimiCert+";" {
t.Fatalf("records = %+v", got)
}
if out.UpdatedCount != 1 {
t.Errorf("updated = %d, want 1 (an update, not a second record)", out.UpdatedCount)
}
// And back, when the certificate is withheld
_, _ = uc.Execute(context.Background(), bimiInput(bimiLogo, ""))
if got := bimiRecords(dns); len(got) != 1 || strings.Contains(got[0].Value, "a=") {
t.Fatalf("the certificate stayed advertised: %+v", got)
}
}
func TestSyncRemovesTheBimiRecordItPublishedWhenTheLogoGoes(t *testing.T) {
dns := newFakeDnsProvider()
uc := NewSyncDnsRecordsUseCase(dns, &fakeSystemAliasRepository{})
_, _ = uc.Execute(context.Background(), bimiInput(bimiLogo, ""))
out, err := uc.Execute(context.Background(), bimiInput("", ""))
if err != nil {
t.Fatal(err)
}
if got := bimiRecords(dns); len(got) != 0 {
t.Fatalf("the record survived the logo: %+v", got)
}
if out.DeletedCount != 1 {
t.Errorf("deleted = %d", out.DeletedCount)
}
}
// A record the operator made by hand is theirs, even if it looks like ours.
func TestSyncLeavesAHandMadeBimiRecordAlone(t *testing.T) {
dns := newFakeDnsProvider()
dns.seed(entity.DnsRecord{Type: "TXT", Name: bimiName, Value: "v=BIMI1; l=https://cdn.example.test/logo.svg;", TTL: 1, Comment: "made by hand"})
uc := NewSyncDnsRecordsUseCase(dns, &fakeSystemAliasRepository{})
out, err := uc.Execute(context.Background(), bimiInput("", ""))
if err != nil {
t.Fatal(err)
}
if got := bimiRecords(dns); len(got) != 1 || out.DeletedCount != 0 {
t.Fatalf("a record this reconciler did not publish was touched: %+v deleted=%d", got, out.DeletedCount)
}
}
func TestSyncPublishesNothingForADomainWithoutALogo(t *testing.T) {
dns := newFakeDnsProvider()
uc := NewSyncDnsRecordsUseCase(dns, &fakeSystemAliasRepository{})
if _, err := uc.Execute(context.Background(), bimiInput("", "")); err != nil {
t.Fatal(err)
}
if got := bimiRecords(dns); len(got) != 0 {
t.Fatalf("records = %+v", got)
}
}
@@ -38,6 +38,11 @@ type SyncDnsRecordsInput struct {
DaneEnabled bool
RelaySpfInclude string
AdminEmailAddress string
// BimiLogoURL and BimiVmcURL are what the BIMI record should say (see
// bimi.Publish). With no logo URL the domain has no BIMI record, and one
// this reconciler published earlier is removed.
BimiLogoURL string
BimiVmcURL string
}
// SyncConflict describes a record that could not be reconciled because a
@@ -59,9 +64,12 @@ type SyncDnsRecordsOutput struct {
CreatedCount int
UpdatedCount int
UnchangedCount int
ConflictCount int
Conflicts []SyncConflict
Errors []string
// DeletedCount is the records retired because they are no longer wanted
// (a BIMI record whose logo was removed).
DeletedCount int
ConflictCount int
Conflicts []SyncConflict
Errors []string
// Unverified lists records that were written but could not be read back
// through the public resolvers.
Unverified []string
@@ -108,6 +116,8 @@ func (uc *SyncDnsRecordsUseCase) Execute(ctx context.Context, input SyncDnsRecor
TlsaHashes: tlsaHashes,
DaneEnabled: input.DaneEnabled,
RelaySpfInclude: input.RelaySpfInclude,
BimiLogoURL: input.BimiLogoURL,
BimiVmcURL: input.BimiVmcURL,
})
zoneID, err := uc.dnsProvider.GetZoneID(ctx, input.DomainName)
@@ -165,6 +175,8 @@ func (uc *SyncDnsRecordsUseCase) Execute(ctx context.Context, input SyncDnsRecor
}
}
uc.retireBimi(ctx, zoneID, input, existingByKey, output)
if input.AdminEmailAddress != "" && uc.aliasRepo != nil {
if err := uc.aliasRepo.EnsureSystemAliases(ctx, input.DomainName, input.AdminEmailAddress); err != nil {
return nil, fmt.Errorf("ensure system aliases: %w", err)
@@ -187,6 +199,31 @@ func (uc *SyncDnsRecordsUseCase) Execute(ctx context.Context, input SyncDnsRecor
return output, nil
}
// retireBimi removes the BIMI record this reconciler published once the domain
// no longer has a logo: left in place it advertises a logo URL that answers 404.
//
// Only a record carrying this reconciler's comment is touched. One the
// operator created by hand, even with the same v=BIMI1 tag, is theirs.
func (uc *SyncDnsRecordsUseCase) retireBimi(
ctx context.Context, zoneID string, input SyncDnsRecordsInput,
existingByKey map[string][]entity.DnsRecord, output *SyncDnsRecordsOutput,
) {
if input.BimiLogoURL != "" {
return
}
name := "default._bimi." + input.DomainName
for _, rec := range existingByKey[recordKey("TXT", name)] {
if !strings.HasPrefix(rec.Comment, ownedRecordCommentPrefix) {
continue
}
if err := uc.dnsProvider.DeleteRecord(ctx, zoneID, rec.ID); err != nil {
output.Errors = append(output.Errors, fmt.Sprintf("delete TXT %s: %v", name, err))
continue
}
output.DeletedCount++
}
}
// verifyPublished re-reads every desired record through public resolvers and
// returns the ones that did not check out.
func (uc *SyncDnsRecordsUseCase) verifyPublished(ctx context.Context, specs []entity.DnsRecord) []string {