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
@@ -155,6 +155,9 @@ func (s *stubWebmailRepo) GetMessageBlob(context.Context, string, string, uint32
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
func (s *stubWebmailRepo) MarkSeen(context.Context, string, string, uint32, bool) error { return nil }
|
||||
func (s *stubWebmailRepo) MarkFlagged(context.Context, string, string, uint32, bool) error {
|
||||
return nil
|
||||
}
|
||||
func (s *stubWebmailRepo) Expunge(_ context.Context, _, _ string, uid uint32) error {
|
||||
s.expunged = append(s.expunged, uid)
|
||||
return s.expungeErr
|
||||
@@ -472,3 +475,43 @@ func TestMoveRejectsAnEmptyTarget(t *testing.T) {
|
||||
t.Error("an empty target should be refused")
|
||||
}
|
||||
}
|
||||
|
||||
// High importance is written as both headers clients read, and only on request.
|
||||
func TestImportantMessageCarriesPriorityHeaders(t *testing.T) {
|
||||
in := ComposeInput{From: "a@x.test", To: []string{"b@y.test"}, Subject: "s", Text: "t", Important: true}
|
||||
got := string(buildMimeMessage(in, []string{"b@y.test"}, "x.test"))
|
||||
for _, h := range []string{"Importance: high\r\n", "X-Priority: 1\r\n"} {
|
||||
if !strings.Contains(got, h) {
|
||||
t.Errorf("missing %q in\n%s", h, got)
|
||||
}
|
||||
}
|
||||
in.Important = false
|
||||
if got := string(buildMimeMessage(in, []string{"b@y.test"}, "x.test")); strings.Contains(got, "Importance") || strings.Contains(got, "X-Priority") {
|
||||
t.Error("an ordinary message was marked with a priority")
|
||||
}
|
||||
}
|
||||
|
||||
// A reply threads under the original through In-Reply-To and References.
|
||||
func TestReplyCarriesThreadingHeaders(t *testing.T) {
|
||||
in := ComposeInput{From: "a@x.test", To: []string{"b@y.test"}, Subject: "Re: s", Text: "t", InReplyTo: "<orig-1@y.test>"}
|
||||
got := string(buildMimeMessage(in, []string{"b@y.test"}, "x.test"))
|
||||
for _, h := range []string{"In-Reply-To: <orig-1@y.test>\r\n", "References: <orig-1@y.test>\r\n"} {
|
||||
if !strings.Contains(got, h) {
|
||||
t.Errorf("missing %q", h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The reply id comes from the request and lands in a header, so anything that
|
||||
// could start a new header must be dropped, not written.
|
||||
func TestInReplyToCannotInjectHeaders(t *testing.T) {
|
||||
for _, bad := range []string{
|
||||
"<a@b>\r\nBcc: evil@x.test", "<a@b>\nX: y", "no-brackets@b", "<a b@c>", "<a@b><c@d>", "", "<>",
|
||||
} {
|
||||
in := ComposeInput{From: "a@x.test", To: []string{"b@y.test"}, Subject: "s", Text: "t", InReplyTo: bad}
|
||||
got := string(buildMimeMessage(in, []string{"b@y.test"}, "x.test"))
|
||||
if strings.Contains(got, "In-Reply-To") || strings.Contains(got, "evil@x.test") {
|
||||
t.Errorf("accepted %q:\n%s", bad, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user