From caec501ec533e0689d338871865c4759d36df7d0 Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Sat, 22 Aug 2026 13:49:33 -0300 Subject: [PATCH] =?UTF-8?q?fix(sieve):=20reconhece=20endere=C3=A7os=20auto?= =?UTF-8?q?m=C3=A1ticos=20com=20sufixo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encontrado a correr o script guardado na caixa real antes de o pôr no ar. O Google envia os relatórios DMARC de noreply-dmarc-support@google.com, e a verificação só reconhecia a palavra exacta ou um sufixo com "+": a caixa teria respondido automaticamente a todos os relatórios diários que recebe. O prefixo passa a ser comparado com os separadores que os remetentes usam de facto - "+", "-", "." e "_" - e a lista cresce com os nomes correntes deste tipo de endereço. Continua a responder a uma pessoa cujo endereço apenas começa por uma dessas palavras, o que também tem teste: "bouncer" é gente, "bounces-123" não é. Co-Authored-By: Claude Opus 5 --- .../internal/domain/sieve/sieve_test.go | 41 +++++++++++++++++++ .../internal/domain/sieve/vacation.go | 19 ++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/services/protocols/internal/domain/sieve/sieve_test.go b/services/protocols/internal/domain/sieve/sieve_test.go index 26208ca..b1bba00 100644 --- a/services/protocols/internal/domain/sieve/sieve_test.go +++ b/services/protocols/internal/domain/sieve/sieve_test.go @@ -392,3 +392,44 @@ func TestEscapedQuestionMarkIsALiteralCharacter(t *testing.T) { t.Errorf("the escaped question mark matched any character: %q", out.Folder) } } + +// A reserved local part is recognised however the sender joined it to the rest +// of the address. +// +// Found by running the script stored on a real mailbox: Google sends its DMARC +// reports from noreply-dmarc-support@google.com, and matching only the exact +// word or a "+" suffix let that through - so the responder would have replied +// to every daily report, forever. +func TestVacationStaysSilentForReservedAddressesWithSuffixes(t *testing.T) { + senders := []string{ + "noreply-dmarc-support@google.com", + "no-reply.billing@example.test", + "bounces+abc123@example.test", + "mailer-daemon@example.test", + "notifications-noreply@example.test", + "MAILER-DAEMON@EXAMPLE.TEST", + } + for _, sender := range senders { + out := evaluate(t, vacationScript, Message{ + Sender: sender, + Headers: map[string][]string{"from": {sender}}, + }) + if out.Vacation != nil { + t.Errorf("would have replied to %s", sender) + } + } +} + +// The guard must not swallow a real person whose address merely starts with +// one of those words. +func TestVacationStillRepliesToAPersonWithASimilarAddress(t *testing.T) { + for _, sender := range []string{"noreplygames@example.test", "bouncer@example.test", "postmasters@example.test"} { + out := evaluate(t, vacationScript, Message{ + Sender: sender, + Headers: map[string][]string{"from": {sender}}, + }) + if out.Vacation == nil { + t.Errorf("stayed silent for %s, which is a person", sender) + } + } +} diff --git a/services/protocols/internal/domain/sieve/vacation.go b/services/protocols/internal/domain/sieve/vacation.go index 1601c7f..15ac5df 100644 --- a/services/protocols/internal/domain/sieve/vacation.go +++ b/services/protocols/internal/domain/sieve/vacation.go @@ -26,10 +26,25 @@ func shouldAutoReply(msg Message) bool { } // Addresses that exist to carry automated mail. Answering one is at best // noise and at worst a loop. - for _, reserved := range []string{"mailer-daemon", "postmaster", "no-reply", "noreply", "bounce", "bounces"} { - if local == reserved || strings.HasPrefix(local, reserved+"+") { + // + // The prefix is matched against every separator a real sender uses, not + // just "+". Google sends its DMARC reports from + // noreply-dmarc-support@google.com: checking only for the exact word or a + // "+" suffix let that through, and this mailbox would have auto-replied to + // every daily report it receives. + for _, reserved := range []string{ + "mailer-daemon", "mailerdaemon", "postmaster", "no-reply", "noreply", + "donotreply", "do-not-reply", "bounce", "bounces", "notification", + "notifications", "automated", "auto-reply", "autoreply", + } { + if local == reserved { return false } + for _, separator := range []string{"+", "-", ".", "_"} { + if strings.HasPrefix(local, reserved+separator) { + return false + } + } } // Headers that say, in one way or another, "this was not typed by a