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>
80 lines
2.1 KiB
Go
80 lines
2.1 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// BimiRepository stores the logo a domain publishes for BIMI.
|
|
type BimiRepository struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewBimiRepository(pool *pgxpool.Pool) *BimiRepository {
|
|
return &BimiRepository{pool: pool}
|
|
}
|
|
|
|
// BimiLogo is a stored mark.
|
|
type BimiLogo struct {
|
|
SVG []byte
|
|
ETag string
|
|
VmcURL string
|
|
}
|
|
|
|
// Save replaces a domain's logo, returning the digest receivers can cache on.
|
|
func (r *BimiRepository) Save(ctx context.Context, domain string, svg []byte, vmcURL string) (string, error) {
|
|
sum := sha256.Sum256(svg)
|
|
etag := hex.EncodeToString(sum[:])
|
|
|
|
_, err := r.pool.Exec(ctx, `
|
|
INSERT INTO domain_bimi (domain_id, svg, etag, vmc_url, updated_at)
|
|
SELECT d.id, $2, $3, NULLIF($4, ''), NOW() FROM domains d WHERE lower(d.name) = lower($1)
|
|
ON CONFLICT (domain_id) DO UPDATE
|
|
SET svg = EXCLUDED.svg, etag = EXCLUDED.etag,
|
|
vmc_url = EXCLUDED.vmc_url, updated_at = NOW()
|
|
`, domain, svg, etag, vmcURL)
|
|
if err != nil {
|
|
return "", fmt.Errorf("save BIMI logo for %s: %w", domain, err)
|
|
}
|
|
return etag, nil
|
|
}
|
|
|
|
// Load returns a domain's logo, or nil when it has none.
|
|
func (r *BimiRepository) Load(ctx context.Context, domain string) (*BimiLogo, error) {
|
|
var logo BimiLogo
|
|
var vmc *string
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT b.svg, b.etag, b.vmc_url
|
|
FROM domain_bimi b JOIN domains d ON d.id = b.domain_id
|
|
WHERE lower(d.name) = lower($1)
|
|
`, domain).Scan(&logo.SVG, &logo.ETag, &vmc)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load BIMI logo for %s: %w", domain, err)
|
|
}
|
|
if vmc != nil {
|
|
logo.VmcURL = *vmc
|
|
}
|
|
return &logo, nil
|
|
}
|
|
|
|
// Delete removes a domain's logo.
|
|
func (r *BimiRepository) Delete(ctx context.Context, domain string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
DELETE FROM domain_bimi b USING domains d
|
|
WHERE b.domain_id = d.id AND lower(d.name) = lower($1)
|
|
`, domain)
|
|
if err != nil {
|
|
return fmt.Errorf("delete BIMI logo for %s: %w", domain, err)
|
|
}
|
|
return nil
|
|
}
|