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.
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.
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.
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.
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.
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.
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.
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.
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
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.
- 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.
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.
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.
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
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>
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).
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.
Invalidate thread list when the user deletes its own
access to a thread to prevent buggy situation where the
user still display a thread that has no more access..
The unread filter relied on active_messaged_at and has_active which excluded
sent and archived threads from being marked unread. Switching to messaged_at
(now excluding both drafts and trashed messages) makes the filter view-agnostic
and consistent across inbox, sent, archive, and trash.
Sent and autoreply messages now update the sender's ThreadAccess.read_at so
threads don't flash as unread in the sender's own mailbox.
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
String interpolation of user-controlled values (router.query.mailboxId,
searchParams) into router.push URLs was flagged by github-advanced-security.
Extract safe shallow push logic into a reusable hook that uses Next.js
object-form router.push, eliminating the attack vector entirely.
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.
The starred flag was previously global (Message.is_starred + Thread.has_starred),
meaning if one mailbox starred a thread, all mailboxes would see it as starred.
This migrates starred to ThreadAccess.starred_at (DateTimeField), following the
same per-mailbox scoping pattern already established for read_at. Each mailbox
now independently controls its own starred state.
Update all version files and changelog for minor release.
Added
- Store thread read state per thread access #575⚠️ This migration requires a search reindex to be run after the upgrade.
- Store and display the user who sent a message #574
- Display selected threads count in right panel #576
- Add skip navigation link for keyboard users #573
- Add DeployCenter backend for syncing maildomain admins #572
- Add management command to print all users of the instance
Changed
- Bump keycloak to 26.5.4 #571
- Add migrations-check Makefile command
Fixed
- Preserve scroll position across renders #578
- Convert newlines to `<br>` in styled text #577
- Scope labels and user_role to the requested mailbox