feat(regras): aplica as regras na entrega e envia a resposta de ausência

A metade que faltava. O motor já existia e nada o chamava; agora o
caminho de entrega consulta as regras da caixa que está a receber, e a
resposta de ausência é mesmo enviada.

Cada destinatário tem as suas próprias regras, avaliadas sobre a sua
própria cópia: duas caixas neste servidor que recebem a mesma mensagem
podem arquivá-la em sítios diferentes.

Nada disto pode impedir o correio de chegar. Um script que não faz
parse, ou uma base de dados que não responde, deixa a mensagem ser
entregue sem filtro em vez de a recusar - recusar diria ao remetente
que a mensagem falhou, e o servidor dele voltaria a tentar para sempre.
discard é aceitar e depois deitar fora, pela mesma razão: uma regra de
arquivo não é uma rejeição.

A resposta de ausência sai pela mesma fila, assinatura e limites de
envio que qualquer outra mensagem deste servidor - não é um caminho
especial - e só depois de a mensagem que a desencadeou estar guardada,
para não responder a correio que a caixa afinal não recebeu. Leva
Auto-Submitted: auto-replied, que é o que impede duas caixas ambas de
férias de se responderem uma à outra até uma esgotar a quota.

Pastas próprias. Só existiam as seis padrão, portanto uma regra que
dizia "arquiva isto em Faturas" nomeava uma pasta que não havia como
criar. Criar, renomear e apagar, com as reservadas protegidas: cada uma
delas significa alguma coisa para o protocolo ou para este servidor, e
deixar renomear a INBOX partia caminhos que nenhuma mensagem de erro
explica. Renomear não mexe nas mensagens nem nos UIDs, senão um cliente
IMAP perdia de vista tudo o que lá está dentro.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Opus 5 committed 2026-08-22 13:35:17 -03:00
1 parent b9ebf606e1
commit 42420a7de2
14 files changed
+1082 -6

No files matched your search

@@ -28,8 +28,8 @@ type PersistInboundMessageInput struct {
// reader ever opens their own outgoing copy, so the flag was never set.
AlreadySeen bool
SPFResult string
DKIMResult string
DMARCResult string
DKIMResult string
DMARCResult string
}
// InboundMessageRepository durably records an accepted inbound message: for
@@ -31,3 +31,13 @@ type DeliveredReportStore interface {
// MoveToReportsFolder files a backfilled message out of the inbox.
MoveToReportsFolder(ctx context.Context, messageID uuid.UUID) error
}
// SieveScriptReader reads the active rule script for a mailbox.
//
// The delivery path needs this and nothing else about Sieve: the scripts are
// written by ManageSieve and by the settings screen, and read here.
type SieveScriptReader interface {
// ActiveScriptFor returns the mailbox's active script, or empty when it
// has none.
ActiveScriptFor(ctx context.Context, mailboxID uuid.UUID) (string, error)
}
@@ -74,6 +74,10 @@ type WebmailRepository interface {
MoveToTrash(ctx context.Context, mailboxID, folderName string, uid uint32) (uint32, error)
}
// ErrFolderMissing reports a folder that is not there, or that is a standard
// folder and therefore not the user's to change.
var ErrFolderMissing = errors.New("folders: no such folder for this mailbox")
// ErrNoTrashFolder reports a mailbox with nowhere to put deleted mail, rather
// than the message being destroyed instead.
var ErrNoTrashFolder = errors.New("webmail: this mailbox has no Trash folder")
@@ -81,3 +85,16 @@ var ErrNoTrashFolder = errors.New("webmail: this mailbox has no Trash folder")
// ErrAlreadyInTrash tells the caller to expunge rather than move: a message
// already in Trash has no further destination.
var ErrAlreadyInTrash = errors.New("webmail: message is already in Trash")
// FolderAdmin manages the folders a mailbox owner creates for themselves.
//
// Separate from WebmailRepository because these are writes to the folder list
// itself rather than reads of the mail inside it, and because the reserved
// folders are protected in the use case above this, not here.
type FolderAdmin interface {
ListFolders(ctx context.Context, mailboxID string) ([]WebmailFolder, error)
CreateFolder(ctx context.Context, mailboxID, name string) error
RenameFolder(ctx context.Context, mailboxID, from, to string) error
// DeleteFolder removes the folder and the messages filed in it.
DeleteFolder(ctx context.Context, mailboxID, name string) error
}