mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-20 06:35:40 +02:00
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.
7.7 KiB
7.7 KiB
Permissions & Data Model
Core Data Model
Entity Relationships
User
├── MailboxAccess (role: VIEWER|EDITOR|SENDER|ADMIN)
│ └── Mailbox
│ ├── ThreadAccess (role: VIEWER|EDITOR)
│ │ └── Thread
│ │ ├── Message
│ │ │ ├── sender → Contact
│ │ │ ├── recipients → MessageRecipient → Contact
│ │ │ ├── parent → Message (reply chain)
│ │ │ ├── blob → Blob (raw MIME)
│ │ │ ├── draft_blob → Blob (JSON draft content)
│ │ │ └── attachments → Attachment → Blob (only for drafts)
│ │ ├── events → ThreadEvent (im / assign / unassign)
│ │ │ └── user_events → UserEvent (mention / assign)
│ │ ├── accesses → ThreadAccess (multiple mailboxes)
│ │ └── labels → Label (M2M)
│ ├── contacts → Contact
│ ├── labels → Label
│ └── blobs → Blob
├── user_events → UserEvent (per-user notifications, thread-scoped)
└── MailDomainAccess (role: ADMIN)
└── MailDomain
└── mailboxes → Mailbox (via domain FK)
Key Models
| Model | Purpose | Key Fields |
|---|---|---|
| User | Identity (OIDC) | sub, email, full_name |
| Mailbox | Email account | local_part, domain (FK) |
| MailboxAccess | User→Mailbox permission | user, mailbox, role (unique together) |
| Thread | Message thread | subject, denormalized flags (has_trashed, is_spam, etc.) |
| ThreadAccess | Mailbox→Thread permission | thread, mailbox, role (unique together) |
| Message | Email message | thread, sender, parent, flags (is_draft, is_trashed, etc.) |
| ThreadEvent | Timeline entry on a thread (comment, assign, unassign) | thread, type, author, data (JSON, schema-validated per type) |
| UserEvent | Per-user notification derived from a ThreadEvent | user, thread, thread_event, type, read_at |
| Contact | Email address entity | email, mailbox, name |
| Label | Folder/tag (hierarchical) | name, slug, mailbox, threads (M2M) |
Role Hierarchies
MailboxRoleChoices (User access to Mailbox)
VIEWER = 1 # Read-only: view mailbox threads/messages
EDITOR = 2 # Edit: create drafts, flag, delete, manage thread access
SENDER = 3 # Send: EDITOR + can send messages
ADMIN = 4 # Admin: SENDER + manage mailbox accesses, labels, templates, import
Role groups defined in enums.py:
MAILBOX_ROLES_CAN_EDIT = [EDITOR, SENDER, ADMIN]MAILBOX_ROLES_CAN_SEND = [SENDER, ADMIN]
ThreadAccessRoleChoices (Mailbox access to Thread)
VIEWER = 1 # Read-only: view thread messages and events
EDITOR = 2 # Edit: create replies, flag messages, manage thread sharing, assign
Role group:
THREAD_ROLES_CAN_EDIT = [EDITOR]
Event Types
# ThreadEvent.type (stored on the thread timeline)
IM = "im" # Internal comment, may embed mentions in data
ASSIGN = "assign" # User(s) newly assigned to the thread
UNASSIGN = "unassign" # User(s) removed from the thread
# UserEvent.type (per-user notification, derived from ThreadEvent)
MENTION = "mention" # One per (user, message mention); read_at tracks ack
ASSIGN = "assign" # At most one per (user, thread); source of truth for "assigned"
UserEvent is not mailbox-scoped: a user reachable through several mailboxes sees the same notification everywhere.
Permission Classes
Defined in core/api/permissions.py. The table below lists the main ones and the rule they enforce.
| Class | Rule |
|---|---|
IsAuthenticated |
Baseline — user is logged in. |
IsAllowedToAccess |
Read access to a Mailbox/Thread/Message/ThreadEvent via any MailboxAccess → ThreadAccess path. |
HasThreadEditAccess |
Full edit rights: ThreadAccess.role == EDITOR AND MailboxAccess.role ∈ MAILBOX_ROLES_CAN_EDIT on the same mailbox. |
HasThreadCommentAccess |
Allowed to author internal comments: any ThreadAccess (viewer or editor) on a mailbox where the user has MAILBOX_ROLES_CAN_EDIT. |
HasThreadEventWriteAccess |
Type-aware: im events follow the comment rule; every other ThreadEvent type requires full edit rights. Update/destroy is author-only. |
IsAllowedToCreateMessage |
User must have MAILBOX_ROLES_CAN_EDIT on the sender mailbox (plus EDITOR ThreadAccess when replying). |
IsAllowedToManageThreadAccess |
Managing a ThreadAccess requires full edit rights on the thread. |
IsMailboxAdmin / IsMailDomainAdmin |
Admin paths for mailbox and maildomain management. |
HasChannelScope |
Scope check for Channel-authenticated calls; CHANNEL_API_KEY_SCOPES_GLOBAL_ONLY further requires scope_level=global. |
Shared ORM helpers (core/models.py):
ThreadAccess.objects.editable_by(user, mailbox_id=None)— rows matching the full-edit-rights rule.ThreadAccess.objects.editor_user_ids(thread_id, user_ids=None)— user ids with full edit rights on a thread.
Key Design Principles
- Two-level permission model. User→Mailbox (
MailboxAccess) and Mailbox→Thread (ThreadAccess) are independent and composed for every access check. Edit-level actions always verify both sides. - ThreadAccess is per-mailbox. Each mailbox has its own role on a given thread, enabling selective sharing and per-mailbox
read_at/starred_atstate. - Flags are shared state. Message flags (
is_trashed,is_spam,is_unread, etc.) live on the Message and mutate the thread for everyone — they require EDITORThreadAccess. - Thread stats are denormalized. Thread has boolean fields (
has_trashed,is_spam, …) updated bythread.update_stats()after message flag changes. Mention/assignment stats (has_mention,has_unread_mention,has_assigned_to_me,has_unassigned) are not stored: they are computed per request viaExists(UserEvent...)annotations inThreadViewSet. - Comments relax the thread role. Posting or editing an
imThreadEventonly requires VIEWERThreadAccess+ mailbox edit rights. Assign/unassign and any other event type keep the stricter full-edit-rights policy. - Event mutations are author-only. Update and destroy of a
ThreadEventare refused for non-authors, regardless of role. A configurable window (settings.MAX_THREAD_EVENT_EDIT_DELAY) can close the edit/delete path entirely after creation. - Assignment is derived from the event log.
UserEvent(type=ASSIGN)is the source of truth for "who is assigned"; there is no denormalized field on Thread. A partialUniqueConstraintenforces at most one active ASSIGN per(user, thread)and absorbs races between concurrent ASSIGN requests. - Undo window for assignments. An UNASSIGN within
UNDO_WINDOW_SECONDS(120s) of the matching ASSIGN, by the same author, is absorbed: the original ASSIGNThreadEventis trimmed or deleted, theUserEvent ASSIGNis removed, and no UNASSIGN event is emitted. - Access changes cascade to assignments. Downgrading or removing a
ThreadAccess/MailboxAccesstriggerscleanup_invalid_assignments, which emits a single systemThreadEvent(type=UNASSIGN, author=None)for any assignee who lost full edit rights (re-evaluated across all their mailboxes). - Mentions survive edits idempotently. Editing an
imevent diffs the mentions payload and reconcilesUserEvent(MENTION)rows; unchanged mentions keep theirread_at, removed ones disappear from the user's "Mentioned" view, new ones are created.