fix(vacation): descodifica o assunto e responde a cada pessoa uma vez

O assunto que chegou a uma caixa real: "Fora do escritorio: Re:
=?UTF-8?Q?Re=3A_Fora_do_escrit=C3=B3rio?=". Duas coisas erradas, e a
segunda maior do que parece.

Um assunto com acento viaja como palavra codificada (RFC 2047). Colei o
cabecalho em bruto, portanto aquilo foi lido tal e qual por uma pessoa.
E foi tambem o que derrotou a verificacao do "Re:" ja existente:
codificado, todo o assunto comeca por "=?", nunca por "Re:", e por isso
foi acrescentado outro por cima. Descodificar corrige os dois sintomas
de uma vez. Os prefixos acumulados ("Re: RE: Re:") passam a ser
reduzidos a um.

O que estava por tras: aquele assunto era "Re: Fora do escritorio" -
alguem tinha respondido a propria mensagem automatica, e ela respondeu
de novo. Faltava a supressao por remetente. O RFC 5230 seccao 4.1 pede
exactamente isto e propoe sete dias: sem ela, uma conversa de cinco
mensagens produzia cinco copias do mesmo aviso, e quem respondesse ao
aviso recebia outro - que e como uma pessoa e um respondedor automatico
acabam a falar um para o outro indefinidamente.

A supressao falha aberta: um registo que nao se consegue ler deixa a
resposta sair. Ser avisado duas vezes e uma falha menor do que nunca
saber que a caixa esta ausente, que e o proposito da funcionalidade. O
envio so e registado depois de a resposta estar mesmo na fila, para que
um envio falhado nao silencie a mensagem seguinte da mesma pessoa.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Opus 5 committed 2026-08-22 15:15:58 -03:00
1 parent 866bd21846
commit e4491479ef
10 files changed
+451 -6

No files matched your search

@@ -70,6 +70,7 @@ type ProcessInboundEmailUseCase struct {
submission *ProcessOutboundEmailUseCase
auth port.AuthRepository
replyHost string
suppression *VacationSuppression
}
func NewProcessInboundEmailUseCase(mailboxes port.MailboxRepository, blobs port.BlobStorage, messages port.InboundMessageRepository) *ProcessInboundEmailUseCase {
@@ -307,7 +308,11 @@ func (uc *ProcessInboundEmailUseCase) Handle(ctx context.Context, input ProcessI
if decision.Folder != "" {
folder = decision.Folder
}
if decision.Vacation != nil {
// Someone who has already been told recently is not told again:
// otherwise a conversation of five messages produces five copies
// of the same notice, and a person replying to the automatic
// message is answered once more.
if decision.Vacation != nil && uc.suppression.ShouldReply(ctx, recipient.ID, input.Sender) {
// The message being answered travels with the reply, so it
// arrives inside that conversation instead of as a loose note
// the sender has to match up themselves.
@@ -320,6 +325,7 @@ func (uc *ProcessInboundEmailUseCase) Handle(ctx context.Context, input ProcessI
OriginalMessageID: firstHeader(original, "message-id"),
OriginalReferences: firstHeader(original, "references"),
OriginalSubject: firstHeader(original, "subject"),
MailboxID: recipient.ID,
})
}
}