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
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.
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.
Mails sent without a To recipient (Bcc- or Cc-only) ship with no To
header, which anti-spam filters treat as a negative signal. Inject an
RFC 4356 empty-group "To: undisclosed-recipients:;" before DKIM signing
so it's covered by the signature.
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>
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.
Two distinct AttributeError crashes were firing on legitimate inbound
mail and producing 5xx-equivalent failures (parse aborts, autoreply
skipped).
1. Flanker scanner crashed on `multipart/report` bounces whose status
part used an RFC 6533 i18n content type (`message/global-delivery-
status` and siblings). The same crash path was reachable for every
`message/*` subtype not in flanker's hardcoded list — `partial`,
`imdn+xml`, `sip`, `cpim`, future / vendor subtypes. Fixed in the
flanker fork (i18n predicates + traverse fallback + make_part guard);
pin bumped to 77f4582044f1a8549d49333d50d9bded1176ca09.
2. `parsed["headers"]` returned `str` for single occurrences and
`list[str]` when duplicated. Every scalar consumer (`.strip()`,
`.lower()`, `.startswith()` on Subject / Message-ID / Precedence /
Return-Path / etc.) crashed the moment a header was repeated. The
parser now applies a fixed per-header type contract driven by the
IANA Provisional Message Header Field Registry: headers registered
with max=1 (RFC 5322 §3.6, RFC 3834, RFC 2045/2046/2183, RFC 4021,
RFC 3798, RFC 5703, RFC 8058, RFC 8098) are `str` with first-wins
semantics matching stdlib `email.message.Message[name]`; every
other header is `list[str]` in document order. `headers_blocks`
stays uniformly list-typed for the trusted-relays cut.
CalDAV-backed mailbox calendar actions: RSVP (Accept/Decline/Maybe), Add to calendar, calendar chooser, calendar listing, and conflict detection — actions enqueue background tasks and return task IDs; new API endpoints documented.
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.
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.
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.
- 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.
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>
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
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
Personal mailboxes now enforce configurable prefix restrictions; blocked prefixes return a clear error. Domains can indicate identity synchronization status; when disabled, creating personal mailboxes is prevented and returns a specific error.
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.
* 🩹(mime) fix inline HTML being treated as attachment
Previously, any MIME part with a Content-Disposition header (including
'inline') was treated as an attachment. This caused HTML email bodies
with 'Content-Disposition: inline' to appear as downloadable attachments
instead of being rendered as the message body.
The fix clarifies the classification logic:
- Parts with 'Content-Disposition: attachment' are always attachments
- Parts with 'Content-Disposition: inline' and text/plain or text/html
are treated as message body
- Parts with filenames but no disposition follow existing rules
* ♻️(mime) refactor parser to use Flanker's built-in methods
Replace manual disposition parsing with Flanker's is_attachment(),
is_body(), and is_inline() methods which properly implement RFC standards.
This fixes issues where:
- text/calendar parts were incorrectly treated as attachments
- inline HTML was misclassified
- Parts without disposition were not properly categorized
The new logic uses Flanker's built-in classification:
- is_attachment(): Content-Disposition is 'attachment'
- is_body(): No filename AND (text/* or message/*)
- is_inline(): Content-Disposition is 'inline'
* fix: Add missing disposition_header variable definition
* 🛡️(mime) add defensive exception handling for MIME classification
Add try/except blocks around Flanker's is_attachment(), is_body(), and
is_inline() methods to prevent malformed MIME from crashing the parser.
When classification fails, log the error and safely fall back to treating
all checks as False, allowing parsing to continue.
Also preserve actual Content-Type for attachments instead of forcing
text/plain to application/octet-stream, and infer appropriate file
extensions from MIME types when no filename is provided.
* ♻️(mime) refactor attachment handling with helper functions
Extract duplicate attachment construction logic into reusable helper
functions and add consistent filename extension inference across all
attachment types (attachment, inline, and fallback paths).
Changes:
- Add _infer_filename_from_content_type() to map MIME types to file
extensions for better unnamed attachment handling
- Add _build_attachment_dict() helper to eliminate ~45 lines of
duplicated attachment dict construction code
- Apply extension inference consistently to is_attachment, is_inline,
and fallback branches
- Update tests to expect new filename format (e.g., "unnamed.pdf")
- Clarify exception handler comment for better code documentation
* ✅(rfc5322) fix tests & lint
* 🐛(rfc5322) sanitize attachment filenames and add many more tests
* 🐛(rfc5322) improve RFC5322 parsing logic with nested parts, per JMAP spec
* ✨(rendering) render multipart messages in frontend
* fixup! ✨(rendering) render multipart messages in frontend
* 🐛(review) address PR review comments
- Fix _sanitize_filename to preserve file extension when truncating
- Fix MessageBodyItemSerializer: use required fields, remove Meta class
- Fix misleading comment about text body drive attachments
- Regenerate OpenAPI schema and frontend API client
* 🐛(types) fix null fallback
* fix lint and tests
---------
Co-authored-by: Riël Notermans <riel@zzapps.nl>
Co-authored-by: Riël Notermans <riel@mosa.cloud>
Co-authored-by: jbpenrath <jb.penrath@gmail.com>
This adds an intermediate Postgres-backed queue for inbound messages, that
allows us to run filters like spam processing before inserting messages in their
final storage (soon to be object storage). Also include misc. refactorings.
This will catch the case where people try to send email before having
correctly configured their DNS, and would also avoid unwanted sends
of messages not supposed to go out.
Adds new config vars for incoming & outgoing attachment sizes, validated at draft stage
and when receiving emails.
---------
Co-authored-by: Riël Notermans <riel@mosa.cloud>