Added
- BIMI verified sender brand logo. On the single-message read, when the
message passed DMARC (implying SPF-or-DKIM alignment with theFromdomain,
reused from the already-parsedAuthentication-Results— no
re-authentication), lilmail looks updefault._bimi.<domain>, fetches the
l=logo, sanitizes it, and attaches it asmodels.Email.Brandfor the
reading pane to render. Every gate fails closed: no DMARC pass → no lookup;
no/invalid BIMI record → no logo; a non-httpsl=is never fetched; the
fetch is SSRF-screened (screenDialIP, no private/loopback/metadata, no
redirects, size-capped, and regression-tested against the real dial screen)
so a sender-controlled URL can't probe the server's network; any
<script>/<foreignObject>/event-handler/external-reference in the SVG
voids the whole logo. Resolution is per-domain and cached server-side (never
per-recipient), so a sender can't use it as a tracking beacon, and never
blocks a message open. - Send-as identities write path —
PUT /v1/settings/identities(the
surface was previously read-only, so the compose "From" selector could only
ever show the primary address andPOST /v1/messagesignoredfrom
entirely). For a vulos-mail-hosted mailbox the alias is pushed to the
engine's broker-gated/internal/identitiesfirst, which accepts it only at
a verified domain owned by the tenant (or ayou+tag@subaddress); a
refusal (403, now propagated verbatim instead of a generic502) or an
unreachable engine stores nothing. Compose, draft-save, and scheduled send
all gateFromon the primary address or a registered identity (else
403); a scheduled send's ownership key stays the authenticated mailbox
even whenFromis an alias. Send-as only — an alias does not become an
inbound address. Documented indocs/API.md. - Third-party licence notices. lilmail redistributes other people's code
(51 Go modules plus vendored htmx/Alpine.js) but shipped none of the
required attribution — vendored bundles had their licence headers stripped
by the minifier and nothing was surfaced to users.THIRD-PARTY-NOTICES.txt
(generated from the real module graph byscripts/gen-notices.sh, now also
covering the Go standard library's BSD-3 licence + patent grant) is embedded
in the binary and served at/licenses.txt, linked from the login page and
Settings → About; upstream licence files sit next to each vendored bundle.
The long-dead, no-longer-loadedassets/vendor/tailwind.jswas removed
rather than attributed.
Changed
- Reframed as an independent PIM client. lilmail is a standalone mail +
calendar + contacts client that talks to the user's own
IMAP/SMTP/CalDAV/CardDAV account and exposes a stable/v1JSON API. It
hosts no mail and depends on no central Vulos server; the Vulos OS
integrates it over/v1. README, ROADMAP, ARCHITECTURE, and API docs were
reworked to this model across several passes: dropping the stale "Vulos
Mail product" / central-coupling framing from docs, code comments, and the
bundled AI-assistant prompts; correcting the "Part of VulOS" product map
(OS, Office, Files, Relay, llmux — Talk/Meet are archived); making the
README self-contained; and fixing the GitHub org in badges/links
(exolutionza→vul-os, matching the actual remote).
Removed
- Dropped the central
vulos-mailfeature-proxy coupling from/v1.
Several/v1surfaces existed only to reverse-proxy to a central
vulos-mailengine's/internal/*endpoints and were permanent501s in
every standalone deployment. Removed the six proxies — rules/filters,
threads, categories, smart-folders, team-inbox, spam-settings — and the
best-effort vacation/identities/snooze push-to-central paths, keeping the
local KV read-model + IMAP behaviour. The header credential-injection seam
remains but is now a generic per-request credential injector (not a "Vulos
Cloud CP" custody path). The standalone/v1contract (mail +
/v1/calendar+/v1/contacts) is unchanged; the removed routes are locked
out by a regression guard (~5,000 LOC removed). - Dead central-engine remnants.
vacationActive/shouldAutoReply(an
out-of-office responder is delivery-path logic owned by the account's own
mail server, not lilmail) and the orphanedThreadID/Category/
SmartFolder/SmartFieldsfields onmodels.Email— server-side
classification written only by the deleted/v1augmentation, always
absent (omitempty) from standalone output — were deleted along with a
scattering of other provably-unused non-exported declarations
(unreferenced regexes/maps/fields flagged bystaticcheck U1000). No
exported symbol or behaviour change. - HTML marketing landing site. Product landing pages are now centralized
in vulos-cloud; lilmail no longer embeds or serves its own (/site/*mount
and embeddedsiteFSremoved)./now redirects a logged-out visitor
straight to/login(a signed-in user still lands on/inbox).
Fixed
- Single-part
text/htmlmessages rendered as raw source, and quoted-
printable bodies were left undecoded (#9). Message parsing never reversed
Content-Transfer-Encodingand only inspected the top level of the MIME
tree, so a single-parttext/htmlmessage (e.g. Anthropic's "Secure link to
log in to Claude.ai") fell into the non-multipart branch, was assigned to
Email.Bodyinstead ofEmail.HTML, and showed its HTML source instead of
its content, with=3D/soft line breaks throughout. Replaced with a
recursivecollectBodies()that routes leaves by media type,
transfer-decodes base64/quoted-printable, recurses through nested
containers (multipart/mixed›multipart/alternativepreviously matched
neithertext/plainnortext/htmland rendered blank), and skips
Content-Disposition: attachmentleaves. The message-list preview had the
matching problem — it now transfer-decodes first, skips<style>/<script>
and comments, resolves entities, and drops zero-width preheader padding
instead of showing raw CSS resets or base64 gibberish; the preview window
grows 512 → 4096 bytes.
Security
- CRITICAL: stored XSS via Edit Draft. The Edit-Draft path stashed the
raw, unsanitized HTML of a viewed message intodata-html;restoreDraft()
then assigned it viainnerHTMLinto the compose editor in the main
(non-sandboxed) app document, where<img src=x onerror=...>/
<svg onload=...>execute under the CSP's'unsafe-inline'. Any
attacker-controlled HTML mail filed into a folder whose name contains
"draft" therefore yielded stored XSS in lilmail's own origin on Edit Draft.
Fixed server-side: the HTML feeding the Edit-Draft slot is now defanged by
a strict allowlist/denylist policy (stripsscript/style/iframe/svg/
object/embed/form, allon*handlers, andjavascript:/vbscript:/
data:URLs), extracted into a sharedhandlers/htmlsafepackage. The
sandboxed reading-pane iframe path is untouched. - Reading-pane "Display images" opt-in only blocked
<img>, letting other
remote-content vectors auto-load before consent (tracking-pixel /
privacy bypass, not XSS — the frame has noallow-scripts).
blockRemoteContentis now tag-aware and neutralises every remote
(http/https/protocol-relative) fetch vector:<img>/<image>(the old
regex never matched<image>),<input type=image>,<video>poster,
srcon<video>/<audio>/<source>/<track>/<embed>/<iframe>,
<object data>,<link href>, and remoteurl()/@importin inline
style=and<style>blocks.data:,cid:, and relative references
are left untouched. - Bumped
golang.org/x/crypto0.45.0 → 0.52.0, clearing 13 Dependabot
advisories (7 critical / 2 high / 4 moderate) inx/crypto/sshand related
packages pulled in transitively;govulncheckreports 0 reachable
vulnerabilities after the bump. Pulls forwardx/sync,x/sys,x/text
as required. - Pinned the build toolchain to go1.25.12, clearing the one reachable
govulncheckfinding (GO-2026-5856, acrypto/tlsECH privacy leak
reached via the S3 and SMTP TLS paths); Dependabot's other 14 alerts are in
unreachable modules.
What's Changed
New Contributors
Full Changelog: v1.12.1...v1.13.0