fix(webmail): acertos de interface e mover mensagens entre pastas
Cc/Cco. Os dois rótulos do compositor eram o último texto em inglês que restava, e "Bcc" tem nome próprio em português - quem não conhece a abreviatura inglesa não adivinha o que o campo faz. Larguras. A barra lateral tinha 244px fixos e nela não cabia "Console de administração", que transbordava no telemóvel e no computador: o botão passa a mostrar um rótulo curto e guarda o nome completo no tooltip. O endereço da conta usava break-words e partia-se a meio do domínio, deixando um "br" sozinho na linha de baixo; passa a truncar com o valor inteiro no tooltip. A lista tinha 404px fixos e o leitor 760px centrados, portanto num ecrã largo os assuntos eram espremidos numa coluna estreita enquanto o leitor ficava entre duas margens enormes. Ambos crescem agora com a janela. O assunto ganha overflow-wrap: os Message-ID dos relatórios do Google são uma única "palavra" sem espaços, mais longa que a coluna, e sem sítio onde quebrar saíam da linha. Imagens remotas. A escolha era reposta sempre que uma mensagem era aberta, por isso a mesma newsletter tinha de ser desbloqueada em cada leitura. A decisão passa a ser lembrada por remetente, com um segundo botão para a retirar. Bloqueado continua a ser o que acontece por omissão: uma imagem remota é um aviso de leitura que o remetente não pediu licença para receber. Assinatura. Era escrita num textarea - texto simples com quebras de linha reais - e injetada no compositor com innerHTML, onde uma quebra de linha é espaço em branco: todas desapareciam. Passa a ser um campo rico, com negrito, itálico, links e imagens, guardado como HTML e sanitizado nos dois sentidos, já que este valor é injetado como marcação na origem do webmail. Assinaturas guardadas pelo campo antigo continuam a funcionar: as suas quebras de linha são convertidas. Mover entre pastas. Faltava a única ação corrente que o leitor não tinha: arquivar uma mensagem para fora da caixa de entrada era impossível. Enviados e Rascunhos nunca são destino - descrevem como a mensagem veio a existir, não onde o leitor a arrumou - mas sair deles é permitido. Pastas criadas pelo utilizador, que não têm papel special-use, são destino como qualquer outra. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ac0521cb9b
commit
f179cb1f27
20 files changed
+911
-37
No files matched your search
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/emersion/go-message"
|
||||
|
||||
"lambdamail/protocols/internal/application/port"
|
||||
"lambdamail/protocols/internal/application/usecase"
|
||||
"lambdamail/protocols/internal/infrastructure/postgres"
|
||||
)
|
||||
@@ -290,6 +291,37 @@ func (m *mailAPI) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
// handleMove files one message into another folder.
|
||||
func (m *mailAPI) handleMove(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := m.authenticate(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Folder string `json:"folder"`
|
||||
UID uint32 `json:"uid"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4096)).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "Invalid JSON")
|
||||
return
|
||||
}
|
||||
if body.Folder == "" {
|
||||
body.Folder = "inbox"
|
||||
}
|
||||
if body.UID == 0 {
|
||||
writeError(w, http.StatusBadRequest, "INVALID_UID", "Message UID is required")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.useCase.Move(r.Context(), session.Email, body.Folder, body.UID, body.Target); err != nil {
|
||||
m.fail(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"moved": true})
|
||||
}
|
||||
|
||||
// handleSaveDraft stores the message being composed in the Drafts folder.
|
||||
func (m *mailAPI) handleSaveDraft(w http.ResponseWriter, r *http.Request) {
|
||||
session, ok := m.authenticate(w, r)
|
||||
@@ -370,6 +402,13 @@ 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.ErrFolderNotATarget):
|
||||
writeError(w, http.StatusBadRequest, "FOLDER_NOT_A_TARGET",
|
||||
"Messages cannot be moved into Sent or Drafts")
|
||||
case errors.Is(err, usecase.ErrNoTargetFolder):
|
||||
writeError(w, http.StatusBadRequest, "NO_TARGET_FOLDER", "A destination folder is required")
|
||||
case errors.Is(err, port.ErrNoTrashFolder):
|
||||
writeError(w, http.StatusNotFound, "NOT_FOUND", "That folder does not exist")
|
||||
case errors.Is(err, usecase.ErrDraftsUnavailable):
|
||||
writeError(w, http.StatusServiceUnavailable, "DRAFTS_UNAVAILABLE", "Draft storage is not configured")
|
||||
default:
|
||||
|
||||
Reference in new issue
Block a user