fix(mail): relatorios DMARC nao mostravam o clipe de anexo
has_attachments so era definido para mensagens multipart. Um relatorio agregado DMARC do Google e de parte unica: o corpo E o anexo, um application/zip sem qualquer parte de texto. Ficava guardado como nao tendo anexos, ao lado dos relatorios TLS-RPT do mesmo remetente e na mesma pasta, que trazem uma linha de texto e por isso sao multipart/mixed e apareciam marcados. A regra passa a olhar tambem para a parte de topo: uma disposicao explicita decide (um .txt enviado como a mensagem inteira e anexo, uma parte inline nao e), e sem ela conta como anexo tudo o que nao seja texto nem multipart -- um zip, um PDF, uma mensagem reencaminhada. As mensagens ja guardadas mantinham a marca antiga, e sao justamente aquelas que o utilizador esta a olhar. Ha um backfill que as re-le uma vez, em segundo plano, sem atrasar o arranque. So examina as que estao marcadas como sem anexos: a regra antiga nunca inventou um anexo que nao existisse, portanto uma marca verdadeira nao precisa de segunda opiniao e a varredura fica do tamanho do defeito. A tabela backfills guarda o marcador para nao repetir a cada arranque. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7f8d9b7ff1
commit
c06691a056
7 files changed
+279
-6
No files matched your search
@@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"lambdamail/protocols/internal/application/usecase"
|
||||
"lambdamail/protocols/internal/infrastructure/diskstorage"
|
||||
"lambdamail/protocols/internal/infrastructure/postgres"
|
||||
)
|
||||
|
||||
// attachmentBackfillName identifies this repair in the backfills table.
|
||||
const attachmentBackfillName = "has_attachments_single_part"
|
||||
|
||||
// backfillAttachmentFlags re-reads stored messages and marks the ones whose
|
||||
// body is itself an attachment.
|
||||
//
|
||||
// has_attachments was only set for multipart messages, so a DMARC aggregate
|
||||
// report - a single-part application/zip with no text at all - was filed as
|
||||
// having none. Correcting the rule fixes new deliveries; the messages already
|
||||
// in the mailbox need re-reading, which is what this does, once.
|
||||
//
|
||||
// It runs in the background and never blocks startup: this is cosmetic
|
||||
// metadata, and no message is at risk if it never finishes.
|
||||
func backfillAttachmentFlags(ctx context.Context, pool *pgxpool.Pool) {
|
||||
repo := postgres.NewAttachmentBackfill(pool)
|
||||
|
||||
done, err := repo.Done(ctx, attachmentBackfillName)
|
||||
if err != nil {
|
||||
log.Printf("attachment backfill: cannot read the marker, skipping: %v", err)
|
||||
return
|
||||
}
|
||||
if done {
|
||||
return
|
||||
}
|
||||
|
||||
blobs := diskstorage.NewLocalDiskBlobReader(pool)
|
||||
|
||||
// Bounded: a mailbox with a million read messages should not turn a deploy
|
||||
// into an hour of disk reads. What is left is picked up on the next start,
|
||||
// because the marker is only written once a pass changes nothing.
|
||||
const batch = 5000
|
||||
candidates, err := repo.Candidates(ctx, batch)
|
||||
if err != nil {
|
||||
log.Printf("attachment backfill: cannot list candidates: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
corrected := 0
|
||||
for _, c := range candidates {
|
||||
payload, err := blobs.ReadByID(ctx, c.BlobID)
|
||||
if err != nil {
|
||||
// A blob that cannot be read is not a reason to stop; the flag
|
||||
// stays as it was and the message is untouched.
|
||||
continue
|
||||
}
|
||||
if !usecase.ExtractMessageHeaders(payload).HasAttachments {
|
||||
continue
|
||||
}
|
||||
if err := repo.SetHasAttachments(ctx, c.ID); err != nil {
|
||||
log.Printf("attachment backfill: %v", err)
|
||||
continue
|
||||
}
|
||||
corrected++
|
||||
}
|
||||
|
||||
if len(candidates) < batch {
|
||||
// The whole set fit in one pass, so there is nothing left to do.
|
||||
if err := repo.MarkDone(ctx, attachmentBackfillName,
|
||||
"corrected single-part attachments"); err != nil {
|
||||
log.Printf("attachment backfill: %v", err)
|
||||
}
|
||||
}
|
||||
log.Printf("attachment backfill: examined %d message(s), corrected %d", len(candidates), corrected)
|
||||
}
|
||||
@@ -248,6 +248,11 @@ func run(cfg config) {
|
||||
}
|
||||
router.SetAdminTlsAPI(tlsStatus, cfg.PrimaryMailHost, cfg.TLSMode, cfg.JwtSecret, cfg.CertPollInterval)
|
||||
|
||||
// One-shot repair of has_attachments on messages stored before single-part
|
||||
// attachments were recognised. In the background: it is display metadata,
|
||||
// and nothing waits on it.
|
||||
go backfillAttachmentFlags(ctx, pool)
|
||||
|
||||
// Real-time updates: the outbox relay reads the events delivery wrote in
|
||||
// its own transaction and hands them to the hub, which pushes them to the
|
||||
// browser (PLAN.md sections 9.4 and 14.2).
|
||||
|
||||
Reference in new issue
Block a user