feat(webmail,auth): leitor estilo Outlook, responder a todos, importantes e isolamento do admin de dominio
Webmail - Leitor de mensagem em componente proprio, com barra de comandos fixa, cabecalho compacto, faixa de imagens bloqueadas e chips de anexo. - Divisor arrastavel entre lista e leitor (mouse, setas, duplo clique), largura persistida e limites que preservam os dois paineis. - Destinatarios em chips (Para, Cc, Cco): aceita nomes com virgula, colagem de listas, validacao e bloqueio de envio com endereco invalido. - Responder a todos, sem o proprio endereco e sem repetidos; In-Reply-To e References nas respostas; prefixo Re:/Fwd: sem repeticao. - Marcar como importante (estrela, filtro, linha da lista) e enviar com alta importancia (Importance/X-Priority), com etiqueta no leitor. - Sanitizador: a regexp de URI customizada descartava todo atributo que nao fosse link (width, colspan, bgcolor...), o que desmontava tabelas de e-mail; imagens de fundo agora sao bloqueadas de forma reversivel. Protocols - POST /api/v1/mail/flagged (\Flagged) com bump de modseq para IMAP. - Renderizacao expoe message_id, in_reply_to e important. - Cabecalhos de resposta validados contra injecao. Auth - As views do admin de dominio eram globais: dashboard, fila de saida, DMARC, preflight e diagnostico mostravam dados de todos os dominios. Agora todas respeitam o escopo da sessao; super admin segue vendo tudo. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b5baf33a63
commit
50a67531ba
30 files changed
+1810
-348
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -34,6 +35,7 @@ type fakeMailStore struct {
|
||||
addresses map[string]string // address -> mailbox ID
|
||||
|
||||
seenCalls []string
|
||||
flaggedCalls []string
|
||||
deletedCalls []string
|
||||
movedCalls []string
|
||||
expungeErr error
|
||||
@@ -80,6 +82,11 @@ func (f *fakeMailStore) MarkSeen(_ context.Context, mailboxID, folder string, ui
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeMailStore) MarkFlagged(_ context.Context, mailboxID, folder string, uid uint32, flagged bool) error {
|
||||
f.flaggedCalls = append(f.flaggedCalls, fmt.Sprintf("%s/%s/%d/%t", mailboxID, folder, uid, flagged))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeMailStore) Expunge(_ context.Context, mailboxID, folder string, uid uint32) error {
|
||||
if f.expungeErr != nil {
|
||||
return f.expungeErr
|
||||
@@ -308,6 +315,43 @@ func TestMessageListDefaultsToTheInbox(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- flagging ------------------------------------------------------------
|
||||
|
||||
// Marking important is scoped to the session's own mailbox, never to an
|
||||
// identifier the request supplies.
|
||||
func TestFlaggingActsOnTheSessionsMailbox(t *testing.T) {
|
||||
store := storeWithMail()
|
||||
api := newMailAPI(t, store, nil)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
api.handleFlagged(rec, request(http.MethodPost, "/api/v1/mail/flagged", `{"uid":1,"flagged":true}`))
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("answered %d", rec.Code)
|
||||
}
|
||||
if len(store.flaggedCalls) != 1 || store.flaggedCalls[0] != "mailbox-1/inbox/1/true" {
|
||||
t.Errorf("flag call = %v, want [mailbox-1/inbox/1/true]", store.flaggedCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlaggingRefusesARequestWithNoSession(t *testing.T) {
|
||||
api := newMailAPI(t, storeWithMail(), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
api.handleFlagged(rec, httptest.NewRequest(http.MethodPost, "/api/v1/mail/flagged", strings.NewReader(`{"uid":1}`)))
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("answered %d without a session, want 401", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlaggingRejectsMalformedJson(t *testing.T) {
|
||||
api := newMailAPI(t, storeWithMail(), nil)
|
||||
rec := httptest.NewRecorder()
|
||||
api.handleFlagged(rec, request(http.MethodPost, "/api/v1/mail/flagged", `{`))
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("answered %d, want 400", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// --- deleting ------------------------------------------------------------
|
||||
|
||||
func TestDeleteMovesToTrashFromAnOrdinaryFolder(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user