Commit Graph
836 Commits
Author SHA1 Message Date
BastienandGitHub bfe9492509 ⬆️(chore) bump keycloak to 26.7.1 (#776) 2026-08-08 10:00:53 +02:00
jbpenrath 5ab49f78d4 ⬆️(backend) upgrade to jmap-email 0.3.0
The 0.3.0 parser refuses what 0.1.0 truncated and the composer raises
where it silently mangled, so the app has to take a position at each
seam: a ComposeError on send becomes a 400 (a property of the draft,
not a server fault), an unparseable inbound message is abandoned
outright instead of retried for 48h (deterministic failure — logged
at error level since abandoned rows are purged after 7 days), and a
stored message the stricter parser now refuses is flagged unreadable
to the UI rather than rendered blank. Attachment display names move
to a single service so serializer, blob download and draft builder
synthesize the same name for a nameless MIME part — the bug that
started this branch. The inbound retry sweep gains age-based backoff
so a dependency outage is not polled harder the longer it lasts. The
dev compose mounts the jmap-email working tree over the installed
wheel so local edits propagate without a rebuild.

Archive reconstruction (PST) composes with allow_smtputf8: an EAI
address is legal in an Exchange archive and the reconstructed .eml is
stored, never retransmitted, so refusing it would exclude the message
from the import. The unquote-message reply patterns bound every
whitespace quantifier that could cross newlines: under the m flag an
unbounded \s* backtracks once per line start, quadratic in the line
count of an attacker-supplied body.
2026-08-06 00:19:02 +02:00
jbpenrath 1ab73890b1 🔖(jmap_email) release 0.3.0
Bump to version 0.3.0
jmap-email-0.3.0
2026-08-05 23:09:55 +02:00
jbpenrath d03e56de22 (jmap_email) harden parsing and composition against hostile mail
Fuzz testing and CVE research showed the 0.2.0 parser trusted its
input too much: a padded From could forge the stored sender and DKIM
alignment domain, a display name could smuggle in a second recipient,
crafted messages hit quadratic regexes and O(depth×lines) MIME
nesting. Over-long header fields are now refused instead of truncated,
addr-spec validation is shared between parser and composer, seventeen
_ext.defects markers surface the MIME ambiguities catalogued by Inbox
Invasion (CCS '24) and Email Smuggling (2025), and sanitize_filename /
is_valid_addr_spec go public so consumers apply the same policy.
IDNA encoding moves from the stdlib IDNA2003 codec to the idna package
(UTS 46, capped >=3.7,<4) because nameprep folding silently routed
mail to distinct registrable domains.
2026-08-05 23:03:35 +02:00
jbpenrath b6581a239d 🐛(frontend) keep autosave out of the send window
The 30s autosave tick could fire between the submit's awaits (draft
save, editor export) and the send mutation, dispatching a draft PUT
concurrently with POST /send/ — the client half of the recipient-rewrite
race fixed backend-side. Stop the timer before any await, wait for a
blur-triggered save to settle right before sending, and restore the
timer when the submit aborts since the draft stays open.
2026-08-05 16:10:39 +02:00
jbpenrath 02225fae82 🐛(backend) close the draft-update/send race on message recipients
A draft PUT racing a send could pass its is_draft=True fetch before the
send finalized the message, then rewrite the MessageRecipient rows
(delete + recreate, new UUIDs) while the outbound worker held the old
rows. The worker's post-SMTP status save then crashed the delivery with
"Save with update_fields did not affect any rows", and the recreated
rows were left without delivery status, so the retry task re-sent an
already-delivered email.

The PUT now locks the message row and re-checks is_draft in the same
transaction as the rewrite, serializing it against the send's finalize.
The worker records statuses through a queryset UPDATE (warning instead
of crash when the row is gone), and the SMTP-failure fallback no longer
flips already-delivered recipients back to RETRY.
2026-08-05 16:10:39 +02:00
cf0b70e7ee (inbound) ARC relay-trust: inbound_auth "arc" + arc_verdict rule (#763)
New "arc" inbound_auth mode derives dkim/dmarc only from an ARC-sealed
ARC-Authentication-Results whose outermost sealer is in
SPAM_CONFIG["trusted_arc_sealers"] (empty = any valid seal); plaintext
headers are never trusted.

New SPAM_CONFIG["rules"][..]["arc_verdict"] (trusted|untrusted) marks 
or drops inbound mail lacking a valid trusted seal, so a publicly-reachable
mta-in effectively accepts only mail sealed by a trusted relay. 
The chain is verified with dkimpy; a verification/DNS failure never spams or drops.

Config reference in docs/spam.md.
---------

Co-authored-by: Riël Notermans <riel@mosa.cloud>
Co-authored-by: Riël Notermans <riel@zzapps.nl>
2026-07-24 01:20:12 +02:00
jbpenrath 461152179d (jmap_email) clean text preview
Add a `preview_text` helper that is exposed by the lib.
It stripped html tags through a HTMLParser and also strip
markdownish syntax that can be found into text body.
Previously preview attribute could contains html/md noise,
now it is a clean display ready text string.
2026-07-23 11:49:10 +02:00
jbpenrath 3214cfb57c 🔖(minor) release version 0.9.0
Update all version files and changelog for minor release.
v0.9.0
2026-07-22 18:03:46 +02:00
jbpenrath 71300c99a8 🔨(release) ignore jmap_email package and vite build folder
Do not upgrade jmap_email as it has its own release process.
Then ignore vite build folder than contain package.json
2026-07-22 18:03:45 +02:00
Sylvain Zimmerandjbpenrath 7fdca12426 (build) add cache-busting source version in build 2026-07-22 16:11:19 +02:00
Sylvain Zimmerandjbpenrath 31144b1a88 📝(spam) add full documentation on spam processing 2026-07-22 16:11:19 +02:00
Sylvain Zimmerandjbpenrath 07e906a390 🔥(testdomain) remove the TESTDOMAIN feature
It was intended for early tests. We can create autojoin domains now instead.
2026-07-22 16:11:19 +02:00
jbpenrath 5582246763 🚸(frontend) improve no mailbox view
Add a clearer message when user has no mailbox.
2026-07-22 16:11:19 +02:00
jbpenrath aa21f31ce2 🚸(frontend) explicit auth issue
In some case, a user can be authenticated on the identity provider
but do not have account on Messages. In this precise case, we know
display a toast to explicit what's wrong.
2026-07-22 16:11:19 +02:00
jbpenrath 302353e020 (frontend) support login next param
When a user tries to access to a message route when it is not
authenticated, it is redirect on the homepage and have to authenticate.
Now in this case, we redirect on homepage and persist the previous route
within a next query param, in this way, we are able to automatically
redirect the user on the right view once it is authenticated.
2026-07-22 16:11:18 +02:00
Sylvain Zimmerandjbpenrath 19cfa2dcfc (push) add Push Notifications system for iOS, Android, Web 2026-07-22 16:11:18 +02:00
Sylvain Zimmerandjbpenrath 732b6028d3 (imports) refactor import code: retries, continuous mode, list UI (#742)
Largely improves user & operator visibility on imports, resiliency with retries,
direct-to-offload storage, import cancellation (deleting imported messages) & more.
Remove the modal import and manage all within the import tab in the settings modal
2026-07-22 16:11:18 +02:00
Sylvain Zimmerandjbpenrath f4d235825f ️(devx) improve "make bootstrap" setup time and overall DevX
Notably, we try to reduce disk usage by standardizind on common
base Docker images. We also improve node_modules by reducing
duplicate dependencies and install speed.
2026-07-22 11:34:27 +02:00
jbpenrath 94d079bc1b (mobile) add self-hosted OTA update chain
Store review cycles make shipping web-layer fixes through the stores
too slow, so the apps update their JS bundle over the air. The chain is
fully self-hosted to keep sovereignty: bundles and channel manifests
live on an anonymous-read S3 bucket (create_bucket --public / the
create-ota-bucket script) and the Capgo plugin is driven entirely from
JS against that manifest (autoUpdate off — no Capgo server involved).
Bundles are RSA-signed at publish time and verified against the
per-instance public key baked in at cap sync, so a tampered zip on the
public bucket is rejected.
Versions use a git-derived <count>-<sha> id stamped into the builtin
bundle so a fresh install does not re-download its own commit, and
channels (dev/staging/prod) are fully independent because
NEXT_PUBLIC_* vars are inlined at build time.
Also ships docs/mobile.md.
2026-07-22 11:21:05 +02:00
jbpenrath ace7991084 (frontend) bootstrap Capacitor mobile apps
Ship the existing SPA as native iOS/Android apps without forking the
codebase: Capacitor wraps the web build, and every mobile-specific
behavior is gated behind isNativePlatform() so the web app is
untouched. The native shells route fetch/cookies through the native
HTTP layer (CapacitorHttp) — the WebView cookie jar is unreliable for
cross-origin sessions — which is why login runs in the system browser
(cross-app SSO via the shared IdP cookie) and finishes through the
backend session handoff, with the deep-link scheme pinned by
sso-invariants tests. Downloads/share go through the Filesystem/Share
plugins since WebView navigation would lose the session.
2026-07-22 11:21:02 +02:00
jbpenrath 4ad0a80999 🐛(keycloak) map eidas1 acr value in dev realm
The backend sends acr_values=eidas1 on every authorization request
(OIDC_AUTH_REQUEST_EXTRA_PARAMS) but the dev realm had an empty
acr.loa.map, so Keycloak treated the value as an unknown essential acr
claim. Web logins survived it, but the mobile system-browser flow
(ASWebAuthenticationSession / Custom Tabs) failed the login round-trip,
breaking cross-app SSO in dev. Mapping eidas1 to LoA 1 mirrors what the
production IdP declares.
2026-07-22 11:16:42 +02:00
jbpenrath b7b5ff0604 (backend) add mobile OIDC session handoff
Capacitor apps must run the OIDC flow in the system browser (the IdP
cookie has to live there to provide cross-app SSO), but the browser's
cookies never reach the app's native HTTP layer, so the Django session
created by the callback would be stranded. The callback now redirects
to an allowlisted app deep link with a one-time token that the app
exchanges for its session cookie and CSRF token. The token is bound to
the initiating app instance with a PKCE S256 verifier, single-use,
short-lived (MOBILE_AUTH_TOKEN_TTL) and the anonymous exchange endpoint
is throttled per IP to cap brute-force guessing. An empty
MOBILE_AUTH_CALLBACK_SCHEMES (the default) keeps the whole handoff
disabled.
2026-07-22 11:16:41 +02:00
jbpenrath 5038dd977a ♻️(auth) deliver CSRF token via session instead of cookie
The upcoming Capacitor mobile shell replays the Django session cookie
through its native HTTP layer but not the `csrftoken` cookie, so
cookie-based CSRF would break every mutation on mobile. Enabling
CSRF_USE_SESSIONS moves the secret server-side and removes the need for
a JS-readable cookie: the token is now delivered on the authenticated
/users/me/ response, cached in memory by the SPA and echoed as
X-CSRFToken. On web this is equivalent or safer — the secret is no
longer readable by scripts nor overridable via cross-subdomain cookie
tossing.
2026-07-22 11:16:41 +02:00
Jean-Baptiste PENRATHandGitHub f058ee4d4a 📦(frontend) setup browserlist and support chrome >= 109 (#750)
Some of our users has reported issues using the application
with Chrome 109. Instead of polyfilling one by one each
method, we setup vite legacy plugin and configured a
browserlist. It adds a 23.5Kb Gzipped module but the
polyfill strategy management is more standard and robust.

Close #741
2026-07-20 18:12:00 +02:00
Jean-Baptiste PENRATHandGitHub 1eb68eeb61 🐛(frontend) fix outlook web handler in unquote logic (#754)
About the unquote-message logic, we encount a bug with a thread
implying Outlook Desktop quotes. Actually, for Outlook web we were looking for
a hr tag as quote separtor element. But sometimes this one can be wrap into a div
and we missed it.
2026-07-20 10:55:10 +02:00
39f2d9ca98 (frontend) detect text link in html body and warn user before redirect (#744)
Parse sanitized html before rendering to detect links that are just raw text then
transform them into anchor.
Furthermore, for security purpore, we catch event when a user clicks on a link then
display a confirmation modal displaying the real link.

Co-authored-by: Valentin Regnault <valentinregnault22@gmail.com>
2026-07-09 14:12:09 +02:00
Jean-Baptiste PENRATHandGitHub 983df0fe6f 🔧(global) set frontend configuration from backend (#734)
Pass frontend configuration through backend to prevent be able to edit it
without requiring frontend application.
2026-07-07 15:13:00 +02:00
Jean-Baptiste PENRATHandGitHub ce2b8f6ab0 🚸(frontend) reset search on mailbox switch (#743)
When the user switches to anoter mailbox while it is on a search view,
we reset search params and go to to the inbox as it does not make
sens to keep search params on the new mailbox
2026-07-07 08:41:42 +02:00
Sylvain Zimmerandjbpenrath c668326896 🐛(inbound) improve re-processing of inboundmessages from the admin 2026-07-06 16:20:52 +02:00
Sylvain Zimmerandjbpenrath 109211e0a0 (ssrf) add a host allowlist to bypass SSRF in internal networks 2026-07-06 16:20:52 +02:00
Sylvain Zimmerandjbpenrath 85dd0b4806 🐛(pymta) save the origin IP across STARTTLS restarts 2026-07-06 16:20:52 +02:00
Sylvain Zimmerandjbpenrath 23a63d9730 (inbound) add webhooks, message postmarks, spam fixes
This refactors our inbound pipeline into a more future-proof, extensible system.
2026-07-06 16:20:52 +02:00
Jean-Baptiste PENRATH 114e537499 👔(backend) use builtin method to generate message-id (#730)
With some mail providers, your current message-id could cause rejection
so we migrate to the standard email.utils.make_msgid method.
2026-07-06 16:20:52 +02:00
Sylvain Zimmerandjbpenrath d8fb367cfe 🚨(lint) lint subprojects 2026-07-06 16:20:51 +02:00
Sylvain Zimmerandjbpenrath 632f38da2d (mta-in) rewrite MTA-in in pure Python to remove dep on Postfix (#692)
Postfix was already removed as a mta-out dependency, this is the second step so we have a pure python, more auditable path for incoming emails. We plan to keep postfix as a compatible option for a while but it won't be the default once this is battle tested.
2026-07-06 16:20:51 +02:00
jbpenrath a1ffcf3673 🔥(frontend) remove react-email component
We are currently using react-email to generate html bodies. This
library aims to generate marketing email consistent in all mail
providers. For personal message, it generates too much custom styles
that can increase spam score of those messages.
2026-07-06 16:20:51 +02:00
jbpenrath 76dc87fe5f 👔(backend) add X-Mailer header at outbound
We miss to add X-Mailer header to our outbound messages and this
missing can increase the spam score of our messages.
2026-07-06 16:20:51 +02:00
db43a05f11 🐛(frontend) fix premature line wrapping in composer (#740)
Safari 26 changed `text-wrap: pretty` to re-optimize line breaks over
the whole paragraph (webkit.org/blog/16547). The global CSS reset
applies it to every paragraph, so BlockNote editor paragraphs get
re-balanced on every keystroke, wrapping lines well before the editor
width. Restore `text-wrap: wrap` inside .bn-editor, which covers the
message, signature and template composers. Received messages render in
an iframe and are not affected by the reset.

Fixes #735

Signed-off-by: Nicolas Aunai <nicolas.aunai@lpp.polytechnique.fr>
Co-authored-by: Jean-Baptiste Penrath <jbpenrath@gmail.com>
2026-07-06 16:10:05 +02:00
jbpenrathandNicolas Aunai 27b56ee058 (frontend) display unread count into mailbox dropdown
Display the count of unread messages next to the mailbox name
to help to quickly identify mailbox with new unread messages.

Resolve #738

Co-authored-by: Nicolas Aunai <nicolas.aunai@lpp.polytechnique.fr>
2026-07-06 15:55:50 +02:00
Corentin GiraudandGitHub 6e077eab89 🐛(mta-out) de-indent relay block in main.cf.j2 (#733)
The relay section of main.cf.j2 is indented by one space. Jinja keeps that
leading whitespace in the rendered /etc/postfix/main.cf, and Postfix treats any
line starting with whitespace as a continuation of the previous parameter. So
the smtp_sasl_* directives get folded into the relayhost value instead of being
set as parameters, leaving smtp_sasl_auth_enable at its default (no) and the
relayhost value malformed.

Outbound mail through an authenticated relay then fails: the relay rejects it
with "502 5.7.0 Please authenticate first", and Postfix logs "fatal: valid
hostname or network address required in server description: =". De-indenting the
block to column 0 makes each directive a real parameter. Verified against
Scaleway TEM: postconf then shows smtp_sasl_auth_enable = yes and delivery
succeeds (250 queued).

Signed-off-by: Corentin Giraud <corentin@giraud.dev>
2026-06-30 21:18:47 +02:00
BastienandGitHub e16ff27abd ⬆️(keycloak) upgrade theme to 2.3.4 (#732) 2026-06-30 11:01:36 +02:00
BastienandGitHub fc0ff1b354 🔒️(chore) bump keycloak to 26.6.4 (#729)
https://www.cert.ssi.gouv.fr/avis/CERTFR-2026-AVI-0815/
2026-06-29 16:15:46 +02:00
Jean-Baptiste PENRATHandGitHub a7cfcea673 🐛(frontend) fix compose line break issue on chrome android (#725)
In message form, we prevent the user to press "Enter" to submit
the form by error when composing message. But this logic breaks
the line-break on chrome for android.
2026-06-24 17:11:05 +02:00
jbpenrath aab5412129 ⬆️(backend) update django-lasuite to 0.0.27
Upgrade django-lasuite to 0.0.27 and add settings to configure
forwarded params.

https://github.com/suitenumerique/django-lasuite/pull/77
2026-06-23 23:15:18 +02:00
jbpenrath 35260fe7f8 🐛(backend) set attachment name fallback
The openapi schema specifies that an attachment has always
a name but we have some case where this attribute is None.
To guarantee this contract, we use a fallback value `unnamed`
when name is None during serialization.

Also fix other issues of the same kind identified.
2026-06-23 22:49:46 +02:00
BastienandGitHub 11d191d967 (chore) bump keycloak to 26.6.3 (#718) 2026-06-23 11:51:35 +02:00
Sylvain Zimmer 39c385e112 🐛(lang) fix hardcoded lang=en that might trigger auto-translate
That would result in garbled icons like some users reported
2026-06-19 12:00:42 +02:00
Jean-Baptiste PENRATHandGitHub f0d8306902 🔖(minor) release version 0.8.0 (#715)
Update all version files and changelog for minor release.
v0.8.0
2026-06-18 20:34:06 +02:00
Jean-Baptiste PENRATHandGitHub 3962ff7dd6 🐛(frontend) make thread-item select checkbox non interactive (#714)
When a user clicks on the checkbox of the thread-item to select it,
the preventDefault call prevent the checkbox to update its state so
under the hood the thread was well added to the selection but the
user has a wrong state. Now we make the checkbox fully controlled
and non-interactive, in this way, user always interact with the
thread-item link and the selection state is used to set the
checkbox state
2026-06-18 12:34:03 +02:00