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
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.
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.
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>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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>
With the latest version of Drive, items have no a url_permalink property which
is more robust than the previous url property we were using as download link
for drive attachments.
Furthermore, we totally revamp the logic to save attachment to drive. Actually
the current implementation triggered n requests for n attachments each time a
user opened a thread... that was great for ux as the user always know if the
attachment exists in its workspace but it triggers too muck request to be a
production ready implementation. So now, the user is no more able to know
if an attachment exists in its workspace until it clicks to upload the
attachment and the backend checks if the file already exists in the user
workspace.
Finally, we also replace the DriveIcon which was used to open drive item
preview by an eye icon.
Resolve#567
Allow users to quickly filter threads in the panel by "unread only"
and/or "starred only" via URL search params, keeping consistency
with the existing folder navigation pattern. Also fixes folder title
resolution and active state highlighting when filter params are present.
The backend already scopes the starred flag per mailbox via
ThreadAccess.starred_at. This commit exposes the feature in the
frontend: a useStarred hook, a toggle button in the thread action
bar, a starred badge in thread items, an important icon in the
thread view subject, and a search filter checkbox for starred
threads (is:starred / est:important).
Also fixes the undo toast in use-flag to propagate mailboxId so
that mailbox-scoped flags (unread, starred) can be properly undone.
Currently thread sharing is confusing as the read state is shared from
all thread accesses so we deprecate the `read_at` of the Message model
in favor of a `last_read_at` timestamp pointer on the thread access.
Furthermore, on the thread model, we add several date fields to store relevant
dates in which thread should be sorted according to the current view.
For shared mailboxes, knowing which user actually sent a message is
important UX information. This adds a `sender_user` field on Message,
set at send time (not draft creation), and displays "Sent by <name>"
in the sender's contact popover when viewing from a shared mailbox.
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
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.
Avoid transferring heavy body content (especially base64 images) when
only metadata is needed. The frontend now explicitly requests the body
fields it needs via ?bodies=raw,html,text, and modals lazily fetch
body content only when editing a template.
Key changes:
- Replace 3 read serializers with a single ReadMessageTemplateSerializer
that dynamically includes body fields based on ?bodies= query param
- Cache parsed blob content on the model to avoid redundant JSON parsing
- Skip expensive base64 image validation on update when body is unchanged
- Refactor frontend modals to lazy-load body via retrieve on edit
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.
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.
Allow users to report a thread as a spam.
Also allow the user to mark the thread has not spam. Then in case of spam,
disable the attachment download for security purpose.
* 🩹(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>