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>
Mostrava tambem o tamanho de cada pasta. Sao numeros que nao mudam e
que ninguem esta a espera: enchiam a barra sem dizer nada de util.
Agora aparece um numero apenas onde ha correio por ler, e nada nas
restantes.
Consequencia a saber: Rascunhos nunca tem mensagens por ler - nada
marca um rascunho como lido - portanto passa a nao ter numero nenhum.
Quem quiser saber quantos rascunhos tem, ve-o no cabecalho da pasta.
O tamanho continua no tooltip do link, e o cabecalho da lista continua
a dize-lo por extenso, portanto a informacao nao se perdeu - deixou
apenas de estar sempre no campo de visao.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Encontrado a validar na produção, depois do deploy: o selector de pasta
de destino na tela de regras mostrava apenas INBOX, Archive, Junk e
Trash - exactamente sem as pastas que o utilizador tinha criado, que
são aquelas para que uma regra de arquivo costuma ser escrita.
A tela de regras escolhe um destino sem nenhuma mensagem aberta, por
isso não tem "pasta actual" para excluir e passava uma string vazia.
Uma pasta criada pelo utilizador não tem papel special-use, portanto
"role === current" era verdadeiro para todas elas de uma vez.
A exclusão passa a só se aplicar quando há de facto uma pasta actual.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Regras. A tela pedia um tipo de correspondência do Sieve pelo nome -
:contains, :is, :matches (regex) - e colava o valor escrito
diretamente num script. Punha uma linguagem de padrões à frente de
quem só queria dizer "o que vem do banco vai para Faturas", e bastava
uma aspa no valor para o script deixar de fazer parse, desativando
silenciosamente todas as regras de uma vez.
O vocabulário passa a ser o de quem escreve: um campo, uma condição em
português, um valor e uma ação. Os curingas são gerados a partir de
"começa com" e "termina com" - ninguém os escreve, e um asterisco
escrito por acaso continua a ser um asterisco. A pasta de destino é
escolhida de uma lista das pastas que existem mesmo, em vez de ser
escrita à mão: antes uma regra podia nomear uma pasta inexistente e
nunca disparar, sem dizer porquê.
A ordem passa a ser visível e alterável, porque a primeira regra que
arquiva uma mensagem decide para onde ela vai.
Um script que esta tela não consegue exprimir - escrito à mão, ou por
um cliente de secretária sobre ManageSieve - é mostrado tal como está,
em vez de ser reescrito na coisa mais parecida que este formulário sabe
dizer, o que mudaria em silêncio o que o correio de alguém faz.
Pastas próprias. Existiam na base de dados e as regras podiam arquivar
nelas, mas a barra lateral listava só as seis fixas, portanto não havia
como ver uma - nem como criar. Criar fica onde a vontade aparece, na
própria barra; apagar avisa que leva as mensagens junto, porque leva.
Encontrado a escrever isto: o motor tratava "\\*" como barra seguida de
curinga em vez de um asterisco literal. Como é esta tela que gera os
curingas, uma regra sobre um assunto com "50% *" teria correspondido a
todos os assuntos começados por "50% ". Corrigido no motor, com testes
dos dois lados.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Todos estes foram reproduzidos na interface real, com a conta em
mail.lucasrguerra.dev.br, e não em teste.
Contadores das pastas. O badge caía para o total sempre que não havia
nada por ler, no mesmo lugar e no mesmo estilo do contador de não
lidas: uma caixa de entrada com catorze mensagens lidas mostrava "14",
e ler mais uma nunca movia o número - ele nunca esteve contando o que
o leitor pensava. Os dois números agora são visualmente distintos e o
de não lidas só aparece quando existe mensagem por ler.
Em Rascunhos o badge e o total são o mesmo número por definição, então
o sidebar imprimia-o duas vezes: "Rascunhos 1 1". A regra passou para
folderBadge, onde é testada, e o tamanho da pasta é renderizado num
único sítio.
Ações do leitor. Todas as mensagens ofereciam os mesmos quatro botões,
onde a pasta devia decidir. Um e-mail que o próprio utilizador enviou
oferecia "marcar como não lida" - uma flag que nada lê numa pasta onde
nada é entregue - e um rascunho por acabar oferecia Responder e
Encaminhar, mas nenhuma forma de o terminar. Abrir um rascunho agora
abre o compositor com o que estava escrito e adota o seu UID, para que
continuar a escrever substitua esse rascunho em vez de deixar o antigo
para trás. Em Lixeira o botão de apagar diz que apaga em definitivo,
porque é o que faz.
Anexos. t("mail.attachments") era chamado sem o parâmetro que a string
declara, e a interface mostrava literalmente "Anexos ({count})" nos
três idiomas. Na lista, que sabe que há anexos mas não quantos, passa a
nomear o facto em vez de imprimir uma contagem que não tem.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The counts were computed inline in JSX where nothing could test them. The
sidebar matched a folder by a special-use role folders do not always carry, its
badges were fetched once and then froze - new mail did not raise them and
reading a message did not lower it - and the list header printed the size of
the loaded page, capped at 50, as the size of the folder. The rules now live in
mailCounts.ts behind tests, useFolders keeps them current from the server's
push and from this tab's own changes, and each filter chip carries its number.
Inline images never left the server: RenderMessage dropped every part a body
references as cid:, so mail written by any normal client rendered with broken
icons. They now come back as bounded data URIs, refusing image/svg+xml, and the
reader resolves them, sizes its frame from the message instead of a fixed
500px, and renders on the light surface mail assumes.
Reading mail no longer asks for a second factor - the mailbox is behind the
account's own password - while the console still requires one unconditionally,
now collected by a step-up that does not ask again for the password the session
in hand just proved. An admin sign-in issues the webmail session too, so
leaving the console is a link rather than another login.
Two defects found while testing the above:
- The admin sign-in checked the password and the second factor and never the
role, so any account that had enrolled a factor from its own settings could
obtain an admin-audience session.
- Session tokens carried nothing that varied below one second, so signing in
twice within the same second minted the identical token and collided with
the unique index on web_sessions.refresh_token_hash. A correct password
answered 500, and a double-clicked sign-in was enough to do it. Tokens now
carry a jti.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>