Security model
MailDesk's access control rests on two groups, a set of record rules that all key on one field — mailbox.account.accessuserids — and the per-model ir.model.access.csv ACLs. Pro layers the AI subsystem on top with its own record rules plus a runtime AI gate. This page is the developer reference for what is enforced where.
MailDesk's access control rests on two groups, a set of record rules that all key on one
field — mailbox.account.access_user_ids — and the per-model ir.model.access.csv ACLs. Pro layers
the AI subsystem on top with its own record rules plus a runtime AI gate. This page is the developer
reference for what is enforced where.
The provider credentials (
password,outlook_graph_refresh_token, etc.) live onmailbox.accountrows, which are themselves gated by the rules below; AI API keys live only inir.config_parameterand never in a model table. See Models and AI architecture.
1. Groups
Defined in Basic security/groups.xml (under category MailDesk):
| XML ID | Display | Implies | Role |
|---|---|---|---|
maildesk_mail_client.group_mailbox_user |
Mailbox User | — | End user: sees only the mailboxes they are a member of (access_user_ids), manages own tags/drafts/signatures |
maildesk_mail_client.group_mailbox_admin |
Mailbox Admin | group_mailbox_user (via implied_ids) |
Full access to all mailboxes and all MailDesk records |
There is no dedicated AI group. AI access is gated by per-mailbox allow_ai plus global/per-feature
config switches (see §5), not by a res.groups. The Cockpit bridge adds its own 4-tier manager
hierarchy on top, scoped to that addon (maildesk_mail_client_cockpit/security/) and out of scope here.
2. The isolation key: access_user_ids
Shared-inbox isolation is enforced by a single field, mailbox.account.access_user_ids (M2m to
res.users). Every "user-strict" record rule derives its domain from membership in that field — either
directly on the account or by walking a relation back to it. A user who is not in an account's
access_user_ids cannot read its folders, messages, cache, state, drafts, AI jobs, or insights. Admins
bypass all of this via [(1, '=', 1)] rules.
# Basic: rule_mailbox_account_user_strict
domain_force = [('access_user_ids', 'in', user.id)] # the account itself
# Basic: rule_maildesk_message_index_user_strict
domain_force = [('account_id.access_user_ids', 'in', user.id)] # walk to the account
3. Record rules
3.1 Basic — security/rules.xml (<odoo noupdate="1">)
Each model gets a paired user-strict rule (membership domain) and an admin-all rule
([(1,'=',1)]).
| Model | User-strict domain | Notes |
|---|---|---|
mailbox.account |
[('access_user_ids', 'in', user.id)] |
User rule grants read + write only (perm_create=False, perm_unlink=False) — users cannot create or delete mailboxes |
mailbox.folder |
[('account_id.access_user_ids', 'in', user.id)] |
|
maildesk.message_index |
[('account_id.access_user_ids', 'in', user.id)] |
SSOT isolation |
maildesk.ui_cache |
[('index_id.account_id.access_user_ids', 'in', user.id)] |
walks index → account |
maildesk.email_state |
[('account_id.access_user_ids', 'in', user.id)] |
|
maildesk.ingest_queue |
[('index_id.account_id.access_user_ids', 'in', user.id)] |
walks index → account |
maildesk.draft |
[('account_id.access_user_ids', 'in', user.id)] |
3.2 Pro AI — security/maildesk_ai_rules.xml
Same paired user-strict / admin-all pattern, scoped through account_id (or, for conversations,
through the conversation's account):
| Model | User-strict domain |
|---|---|
maildesk.ai.job |
[('account_id.access_user_ids', 'in', user.id)] |
maildesk.ai.message.insight |
[('account_id.access_user_ids', 'in', user.id)] |
maildesk.ai.thread.insight |
[('account_id.access_user_ids', 'in', user.id)] |
maildesk.ai.attachment.extract |
[('account_id.access_user_ids', 'in', user.id)] |
maildesk.ai.conversation |
['|', ('account_id.access_user_ids', 'in', user.id), '&', ('account_id', '=', False), ('user_id', '=', user.id)] — mailbox-shared, but a conversation with no mailbox stays private to its creator |
maildesk.ai.conversation.message |
scoped through conversation_id.account_id with the same private-fallback branch |
⚠️ Observed fact —
maildesk_ai_rules.xmllacksnoupdate="1". Its root element is plain<odoo>, unlike Basicrules.xml, Basicgroups.xml, and Promailbox_signature_rules.xml, which all use<odoo noupdate="1">. Consequence: these six AI record-rule records are re-applied on every module upgrade, so any manual edit to those rules in a running database is reverted on the next-u maildesk_mail_client_pro. This is documented as the current state of the source, not a recommendation. (Verified:maildesk_ai_rules.xml:19=<odoo>;mailbox_signature_rules.xml:19=<odoo noupdate="1">.)
3.3 Pro signatures — security/mailbox_signature_rules.xml (<odoo noupdate="1">)
mailbox.signature splits read from write so users can see shared signatures but only manage their
own:
| Rule | Perms | Domain |
|---|---|---|
rule_mailbox_signature_user_read |
read only | own (user_id = user.id) or shared signatures of mailboxes the user can access (is_shared = True and account_id.access_user_ids in user.id or account_id.owner_id = user.id) |
rule_mailbox_signature_user_write_own |
write/create/unlink | [('user_id', '=', user.id)] — only own signatures |
rule_mailbox_signature_admin_all |
all | [(1, '=', 1)] |
4. Model access (ir.model.access.csv)
4.1 Basic
| Model | User (r/w/c/u) | Admin (r/w/c/u) | Notes |
|---|---|---|---|
mailbox.account |
1/0/0/0 | 1/1/1/1 | users read-only at ACL level; the user-strict rule additionally allows write |
mailbox.folder |
1/0/0/0 | 1/1/1/1 | |
mail.message.tag |
1/1/1/0 | 1/1/1/1 | users create/edit tags, cannot delete |
mailbox.account.wizard |
1/1/1/1 | 1/1/1/1 | transient setup wizard |
maildesk.email_state |
1/1/1/1 | 1/1/1/1 | user-mutable (read/star/move/delete intent) |
maildesk.account_lease |
— | 1/1/1/1 | admin-only (no user line) |
maildesk.message_index |
1/0/0/0 | 1/1/1/1 | SSOT is read-only to users; writes happen via sudo service code |
maildesk.ingest_queue |
1/0/0/0 | 1/1/1/1 | |
maildesk.ui_cache |
1/0/0/0 | 1/1/1/1 | |
maildesk.ui_presence |
1/1/1/0 | 1/1/1/1 | users ping presence |
maildesk.draft |
1/1/1/1 | 1/1/1/1 | |
mailbox.sync |
1/1/1/1 | (inherits user line) | RPC service façade |
4.2 Pro
| Model | User (r/w/c/u) | Admin (r/w/c/u) | Notes |
|---|---|---|---|
mailbox.signature.wizard |
1/0/0/0 | 1/1/1/1 | |
mailbox.signature |
1/1/1/1 | 1/1/1/1 | rule-restricted to own/shared (§3.3) |
maildesk.email.link |
1/0/0/0 | 1/1/1/1 | links created by service code |
maildesk.update_queue |
0/0/1/0 | 1/1/1/0 | users may only create an outbound mutation (enqueue), never read/edit it; no unlink even for admin |
maildesk.ai.provider |
1/0/0/0 | 1/1/1/1 | provider config is admin-managed |
maildesk.ai.set.key.wizard |
0/0/0/0 | 1/1/1/1 | admin-only — key entry is locked out for plain users |
maildesk.account.remove.wizard |
0/0/0/0 | 1/1/1/1 | admin-only mailbox removal |
maildesk.ai.job |
1/0/0/0 | 1/1/1/1 | scheduled/written by service code |
maildesk.ai.message.insight |
1/0/0/0 | 1/1/1/1 | derived-only, read to users |
maildesk.ai.thread.insight |
1/0/0/0 | 1/1/1/1 | derived-only |
maildesk.ai.conversation |
1/0/0/0 | 1/1/1/1 | rule-scoped (§3.2) |
maildesk.ai.conversation.message |
1/0/0/0 | 1/1/1/1 | |
maildesk.ai.attachment.extract |
1/0/0/0 | 1/1/1/1 | derived-only |
5. Per-mailbox AI gate and the runtime chokepoint
Above the record rules, all AI runs through one authorization function:
application/services/ai_guard.py → assert_ai_allowed(). It checks, in order:
- Global kill switch —
ir.config_parametermaildesk.ai.enabled(default on). Off → no AI in the database. (is_ai_enabled_global.) - Per-feature switch —
maildesk.ai.feature.<feature>.enabled(default on). (is_feature_enabled.) - Per-mailbox
allow_ai— when anaccount_idis supplied, the booleanmailbox.account.allow_ai(defaultTrue) is enforced.allow_ai = FalseraisesAIDisabledError("AI is disabled for this mailbox."). This is backend-enforced inai_guard.py(is_mailbox_ai_allowed→bool(account.allow_ai)), not just a UI hint. - Provider availability — if no provider is configured, raises
AIDisabledError("No AI provider is configured.").
Attachment content is additionally gated by the opt-in mailbox.account.allow_ai_attachments
(default False) — binary attachments are only handed to a provider with that flag on and an
explicit user action. AI keys themselves are admin-only at both the ACL level (maildesk.ai.set.key.wizard
is 0/0/0/0 for users) and storage level (ir.config_parameter, never on a row).
6. Practical isolation summary
- A Mailbox User sees and acts on only the mailboxes listed in their
access_user_idsmembership — for messages, bodies, state, drafts, AI jobs, insights, and conversations alike. - A Mailbox User cannot create or delete a
mailbox.account(ACL1/0/0/0+ create/unlink off in the rule), enter AI keys, or remove a mailbox — those are admin wizards. - The SSOT (
maildesk.message_index) and the queues/cache are read-only to users; all writes go throughsudo-running service code inmailbox.syncand the cron jobs. - The outbound
maildesk.update_queueis write-once for users (create only) — they enqueue an action, the cron applies it. - AI is off-by-database with one switch (
maildesk.ai.enabled), off-per-mailbox withallow_ai, and off-per-feature withmaildesk.ai.feature.<feature>.enabled; all three are enforced server-side.