Commit Graph
100 Commits
Author SHA1 Message Date
jbpenrath 8d12995d3a !fix login workflow conflict with ota 2026-08-13 18:58:44 +02:00
jbpenrath 5d92b9518f !stage OTA bundle from scalingo-postbuild hook 2026-08-13 18:58:44 +02:00
jbpenrath 1bdc7dfb2b !feat ota scalingo 2026-08-13 18:58:44 +02:00
jbpenrath ed7d36d12c 🐛(mobile) wire the iOS build number to MOBILE_VERSION_CODE
CURRENT_PROJECT_VERSION was left at the Xcode default of 1, so every iOS
build shipped the same CFBundleVersion. App Store Connect refuses an upload
reusing a build number within a MARKETING_VERSION, which would have blocked
the second upload of any release, and App.getInfo().build reported a constant
"1" — leaving the build number meaningless in the support version report.

Feed it from MOBILE_VERSION_CODE, the variable Android already reads for its
versionCode, so both stores order a release by the same git-derived commit
count. Xcode release builds run from the host IDE, where the container
environment never lands, so the value has to travel through generated.xcconfig
— hence make mobile-build now passes the variable too.
2026-08-13 18:58:37 +02:00
jbpenrath a548c7c60f !feat display version number in ui 2026-08-13 18:57:42 +02:00
jbpenrath 245c012c19 !icon/splash clean generate 2026-08-13 18:57:42 +02:00
jbpenrath e9b5c4669d fixup! !few fixes and ui/ux feedbacks 2026-08-13 12:14:35 +02:00
jbpenrath 829667736f !feat window composer 2026-08-13 12:14:35 +02:00
jbpenrath e418c5f93d !few fixes and ui/ux feedbacks 2026-08-13 12:14:35 +02:00
jbpenrath e81712112d 🔧(mobile) single-source the app version from package.json
The displayed version was scattered and inconsistent: a 1.0 fallback in
gradle and the Makefile, a hardcoded MARKETING_VERSION = 1.0 in the Xcode
project, and an unrelated 0.9.0 in package.json. The package.json
"version" field becomes the single source of truth, bumped manually per
release: gradle parses it directly and iOS receives it through the
generated xcconfig, so a bump is a one-line change. Set to 0.1.0 — the
app is pre-1.0. The MOBILE_VERSION_NAME env plumbing is dropped
accordingly.
2026-08-13 12:14:35 +02:00
jbpenrath a5b799d2f9 📝(mobile) refresh the mobile onboarding doc
Align the doc with the code it describes: the logout section still
documented the SSO-preserving flow replaced by the IdP round-trip,
the release checklist tested the old behaviour, and the new identity /
Play publishing tooling and the HTTPS CSRF Origin constraint were
undocumented.
2026-08-13 12:14:35 +02:00
jbpenrath 51fa7d1a35 🐛(frontend) route native mutations through CapacitorHttp with an explicit Origin
Against an HTTPS backend Django's CSRF check rejects mutations carrying
neither Origin nor Referer — headers the native HTTP client never sends
and that the Capacitor-patched fetch silently strips (browser forbidden
header). Mutations in the shell now call the CapacitorHttp plugin
directly with an explicit Origin; reads stay on the patched fetch to
keep request cancellation. The web app caches the session-bound CSRF
token served by /users/me (no csrftoken cookie under CSRF_USE_SESSIONS),
and shareable deep links use the deployed origin instead of the
WebView-local one. Invisible in dev: the plain-HTTP backend never
triggers the check.
2026-08-13 12:14:34 +02:00
jbpenrath cf67ad0b4f (frontend) mobile UI overhaul
Adapt the whole webmail UI to the native shell in one pass: bottom
navigation bar, mobile thread header/toolbar, swipe actions on thread
rows, pull-to-refresh, drawer, a BlockNote formatting toolbar pinned
above the keyboard, a dedicated icon set, haptics and keyboard handling.
Everything branches on isNativePlatform() (with a dev-only
DEV_FAKE_NATIVE escape hatch for desktop devtools) so the desktop web UI
keeps its existing layout. Dependency bumps (BlockNote, Cunningham,
Capacitor plugins) support these components.
2026-08-13 12:14:34 +02:00
jbpenrath 9066c50961 🔧(mobile) per-environment app identity and Play release tooling
A staging and a production build must coexist on one device: the app id,
displayed name and OIDC deep-link scheme become env-driven
(MOBILE_APP_ID / MOBILE_APP_NAME / MOBILE_AUTH_SCHEME) across cap sync,
gradle and Xcode (generated xcconfig) — a divergence between the JS and
native halves strands the login callback silently, and a wrong identity
is frozen forever by the first store upload, so release builds now fail
on any mismatch. mobile-android-release produces the signed Play bundle;
the upload keystore and google-services.json stay per-instance and
gitignored.
2026-08-13 12:14:34 +02:00
jbpenrath e36926074f 🔧(frontend) revert linked npm install-strategy
The pnpm-style layout bakes install-local .store/<hash> paths into the
native project files committed by cap sync, and the named node_modules
volume masked the host tree — breaking the IDE and the Gradle/Xcode
builds that resolve Capacitor plugins through relative paths into
src/frontend/node_modules. Back to the hoisted layout installed in the
bind mount, with the bootstrap check updated accordingly.
2026-08-13 12:14:34 +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
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
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
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.
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
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
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
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
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
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
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
Jean-Baptiste PENRATHandGitHub f0d8306902 🔖(minor) release version 0.8.0 (#715)
Update all version files and changelog for minor release.
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
jbpenrath 81055813ae 🐛(frontend) fix composer issues
Fix overflow issue of composer toolbar popovers.
Upgrade to latest blocknote version
2026-06-18 00:09:21 +02:00
jbpenrath 08c47baaa9 ⬆️(frontend) bump dompurify to latest version 2026-06-17 23:22:53 +02:00
jbpenrath 37e7840c71 🐛(frontend) unpin threads on draft deletion
We recently add draft deletion but we forgot to unpin threads on
this mutation so in some they could remain in the list until a
refresh is done.
2026-06-17 23:22:53 +02:00
jbpenrath 491d05afd6 💅(frontend) wrap autoreply date column
Allow autoreply date column to wrap its content as often date range
and badge are clipped on a single line.
2026-06-15 22:40:05 +02:00
jbpenrath 1ea79ad373 (threads) allow permanently deleting drafts, and improve draft edition
Drafts had no deletion path: the only "delete" affordance was the soft
trash flag, which makes no sense for a draft that was never sent. Users
accumulated drafts they could not get rid of.

Add a generic bulk hard-delete endpoint (POST /threads/bulk-delete/)
scoped by message flag (draft/trashed), mirroring the flag endpoint's
batch + editable_by authorization pattern. It deletes only the
scope-matching messages so reply-draft threads keep their real messages,
and removes the thread only once it is emptied.

Furthermore, the draft auto save feature was too agressive and can result
to too many blank draft persisted (signature, quote insertion
triggered a save as soon as user clicks on new message / reply
or forward. Now 30s autosave is only enabled when the draft
is created and logic to trigger auto save on form change has been
improved.

Finally, the ui has been revamp to improve draft display.
Currently draft are always wrapped into parent message component
that was make sens for reply/forward but now for new message.
2026-06-15 22:40:05 +02:00
Jean-Baptiste PENRATHandGitHub ca04508b56 🔧(deps) install jmap-email from pypi (#711)
Now that jmap-email 0.1.0 is available on pypi we install it from
this registry and remove all tweaks to install the deps from local
folder. We keep the volume override for backend services in order
to be able to work on jmap-email and test it with ease in local
development environment.
2026-06-15 13:45:23 +02:00
Jean-Baptiste PENRATHandGitHub e0cb1f4f08 🚸(frontend) improve thread navigation a11y and multiselect ux (#708)
Declare the thread list as a listbox with multiselectable elements.
Now when multiselect is enable, clicking on a thread add it to the
current selection, it does not reset the selection.
Furthermore, the keyboard navigation has been improved.
2026-06-15 08:36:03 +02:00
Jean-Baptiste PENRATHandGitHub 08663a271c 🩹(frontend) remove useless perspective attribute (#709)
In previous work we forgot to remove a css property that
was a test but create a scroll issue..
2026-06-12 01:54:30 +02:00
Jean-Baptiste PENRATHandGitHub 5d12e61644 💅(frontend) refine mailbox dropdown menu (#705)
Customize mailbox dropdown menu to make it more 'interactible'.
Distinguish shared vs personal mailbox through avatar shape and
display Name + email.
2026-06-12 00:16:50 +02:00
jbpenrath ff01cf42da 🚸(frontend) improve mailbox settings
- Warn user when it updates mailbox name without saving it in general
  tab
  and it is exiting this tab
- Revamp a little bit the integration dialog
2026-06-11 11:58:45 +02:00
jbpenrath 09b966aba8 (frontend) translate template placeholder and add user_name builtin var
Since we disabled backend i18n, template placeholders were not
translated according to the active frontend language.
We revamp the logic to delegate translation of those variables
to the frontend. Furthermore, we add a new builtin variable
that allows to bind the username into template.
Last but not least we add support of style to template
variable inline elements and we render name instead of value
into the editor.
2026-06-11 11:58:45 +02:00
jbpenrath 74be53a803 🌐(frontend) internationalize missing strings
Some strings did not use `t()`. We know use that everywhere.
In order to prevent the use of literal string as text, we enable
the eslint rule `i18next/no-literal-string`. As this rule triggers
warning for all material-icons span elements we replace all of them
by Icon component provided by UI-Kit (which was technical debt)
2026-06-11 00:40:13 +02:00
jbpenrath 7bc1ba7039 ♻️(frontend) use LaGaufreV2 component
UI-Kit expose LaGaufreV2 component we can now remove our custom
implementation and use this component instead.
Note: There is currently a bug that prevent to close lagaufre when
we open a Dropdown menu. We implement a quick fix here that must be
removed once https://github.com/suitenumerique/integration/pull/55
will be merged
2026-06-11 00:40:13 +02:00
jbpenrath 9457fdacae (frontend) gather mailbox settings into dialog
Gather all mailbox settings (accesses, templates, auto-replies,
signatures and integrations) into a setting dialog. This one is
only accessible to mailbox admin users. Furthermore a general tab
has been added to allow user to edit the mailbox sender name.
2026-06-10 14:43:37 +02:00
jbpenrath 7103d1934f 🐛(backend) persist mailbox name when contact is missing
Renaming a mailbox silently no-oped whenever its Contact was missing
(`contact` is nullable and not always created): the update filtered on a
null pk and changed zero rows while still returning 200. Route the rename
through a new `Mailbox.set_display_name()` helper that creates and links a
Contact when absent, so the name is always persisted.
2026-06-10 11:18:28 +02:00
Jean-Baptiste PENRATHandGitHub 15b02f6641 🍱(frontend) new homepage illustration (#702)
Use a new homepage illustration compatible with theme variant
2026-06-09 14:48:41 +02:00
jbpenrath c77fd0d3c7 🐛(backend) manage message/delivery-status attachments at compose
DSN/bounce and read-receipt reports embed a message/delivery-status part.
A PST stores it as a flat byte blob, and reconstructing the .eml fed it to
compose_email as a message/delivery-status attachment. There, email.generator
dispatches to _handle_message_delivery_status, which assumes a structured
(list) payload: given our flat base64 string it iterated character by
character and raised "'str' object has no attribute 'policy'", aborting the
whole compose. Every PST message carrying such a report was silently dropped,
and any send/widget/autoreply with the same attachment type would fail too.

Relabel message/delivery-status to text/plain at the single choke point
(create_attachment_part) so all compose callers are covered without
duplicating the guard. The bytes are RFC822-style text, so they stay readable
and intact; no other attachment type reaches a payload-structured generator
branch, so normal mail is unaffected.

Also skip empty / whitespace-only PST attachments: DSN reports expose blank
diagnostic parts that libpff surfaces as attachments, which imported as
0-byte parts rendering as broken in the UI while carrying no information.
2026-06-04 16:22:45 +02:00
jbpenrath bd5eb113a0 🐛(backend) complete pst email folder prefixes list
Previously, we considered as email container only ones
prefixed by `IPF.Note` but it appears `IPF.Imap` can also
contains email so we can miss some mails during import.
2026-06-04 16:22:44 +02:00
Jean-Baptiste PENRATHandGitHub e58a6f323e ⬆️(backend) bump django-lasuite to 0.0.26 (#689)
Upgrade to this version to be able to use `login_hint` param
during oidc auth.
2026-06-02 15:50:07 +02:00
jbpenrath 3f200ed04d 🔖(minor) release version 0.7.0
Update all version files and changelog for minor release.
2026-05-28 18:56:07 +02:00
jbpenrath ea9f33f6ff 🐛(frontend) do not refetch thread messages on draft deletion
If a draft is the single thread message, delete it should not
trigger a request to refresh thread message list because this is
wasteful and it also display a toast error as the thread does not
exist anymore.
2026-05-28 18:00:25 +02:00
jbpenrath afdfaf938a 🚸(frontend) improve sending experience
First when the sending reach the timeout, instead to display a toast
error
with a message that lets believe the message cannot be sent, we display
a warning message mentionning that sending takes more time than
expected.

Then, once a message is sent, we optimistically update thread cache to
hide
immediately the sent draft and show instead the message in the thread
view.
2026-05-28 11:02:15 +02:00
jbpenrath 77b16e5dc6 🐛(frontend) unmount thread view immediately on unselect thread
On slow machines, a race condition can occur when unselectThread is
trigger and another action is also triggered. e.g: when user mark as
unread a thread, the thread is unselect and the request to mark it as
unread is trigger but sometimes, the request is completed before the
router navigation so the observer in charge to mark visible message has
unread is trigger again and finally, the thread is not marked as
unread...
2026-05-27 18:42:02 +02:00
jbpenrath 4907849faf (frontend) attachments preview
Use the Preview component to allow user to
preview message attachment from a thread.
2026-05-27 17:05:16 +02:00
Jean-Baptiste PENRATHandGitHub 567a73a7e4 🗃️(backend) remove _deprecated model fields (#678)
In f360570798, a
migration in 3 parts has been done. This was
a progressive migration and in order to be able
to restore data model in case of failure, we simply
deprecate some fields. Now that everything is fine in
production, we can safely remove those fields.
2026-05-27 10:06:43 +02:00
Jean-Baptiste PENRATHandGitHub 98eabf96dd (backend) add defusedxml as deps (#677)
defusedxml was not declared into dependencies. In local
environment this one should be installed as transitive
deps but during deployment this deps was not found.
2026-05-27 09:39:13 +02:00
Jean-Baptiste PENRATHandGitHub 550af75214 🔖(minor) release version 0.6.0 (#671)
Update all version files and changelog for minor release.
2026-05-26 16:21:59 +02:00
jbpenrath 1f7427044e 🐛(backend) preserve obs-id-left In-Reply-To via UnstructuredHeader
Follow-up to 6144ccb2: the graceful drop stopped the 500 on /send/,
but every reply to an Outlook/MAPI thread lost threading because
Python 3.14's MsgIDListHeader (now the default for In-Reply-To and
References) truncates obs-id-left ids with multiple '@' in the local
part at the first '@' on serialize. The pre-stdlib flanker composer
used to write those bytes through unchanged.

Route both headers to UnstructuredHeader through a dedicated
HeaderRegistry. The instance has to be dedicated: policy.clone()
shares header_factory by reference with policy.SMTP and policy.default,
so mutating in place would silently change parsing process-wide. The
msg-id regex is loosened to allow multiple '@' now that the value
goes out verbatim; the whitespace ban stays (UnstructuredHeader folds
mid-id and receivers would then truncate at the fold), as does the
graceful drop from 6144ccb2 for the cases that genuinely can't ship
(whitespace, no '@', nested brackets, CR/LF injection attempts).

The PST importer's mirror regex is loosened in sync so archive imports
preserve threading on the same kind of ids. Two parametrized tests
lock the regression surface against silent narrowing — eight real-world
shapes that must round-trip on the wire and eight that must drop.
2026-05-21 14:19:18 +02:00
Jean-Baptiste PENRATH 7690d08ef0 👔(backend) allow to delete internal message at any time (#669)
Currently, user can delete/edit an internal message while
it is within the edit timeframe defined by
`MAX_THREAD_EVENT_EDIT_DELAY`. First feedbacks raises that
this limit is not relevant for deletion.
2026-05-20 20:49:50 +02:00
jbpenrath fedc4969bb 🐛(backend) improve pst import logic
- Improve error management for pst

We get some pst that are unparsable by pypff. To help user to understand
that the issue is coming from the PST file, we improve the exception
raised by pst task and display a custom error message according to the
error format.

- Recover Exchange X.500-only senders during PST import

Sent items from shared mailboxes — and many internal Exchange messages —
expose every PR_SENDER_*/PR_SENT_REPRESENTING_* slot as an unresolvable
X.500 DN. compose_email then rejected the EML for lack of a valid From
address and pst_tasks silently dropped the message at debug level, so
entire folders disappeared from the import without a trace.

- Prevent duplicate messages on PST re-import

PST messages without transport_headers (drafts, locally composed items)
were reconstructed with no Message-ID at all, and Exchange/O365 exports
sometimes drop the header even on received items. With no mime_id to
key on, deliver_inbound_message skipped its dedup check and inserted
the same message on every import — and even twice within a single
import when the same message appeared in multiple Outlook folders.
2026-05-20 20:49:01 +02:00
Jean-Baptiste PENRATH 49fcf22438 (frontend) thread deep linking (#664)
Allow user to copy/paste a thread link with other mailbox users.
Currently, if the user copy the current url, the link is broken once
the thread has been moved from the origin folder. It is also possible
to target a message or internal message.
2026-05-20 20:48:00 +02:00
Jean-Baptiste PENRATH ebab36d577 (frontend) improve message composer
* allow to render table in email

Improve email exporter to support table elements. We do not add
blocknote tool to add explicitly table but we allow user to
copy/paste it and render it properly.

* upgrade to blocknote 0.49.0

Remove a bug that prevent to use backspace in an empty block.
https://github.com/TypeCellOS/BlockNote/pull/2610
2026-05-20 20:48:00 +02:00
Jean-Baptiste PENRATH 21255a4c4a (global) allow thread assignation (#645)
As a follow-up of the mention feature, we build upon ThreadEvent & UserEvent
models a feature to assign users to a thread.
We allow to filter mailboxe's inbox through assignation state (assigned to me, unassigned).
The thread share modal has been forked from ui-kit to be able to list users of each
mailbox and add a cta to assign them to the thread. A section above shows assigned users.
2026-05-19 14:44:26 +02:00
Jean-Baptiste PENRATHandGitHub 8e009c5972 🐛(backend) allow thread editor to destroy thread accesses (#668)
Currently, only mailbox editors can remove thread access to their
mailbox.
Actually, thread access deletion must be symmetric with creation rights.
So
any user with thread management ability should be able to delete a
thread
access.
2026-05-19 14:44:06 +02:00
jbpenrath 1b03e1d240 🌐(frontend) localized attachment separator
The `ATTACHMENT_SEPARATORS` only contains a english separator and as
this
string is displayed within other mail client, for non-english users it
can
be weird to see an english string. That's why we add new separators and
according to the sender language, we use the localized separator that
correspond to him.
2026-05-12 13:48:38 +02:00
jbpenrath 8a7cb8e0c2 (frontend) make panel sections resizable
Allow user to resize mailbox folder and mailbox label list sections.
2026-05-11 16:39:30 +02:00
jbpenrath 858e995062 🚸(frontend) show tooltip to confirm mailbox refresh
Currently when the user clicks on the refresh button, a spinner
is displayed during query is pending. Often, this query is really
fast so the user does not see the spinner and wonder if something
really happend. In order to improve ux here, we now display the
spinning icon at least for 700ms in this way, no matter the query
timing, the user will see something happend. Furthermore, we also
add a transient tooltip that display refresh state (number of
new message or Up to date).
2026-05-11 10:46:29 +02:00
Jean-Baptiste PENRATHandGitHub 43e368a501 🚸(frontend) add read/unread action on thread action bar (#659)
Currently to mark a thread as read/unread, the user have to open a
dropdown
menu then click on the action... as this action can be done often, we
put it
directly in the thread action bar.
2026-05-07 12:00:28 +02:00
Jean-Baptiste PENRATHandGitHub 6be3b9e129 ♻️(frontend) refactor thread query cache management (#642)
The thread query is an infinite one and the frontend logic is
based on the structuralSharing concept of react-query to
optimiscally update the react query cache on thread mutation in
order to improve ux. This part is a tricky one and it's easy to
introduce regression, that's why refactor it by moving the corresponding
logic into a mailbox-cache module, use a better naming (pin instead of optimistic)
and battle test it.
2026-05-06 19:39:07 +02:00
Jean-Baptiste PENRATHandGitHub ad3307e448 🐛(frontend) fix thread panel header with nested label (#658)
When the current view is filtering through a nested label, the thread
panel
title has no text because we only traverse root labels to try to find
the
one selected. Now we also recursively traverse children.
2026-05-06 18:17:48 +02:00
Jean-Baptiste PENRATHandGitHub 22a8c35140 🔧(frontend) allow to use specific channel id for home feedback widget (#655)
Add `NEXT_PUBLIC_FEEDBACK_WIDGET_HOME_CHANNEL` env var to be able to
set a specific channel id to receive feedback from unauthenticated
users.
For configuration ease and backward compatibility, if
`NEXT_PUBLIC_FEEDBACK_WIDGET_HOME_CHANNEL` is not set, the home feedback
widget fallback to `NEXT_PUBLIC_FEEDBACK_WIDGET_CHANNEL`.
2026-05-05 14:23:42 +02:00
Jean-Baptiste PENRATHandGitHub 485e4a2e66 🌐(frontend) dns add propagation delay info (#654)
DNS configuration can take up to 24 hours to propagate. To prevent
support
question, we add this explanation in the admin DNS view.
2026-05-05 11:50:56 +02:00
Jean-Baptiste PENRATHandGitHub 5ca59329ab 🔒(backend) stop flagging inbound From=To mails as is_sender (#652)
A spoofed inbound with From == To was being marked is_sender=True via
the
`sender_email == recipient_email` shortcut in
_create_message_from_inbound.
Because MessageRecipient rows from the inbound path carry no
delivery_status, those messages matched retry_messages_task's
(is_sender=True AND delivery_status IN (RETRY, NULL)) filter and went
through send_message on every 5-min beat — DKIM-signing and re-emitting
the spam to every recipient on the envelope, externals included.

Legitimate self-sends are unaffected: send_message's internal redelivery
hits the mime_id dedup in deliver_inbound_message before reaching
_create_message_from_inbound, so the shortcut was already dead code on
the legitimate path.
2026-05-04 15:54:44 +02:00
Jean-Baptiste PENRATHandGitHub 44d455c488 ♻️(frontend) support legacy and new widget attribute (#650)
The new widget loader consume `window._lasuite_widget` property to
know which widget to load. The previous version was using
`window._stmsg_header`. We refactor widget loading logic to support
both version with ease.
2026-04-30 16:18:51 +02:00
Jean-Baptiste PENRATHandGitHub b12ebbf8c6 ⬆️(frontend) update widget logic to latest version (#649)
The new widget loader consume `window._lasuite_widget` property to
know which widget to load. The previous version was using
`window._stmsg_header`.

/!\ Update NEXT_PUBLIC_LAGAUFRE_WIDGET_PATH and
NEXT_PUBLIC_FEEDBACK_WIDGET_PATH
to target the new widget version before deploying this commit.
2026-04-30 14:28:25 +02:00
jbpenrath bff7464571 🔧(backend) allow to reindex from date
Add an option `--from-date` to the `search_reindex` management command.
In this way, in case of index task failure, we are able to reindex only
resources updated in a recent timeframe.
2026-04-29 10:19:33 +02:00
jbpenrath a29b25a7c0 ️(backend) replace delete_by_query with bulk delete by _id
OpenSearch was returning 503/429 on delete_by_query under the load of
the periodic reindex. Each bulk_reindex_threads_task could fire up to
100 delete_by_query calls (one per chunk of 100 threads) to sweep
orphan messages, and bulk_delete_threads_task did one more to drop a
thread and all its children. delete_by_query holds a scroll context,
scans the index and refreshes per call — far heavier than the bulk
upserts running alongside it.

Tracking message deletes explicitly at signal time lets us replace
every hot-path delete_by_query with a bulk delete by _id:

- New search:pending_delete_messages set storing thread_id:message_id
  pairs, fed by Message.post_delete (covers cascaded deletes too).
- New bulk_delete_messages_task issuing bulk DELETE actions with the
  parent thread_id as routing.
- bulk_delete_threads_task rewritten to bulk DELETE thread parent docs
  by _id; child message docs ride the new task via cascaded signals.
- _purge_orphan_docs and the per-chunk purge in reindex_bulk_threads
  removed; reindex is now pure upsert.
2026-04-29 10:19:33 +02:00
jbpenrath 2603b2bb62 🐛(backend) retryable opensearch transport errors
Currently, we retry indexing task only on ConnectionError (socket-level)
but some error at http-level are also relevant to a retry.
So now we filter TransportError by status code: retryable (5xx + 429)
propagate so `bulk_reindex_threads_task` can autoretry with the existing
exponential backoff, 4xx stay swallowed since retrying caller bugs only
burns worker time.
2026-04-29 10:19:33 +02:00