Commit Graph
13 Commits
Author SHA1 Message Date
Lucas RayanandClaude Opus 5 6a602ab88f feat(bimi): logotipo do dominio validado, alojado e publicado no DNS
A foto de perfil nao viaja: nem o Gmail nem o Outlook leem uma imagem por
utilizador do servidor de quem envia, e nao existe protocolo que o permita. O
avatar que esses mostram vem dos contactos de quem recebe ou da conta Google
ou Microsoft do remetente.

O que eles leem e BIMI, que e por dominio. Fica implementado: validacao,
alojamento e o registo.

O validador reporta todas as violacoes de uma vez, e nao a primeira. Um
receptor que recusa a marca nao explica nada -- simplesmente nao desenha -- por
isso descobrir as regras uma viagem de cada vez seria a pior maneira possivel
de aprender este formato. Sao verificados o baseProfile e a versao que os
receptores procuram, o viewBox quadrado (uma marca nao quadrada e cortada de
forma diferente por cada cliente), exactamente um title, e a ausencia de tudo
o que seja activo ou externo: script, handlers de evento, links, imagens
rasterizadas, animacoes e referencias para fora do documento.

O registo so e publicado quando ha mesmo uma marca guardada. Um registo que
aponta para um logotipo inexistente e pior do que nenhum, porque o receptor
para de procurar assim que encontra um.

O logotipo e servido aberto, porque quem o vai buscar sao os fornecedores de
correio a avaliar uma mensagem e esses nunca terao sessao aqui.

Fica escrito no README que o Gmail e o Outlook so desenham a marca com um VMC,
e o painel avisa quando nao ha nenhum configurado.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 08:12:11 -03:00
Lucas RayanandClaude Opus 5 fcfee6a3f9 feat(avatar): foto de perfil com editor de recorte circular
Carregar, enquadrar num circulo com zoom e arrasto, e guardar. A foto aparece
na barra lateral, na lista de mensagens e no leitor, caindo para as iniciais
quando nao ha nenhuma.

A geometria vai em funcoes puras com testes, porque e onde os erros moram: a
escala minima cobre o quadrado em vez de caber nele (caber deixa um canto
vazio e o ficheiro exportado leva uma cunha transparente), e o deslocamento e
preso a saliencia da imagem para que nenhum arrasto consiga trazer uma borda
para dentro do circulo. Uma dimensao NaN -- o que uma imagem que nao
descodificou reporta -- passava pelo guarda `<= 0`, porque NaN falha qualquer
comparacao, e propagava-se ate apagar o editor.

O recorte e exportado a partir dos pixeis da origem e nao da pre-visualizacao,
por isso a foto guardada e tao nitida quanto o original permite em vez de
ficar limitada ao tamanho do editor no ecra.

Do lado do servidor o tipo e decidido pelos bytes, nunca pelo Content-Type nem
pelo nome do ficheiro, que sao ambos escolhidos por quem carrega. SVG e
recusado apesar de ser imagem: e um documento que pode levar script e seria
servido de volta a partir da nossa propria origem.

A leitura exige sessao, verificada na propria rota porque o guarda de prefixo
so cobre /user/ e /admin/. Um endpoint aberto indexado por endereco responde
"esta caixa existe" a quem quer que pergunte, para todos os enderecos que
quiser experimentar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 08:04:12 -03:00
Lucas RayanandClaude Opus 5 c06691a056 fix(mail): relatorios DMARC nao mostravam o clipe de anexo
has_attachments so era definido para mensagens multipart. Um relatorio
agregado DMARC do Google e de parte unica: o corpo E o anexo, um
application/zip sem qualquer parte de texto. Ficava guardado como nao tendo
anexos, ao lado dos relatorios TLS-RPT do mesmo remetente e na mesma pasta,
que trazem uma linha de texto e por isso sao multipart/mixed e apareciam
marcados.

A regra passa a olhar tambem para a parte de topo: uma disposicao explicita
decide (um .txt enviado como a mensagem inteira e anexo, uma parte inline nao
e), e sem ela conta como anexo tudo o que nao seja texto nem multipart -- um
zip, um PDF, uma mensagem reencaminhada.

As mensagens ja guardadas mantinham a marca antiga, e sao justamente aquelas
que o utilizador esta a olhar. Ha um backfill que as re-le uma vez, em segundo
plano, sem atrasar o arranque. So examina as que estao marcadas como sem
anexos: a regra antiga nunca inventou um anexo que nao existisse, portanto uma
marca verdadeira nao precisa de segunda opiniao e a varredura fica do tamanho
do defeito. A tabela backfills guarda o marcador para nao repetir a cada
arranque.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:28:37 -03:00
Lucas RayanandClaude Opus 5 e4491479ef fix(vacation): descodifica o assunto e responde a cada pessoa uma vez
O assunto que chegou a uma caixa real: "Fora do escritorio: Re:
=?UTF-8?Q?Re=3A_Fora_do_escrit=C3=B3rio?=". Duas coisas erradas, e a
segunda maior do que parece.

Um assunto com acento viaja como palavra codificada (RFC 2047). Colei o
cabecalho em bruto, portanto aquilo foi lido tal e qual por uma pessoa.
E foi tambem o que derrotou a verificacao do "Re:" ja existente:
codificado, todo o assunto comeca por "=?", nunca por "Re:", e por isso
foi acrescentado outro por cima. Descodificar corrige os dois sintomas
de uma vez. Os prefixos acumulados ("Re: RE: Re:") passam a ser
reduzidos a um.

O que estava por tras: aquele assunto era "Re: Fora do escritorio" -
alguem tinha respondido a propria mensagem automatica, e ela respondeu
de novo. Faltava a supressao por remetente. O RFC 5230 seccao 4.1 pede
exactamente isto e propoe sete dias: sem ela, uma conversa de cinco
mensagens produzia cinco copias do mesmo aviso, e quem respondesse ao
aviso recebia outro - que e como uma pessoa e um respondedor automatico
acabam a falar um para o outro indefinidamente.

A supressao falha aberta: um registo que nao se consegue ler deixa a
resposta sair. Ser avisado duas vezes e uma falha menor do que nunca
saber que a caixa esta ausente, que e o proposito da funcionalidade. O
envio so e registado depois de a resposta estar mesmo na fila, para que
um envio falhado nao silencie a mensagem seguinte da mesma pessoa.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 15:15:58 -03:00
Lucas Rayan f330c4324e Add end-to-end tests for TLS RPT report ingestion and WebSocket event handling
- Implemented tests for ingesting TLS RPT reports from emails and ensuring they are stored correctly in the database.
- Added tests to verify that the same report can be ingested multiple times without duplication.
- Created tests for backfilling reports that were previously delivered to the inbox.
- Developed WebSocket event handling tests to ensure events are correctly routed to subscribers based on mailbox ownership.
- Included tests for the mail API to validate session handling, mailbox isolation, and error mapping.
- Ensured that the API rejects unauthorized requests and handles malformed JSON gracefully.
2026-08-21 09:55:37 -03:00
Lucas Rayan 880dced9e4 feat: implement message deletion functionality and improve draft handling
- Added `MoveToTrash` method in `WebmailRepository` to move messages to the Trash folder instead of deleting them permanently.
- Introduced `handleDelete` method in `mailAPI` to handle message deletion requests from the client.
- Updated router to include a new route for message deletion.
- Enhanced the compose page to manage draft integrity, ensuring drafts are correctly associated with sent messages.
- Implemented UI changes to allow users to delete messages from the mail folder view.
- Added tests to verify the correct behavior of message deletion and draft handling.
- Updated database migration scripts to reconcile folder counters and ensure accurate unread counts.
- Added tests for TLS-RPT report parsing and webmail repository functionalities.
2026-08-20 15:32:14 -03:00
Lucas RayanandClaude Opus 5 30014f0f41 #21: fix the DKIM, onboarding and console defects found reviewing the new work
DKIM rotation broke the domain it was run on. It retired the live key and
inserted a placeholder as ACTIVE - private_key_enc was the literal string
"dummy-encrypted-key" - so FindActiveKeys then failed for the whole
domain and outbound mail stopped being signed, with the real key already
gone. Reproduced against a database before changing anything.

The root cause runs deeper: the auth service derives its encryption key
with a plain SHA-256 of the master key while the protocols service uses
HKDF with its own info string, so nothing the auth service seals can ever
be opened by the signer. Rotation therefore moves to the protocols
service, which owns that vault and the real key generator. It retires the
previous key with a seven-day overlap in the same transaction rather than
deleting it, because mail already sent under it is still being verified.

Domain onboarding had three defects in one function. It built the alias
destination by interpolating the domain name into the SQL text, which was
injectable; it reused one placeholder for a CITEXT and a VARCHAR column,
which made the statement fail and happened to hide the injection; and it
ignored its scope argument entirely, so any DOMAIN_ADMIN could create
domains. It also provisioned postmaster, abuse and security, missing the
dmarc and tlsrpt aliases that the published records point at - PLAN.md
section 7.4b is explicit that reports bounce without them.

The TLS panel returned fixed values: acme_status "VALID" and 72 days to
expiry, whatever the process actually held. It now reads the certificate
watcher, and a compile-time assertion keeps the wiring from silently
regressing into "disabled". The audit log and the readiness scorecard were
fixtures too, and their field names did not match the API they now read.

Two tests failed and a third asserted a fabricated constant: one depended
on test order through a shared fixture that had a second factor enrolled,
one used a fixed domain name and so passed once per database, and one
expected "records: 13" from a function that never touches DNS. All three
are fixed, and the suite now passes twice in a row against one database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 13:33:33 -03:00
Lucas Rayan bd0af98076 #17: implement DANE TLSA, MTA-STS policy resolution, ARC sealing and mail authentication 2026-08-01 22:48:38 -03:00
Lucas Rayan 6ef427dc09 #15: implement ACME certificate store, Traefik/Coolify cert watcher & secrets vault 2026-08-01 22:47:53 -03:00
Lucas Rayan 769e0b59f0 #14: add DEFAULT fallback partition to email_messages table schema 2026-08-01 22:42:43 -03:00
Lucas Rayan 314eb67dda #11: implement MTA-STS, TLS-RPT & Autoconfig HTTP services 2026-07-25 10:18:03 -03:00
Lucas Rayan 10836ea98f #10: implement Cloudflare DNS automation engine (13 records & system aliases) 2026-07-25 05:26:14 -03:00
Lucas Rayan eb09fa0ced #1: implement F0 foundation - monorepo, migrations, Go domain layer, service skeletons, CI
Stands up the LambdaMail monorepo per PLAN.md's F0 milestone: local dev
scaffolding (.env.example, Makefile, .gitignore excluding PLAN.md/docs/
.superpowers/), the full initial Postgres schema as a golang-migrate
migration, the complete Go domain layer for services/protocols (value
objects, entities, domain events - zero external imports beyond
github.com/google/uuid) with unit tests, health-checked service skeletons
for auth/storage (TypeScript) and webmail (Next.js), a local
docker-compose.yaml wiring all services together, a language-enforcement
lint script, and a GitHub Actions CI workflow.

Built via 11 sequential tasks under subagent-driven development, each with
an independent implementer + reviewer cycle. Fixes applied during review:
DomainName.IsPunycode() added to satisfy the promised interface; ioredis
default-import typing under NodeNext; next.js bumped 15.0.3->15.5.21 to
resolve high/critical CVEs; lang-lint.mjs corrected to not flag its own
banned-terms list; web service healthcheck added so compose --wait
actually waits on it; webmail Dockerfile's missing public/ directory:
protocols go.mod version aligned with CI's Go 1.22 pin.

Known follow-ups for later phases (not blocking F0): go-arch-lint isn't
wired into CI yet; live docker compose up and migration apply have not
been exercised in this dev environment (no Docker/psql available here)
and will get their first real run in CI, which does have Docker.
2026-07-23 05:05:46 -03:00