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:
1 parent
b9ebf606e1
commit
42420a7de2
14 files changed
+1082
-6
No files matched your search
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
@@ -118,3 +119,27 @@ func (r *SieveRepository) RenameScript(ctx context.Context, mailboxID string, ol
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActiveScriptFor returns the mailbox's active script, for the delivery path.
|
||||
//
|
||||
// Delivery needs only this one question answered, so it is a method of its own
|
||||
// rather than the delivery path reaching for the whole script-management
|
||||
// surface that ManageSieve uses.
|
||||
//
|
||||
// A mailbox with no active script returns empty rather than an error: having
|
||||
// no rules is the ordinary case, not a failure.
|
||||
func (r *SieveRepository) ActiveScriptFor(ctx context.Context, mailboxID uuid.UUID) (string, error) {
|
||||
var script string
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT script FROM sieve_scripts
|
||||
WHERE mailbox_id = $1 AND is_active
|
||||
LIMIT 1
|
||||
`, mailboxID).Scan(&script)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read the active script for %s: %w", mailboxID, err)
|
||||
}
|
||||
return script, nil
|
||||
}
|
||||
@@ -413,3 +413,76 @@ func boolToInt(b bool) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// --- folders the mailbox owner creates for themselves --------------------
|
||||
|
||||
// CreateFolder adds a folder with no special-use role.
|
||||
//
|
||||
// No role, because IMAP defines roles only for the standard set: a folder a
|
||||
// user invents is addressed by name, which is why the move and the rules match
|
||||
// on names as well as roles.
|
||||
func (r *WebmailRepository) CreateFolder(ctx context.Context, mailboxID, name string) error {
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
INSERT INTO folders (mailbox_id, name, special_use) VALUES ($1, $2, NULL)
|
||||
`, mailboxID, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create folder %q: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenameFolder changes a folder's name, keeping its messages and its UIDs.
|
||||
//
|
||||
// The messages are not touched: a UID belongs to a folder, and the folder is
|
||||
// still the same one, so renaming must not renumber anything or an IMAP client
|
||||
// holding those UIDs would lose track of every message in it.
|
||||
func (r *WebmailRepository) RenameFolder(ctx context.Context, mailboxID, from, to string) error {
|
||||
tag, err := r.pool.Exec(ctx, `
|
||||
UPDATE folders SET name = $3
|
||||
WHERE mailbox_id = $1 AND LOWER(name) = LOWER($2) AND special_use IS NULL
|
||||
`, mailboxID, from, to)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename folder %q: %w", from, err)
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return port.ErrFolderMissing
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteFolder removes a folder and the mail filed in it.
|
||||
//
|
||||
// The messages are expunged rather than deleted outright, matching what the
|
||||
// rest of the schema does: the blobs stay referenced until the retention sweep,
|
||||
// so a folder deleted by mistake is still recoverable from the database.
|
||||
func (r *WebmailRepository) DeleteFolder(ctx context.Context, mailboxID, name string) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var folderID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT id::text FROM folders
|
||||
WHERE mailbox_id = $1 AND LOWER(name) = LOWER($2) AND special_use IS NULL
|
||||
`, mailboxID, name).Scan(&folderID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return port.ErrFolderMissing
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE email_messages SET expunged_at = NOW()
|
||||
WHERE folder_id = $1 AND expunged_at IS NULL
|
||||
`, folderID); err != nil {
|
||||
return fmt.Errorf("expunge the folder's messages: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM folders WHERE id = $1`, folderID); err != nil {
|
||||
return fmt.Errorf("delete folder %q: %w", name, err)
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
Reference in new issue
Block a user