Model reference
This is the per-model reference for the two MailDesk apps: Basic (maildeskmailclient) and Pro (maildeskmailclientpro). It covers the models each app defines (name) and the core Odoo models each app extends (inherit). For the synchronization data-flow that connects maildesk.messageindex → maildesk.uicache → maildesk.ingestqueue → mail.thread, see Architecture; for the outbound maildesk.updatequeue path see Realtime architecture.
This is the per-model reference for the two MailDesk apps: Basic (maildesk_mail_client) and
Pro (maildesk_mail_client_pro). It covers the models each app defines (_name) and the
core Odoo models each app extends (_inherit). For the synchronization data-flow that connects
maildesk.message_index → maildesk.ui_cache → maildesk.ingest_queue → mail.thread, see
Architecture; for the outbound maildesk.update_queue path see
Realtime architecture.
Conventions. "Providing module" is the addon whose
models/defines or extends the model. Field types are abbreviated (M2o= Many2one,M2m= Many2many,O2m= One2many,Sel= Selection). Identity column =delivery_keyshorthand for(account, provider, folder, uid).
1. Basic — maildesk_mail_client
The Basic app is the synchronization engine and the data owner. It defines the SSOT and all of the sync/state/cache infrastructure, plus a small set of extensions on native Odoo mail models.
1.1 Models defined by Basic (_name)
| Model | Purpose | Key fields | Key methods | Relations | Source file |
|---|---|---|---|---|---|
maildesk.message_index |
SSOT — authoritative metadata row for every synced message. Identity = delivery_key. _order = "sort_ts desc, id desc" |
account_id, provider (Sel imap/gmail/outlook), folder, uid, message_id, from_addr, to_addrs/cc_addrs/bcc_addrs, subject, date, preview, body_text, has_attachments, is_read, is_starred, flags, tag_ids, thread_id, sort_ts, deleted_on_server, ingest_allowed, pending_move_to, pending_delete, local_pending |
upsert_one, upsert_one_with_inserted, update_state_projection, update_tags, find_by_triplet, confirm_outbound_delivery, reconcile_thread_id_from_reply, delete_or_tombstone, store_search_body_text, init (creates trigram indexes) |
account_id→mailbox.account; tag_ids→mail.message.tag (M2m) |
models/message_index.py |
mailbox.account |
A configured Gmail/Outlook/IMAP mailbox: credentials, OAuth state, sync watermarks, ACL. _order = "sequence, name" |
name, email, password, sender_name, mail_server_id, mail_send_server_id, imap_caps (Json), gmail_last_history_id, gmail_last_sync_at, outlook_delta_link, outlook_delta_tokens (Json), outlook_graph_refresh_token/_access_token/_auth_state, backoff_until, consecutive_failures, needs_admin_attention, owner_id, company_id, is_shared, signature (Html); is_outlook/is_gmail (computed) |
button_test_incoming/button_test_outgoing, action_setup_gmail/_outlook/_imap, action_auth_outlook_graph, action_reset_mailbox_sync_state, get_account_list, get_folder_tree, get_message_with_attachments, poll_mail_accounts, refresh_imap_caps, _get_imap_client |
folder_ids→mailbox.folder (O2m); owner_id→res.users; company_id→res.company; mail_server_id/mail_send_server_id→ir.mail_server/fetchmail.server |
models/mailbox_account.py |
mailbox.folder |
Per-account folder with bootstrap/backfill progress and provider-specific cursors. _order = "account_id, sequence, name" |
name, imap_name, account_id, parent_id, is_visible, uid_validity, last_uid, last_uidnext, last_highest_modseq, needs_sync, sync_state (Sel), bootstrap_attempts, backfill_started_at/_completed_at/_completion_reason/_total_estimate/_fetched_count/_last_uid, gmail_label_id, gmail_backfill_page_token, outlook_graph_id, outlook_backfill_next_link, unread_count, folder_type (Sel); backfill_progress/backfill_status_display (computed) |
sync_folders_for_account, bootstrap_if_pending, progressive_backfill, trigger_backfill, action_reset_bootstrap, get_folder_tree, _try_acquire_folder_sync_lock (advisory lock ns 48232), _sync_outlook_graph |
account_id→mailbox.account; parent_id/child_ids self-referential |
models/mailbox_folder.py |
maildesk.ui_cache |
TTL cache for message bodies/attachment lists, guarded by PostgreSQL advisory locks (ns 48231). _order = "id desc" |
index_id, json_cache (Json), json_cache_until, cache_until |
acquire_index_lock, acquire_message_lock, acquire_cache_lock_for_index, resolve_canonical_index_id, fetch_for_index_records, upsert_list_cache, fetch_body_cache/upsert_body_cache, invalidate_message_cache, prepare_reply/prepare_forward, _cron_cleanup_expired_cache |
index_id→maildesk.message_index |
models/maildesk_ui_cache.py |
maildesk.ingest_queue |
Durable queue that materialises SSOT rows into Odoo's native mail.thread. _order = "priority asc, id asc" |
index_id, state (Sel), retry_count, next_try_at, error_message, raw_body (Binary), processed_at/processed_model/processed_res_id, priority, started_at |
cron_scan_aliases, cron_import, cron_recover_stuck, action_requeue, action_clear_raw_body |
index_id→maildesk.message_index |
models/ingest_queue.py |
maildesk.email_state |
Per-message read/star/move/delete intent, projected onto SSOT rows. _order = "write_date desc" |
account_id, folder, uid, seen/has_seen, starred/has_starred, target_folder/has_move, dirty_flags/dirty_move, is_delete/has_delete, expire_at |
record_flags, record_delete, record_move, apply_overrides_to_records, cron_gc_states, _feature_enabled |
account_id→mailbox.account |
models/maildesk_email_state.py |
maildesk.draft |
Composer drafts (incl. provider-sync state for Gmail/Outlook draft mirroring). _order = "write_date desc, id desc" |
user_id, account_id, subject, body_html (Html, sanitize=False), to_emails/cc_emails/bcc_emails, attachment_ids (M2m), tag_ids (M2m), reply_to_message_id, model/res_id, request_read_receipt/request_delivery_receipt, message_id, provider_type, provider_draft_id, provider_message_uid, provider_synced_at, provider_sync_error |
(CRUD via mailbox.sync: save_draft/update_draft/load_draft/delete_draft) |
user_id→res.users; account_id→mailbox.account; attachment_ids→ir.attachment |
models/maildesk_draft.py |
maildesk.account_lease |
Cooperative sync lock per account (TTL-based) so only one worker syncs an account at a time. _order = "lease_until desc" |
account_id, lease_until, owner |
try_acquire, renew, release, reap_expired, _compute_lease_until |
account_id→mailbox.account |
models/maildesk_account_lease.py |
maildesk.ui_presence |
Lightweight per-user "online in MailDesk" ping for presence-aware bus broadcasts. _rec_name = "user_id" |
user_id, last_ping_at |
ping, online_user_ids |
user_id→res.users |
models/maildesk_ui_presence.py |
mail.message.tag |
User-defined coloured tag applied to indexed messages | name (translate), color |
get_tag_list |
referenced by maildesk.message_index.tag_ids (M2m) |
models/mail_message_tag.py |
mailbox.sync |
RPC service façade (no persistent rows of its own) — the frontend's main backend entry point for listing, opening, flagging, moving, drafting, sending | — (transient-style service model) | message_search_load, poll_for_updates, get_message_with_attachments, fetch_thread_messages/open_thread_messages, set_flags/set_flags_bulk, move_messages_to_folder, update_tags, save_draft/update_draft/load_draft/delete_draft, send_email, delete_messages, unread_counts_for_folder, _check_account_access |
calls into mailbox.account, maildesk.message_index, maildesk.ui_cache, maildesk.email_state, maildesk.draft |
models/mailbox_sync.py |
1.2 Native Odoo models extended by Basic (_inherit)
| Model | What Basic adds | Source file |
|---|---|---|
mail.message |
account_id (M2o mailbox.account), recipient_cc_ids / recipient_bcc_ids (M2m res.partner) |
models/mail_message.py |
mail.mail |
account_id (M2o mailbox.account) so outbound mail routes through the right mailbox / SMTP server and sender_name |
models/mail_mail.py |
mail.thread |
MailDesk-specific thread hooks | models/mail_thread.py |
ir.attachment |
MailDesk attachment handling | models/ir_attachment.py |
ir.websocket |
Account-scoped bus channel subscription (in-app freshness) | models/ir_websocket.py |
ir.mail_server / fetchmail.server |
MailDesk SMTP/IMAP server linkage | models/ir_mail_server.py, models/fetchmail_server.py |
google.gmail.mixin / microsoft.outlook.mixin |
OAuth2 token handling for Gmail / Graph | models/google_gmail_mixin.py, models/microsoft_outlook_mixin.py |
res.company |
Per-company OAuth credentials: google_gmail_client_identifier/_secret, microsoft_outlook_client_identifier/_secret (with legacy global-param fallback) |
models/res_company.py |
res.partner |
trusted_partner, trusted_by_user_id, maildesk_email_count (computed); methods trust_sender, email_history, get_partners_with_email_activity, search_unique_partners_by_email |
models/res_partner.py |
res.config.settings |
MailDesk config-parameter surface | models/res_config_settings.py |
License guard (cross-cutting).
models/mail_thread_ext.pyinjects an access hook on the module's concrete models so that ORM access is gated by the Metzler IT licensing layer. The guard is inert in automated-test / CI contexts. Developers should be aware the hook exists when extending models.
2. Pro — maildesk_mail_client_pro
Pro extends Basic with bidirectional (outbound) mutations, the asynchronous AI subsystem, attachment extraction, signatures, and the email→record link contract. Pro requires Basic.
2.1 Models defined by Pro (_name)
Two-way sync & linking
| Model | Purpose | Key fields | Key methods | Relations | Source file |
|---|---|---|---|---|---|
maildesk.update_queue |
Outbound mutation queue. One coalesced row per delivery for flag/label/move/soft-delete; lease-locked, backoff, max_attempts default 8. _order = "priority asc, id asc" |
account_id, provider (Sel imap/gmail/outlook), folder, uid, email_state_id, state (Sel pending/in_progress/done/failed/skipped), attempt_count, max_attempts (default 8), next_try_at, last_error, priority, locked_by/locked_until, started_at/finished_at |
ensure_queued (coalescing upsert), cron_process (batch 80, 15 s budget), cron_cleanup_update_queue |
account_id→mailbox.account; email_state_id→maildesk.email_state; consumed by per-provider */mutation_executor.py |
models/maildesk_update_queue.py |
maildesk.email.link |
The shared contract linking an external email (SSOT row) to an Odoo record. Bridges (CRM/Helpdesk/Sale/Documents/Calendar) declare link_type semantics on this one field. _order = "create_date desc" |
message_index_id, model, res_id, link_type (Sel — base only generic; bridges extend), display_name (computed/stored), user_id |
copy_links_to_message, _maildesk_target_exists, _maildesk_sync_integration_pointers, _compute_display_name |
message_index_id→maildesk.message_index; model/res_id are a soft (non-FK) reference to any Odoo record |
models/maildesk_email_link.py |
mailbox.signature |
Reusable email signatures, personal or shared per mailbox, with default selection. _order = "is_shared desc, is_default desc, sequence, name, id" |
name (translate), account_id, user_id, is_shared, is_default, sender_name, body_html (Html); preview_text (computed) |
get_signature_picker_data, resolve_signature, _maildesk_validate_scope/_validate_user_access, _maildesk_sync_default_flags |
account_id→mailbox.account; user_id→res.users |
models/mailbox_signature.py |
AI subsystem
API keys are never stored on these rows — they live only in ir.config_parameter
(maildesk.ai.<provider>_key). Insight models are explicitly "derived-only": they store summaries
and structured facts, never raw email bodies. See AI architecture and the
single authorization chokepoint application/services/ai_guard.py.
| Model | Purpose | Key fields | Key methods | Relations | Source file |
|---|---|---|---|---|---|
maildesk.ai.provider |
A configured AI backend (the row is metadata only — the key lives in config params). _order = "sequence, id" |
name, provider (Sel: openai/gemini/anthropic/grok/deepseek/custom), model, base_url, is_active, api_key_set (computed), connectivity_status (Sel) |
action_set_api_key, action_test_connectivity, action_activate, get_active_status, get_task_status, _check_custom_base_url, _sync_active_provider |
per-task routing via config param maildesk.ai.task.<task>.provider |
models/maildesk_ai_provider.py |
maildesk.ai.job |
Async enrichment job (the work queue that produces insights). _order = "priority asc, id asc" |
account_id, subject_type (Sel message/thread/attachment/search/partner), index_id, thread_key, attachment_id, search_token, task_key (Sel: message_brief/priority_score/thread_digest/reply_suggest/record_action_suggest/attachment_extract/attachment_qa/search_rerank/partner_digest), provider_key, model_name, fingerprint, dedup_key, payload_json/result_json, state (Sel), attempt_count/max_attempts (8), prompt_tokens/completion_tokens |
schedule (dedup-aware), cron_ai_jobs, cron_cleanup_ai_jobs, _compute_dedup_key, _insight_is_fresh |
account_id→mailbox.account; index_id→maildesk.message_index |
models/maildesk_ai_job.py |
maildesk.ai.message.insight |
Per-message derived insight (priority, action items, suggested tags/route/record actions). _rec_name = "summary_short" |
index_id, account_id, summary_short, priority_level (Sel)/priority_reason, action_items_json, suggested_tags_json, suggested_route_json, suggested_record_actions_json, reply_intent, detected_language, confidence, insight_hash, provider_key/model_name, token counts, refreshed_at |
upsert, fetch_fresh, is_stale, fetch_for_index_ids, to_ui_dto |
index_id→maildesk.message_index; account_id→mailbox.account |
models/maildesk_ai_message_insight.py |
maildesk.ai.thread.insight |
Per-thread derived insight (long summary, delta, open questions, commitments, next actions) | account_id, thread_key, summary_long, delta_since_last_user_reply, open_questions_json, commitments_json, next_actions_json, participants_json, suggested_record_actions_json, latest_processed_sort_ts, message_count, latest_index_id, insight_hash, token counts, refreshed_at |
upsert, fetch_fresh, fetch_any, is_stale, to_ui_dto |
account_id→mailbox.account |
models/maildesk_ai_thread_insight.py |
maildesk.ai.conversation |
"Ask AI" panel conversation, mailbox-shared (a conversation with no mailbox stays private to its creator). _order = "last_used_at desc, id desc" |
user_id, account_id, thread_key, title, last_used_at |
get_or_create, touch, cron_cleanup_conversations |
account_id→mailbox.account; message_ids→maildesk.ai.conversation.message (O2m) |
models/maildesk_ai_conversation.py |
maildesk.ai.conversation.message |
One turn of an Ask-AI conversation (author-stamped). _order = "id asc" |
conversation_id, role (Sel user/assistant), author_id, content_text, citations_json, token counts |
append, to_ui_dto |
conversation_id→maildesk.ai.conversation; author_id→res.users |
models/maildesk_ai_conversation_message.py |
maildesk.ai.attachment.extract |
Opt-in attachment extraction result (text excerpt + structured facts, keyed by checksum). _order = "id desc" |
index_id, account_id, attachment_ref, checksum, mime_type, file_size, status (Sel), extract_excerpt, structured_facts_json, citations_json, extract_hash, provider_key/model_name, refreshed_at |
upsert, fetch, to_ui_dto |
index_id→maildesk.message_index; account_id→mailbox.account |
models/maildesk_ai_attachment_extract.py |
Integration & handler models
| Model | Purpose | Source file |
|---|---|---|
maildesk.chatter |
Lazy "discussable" creation + write-back that hydrates MailDesk threads into a record's Chatter | models/maildesk_chatter.py |
maildesk.post.send.handler.composer_link |
Post-send handler that re-links a composed reply to the originating Odoo record | models/post_send_handler.py |
2.2 Native / Basic models extended by Pro (_inherit)
| Model | What Pro adds | Source file |
|---|---|---|
mailbox.account |
Per-mailbox AI gate + tuning: allow_ai (Boolean, default True), allow_ai_attachments (Boolean, default False), ai_team_context (Text), ai_reply_tone (Sel: professional/formal/friendly/technical/concise, default professional), ai_brief_activated_at (brief watermark), maildesk_inbox_realtime_sync_at (near-realtime Inbox watermark). Note: there is no ai_enabled field — the per-mailbox gate is allow_ai. |
models/mailbox_account.py |
maildesk.message_index |
Pro outbound/linking extensions on the SSOT | models/maildesk_message_index.py |
maildesk.ui_cache |
Pro cache extensions | models/maildesk_ui_cache.py |
mailbox.folder / mailbox.sync |
Pro outbound-mutation + AI hooks | models/mailbox_folder.py, models/mailbox_sync.py |
maildesk.bus_orchestrator |
Pro bus orchestration extensions | models/maildesk_bus_orchestrator.py |
mail.thread / mail.template / ir.model |
Pro integration hooks | models/mail_thread.py, models/mail_template.py, models/ir_model.py |
res.config.settings |
AI provider + per-task routing settings surface | models/res_config_settings.py |
access_user_ids. Per-mailbox membership (access_user_idsonmailbox.account) is the field every record rule keys on for shared-inbox isolation. The field exists on the Basicmailbox.accountmodel (used directly by the Basic record rules); Pro relies on it heavily for AI and outbound scoping. See Security.4.2.0 (branch-only, not merged). The realtime/prefetch work adds
maildesk.message_prefetch_queueandmaildesk.push_subscriptionon branchesMailDesk-889-realtime-sync-v18/MailDesk-prefetch-cache-v18(MRs !237 Basic, !124 Pro). Those models are not in shipped 4.1.x and are documented in Realtime architecture §2, not above.