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>
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>
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>
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>
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
ç or ·.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- the webmail's Send no longer files its own Sent copy: it asks Submit to
(FileSentCopy), the same path SMTP clients use, so the rule exists once
- every operation now locks the folders first (lowest id first) and the message
after. Opposite moves, and a move racing a flag change, deadlocked (SQLSTATE
40P01) because they took those locks in different orders; the new test
reproduces it (10-19 s of retries, errors) and passes in ~1 s with the order
- TestOneMailboxEverywhere drives SMTP, IMAP and the webmail on one mailbox,
mixing them, and checks after each step that every view agrees with the others
and with the rows. It fails on the previous code and passes now.
The folder list and the message list were two queries over the same tables. IMAP
took a folder's message count from a cached column and the webmail counted rows,
so the two could disagree. Both now ask MailStore, which counts the messages.
The console created Reports, the CLI that makes the first administrator did not,
so that account had no folder for DMARC and TLS-RPT reports. The list now lives
in one SQL function, provision_default_folders (migration 0014), which both call.
The same migration reconciles what the two paths had left wrong in existing
mailboxes: folder counters and the mailbox's used bytes are recomputed from the
rows (only where they differ, so it is safe to run again).
create-admin can now be tested: fail() throws instead of exiting, and the command
runs only when invoked as one. The new test fails on the old code (no Reports).
Flags, expunge, move, copy and folder deletion each existed twice: once for IMAP
(FlagRepository, ExpungeRepository, CopyRepository) and once for the webmail
(WebmailRepository). They kept the cached numbers true on their own and drifted:
- reading a message over IMAP never moved the folder's unread counter
- deleting in the webmail, or deleting a folder, never gave the quota back
- an IMAP expunge could push a counter below zero
MailStore now holds each state change once, in one transaction, with the same
rules everywhere: counters never go negative, a change that alters what a client
can observe raises the folder's modseq (one that alters nothing does not), the
quota follows the bytes, and a message is located by folder. The four repositories
are thin adapters that only translate how each path names a message.
The contract test added before this change (which failed on 8 of its cases)
now passes, as does the full suite.
Runs each state change through IMAP and through the webmail and checks the cached
numbers against the rows. It fails today where the two stacks drifted: IMAP never
moved the unread counter, and the webmail never gave the quota back.