846 Commits
Author SHA1 Message Date
Sylvain ZimmerandGitHub 06e8bdb93c 🔒️(pymta) harden pymta with new settings & limits (#777)
This group of fixes prepares pymta before becoming the recommended mta-in implementation
2026-08-13 21:26:27 +02:00
Sylvain ZimmerandGitHub 75c11c7b15 (provisioning) add new endpoint to list DNS records for all domains (#780) 2026-08-13 19:46:14 +02:00
jbpenrath 31b51b5f67 (mobile) custom logout view to terminates the IdP session
ProConnect ignores prompt=login, so preserving the IdP session locked
mobile users into the same identity forever. The logout endpoint now
accepts a mobile_scheme and ends the RP-initiated round-trip on a new
logout-callback view that deep-links back to the app, so the system
browser — which holds both the Django session handed over at login and
the IdP SSO cookie — terminates both sessions.
Then, Proconnect login page's Content Security Policy blocks
the direct redirect: Chrome enforces its form-action on the whole
redirect chain of the credential form submission, and "*" only matches
network schemes — so our network mobile scheme violates it and the user
stays stuck on the identity provider during logout workflow.
The callback now serves a page that ends the form chain on a
network mobile scheme, then hands off to the app from our own page,
outside the IdP policy: automatically via script (iOS
interception, unchanged) with a button as the always-working fallback.
2026-08-13 12:08:41 +02:00
jbpenrath fea61e1be2 🔧(mobile) make the app identity and auth scheme env-driven
A staging and a production build must be installable side by side on one
device, and two apps claiming the same OIDC deep-link scheme would make
Android ask the user which one receives the login callback, mid-flow. The
app id, the displayed name and the callback scheme therefore become
per-environment (MOBILE_APP_ID / MOBILE_APP_NAME / MOBILE_AUTH_SCHEME).
2026-08-13 12:08:40 +02:00
jbpenrath 72b624e310 💄(frontend) hide sent folder stats
Do not fetch stats for sent folder.
2026-08-13 12:08:40 +02:00
jbpenrath 59d9321dda 💄(frontend) reword doc title
Put app name as suffix not prefix of the page title.
Align all document title accross the app.
Mailbox Name - Folder Name - App Name
2026-08-13 12:08:40 +02:00
jbpenrath bc966355b5 (global) clean thread snippet and display message snippet
Use the new preview_text method of jmap_email to generate
clean snippet for thread (denormalized at update_stats)
and message (at serialization). Now when a mesage is folded, we
display this snippet. In the thread list we also display this snippet above
the subject.
2026-08-13 12:08:40 +02:00
jbpenrath 40ce89dcc7 🐛(frontend) only use blocknote allowed colors
Currently when a user paste content into the composer,
if this one has text or background color, this is preserved as is
then export into the output. Now only color supported by
blocknote are preserved and exported. We also apply this
sanitization to table elements.

Furthermore, we also drop unsupported blocks (file, audio, video)
and fix a bug that prevent to embed external images.
2026-08-11 08:33:47 +02:00
Jean-Baptiste PENRATH a8d8e5b435 🐛(inbound) thread replies whose subject was rewritten (#765)
A reply carrying In-Reply-To to a message we already hold was still
rejected when its subject differed, because the delivery path required
both a reference match and an identical canonical subject. A subject
edited mid-conversation therefore started a brand new thread.

The import path had its own laxer rule (message-ids only), so the very
same conversation was grouped differently depending on whether it was
imported or received over SMTP.

Both paths now share find_thread_for_message: In-Reply-To is trusted on
its own (RFC 8621 §3 only allows splitting on subject, never requires
it), while References — which some clients recycle to start unrelated
topics — still needs a matching canonical subject. The canonicalization
also accepts "Re:subject" with no space, common on mobile clients.
2026-08-11 08:33:47 +02:00
Sylvain ZimmerandGitHub a8c8eb8673 🐛(dnscheck) relax DKIM whitespace check for DNS configs (#778)
Whitespace & other relaxed syntax bits are ignored in DKIM keys but weren't implemented in our validator, which showed "Invalid" in the admin UI for some keys
2026-08-10 14:38:38 +02:00
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