MailDesk docs
Get MailDesk
Technical

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.

5 min read

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 on mailbox.account rows, which are themselves gated by the rules below; AI API keys live only in ir.config_parameter and 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.xml lacks noupdate="1". Its root element is plain <odoo>, unlike Basic rules.xml, Basic groups.xml, and Pro mailbox_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:

  1. Global kill switchir.config_parameter maildesk.ai.enabled (default on). Off → no AI in the database. (is_ai_enabled_global.)
  2. Per-feature switchmaildesk.ai.feature.<feature>.enabled (default on). (is_feature_enabled.)
  3. Per-mailbox allow_ai — when an account_id is supplied, the boolean mailbox.account.allow_ai (default True) is enforced. allow_ai = False raises AIDisabledError("AI is disabled for this mailbox."). This is backend-enforced in ai_guard.py (is_mailbox_ai_allowedbool(account.allow_ai)), not just a UI hint.
  4. 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_ids membership — for messages, bodies, state, drafts, AI jobs, insights, and conversations alike.
  • A Mailbox User cannot create or delete a mailbox.account (ACL 1/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 through sudo-running service code in mailbox.sync and the cron jobs.
  • The outbound maildesk.update_queue is 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 with allow_ai, and off-per-feature with maildesk.ai.feature.<feature>.enabled; all three are enforced server-side.