Commit Graph
725 Commits
Author SHA1 Message Date
Sylvain Zimmer f3e9b67ea3 (rfc5322) switch back to Python's stdlib for email composition
Latest fixes in the stdlib make it a more solid alternative for strict composition than Flanker. We keep Flanker for now for lenient inbound parsing. We add stronger tests and fuzzing to validate we didn't regress.
2026-05-07 14:46:26 +02:00
Sylvain ZimmerandGitHub d0de96ccdf 🐛(flanker) fix email parsing edge cases with UTF8 (#656) 2026-05-06 00:24:45 +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
Sylvain ZimmerandGitHub 99caf3208b (invites) enable inviting users that haven't logged in yet (#644)
No invitation email for now
2026-04-30 14:49:48 +02:00
BastienandGitHub ab4520b01b 🔒(backend) force including special characters in generated passwords (#640)
This makes us able to upgrade the Keycloak password policy
2026-04-30 14:30:29 +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
BastienandGitHub 3cd2fd1aeb (paas) fix Procfile processes names (#648) 2026-04-29 14:52:41 +02:00
BastienandGitHub b98b45008f (paas) place imports and reindex worker queues in dedicated containers (#643) 2026-04-29 14:18:49 +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
Jean-Baptiste PENRATHandGitHub 5054daa879 🌐(frontend) force default language (#647)
Currently, when user has not its language set into local storage, we
retrieve
the default language through the navigator language. In some instance,
we would
like to enforce the default language. So we add a new env var
`NEXT_PUBLIC_FORCED_DEFAULT_LANGUAGE`, if this one is set to `true`, we
skip
the navigator.language and use the `NEXT_PUBLIC_DEFAULT_LANGUAGE` as
default
2026-04-28 19:38:14 +02:00
jbpenrath 81da914ca4 ️(backend) improve search_reindex bulk payload
`bulk_data` method was missing some options (timeout, max_retries) so
sometimes when
the payload was heavy, the request can failed because the timeout was
too slow by default
(10s) and there is no `max_retries`. Furthermore, we build bulk payload
by chunking thread
but it did not check the payload bytes size, so in case of heavy
message, the payload could
be heavy. Now a max_bytes limit is set (50 Mib by default)
2026-04-27 18:26:03 +02:00
jbpenrath 6d0eb0d234 ️(backend) defer indexation tasks
Opensearch index is updated each time a Thread, Message or MessageRecipient
is updated through signals. The current logic has performance issues has
n update of a resource will generate n celery task to update the same
resource... So this work aims to batch updates. Index is updated each
30s and resource ids is deduplicated to only update a resource once.
Furthermore, in an import context, the index will be updated only
when the import will be completed to prevent to spam the celery
worker with numerous indexation task.
2026-04-27 18:26:03 +02:00
jbpenrath a1fdca19b2 Revert "(global) allow thread assignation"
This reverts commit 267372520d.
2026-04-27 18:14:09 +02:00
jbpenrath 682530b3b7 Revert "fixup! (global) allow thread assignation"
This reverts commit b04b07a465.
2026-04-27 18:14:01 +02:00
jbpenrath b04b07a465 fixup! (global) allow thread assignation 2026-04-23 20:21:20 +02:00
jbpenrath 267372520d (global) allow thread assignation
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-04-22 20:21:31 +02:00
jbpenrath 7a0f6fba80 (backend) fix test suites relying on boto3
In a5b2c2d0bc, we mocked `socket.getaddrinfo`
module but this one is also used by boto3 under the hood, so the mock brokes
all tests using boto3.
2026-04-20 15:04:34 +02:00
jbpenrath 9a44d2cf69 🐛(frontend) initialize thread event input on open
Currently, the logic to show/hide thread-event-input was only processed
into a scroll handler and by default the thread-event-input was hiddden.
So in thread view with only few messages, we expect to display the
thread
event input, but it is not. Now we call the near bottom check at mount
to
display the input if it is relevant when a thread is opened.
2026-04-20 10:24:43 +02:00
BastienandGitHub e8d4a00817 (chore) bump keycloak to 26.6.1 (#637) 2026-04-19 15:59:02 +02:00
Sylvain ZimmerandGitHub a5b2c2d0bc 🔒️(ssrf) factorize SSRF code, allow redirects in image proxy (#631)
Also use the SSRF code in IMAP imports.
2026-04-17 10:00:56 +02:00
Sylvain ZimmerandGitHub b4da8a3af7 (inbound) add configurable inbound auth backends (#636)
Added sender authentication checks (DKIM/DMARC) with multiple modes (native, rspamd, authentication-results). Message UI now shows a clear error banner for forged senders and a warning for unverified senders; contact chips display a forged state.
2026-04-17 09:22:00 +02:00
Jean-Baptiste PENRATHandGitHub 21a8cd0413 🐛(frontend) fix label popup stacking with create-label modal (#635)
The popup hosted the modal and portalled into document.body. Because
#__next establishes an isolated stacking context while body does not,
the popup sat on a higher paint layer than anything inside #__next
regardless of z-index, making the modal appear behind it and letting
the popup's overlay steal clicks and Escape from it.

Lift modal ownership and label mutations to LabelsWidget so the modal
renders as a sibling of the popup, portal the popup into #__next so
both share a stacking context, mark the popup aria-modal, and expose
closeOnEsc so the parent can silence the popup's Escape while a modal
is stacked above. thread-selection now defers Escape to any open
dialog so the popup owning Escape no longer exits selection mode.
2026-04-16 19:45:25 +02:00
jbpenrath 9b1e3fa295 🐛(backend) fix race condition in last-editor deletion guard
The select_for_update() was ineffective for two reasons:
- .exclude(id=instance.id) caused each concurrent thread to lock
  a disjoint set of rows, so no serialization occurred
- .count()/.exists() generate aggregate SQL (SELECT COUNT(*))
  that silently drops the FOR UPDATE clause in Django ORM

Now locks ALL editor rows (including self) via values_list()
evaluation, forcing concurrent deletes to serialize properly.
2026-04-16 16:14:16 +02:00
jbpenrath b857122e45 🚸(frontend) show/hide ThreadEventInput when relevant
Currently the ThreadEventInput is always showed and sticked to the
bottom of the screen. But it can be annoying when the user is writing a
message for example as the input is displayed above the message reply
form. Then it is not relevant to show the input when the user is at the
top of the thread view. So we only show the input when the user reach
the end of the view.
2026-04-16 15:41:11 +02:00
jbpenrath 1103721b75 🚸(frontend) focus to field on forward
When user forwards a message, the message form should focus the `to`
field not the message composer.
2026-04-16 15:41:11 +02:00
Jean-Baptiste PENRATHandGitHub 730fea15ee 🐛(global) handle non-serializable celery task errors and stop infinite polling (#633)
When a Celery worker crashes (e.g. OOM during large imports), the task
result contains a raw exception object (WorkerLostError) that is not
JSON-serializable, causing a 500 on the task status endpoint. The
frontend polling hooks never received a FAILURE status and kept polling
indefinitely.

Backend: convert exception objects in task results to serializable
strings instead of letting DRF fail on serialization.
Frontend: stop polling on API errors and surface the failure state to
consumers so they can display appropriate error UI immediately.
2026-04-16 12:17:55 +02:00
Stanislas BruhièreandGitHub 631522b388 🐛(mda) quote error field and log socks proxy in outbound delivery (#626) 2026-04-16 12:12:18 +02:00
Jean-Baptiste PENRATHandGitHub 8f8798cba3 🐛(global) allow thread viewers to post internal comments (#632)
Writing an internal comment is a personal authoring act that should
not require thread edit rights: support teammates invited as thread
viewers must still be able to comment and mention colleagues, as long
as they have edit rights on the mailbox. ThreadEvent IM writes and
ThreadUser listing are relaxed accordingly, while every other thread
mutation keeps the full edit-rights check.

  The message composer is now gated by the thread edit ability so that
  read-only users cannot bypass the check through reply or forward, and
  the thread-panel selection separator is hidden when no bulk action is
  available. A few unrelated UI polish fixes (disabled link button
  style, combobox placeholder visibility) ship alongside.
2026-04-15 17:07:51 +02:00
jbpenrath efbae5f517 🚸(frontend) disable application menu when no option is available
Previously the settings dropdown could open an empty menu when
the selected mailbox granted none of the required abilities, leaving
users to think the button was broken. The menu is now rendered as a
disabled button with an explanatory tooltip so the UI stays stable
across mailbox switches and the reason is surfaced to the user.
2026-04-15 13:06:17 +02:00
jbpenrath 78c1842d99 (frontend) add label assignment with archive and bulk label widget
Dragging threads onto a label now assigns the label and archive by default.
Holding Shift while dropping only assign the label to the threads, mimicking
Gmail's "move" behavior. A custom drag preview follows the cursor
and updates in real-time to reflect the current action.

A new BulkLabelsWidget in the thread selection toolbar lets users
assign labels to multiple threads at once.
The existing LabelsPopup was refactored to support
multi-thread selection with indeterminate checkbox states.

Resolve #396
2026-04-15 13:06:17 +02:00
jbpenrath a7dc4b4ef9 ⬆️(frontend) upgrade cunningham & ui-kit
Upgrade @gouvfr-lasuite/cunningham-react and @gouvfr-lasuite/ui-kit to
its latest version
2026-04-14 23:08:14 +02:00
jbpenrath 01b45a69fb 🐛(global) enforce full edit rights on thread mutations
A user with VIEWER MailboxAccess on a shared mailbox could still mutate
threads that the mailbox had EDITOR ThreadAccess to: the permission
check only looked at ThreadAccess.role, never at MailboxAccess.role.

Both roles must now be satisfied (EDITOR on ThreadAccess AND a role in
MAILBOX_ROLES_CAN_EDIT on MailboxAccess) for archive, spam, trash,
label, split, refresh_summary and thread-event writes. Personal actions
(unread, starred) intentionally stay open to any mailbox access since
they only mutate the caller's own ThreadAccess row.

The rule is centralised in ThreadAccessQuerySet.editable_by(user,
mailbox_id) so viewsets and permission classes share a single source of
truth, and exposed to the frontend via a new Thread.abilities.edit
field consumed by use-ability, which gates the matching UI controls.
2026-04-14 23:08:13 +02:00
jbpenrath 13d34c213f 💅(frontend) align send button on left
To improve ui consistency in thread view, align the send button of the
message form on the left. In this way, its position is in sync with
reply CTAs
2026-04-14 18:27:30 +02:00
jbpenrath 6e8abf991a 🚸(frontend) improve thread event date format
Use relative date to improve date readability in ThreadEvent component
2026-04-14 18:24:34 +02:00
Sylvain ZimmerandGitHub 16acb5d820 (dns) add recursive SPF check and optional send-time validation (#625)
- Optional outgoing SPF validation that can block or mark external deliveries when sender SPF fails.
 - SPF cache invalidation so DNS rechecks are used immediately after DNS validation.
 - More robust SPF evaluation including recursive include resolution, improved TXT handling, duplicate/limit detection, and clearer result statuses.
2026-04-12 00:47:29 +02:00
Jean-Baptiste PENRATHandGitHub 1044614a76 (global) add mention notifications via UserEvent (#621)
ThreadEvent IM mentions previously lived only inside the event payload,
with no per-user tracking, so a user had no way to see or filter the
threads where they were mentioned. The new UserEvent model materializes
mentions as first-class records (one row per mentioned user per event),
reconciled by a post_save signal whenever a ThreadEvent is created or
edited.

ThreadEvent edits and deletes are now bounded by THREAD_EVENT_EDIT_DELAY
(1h default) so UserEvent records cannot drift out of sync with stale
audit data past the window.
2026-04-10 00:54:39 +02:00
Jean-Baptiste PENRATHandGitHub 4a59033a98 🔧(backend) put split thread feature under a feature flag (#624)
Allow to dis/enable split thread feature through environment variable
`FEATURE_THREAD_SPLIT`
2026-04-09 19:19:12 +02:00
Sylvain ZimmerandGitHub a3ccc7a57b (channels) add encryption, custom scopes and levels, auditing (#599)
- Scoped API-key channels with per-scope authorization, personal user channels (users/me/channels), encrypted channel secrets, API-key regeneration (API + admin UI), channel last-used tracking, and a raw email submission endpoint.
 - Provisioning mailbox lookup/listing API.
 - Stricter outbound MIME size checks, DKIM signing improvements, and hardened channel/permission validation.
 - Expanded end-to-end and unit coverage for channels, auth/scopes, provisioning, submission, metrics, and admin flows.
2026-04-09 00:53:03 +02:00
Jean-Baptiste PENRATHandGitHub d72df6c77d 🐛(backend) fix threads ordering (#617)
There was bug in the default thread ordering. Actually, on list that
embed active and draft messages, some draft messages might have a
messaged_at at None (New message) but when we were sort threads by this
field, new draft message was sort at the top of the list because of null
attribute. So in this case, we need to first try to sort by messaged_at
then by draft_message_at.
2026-04-02 16:27:21 +02:00
Jean-Baptiste PENRATHandGitHub c1666b45fe 🔧(frontend) remove npm from engines (#616)
buildpack is no more able to install dependencies due to the npm version
constraint... As npm is installed with node, we can assume to only set
node version into engines
2026-04-02 16:08:57 +02:00
f002a22dd3 (global) allow user to send internal message through ThreadEvent (#566)
When a Thread has several accesses or is linked to a shared mailbox,
an input allows to post internal messages. It also allows to
mention user in a message. The ThreadEvent model
is the foundation to enrich Threads with further kind of event.

Co-authored-by: Sylvain Zimmer <sylvinus@users.noreply.github.com>
2026-04-01 18:22:30 +02:00
Stanislas BruhièreandGitHub 5a9bfd5cb4 (frontend) add lprobe healthchecks and checksum verification for lprobe + caddy (#600)
* (frontend) add lprobe healthchecks and checksum verification for lprobe + caddy

* (frontend) add comment clarifying the absence of shell expansion
2026-04-01 00:40:06 +02:00
Jean-Baptiste PENRATHandGitHub 346198171f 🐛(search) fix double request and flickering on search (#596)
The search queryKey included searchParams.toString(), creating a new
React Query entry per search term (triggering fetch #1). Meanwhile,
resetSearchQueryDebounced cleared the active query 500ms later
(triggering fetch #2 + flicker).
2026-03-19 14:57:51 +01:00
Jean-Baptiste PENRATHandGitHub 7a8cf44479 (search) use bulk API and prefetching for reindex_all (#595)
The previous implementation indexed each thread and message with individual
OpenSearch HTTP calls and triggered N+1 DB queries for accesses, recipients
and sender lookups. This rewrites reindex_all to batch documents via
opensearchpy.helpers.bulk and prefetch related objects in a single queryset,
drastically reducing both DB round-trips and HTTP overhead.
2026-03-18 16:23:17 +01:00
Niels Kersicandjbpenrath 1767e17117 (global) support silent login
Allow to enable silent login through envvar
`FRONTEND_SILENT_LOGIN_ENABLED`
2026-03-18 11:09:52 +01:00