Commit Graph
76 Commits
Author SHA1 Message Date
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
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
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 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
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
Sylvain Zimmerandjbpenrath 109211e0a0 (ssrf) add a host allowlist to bypass SSRF in internal networks 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
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 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
638a2de121 ♻️(frontend) drop next.js for vite + tanstack router (#675)
Next.js has been concerned by several CVEs last time and it's clearly
overkill for our needs (static export) so in order to enlight our stack
we migrate to vite & tanstack-router

Co-authored-by: jbpenrath <jb.penrath@gmail.com>
2026-06-09 11:58:51 +02:00
Sylvain ZimmerandGitHub e3c183a625 🐛(smtp) fix opportunistic TLS against MXes with mismatched certs (#687)
The "may" level was verifying the peer cert and falling back to
cleartext on mismatch, which then bounced on STARTTLS-required
servers (e.g. Mandrill's SES-backed inbound returns 530 to MAIL
FROM in cleartext). Realigned on Postfix's documented behavior:

- "may":    opportunistic TLS, no cert verification.
- "secure": mandatory TLS + CA chain + hostname check; defers if
            STARTTLS isn't advertised or handshake fails.
- "encrypt" is dropped (replaced by "secure").

Also wires MTA_OUT_SMTP_TLS_SECURITY_LEVEL through both the direct
and relay paths — it had been declared but never read — and
collapses the four proxy_* kwargs + sender_hostname of
send_smtp_mail into a single SmtpProxy dataclass.
2026-06-04 16:20:57 +02:00
Sylvain ZimmerandGitHub f7786b3d0f (backend) report selfcheck status to Sentry crons (#694)
Use the new (documented) env var MESSAGES_SELFCHECK_SENTRY_MONITOR_SLUG
to enable
2026-06-04 00:38:59 +02:00
Sylvain Zimmerandjbpenrath f360570798 (blobs) implement tiered storage and refactor blobs/attachments
This allows to use S3-compatible object storage to offload blobs, making Postgres much lighter. We design for storing ~1B emails on a single instance.

We also take this opportunity to do model changes on blobs & attachments.

Migration 0027 is one-way, no going back after this one so check your backups.
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 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 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
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
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
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
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
e1970cc732 (autoreply) add autoreply feature (#569)
This reuses message templates and allows to define scheduling

Co-authored-by: Sylvain Zimmer <sylvinus@users.noreply.github.com>
2026-03-16 16:59:54 +01:00
Sylvain ZimmerandGitHub d153f5ad69 (entitlements) add DeployCenter backend for syncing maildomain admins (#572)
Entitlements system with pluggable backends and automatic mail-domain admin sync on login.
Provisioning API to create/update mail domains (bulk input, custom attributes)
Feature flags to enable/disable mail-domain creation and access management; UI and public config expose these flags
2026-03-03 15:06:55 +01:00
Jean-Baptiste PENRATHandGitHub 516d9dfa58 (frontend) add configurable help center button in header (#537)
Allow to configure a help center URL via the
 NEXT_PUBLIC_HELP_CENTER_URL environment variable. The button
 adapts based on what's configured:
- Help center only: opens external link directly
- Feedback widget only: opens widget directly
- Both: shows dropdown with both options

The button is now displayed in the header to save space in sidebar,
 rather than in the left panel.
2026-02-23 16:34:33 +01:00
Sylvain ZimmerandGitHub f18277b742 🔨(devx) update developer experience: uv, rustfs, caddy, new makefile (#556)
This large PR modernizes the backend of the app:
 - Python Dependency Management: Poetry → uv
 - Object Storage for local dev: MinIO (now unmaintained) → RustFS
 - Makefile Target Standardization to align with other LaSuite repos
 - Internationalization Removal on backend: we only care about i18n on the frontend
 - Backend dependencies upgrade
2026-02-23 12:30:36 +01:00
Sylvain ZimmerandGitHub ab24b60160 (throttle) add outbound message recipients throttling (#506)
This is inspired by #466 but focuses on outbound backend-only changes.
2026-02-20 00:41:58 +01:00
Stanislas BruhièreandGitHub 850929ca48 (mda) use a webhook+logging instead of a pushgateway for selfchecks (#550) 2026-02-19 20:40:14 +01:00
Sylvain ZimmerandGitHub e5ce447905 (exports) add task to export a mailbox in mbox, with labels (#553)
This also makes sure we can reimport X-Keywords later.

For now it's admin-side only, we'll add a user-facing UI. First part of #211.
2026-02-19 18:10:19 +01:00
Sylvain ZimmerandGitHub 9b5375cba3 (import) add support for PST imports & stream data for mbox (#544)
Fixes #183. Replaces #517.
2026-02-19 14:03:43 +01:00
jbpenrath c53e8bfcd4 (global) allow to add image block into template and signature composers
Images are embedded as base64 data URLs directly in the BlockNote content,
unlike the message composer which uses blob uploads + CID references.
This approach keeps templates and signatures self-contained without
requiring an attachment system.

Furthermore, a email-safe safe html exporter has been created to
serialize blocknote content into html.

A new backend setting MAX_TEMPLATE_IMAGE_SIZE (default 2 MiB) controls
the maximum allowed image size for these composers.
2026-02-17 19:02:28 +01:00
jbpenrath ffca2231e6 (global) add outbox conditional folder
Add outbox conditional folder to display messages having message recipients with None, retry or failed state.
Inform visually the user is something is wrong with a message.
Allow user to update MessageRecipient from failed to cancelled to hide ui warning.
2026-02-05 19:04:02 +01:00
Sylvain ZimmerandGitHub e6296f9b3a (integrations) add integrations view in mailbox (#488)
For now this allows users to create feedback widgets linked to their
mailbox. We will also have API keys and recurring imports there.
2026-01-25 17:29:19 +01:00
Sylvain ZimmerandGitHub 9a48eda9c6 ♻️(permissions) refactor some permissions code for viewsets (#503)
We want to avoid relying on role__gte, so we add role list aliases.
2026-01-24 13:48:43 +01:00
Sylvain ZimmerandGitHub 681a1481af (worker) add worker.py command and improve task routing on queues (#504)
This avoids a case where a very large import would overflow the
"celery" queue with reindex tasks that would block the inbound
processing tasks. Now we have good priorization.
2026-01-24 11:53:22 +01:00
Jean-Baptiste PENRATHandGitHub 606f0cc144 🔧(backend) update Django admin URL handling (#498)
Updated the DJANGO_ADMIN_URL setting to ensure it does not end with a '/'.
It allows to keep simple redirect logic from the nginx proxy server and avoid
issues with django admin route paths.
2026-01-22 10:01:37 +01:00
812cf51915 (all) allow to display external images through proxy (#469)
Allow to users to display external images from their email
through a secure proxy endpoint to ensure security and privacy
and respect iframe csp policy.

Co-authored-by: =?UTF-8?q?Ri=C3=ABl=20Notermans?= <riel@mosa.cloud>
2026-01-12 17:09:32 +01:00
jbpenrath c3a439deae 💄(frontend) migrate to ui-kit v2
Migrate frontend to ui-kit v2. Improve layouts on the road and fix
major a11y issues
2025-12-15 17:59:51 +01:00
Jean-Baptiste PENRATHandGitHub 5161978fcd 🔊(frontend) allow to capture exception through Sentry (#448)
Install and setup a generic error handler that send event to
Sentry if configured or just log error in console otherwise.
2025-12-09 17:51:00 +01:00