Commit Graph
193 Commits
Author SHA1 Message Date
Lucas RayanandClaude Sonnet 5.5 1aac8be531 ci: create a Gitea release when a v* tag is pushed
CI / Lint de idioma (R9) (push) Successful in 4s
CI / TypeScript (auth) (push) Successful in 3m4s
CI / TypeScript (storage) (push) Successful in 14s
CI / Go (protocols) (push) Successful in 4m21s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Successful in 11s
CI / Imagem (auth) (push) Successful in 10s
CI / Testes (webmail) (push) Successful in 52s
CI / Imagem (migrations) (push) Successful in 5s
CI / Imagem (protocols) (push) Successful in 6s
CI / Imagem (rspamd) (push) Successful in 5s
CI / Imagem (storage) (push) Successful in 8s
CI / Imagem (webmail) (push) Successful in 8s
CI / Stack completa (push) Failing after 4s
The Release workflow only ever published container images; there was no entry
under Releases. The new job runs after every image is published and writes the
image list and the commit log since the previous tag.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 11:22:08 -03:00
Lucas RayanandClaude Sonnet 5.5 5d4e7147ce ci: cache what is rebuilt on every run
npm and Go module caches, the embedded Postgres binaries, Trivy's database, and
the daemon's own layer cache for image builds. The webmail Next build is no
longer run a second time outside the Dockerfile.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 10:07:21 -03:00
Lucas RayanandClaude Sonnet 5.5 0ff9aaaf88 Merge ci/gitea-registry: publish to the Gitea registry, workflows on Gitea Actions
CI / TypeScript (auth) (push) Successful in 2m53s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Successful in 1m8s
Release / Namespace das imagens (push) Successful in 1s
CI / Lint de idioma (R9) (push) Successful in 4s
CI / TypeScript (storage) (push) Successful in 14s
CI / Go (protocols) (push) Successful in 3m16s
CI / Imagem (auth) (push) Successful in 1m0s
CI / Build (webmail) (push) Successful in 2m15s
CI / Imagem (migrations) (push) Successful in 26s
CI / Imagem (rspamd) (push) Successful in 27s
CI / Imagem (protocols) (push) Successful in 1m32s
CI / Imagem (storage) (push) Successful in 1m19s
CI / Imagem (webmail) (push) Successful in 3m31s
CI / Stack completa (push) Successful in 3m42s
Release / ci (push) Successful in 11m19s
Release / Build migrations (linux/amd64) (push) Successful in 2m12s
Release / Build auth (linux/amd64) (push) Successful in 4m25s
Release / Build rspamd (linux/amd64) (push) Successful in 42s
Release / Build storage (linux/amd64) (push) Successful in 2m47s
Release / Build protocols (linux/amd64) (push) Successful in 12m49s
Release / Build auth (linux/arm64) (push) Successful in 43s
Release / Build migrations (linux/arm64) (push) Successful in 28s
Release / Build protocols (linux/arm64) (push) Successful in 1m29s
Release / Build rspamd (linux/arm64) (push) Successful in 33s
Release / Build storage (linux/arm64) (push) Successful in 42s
Release / Build webmail (linux/arm64) (push) Successful in 2m43s
Release / Build webmail (linux/amd64) (push) Successful in 21m34s
Release / Publica auth (push) Successful in 22s
Release / Publica migrations (push) Successful in 22s
Release / Publica rspamd (push) Successful in 21s
Release / Publica protocols (push) Successful in 21s
Release / Publica webmail (push) Successful in 21s
Release / Publica storage (push) Successful in 21s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 09:59:03 -03:00
Lucas RayanandClaude Sonnet 5.5 7baaab2fd7 test(auth): give real Argon2id hashing a 30s test budget
CI / TypeScript (auth) (push) Successful in 2m51s
CI / TypeScript (storage) (push) Successful in 14s
CI / Go (protocols) (push) Successful in 3m12s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Successful in 1m9s
CI / Lint de idioma (R9) (push) Successful in 4s
CI / Build (webmail) (push) Successful in 2m9s
CI / Imagem (auth) (push) Successful in 58s
CI / Imagem (migrations) (push) Successful in 25s
CI / Imagem (rspamd) (push) Successful in 28s
CI / Imagem (protocols) (push) Successful in 1m32s
CI / Imagem (storage) (push) Successful in 1m18s
CI / Imagem (webmail) (push) Successful in 3m30s
CI / Stack completa (push) Successful in 3m44s
The default 5s does not fit a chain of 11 hashes on a shared CI runner. The
production cost parameters are untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 09:47:16 -03:00
Lucas RayanandClaude Sonnet 5.5 a28a9bc9d4 ci: run Go tests as a non-root user
CI / Lint de idioma (R9) (push) Successful in 22s
CI / TypeScript (auth) (push) Failing after 3m33s
CI / Go (protocols) (push) Successful in 5m6s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Successful in 1m9s
CI / Build (webmail) (push) Successful in 2m6s
CI / Stack completa (push) Skipped
CI / Imagem (rspamd) (push) Successful in 29s
CI / Imagem (storage) (push) Successful in 1m19s
CI / TypeScript (storage) (push) Successful in 17s
CI / Imagem (migrations) (push) Successful in 2m24s
CI / Imagem (auth) (push) Successful in 2m58s
CI / Imagem (protocols) (push) Successful in 1m47s
CI / Imagem (webmail) (push) Successful in 2m35s
initdb, used by the embedded Postgres in the e2e suites, refuses to run as root.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 09:33:43 -03:00
Lucas RayanandClaude Sonnet 5.5 c30d618b3f ci: copy files into containers instead of bind-mounting, patch two advisories
CI / Lint de idioma (R9) (push) Successful in 9s
CI / Go (protocols) (push) Failing after 2m22s
CI / TypeScript (storage) (push) Successful in 12s
CI / TypeScript (auth) (push) Failing after 3m42s
CI / Imagem (migrations) (push) Successful in 1m45s
CI / Build (webmail) (push) Successful in 1m53s
CI / Stack completa (push) Skipped
CI / Trivy (dependências, segredos e Dockerfiles) (push) Successful in 1m3s
CI / Imagem (auth) (push) Successful in 2m53s
CI / Imagem (rspamd) (push) Successful in 27s
CI / Imagem (protocols) (push) Successful in 2m10s
CI / Imagem (storage) (push) Canceled after 2m9s
CI / Imagem (webmail) (push) Canceled after 1m6s
The job workspace is a volume the DinD daemon cannot see, so migrations and the
Trivy filesystem scan now docker cp their inputs. Trivy's image gate flagged
next 15.5.27 (unauthenticated RCE in image optimisation, fixed in 15.5.28) and
go-jose 4.1.4 (DoS, fixed in 4.1.6); both are patch bumps.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 09:22:42 -03:00
Lucas RayanandClaude Sonnet 5.5 248e857b85 ci: rerun on the runner with a shared /workspace
CI / Lint de idioma (R9) (push) Successful in 10s
CI / Go (protocols) (push) Failing after 29s
CI / TypeScript (auth) (push) Failing after 37s
CI / TypeScript (storage) (push) Successful in 25s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Successful in 1m6s
CI / Build (webmail) (push) Successful in 1m50s
CI / Stack completa (push) Skipped
CI / Imagem (migrations) (push) Successful in 1m39s
CI / Imagem (auth) (push) Successful in 2m58s
CI / Imagem (rspamd) (push) Successful in 29s
CI / Imagem (protocols) (push) Failing after 1m50s
CI / Imagem (storage) (push) Successful in 1m20s
CI / Imagem (webmail) (push) Failing after 2m40s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 09:11:32 -03:00
Lucas RayanandClaude Sonnet 5.5 33fa05b019 ci: run Trivy from its official image and drop the gha cache
CI / Imagem (protocols) (push) Failing after 1m29s
CI / Lint de idioma (R9) (push) Successful in 3s
CI / Go (protocols) (push) Failing after 30s
CI / TypeScript (auth) (push) Failing after 36s
CI / TypeScript (storage) (push) Successful in 16s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Successful in 1m14s
CI / Build (webmail) (push) Successful in 1m57s
CI / Stack completa (push) Skipped
CI / Imagem (auth) (push) Successful in 57s
CI / Imagem (migrations) (push) Successful in 39s
CI / Imagem (rspamd) (push) Successful in 30s
CI / Imagem (storage) (push) Successful in 1m20s
CI / Imagem (webmail) (push) Canceled after 2m58s
aquasecurity/trivy-action clones the Trivy repository from the workflow's own
server, which on Gitea is not GitHub. The gha buildx cache has no Gitea
counterpart.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 09:01:45 -03:00
Lucas RayanandClaude Sonnet 5.5 91c5df433e ci: build linux/amd64 under QEMU on the arm64 runner
CI / Lint de idioma (R9) (push) Successful in 10s
CI / Go (protocols) (push) Failing after 31s
CI / TypeScript (auth) (push) Failing after 37s
CI / TypeScript (storage) (push) Successful in 23s
CI / Build (webmail) (push) Successful in 1m47s
CI / Stack completa (push) Skipped
CI / Trivy (dependências, segredos e Dockerfiles) (push) Failing after 3m8s
CI / Imagem (auth) (push) Canceled after 3m32s
CI / Imagem (migrations) (push) Canceled after 2m4s
CI / Imagem (protocols) (push) Canceled after 48s
CI / Imagem (rspamd) (push) Canceled after 48s
CI / Imagem (storage) (push) Canceled after 0s
CI / Imagem (webmail) (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 08:56:34 -03:00
Lucas RayanandClaude Sonnet 5.5 59363734ff ci: publish images to the Gitea registry, use the gitea context
CI / Lint de idioma (R9) (push) Successful in 4s
CI / Go (protocols) (push) Failing after 5s
CI / TypeScript (auth) (push) Failing after 6s
CI / TypeScript (storage) (push) Successful in 12s
CI / Build (webmail) (push) Successful in 1m38s
CI / Stack completa (push) Skipped
CI / Trivy (dependências, segredos e Dockerfiles) (push) Failing after 1m32s
CI / Imagem (auth) (push) Failing after 11s
CI / Imagem (migrations) (push) Failing after 11s
CI / Imagem (protocols) (push) Failing after 11s
CI / Imagem (rspamd) (push) Failing after 11s
CI / Imagem (storage) (push) Failing after 11s
CI / Imagem (webmail) (push) Failing after 11s
Release now pushes to git.lucasrguerra.dev.br instead of ghcr.io, tags each
architecture separately (no artifact upload, no gha cache) and joins them with
imagetools. GITHUB_OUTPUT and GITHUB_STEP_SUMMARY stay: they are the runner's
own interface and the GITEA_* equivalents are not set.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 08:48:44 -03:00
Lucas RayanandClaude Sonnet 5.5 9a722bc5ac ci: publish images to the Gitea registry and move workflows to .gitea
CI / Lint de idioma (R9) (push) Successful in 3s
CI / Go (protocols) (push) Failing after 20s
CI / TypeScript (auth) (push) Canceled after 4s
CI / TypeScript (storage) (push) Canceled after 0s
CI / Build (webmail) (push) Canceled after 0s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Canceled after 0s
CI / Imagem (auth) (push) Canceled after 0s
CI / Imagem (migrations) (push) Canceled after 0s
CI / Imagem (protocols) (push) Canceled after 0s
CI / Imagem (rspamd) (push) Canceled after 0s
CI / Imagem (storage) (push) Canceled after 0s
CI / Imagem (webmail) (push) Canceled after 0s
CI / Stack completa (push) Canceled after 0s
Release now pushes to git.lucasrguerra.dev.br instead of ghcr.io, tags each
architecture separately (no artifact upload, no gha cache) and joins them with
imagetools. Workflows live in .gitea/workflows and use the gitea context.
GITHUB_OUTPUT and GITHUB_STEP_SUMMARY stay: they are the runner's own interface.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 08:48:14 -03:00
Lucas RayanandClaude Sonnet 5.5 fa2dcb578e Merge docs/readme-style: a README with a face, and complete entity decoding in previews
CI / Lint de idioma (R9) (push) Successful in 43s
CI / Go (protocols) (push) Failing after 42s
CI / TypeScript (auth) (push) Failing after 42s
CI / TypeScript (storage) (push) Successful in 12s
CI / Build (webmail) (push) Successful in 1m37s
CI / Stack completa (push) Skipped
CI / Imagem (webmail) (push) Canceled after 0s
CI / Imagem (storage) (push) Canceled after 0s
CI / Imagem (rspamd) (push) Canceled after 0s
CI / Imagem (protocols) (push) Canceled after 0s
CI / Imagem (migrations) (push) Canceled after 3m38s
CI / Imagem (auth) (push) Canceled after 5m39s
Release / ci (push) Canceled after 12m51s
Release / Namespace das imagens (push) Canceled after 0s
Release / Build auth (linux/amd64) (push) Canceled after 0s
Release / Build auth (linux/arm64) (push) Canceled after 0s
Release / Build migrations (linux/amd64) (push) Canceled after 0s
Release / Build migrations (linux/arm64) (push) Canceled after 0s
Release / Build protocols (linux/amd64) (push) Canceled after 0s
Release / Build protocols (linux/arm64) (push) Canceled after 0s
Release / Build rspamd (linux/amd64) (push) Canceled after 0s
Release / Build rspamd (linux/arm64) (push) Canceled after 0s
Release / Build storage (linux/amd64) (push) Canceled after 0s
Release / Build storage (linux/arm64) (push) Canceled after 0s
Release / Build webmail (linux/amd64) (push) Canceled after 0s
Release / Build webmail (linux/arm64) (push) Canceled after 0s
Release / Publica auth (push) Canceled after 0s
Release / Publica migrations (push) Canceled after 0s
Release / Publica protocols (push) Canceled after 0s
Release / Publica rspamd (push) Canceled after 0s
Release / Publica storage (push) Canceled after 0s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Failing after 3m16s
Release / Publica webmail (push) Canceled after 0s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 07:55:12 -03:00
Lucas RayanandClaude Sonnet 5.5 1e06491019 docs: a README with a face, in English and Portuguese; decode every HTML entity in previews
The README opens like a product page: the mark, a line on what it is, the
links, the language switch and a picture of the real interface running on an
invented mailbox, in dark and light and in both languages. scripts/screenshots
holds that invented mailbox and the script that photographs it, so the pictures
can be redone. Previews now decode any HTML entity through the browser rather
than a short list, so a Portuguese or marketing preview no longer prints
&ccedil; or &middot;.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 07:55:12 -03:00
Lucas RayanandClaude Sonnet 5.5 8226c76d7a Merge docs/update-2026-10: document the new webmail
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 06:24:36 -03:00
Lucas RayanandClaude Sonnet 5.5 58d6f8e9e2 docs: sessions, accounts, trusted senders, conversations, themes and the new reader
README: what the webmail now does, the architecture roles, a short guide with the
shortcuts, testing with a real database, and an honest list of what is not
finished. New docs/WEBMAIL.md as the reference for each feature and for the
colour system. OPERATIONS: sessions and trusted senders. DEPLOYMENT: the
two-step release as API calls, and what to expect when upgrading. MAILBOX-CORE:
the conversation invariants and migrations 0015 to 0018.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 06:24:36 -03:00
Lucas RayanandClaude Sonnet 5.5 c00134f2eb chore: keep the logo generator in the repository
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 06:12:49 -03:00
Lucas RayanandClaude Sonnet 5.5 8db69c5af9 Merge feat/viewer-and-logo: a drawn logo and a reader that suits both themes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 05:57:52 -03:00
Lucas RayanandClaude Sonnet 5.5 fece870d49 feat(webmail): a better reader and a drawn logo
The mark is rebuilt as a lambda of two square-cut strokes of one weight, joined
without a seam, on a flat ultramarine tile with continuous corners: the rounded
strokes and the gradient are gone. A message written plainly is now drawn in the
app's own colours instead of a white slab, with text too dark for the ground
lifted and white backdrops cleared; a designed one (newsletter, receipt) keeps
its white sheet. A reply's quoted history folds behind a button. A narrower
reading column, a larger title, and a way to see a plain message as sent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 05:57:52 -03:00
Lucas RayanandClaude Sonnet 5.5 9e50b47b5d chore(wolf): record what this stretch of work taught
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:44:54 -03:00
Lucas RayanandClaude Sonnet 5.5 d29ecec8d9 Merge feat/new-interface: logo and icons, dark and light themes, command menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:33:15 -03:00
Lucas RayanandClaude Sonnet 5.5 e667843b59 feat(webmail): the new interface - logo, dark and light themes, command menu
A mark (a lambda on an ultramarine tile with the flap of an envelope), the
favicon, home-screen icons and an install manifest. Every colour is a variable
defined once per theme, and the Tailwind scales the screens already use read
them, so dark (the default) and light are a change of values and not of
classes. Geist is served by the app itself. Ctrl+K opens a command menu for
folders, actions, accounts and appearance, and typing something that matches no
command searches the mail; c writes a message and / goes to the search field.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:33:15 -03:00
Lucas RayanandClaude Sonnet 5.5 6b97807856 Merge feat/conversations: conversations in cascade, answered marker, reply under the conversation, and Undo send that undoes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:07:40 -03:00
Lucas RayanandClaude Sonnet 5.5 897f0122f4 fix(webmail): Undo send really undoes, and a send is not made twice
The countdown sent from inside a state updater and Undo's button submitted the
form, so Undo stopped the timer and then started the send over. The timer is
now held in a ref, Undo is type=button, and leaving the screen flushes a
pending send once. Snippets drop a stylesheet the preview was cut inside of.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 22:05:43 -03:00
Lucas RayanandClaude Sonnet 5.5 2080489319 feat(webmail): conversations - list, stacked reader, inline reply, settings switch
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:46:11 -03:00
Lucas RayanandClaude Sonnet 5.5 86a69d1a96 refactor(webmail): the composer is a component, so a reply can be written under a conversation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:41:59 -03:00
Lucas RayanandClaude Sonnet 5.5 997dd8848f feat(auth): conversation_view preference, on by default
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:40:36 -03:00
Lucas RayanandClaude Sonnet 5.5 67d0c8aa5a feat(protocols): conversations - thread_id and in_reply_to written at delivery, list and read APIs
A reply now joins the conversation of the message it answers (Message-ID,
In-Reply-To, References), falling back to the subject for replies with no
headers. /mail/threads lists a folder as conversations and /mail/thread/{id}
returns one. Replying from the webmail flags the answered message.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:38:36 -03:00
Lucas RayanandClaude Sonnet 5.5 ff89e9dc44 chore(wolf): log the discard-draft bug
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:03:20 -03:00
Lucas RayanandClaude Sonnet 5.5 657a43d612 fix(webmail): Discard really discards the draft
The button was a link to the inbox, so the autosaved draft stayed in Drafts.
It now removes the stored draft for good (not into Trash), stops the autosave,
and cleans up a save that was already in flight.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 16:02:27 -03:00
Lucas RayanandClaude Sonnet 5.5 d283ff6f8d Merge feat/trusted-senders: a sender the mailbox trusts skips spam and always shows images, kept on the server
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:48:35 -03:00
Lucas RayanandClaude Sonnet 5.5 d2611d39fa feat(webmail): trusted senders on the server, Not spam, and a trust pill in the reader
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:48:35 -03:00
Lucas RayanandClaude Sonnet 5.5 d7f9659b5e feat(protocols): trusted senders are not filed as spam, when the message proves its sender
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:45:10 -03:00
Lucas RayanandClaude Sonnet 5.5 b536ed13e7 feat(auth): trusted senders table and API
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:42:30 -03:00
Lucas RayanandClaude Sonnet 5.5 cc0adcd94d fix(auth,webmail): list a pre-accounts session in the switcher; preview as plain text
A browser signed in before account cookies existed has only lm_user_session,
so the switcher showed it as loading forever. The active session now always
counts and is filed on first listing. Message previews drop the HTML and
entities the server stored.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:23:40 -03:00
Lucas RayanandClaude Sonnet 5.5 026f24a64a fix(auth): sign the webmail token for 30 days, not only its cookie
issueSession raised the cookie and the session row to 30 days but still let
createJwt default to 8 hours, so the session would have died at 8 hours with
a cookie that outlived it. Also covers account switching, sign-out and
renewal against a real database.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:04:24 -03:00
Lucas RayanandClaude Sonnet 5.5 54203d596e Merge feat/persistent-sessions-accounts: 30-day sliding sessions, several accounts at once, mail rendered with its own styles
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:47:44 -03:00
Lucas RayanandClaude Sonnet 5.5 641ab4bbe8 feat(webmail): keep a message's own stylesheet so designed mail renders as sent
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:45:34 -03:00
Lucas RayanandClaude Sonnet 5.5 96e624dd12 feat(webmail): account switcher for the several signed-in accounts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:44:50 -03:00
Lucas RayanandClaude Sonnet 5.5 0efc8f096a feat(auth): 30-day sliding webmail sessions and several accounts signed in at once
The webmail session now lasts 30 days and is swapped for a fresh token once a
day while in use; the console keeps its fixed 8 hours. Each sign-in is also
filed under lm_acct_<mailbox id>, with new endpoints to list and switch
accounts, and sign-out now leaves the other accounts signed in.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 14:42:59 -03:00
Lucas RayanandClaude Sonnet 5.5 aea4c1ee5e Merge fix/bimi-per-domain-urls: each domain's BIMI logo under its own URL, and the DNS publisher wired
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:53:50 -03:00
Lucas RayanandClaude Sonnet 5.5 60c6f5731c fix(bimi): each domain's logo under its own URL, and the publisher actually wired
Two bugs found by the setup check right after the last deploy:

- every domain's BIMI record named the deployment's own mail host, and the
  server picked the domain from the host name, so only the primary domain's
  logo could be fetched: ciberlab.seg.br's record pointed at a 404. The logo
  and the certificate are now served at /.well-known/bimi/<domain>/..., the
  record names that, and the webmail routes it. The old path still answers
- SetBimiPublisher ran before SetBimiAPI, found no BIMI API to attach to and
  was dropped, so saving a logo never published the DNS record by itself. It is
  kept on the router now, so the order does not matter

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:49:52 -03:00
Lucas RayanandClaude Sonnet 5.5 e3366ef09d Merge feat/bimi-dns-reconcile: publish the BIMI record to Cloudflare and keep it in step with the logo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:38:16 -03:00
Lucas RayanandClaude Sonnet 5.5 6e49ca00e2 feat(bimi): reconcile the BIMI record to Cloudflare and keep it in step with the logo
The reconciler never wrote the BIMI record: only the console's verification
knew about it, so a stored logo was never published to the zone.

- SyncDnsRecordsInput carries the BIMI logo and certificate URLs; the record is
  created, updated (a= added or dropped) and, when the logo is removed,
  deleted. Only a record carrying this reconciler's comment is retired: one
  made by hand is left alone
- bimi.Publish decides the record in one place for the timer sweep, the
  console's button and the panel's verification. A held certificate is
  advertised only while it is valid for the logo being served: replacing the
  logo ends the certificate, and a record that kept naming it makes every
  receiver reject BIMI, including those that need no certificate
- saving or removing the logo or the certificate triggers the reconciliation in
  the background; the panel shows the record as the server writes it, says when
  a certificate is held back and why, and has a publish-now button
- a read error is not 'no logo': the sync leaves the record alone instead of
  deleting it over a database hiccup

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 18:17:07 -03:00
Lucas RayanandClaude Sonnet 5.5 0ff9028927 Merge feat/bimi-studio-and-avatar-cache: BIMI logo studio, certificate hosting and instant profile photos
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:15:05 -03:00
Lucas RayanandClaude Sonnet 5.5 e0e5b1b880 test(webmail): keep the byte-count test ASCII (lang-lint R9)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:08:41 -03:00
Lucas RayanandClaude Sonnet 5.5 dabd1568d1 feat(bimi): logo studio, certificate upload and a setup check in the console
The console took pasted SVG text and a URL. It now:

- takes a PNG or SVG and makes the logo BIMI accepts (SVG Tiny PS, square,
  at most 32 KB). An SVG has what the format forbids removed and is told what;
  a PNG is traced into vectors in the browser (imagetracerjs, public domain),
  walking down quality presets until it fits. The person positions it as in the
  photo editor: drag, zoom, background colour, preview in a circle or rounded
  square, and downloads the exact SVG to give a certificate authority
- holds the .pem the authority returns, validated against the logo and domain,
  and serves it at /.well-known/bimi/vmc.pem (routed by the webmail)
- shows the DNS record and runs the setup check (logo, DMARC, record, public
  URLs, certificate)
- a golden-file contract: the SVGs the studio produces are validated by the
  server's own validator (Go) and pinned by the studio's tests (TS)
- found by those tests: an element inside a link kept its onclick when the
  link was unwrapped; links are now unwrapped before cleaning
- the BIMI and photo texts are in Brazilian Portuguese with accents (they had
  been written without), and in Spanish with accents

A VMC cannot be generated from an image: it is issued by a certificate
authority. The README and the panel say so.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 17:04:35 -03:00
Lucas RayanandClaude Sonnet 5.5 109b8360dd feat(bimi): hold the certificate, check the whole setup, serve the VMC
A Verified Mark Certificate cannot be made here: an authority issues it after
checking the organisation. What the service can do is hold the file the
authority returns, serve it where the DNS record points, and refuse one that
would make receivers ignore BIMI without a word.

- migration 0015: domain_bimi keeps the PEM chain next to the logo
- bimi.InspectCertificate reads a chain and reports every reason it would not
  work: private key blocks refused outright, the brand-indicator usage, the
  validity period, the domain (exact, wildcard or organisational), and that
  the logo embedded in the certificate (gzipped data URI in the logotype
  extension) is byte for byte the logo being published. Tested against two
  real public VMCs (eBay via DigiCert, CNN via Valimail) plus mutations
- GET /.well-known/bimi/vmc.pem (public, conditional, cacheable for an hour),
  and PUT/DELETE /api/v1/admin/bimi/certificate; the GET of the logo now
  reports the certificate as it stands against the logo being served, and
  replacing a logo under a held certificate says it no longer matches
- GET /api/v1/admin/bimi/check looks at what a receiver would: the logo, DMARC
  enforcement, the BIMI record, the public logo URL, the certificate and its
  public URL. Its HTTP client is HTTPS only and refuses non-public addresses
  after DNS, since the URLs come from DNS and an operator field
- the DNS record points a= at the held certificate when there is one

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:50:33 -03:00
Lucas RayanandClaude Sonnet 5.5 c0f556b190 fix(webmail): a new profile photo shows up everywhere at once
The photo endpoint was cached for five minutes with no revalidation, and only
the settings page added a version to the URL, so the header, the message list
and the reader kept the old photo until it expired.

- every <Avatar> reads a per-address version (and refetches when it changes);
  saving or removing a photo bumps it, in this tab and the other open tabs
- the server answers private, max-age=60, must-revalidate, so a stale copy
  lives for a minute at most and then costs one 304

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 16:44:14 -03:00
Lucas Rayan d82cf71f5b Merge branch 'refactor/mailbox-core' 2026-10-16 13:47:10 -03:00
Lucas RayanandClaude Sonnet 5.5 f2951c0ff4 docs: describe the mailbox core, its invariants and lock order
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 13:36:21 -03:00