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
@@ -223,6 +223,63 @@ func (r *WebmailRepository) MarkSeen(
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// MarkFlagged sets or clears \Flagged. Unlike \Seen it moves no counter, but a
|
||||
// real change still bumps the folder's modseq: that is how an IMAP client
|
||||
// holding the folder open learns the flag changed without a full resync.
|
||||
func (r *WebmailRepository) MarkFlagged(
|
||||
ctx context.Context, mailboxID, folderName string, uid uint32, flagged bool,
|
||||
) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var messageID uuid.UUID
|
||||
var receivedAt any
|
||||
var folderID string
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT e.id, e.received_at, f.id::text FROM email_messages e
|
||||
JOIN folders f ON f.id = e.folder_id
|
||||
WHERE e.mailbox_id = $1 AND `+folderCondition+`
|
||||
AND e.uid = $3 AND e.expunged_at IS NULL
|
||||
`, mailboxID, folderName, int64(uid)).Scan(&messageID, &receivedAt, &folderID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrMessageNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var changed int64
|
||||
if flagged {
|
||||
tag, err := tx.Exec(ctx, `
|
||||
INSERT INTO message_flags (message_id, received_at, flag)
|
||||
VALUES ($1, $2, '\Flagged') ON CONFLICT DO NOTHING
|
||||
`, messageID, receivedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changed = tag.RowsAffected()
|
||||
} else {
|
||||
tag, err := tx.Exec(ctx, `
|
||||
DELETE FROM message_flags WHERE message_id = $1 AND received_at = $2 AND flag = '\Flagged'
|
||||
`, messageID, receivedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
changed = tag.RowsAffected()
|
||||
}
|
||||
|
||||
if changed > 0 {
|
||||
if _, err := tx.Exec(ctx,
|
||||
`UPDATE folders SET highest_modseq = highest_modseq + 1 WHERE id = $1`, folderID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// Expunge marks one message deleted and keeps the folder counters in step.
|
||||
//
|
||||
// Soft delete, matching the expunged_at column the rest of the schema uses: the
|
||||
|
||||
Reference in new issue
Block a user