Chegava como conversa nova. Ia sem qualquer cabecalho de encadeamento, por isso todos os clientes de correio a mostravam solta, sem relacao com a mensagem que a tinha desencadeado - e quem escreveu tinha de descobrir sozinho a qual dos seus emails aquilo respondia. In-Reply-To nomeia a mensagem que esta a ser respondida e References leva a cadeia inteira, que e o que permite a um cliente colocar a resposta no sitio certo mesmo quando nunca viu as mensagens pelo meio (RFC 5322 seccao 3.6.4). A cadeia e limitada em comprimento: uma conversa longa faria este cabecalho crescer sem fim. O assunto passa a ecoar o original - "Fora do escritorio: Re: Proposta comercial" - porque e o que a pessoa le na sua lista, e porque ha clientes que agrupam pelo assunto e nao apenas pelos cabecalhos. O texto configurado fica como prefixo em vez de ser substituido, para continuar a dizer o que a mensagem e. Um assunto que ja anuncia ser resposta nao recebe um segundo "Re:". Sem Message-ID no original nao se escreve nada: apontar para uma mensagem inexistente faria os clientes mostrarem a resposta agarrada a nada. Um Message-ID com quebra de linha e recusado por inteiro - viria de um remetente remoto, e uma quebra ali terminaria o cabecalho e deixaria o resto ser lido como mais cabecalhos. Os cabecalhos que impedem dois respondedores automaticos de se responderem um ao outro continuam todos la, com teste que o garante. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
112 lines
3.4 KiB
Go
112 lines
3.4 KiB
Go
package usecase
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"log"
|
|
"net/mail"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"lambdamail/protocols/internal/application/port"
|
|
"lambdamail/protocols/internal/domain/sieve"
|
|
)
|
|
|
|
// ApplyRulesUseCase runs a mailbox's own rules over an arriving message.
|
|
//
|
|
// Nothing did this. The scripts were stored by ManageSieve and written by the
|
|
// settings screen, and the delivery path never read either - so a vacation
|
|
// responder that was switched on never replied, and a filter that said "file
|
|
// this in Faturas" filed nothing, ever.
|
|
type ApplyRulesUseCase struct {
|
|
scripts port.SieveScriptReader
|
|
}
|
|
|
|
func NewApplyRulesUseCase(scripts port.SieveScriptReader) *ApplyRulesUseCase {
|
|
return &ApplyRulesUseCase{scripts: scripts}
|
|
}
|
|
|
|
// RuleDecision is what the mailbox's rules decided about one message.
|
|
type RuleDecision struct {
|
|
// Folder overrides where the message is filed, or is empty to leave the
|
|
// delivery path's own choice alone.
|
|
Folder string
|
|
// Discard drops the message after it has been accepted. The sender is
|
|
// still told the message was delivered, because a filing rule is not a
|
|
// rejection - telling them otherwise would have their server retry.
|
|
Discard bool
|
|
Flags []string
|
|
// Vacation is the automatic reply to send, or nil.
|
|
Vacation *sieve.VacationReply
|
|
}
|
|
|
|
// For evaluates the receiving mailbox's rules against one message.
|
|
//
|
|
// It never returns an error, and that is deliberate. Filing is a convenience;
|
|
// delivery is not. A script that does not parse, or a database that cannot be
|
|
// read, must leave the message delivered unfiltered rather than bouncing mail
|
|
// that the sender would then retry forever.
|
|
func (uc *ApplyRulesUseCase) For(
|
|
ctx context.Context, mailboxID uuid.UUID, recipient, sender string, payload []byte,
|
|
) RuleDecision {
|
|
if uc == nil || uc.scripts == nil {
|
|
return RuleDecision{}
|
|
}
|
|
|
|
source, err := uc.scripts.ActiveScriptFor(ctx, mailboxID)
|
|
if err != nil {
|
|
log.Printf("rules: could not read the rules for %s, delivering unfiltered: %v", recipient, err)
|
|
return RuleDecision{}
|
|
}
|
|
if strings.TrimSpace(source) == "" {
|
|
return RuleDecision{}
|
|
}
|
|
|
|
script, err := sieve.Parse(source)
|
|
if err != nil {
|
|
// Logged rather than swallowed silently: the user wrote a rule that
|
|
// cannot run, and that is worth knowing about.
|
|
log.Printf("rules: the rules for %s could not be read, delivering unfiltered: %v", recipient, err)
|
|
return RuleDecision{}
|
|
}
|
|
|
|
outcome := sieve.Evaluate(script, sieve.Message{
|
|
Headers: headersOf(payload),
|
|
Sender: sender,
|
|
Recipient: recipient,
|
|
})
|
|
|
|
return RuleDecision{
|
|
Folder: outcome.Folder,
|
|
Discard: outcome.Discard,
|
|
Flags: outcome.Flags,
|
|
Vacation: outcome.Vacation,
|
|
}
|
|
}
|
|
|
|
// headersOf reads the message's headers, keyed lowercased.
|
|
//
|
|
// A message whose headers cannot be parsed still gets evaluated, against an
|
|
// empty set: a rule simply will not match, which is the harmless outcome.
|
|
func headersOf(payload []byte) map[string][]string {
|
|
parsed, err := mail.ReadMessage(bytes.NewReader(payload))
|
|
if err != nil {
|
|
return map[string][]string{}
|
|
}
|
|
out := make(map[string][]string, len(parsed.Header))
|
|
for name, values := range parsed.Header {
|
|
out[strings.ToLower(name)] = values
|
|
}
|
|
return out
|
|
}
|
|
|
|
// firstHeader returns one header value, or empty when the message has none.
|
|
func firstHeader(headers map[string][]string, name string) string {
|
|
values := headers[name]
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
return values[0]
|
|
}
|