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
@@ -26,6 +26,9 @@ const maxComposeBytes = 32 << 20 // 32 MiB
|
||||
type mailAPI struct {
|
||||
useCase *usecase.WebmailUseCase
|
||||
sessions *WebSessionVerifier
|
||||
// folders is optional: without it the folder routes report that the
|
||||
// feature is not configured rather than 404ing like a typo.
|
||||
folders *usecase.ManageFoldersUseCase
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||
@@ -291,6 +294,49 @@ func (m *mailAPI) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
// handleFolderAdmin creates, renames and deletes the folders a mailbox owner
|
||||
// keeps for themselves.
|
||||
func (m *mailAPI) handleFolderAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := m.authenticate(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if m.folders == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "FOLDERS_UNAVAILABLE",
|
||||
"Folder management is not configured")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Action string `json:"action"`
|
||||
Name string `json:"name"`
|
||||
NewName string `json:"new_name"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "Invalid JSON")
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
switch strings.ToLower(body.Action) {
|
||||
case "create":
|
||||
err = m.folders.Create(r.Context(), session.Email, body.Name)
|
||||
case "rename":
|
||||
err = m.folders.Rename(r.Context(), session.Email, body.Name, body.NewName)
|
||||
case "delete":
|
||||
err = m.folders.Delete(r.Context(), session.Email, body.Name)
|
||||
default:
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST",
|
||||
"action must be create, rename or delete")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
m.fail(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
// handleMove files one message into another folder.
|
||||
func (m *mailAPI) handleMove(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := m.authenticate(w, r)
|
||||
@@ -402,6 +448,15 @@ func (m *mailAPI) fail(w http.ResponseWriter, err error) {
|
||||
writeError(w, http.StatusTooManyRequests, "SEND_LIMIT_EXCEEDED", "Hourly recipient limit reached")
|
||||
case errors.Is(err, usecase.ErrSenderNotOwned):
|
||||
writeError(w, http.StatusForbidden, "FORBIDDEN", "Sender address does not belong to this account")
|
||||
case errors.Is(err, usecase.ErrFolderExists):
|
||||
writeError(w, http.StatusConflict, "FOLDER_EXISTS", "A folder with that name already exists")
|
||||
case errors.Is(err, usecase.ErrFolderReserved):
|
||||
writeError(w, http.StatusForbidden, "FOLDER_RESERVED",
|
||||
"That folder belongs to the mail system and cannot be changed")
|
||||
case errors.Is(err, usecase.ErrInvalidFolderName):
|
||||
writeError(w, http.StatusBadRequest, "INVALID_FOLDER_NAME", "That folder name cannot be used")
|
||||
case errors.Is(err, usecase.ErrFolderNotFound), errors.Is(err, port.ErrFolderMissing):
|
||||
writeError(w, http.StatusNotFound, "NOT_FOUND", "No folder with that name")
|
||||
case errors.Is(err, usecase.ErrFolderNotATarget):
|
||||
writeError(w, http.StatusBadRequest, "FOLDER_NOT_A_TARGET",
|
||||
"Messages cannot be moved into Sent or Drafts")
|
||||
|
||||
@@ -91,6 +91,7 @@ func (r *Router) registerRoutes() {
|
||||
r.mux.HandleFunc("/api/v1/mail/draft", r.handleMailDraft)
|
||||
r.mux.HandleFunc("/api/v1/mail/delete", r.handleMailDelete)
|
||||
r.mux.HandleFunc("/api/v1/mail/move", r.handleMailMove)
|
||||
r.mux.HandleFunc("/api/v1/mail/folders/manage", r.handleMailFolderAdmin)
|
||||
r.mux.HandleFunc("/api/v1/reports/dmarc", r.handleDmarcIngest)
|
||||
r.mux.HandleFunc("/api/v1/reports/tlsrpt", r.handleTlsRptIngest)
|
||||
}
|
||||
@@ -317,6 +318,24 @@ func (r *Router) handleMailDelete(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleMailFolderAdmin creates, renames and deletes a user's own folders.
|
||||
func (r *Router) handleMailFolderAdmin(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "POST required")
|
||||
return
|
||||
}
|
||||
if r.mailReady(w) {
|
||||
r.mail.handleFolderAdmin(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
// SetFolderAdmin enables managing a mailbox's own folders.
|
||||
func (r *Router) SetFolderAdmin(folders *appusecase.ManageFoldersUseCase) {
|
||||
if r.mail != nil {
|
||||
r.mail.folders = folders
|
||||
}
|
||||
}
|
||||
|
||||
// handleMailMove files one message into another folder.
|
||||
func (r *Router) handleMailMove(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
|
||||
Reference in new issue
Block a user