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:
1 parent
0ff9028927
commit
6e49ca00e2
17 files changed
+641
-59
No files matched your search
@@ -187,7 +187,7 @@ func run(cfg config) {
|
||||
// ------------------------------------------------------- background jobs
|
||||
go runDeliveryWorker(ctx, outboundWorker)
|
||||
if cfg.CloudflareToken != "" {
|
||||
go runDnsReconciler(ctx, cfg, aliasRepo, dkimRepo, postgres.NewDomainRepository(pool))
|
||||
go runDnsReconciler(ctx, cfg, aliasRepo, dkimRepo, postgres.NewDomainRepository(pool), postgres.NewBimiRepository(pool))
|
||||
} else {
|
||||
log.Printf("CLOUDFLARE_API_TOKEN is not set: DNS reconciliation is disabled")
|
||||
}
|
||||
@@ -215,9 +215,19 @@ func run(cfg config) {
|
||||
// provider to talk to, so the route can say it is unavailable instead of
|
||||
// reaching Cloudflare with an empty token and reporting a refusal.
|
||||
if cfg.CloudflareToken != "" {
|
||||
bimiRepo := postgres.NewBimiRepository(pool)
|
||||
router.SetDnsReconciler(domainReconciler{run: func(c context.Context, domain string) (*usecase.SyncDnsRecordsOutput, error) {
|
||||
return reconcileDomainOnDemand(c, cfg, aliasRepo, dkimRepo, domain)
|
||||
return reconcileDomainOnDemand(c, cfg, aliasRepo, dkimRepo, bimiRepo, domain)
|
||||
}})
|
||||
// Saving or removing a logo or a certificate publishes the change: the
|
||||
// record in the zone follows the picture, with no button to remember.
|
||||
router.SetBimiPublisher(func(domain string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
if _, err := reconcileDomainOnDemand(ctx, cfg, aliasRepo, dkimRepo, bimiRepo, domain); err != nil {
|
||||
log.Printf("BIMI: publishing the record for %s after a change failed: %v", domain, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
router.SetFolderAdmin(usecase.NewManageFoldersUseCase(
|
||||
postgres.NewWebmailRepository(pool), postgres.NewWebmailRepository(pool)))
|
||||
@@ -578,6 +588,7 @@ func runDeliveryWorker(ctx context.Context, worker *usecase.OutboundWorkerUseCas
|
||||
func runDnsReconciler(
|
||||
ctx context.Context, cfg config, aliasRepo port.SystemAliasRepository,
|
||||
dkimRepo *postgres.DkimRepository, domainRepo *postgres.DomainRepository,
|
||||
bimiRepo *postgres.BimiRepository,
|
||||
) {
|
||||
syncUC := usecase.NewSyncDnsRecordsUseCase(cloudflare.NewCloudflareAdapter(cfg.CloudflareToken), aliasRepo)
|
||||
syncUC.SetVerifier(netdns.NewPublicVerifier())
|
||||
@@ -600,6 +611,9 @@ func runDnsReconciler(
|
||||
// (PLAN.md section 10.4).
|
||||
RelaySpfInclude: relayConfig(cfg).SpfInclude(),
|
||||
}
|
||||
// The logo and certificate a domain publishes through BIMI
|
||||
publication := bimiPublication(ctx, bimiRepo, cfg, domain)
|
||||
input.BimiLogoURL, input.BimiVmcURL = publication.LogoURL, publication.CertificateURL
|
||||
|
||||
if provisioner != nil {
|
||||
keys, err := provisioner.Execute(ctx, domain)
|
||||
@@ -717,19 +731,12 @@ 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.
|
||||
// The same decision the sync makes (bimi.Publish), so the panel expects
|
||||
// exactly what the reconciler writes.
|
||||
if s.bimi != nil {
|
||||
if logo, err := s.bimi.Load(ctx, domain); err == nil && logo != nil {
|
||||
spec.BimiLogoURL = httppresentation.BimiLogoURL(s.cfg.PrimaryMailHost)
|
||||
// The certificate held here wins: it is the one verified against the
|
||||
// logo being served. An external URL is the fallback.
|
||||
if len(logo.VmcPEM) > 0 {
|
||||
spec.BimiVmcURL = httppresentation.BimiVmcURL(s.cfg.PrimaryMailHost)
|
||||
} else {
|
||||
spec.BimiVmcURL = logo.VmcURL
|
||||
}
|
||||
}
|
||||
p := bimiPublication(ctx, s.bimi, s.cfg, domain)
|
||||
spec.BimiLogoURL = p.LogoURL
|
||||
spec.BimiVmcURL = p.CertificateURL
|
||||
}
|
||||
|
||||
// Read rather than provisioned: verifying must not create keys as a side
|
||||
|
||||
Reference in new issue
Block a user