The mail module in src/cluster_erp/modules/mail/ lets a user connect an external mail account and read and write mail inside the product. It keeps the connected accounts, a local cache of messages and attachments, the mail domains an administrator defines, and the OAuth applications for providers that sign in through OAuth. It also answers SendTransactionalEmail, the contract every other module uses to send a system e-mail. This page describes transport and sign-in at mechanism level only.
At a glance¶
| Item | Value | Source |
|---|---|---|
| Package | src/cluster_erp/modules/mail/ |
folder |
| Module class | Module |
src/cluster_erp/modules/mail/module.py:18 |
name / version |
mail / 0.1.0 |
src/cluster_erp/modules/mail/module.py:19-25 |
| API prefix | /api/v1/mail (ModuleBase.api_prefix), with the app gate require_app("mail") |
src/cluster_erp/core/module_base.py:37-38, src/cluster_erp/modules/mail/module.py:34-45 |
| Sub-modules | 4: accounts, domains, messages, providers; plus transport.py, oauth.py, imap_verify.py, crypto.py, privacy_handlers.py |
folder listing |
| Tables | 5: mail_domains, mail_provider_configs, mail_accounts, mail_messages, mail_attachments |
__tablename__ in the four models.py files |
| Entities in metadata | 3 keys in entities |
src/cluster_erp/modules/mail/module.py:143-147 |
| Routes | 28 route decorators in the package | grep for @...router.get/post/put/patch/delete |
| Actions | 0 ActionMeta entries |
grep "ActionMeta(" in the package |
| Migrations | 4 files | files in migrations/versions/ whose text contains one of the five table names |
| Contracts | consumes 3, produces 0 | see the section "Events and queries" |
| Web app presence | System catalogue app mail with its own screen; mail settings under Settings |
src/cluster_erp/modules/app_store/catalog.json (entry mail), webapp/js/mail.js, webapp/js/mail-settings.js |
| Desktop presence | generic widgets only | src/cluster_erp/desktop/modules/ has no mail folder |
What the module does¶
- Connects a mail account of kind
google,microsoft, orimapfor the signed-in user. Google and Microsoft accounts connect through an OAuth authorization-code flow; IMAP accounts connect with an address and an app password, verified by a real login before the account is saved (src/cluster_erp/modules/mail/oauth.py:1-15,src/cluster_erp/modules/mail/imap_verify.py:1-8). - Syncs the inbox of an account into a local cache and keeps attachments in storage (
src/cluster_erp/modules/mail/messages/sync.py:1-3,src/cluster_erp/modules/mail/messages/sync.py:126-203). - Sends mail, saves drafts with attachments, marks messages read, moves them to trash, and shows threads (
src/cluster_erp/modules/mail/messages/routes.py:43-252). - Lets an administrator define mail domains with server coordinates and register OAuth applications per provider (
src/cluster_erp/modules/mail/domains/models.py:9-30,src/cluster_erp/modules/mail/providers/models.py:9-35). - Sends system e-mail for other modules through the platform SMTP settings (
src/cluster_erp/modules/mail/module.py:72-103).
Sub-modules¶
| Sub-module | Purpose | Main entities | Router | Source |
|---|---|---|---|---|
accounts |
Connected accounts, OAuth start and callback, IMAP connect | MailAccount |
/accounts |
src/cluster_erp/modules/mail/accounts/routes.py:30 |
domains |
Administrator-defined mail domains | MailDomain |
/domains |
src/cluster_erp/modules/mail/domains/routes.py:21 |
messages |
Cached messages, drafts, attachments, sync, send | MailMessage, MailAttachment |
/messages |
src/cluster_erp/modules/mail/messages/routes.py:33 |
providers |
OAuth applications per provider | MailProviderConfig |
/oauth-apps |
src/cluster_erp/modules/mail/providers/routes.py:27 |
Data model¶
| Entity | Table | Key fields | Notes | Source |
|---|---|---|---|---|
MailDomain |
mail_domains |
name, kind, imap_host, imap_port, smtp_host, smtp_port, use_tls, is_active |
kind is google, microsoft, or imap |
src/cluster_erp/modules/mail/domains/models.py:9-30, src/cluster_erp/modules/mail/domains/schemas.py:11 |
MailProviderConfig |
mail_provider_configs |
provider, client_id, client_secret_enc, tenant, enabled |
one row per provider; the secret is stored encrypted and never returned | src/cluster_erp/modules/mail/providers/models.py:9-35 |
MailAccount |
mail_accounts |
user_id, provider, email_address, display_name, domain_id, server coordinates, encrypted token and password columns, status, status_detail, last_sync_at |
one user may connect several accounts | src/cluster_erp/modules/mail/accounts/models.py:13-55 |
MailMessage |
mail_messages |
account_id, folder, provider_message_id, thread_id, sender, recipients, subject, snippet, body_text, body_html, sent_at, is_read, has_attachments, threading headers |
local mirror keyed by account and provider message id | src/cluster_erp/modules/mail/messages/models.py:13-57 |
MailAttachment |
mail_attachments |
message_id, filename, mime_type, size_bytes, provider_ref, storage_key, is_inline, content_id |
metadata; bytes are fetched from the provider or held in storage | src/cluster_erp/modules/mail/messages/models.py:60-81 |
Provider secrets, OAuth tokens, and IMAP app passwords are encrypted at rest with a reversible cipher keyed from a server setting, and are decrypted only to talk to the provider (src/cluster_erp/modules/mail/crypto.py:1-49, src/cluster_erp/modules/mail/transport.py:1-12). The settings are listed on settings reference; secret storage is described on second factor and secret storage.
Account, provider, and message path¶
flowchart TD A["admin: MailProviderConfig per provider"] --> B["POST /accounts/oauth/provider/start"] D["admin: MailDomain with server coordinates"] --> I["POST /accounts/imap"] B --> C["provider consent, POST /accounts/oauth/callback"] C --> ACC["MailAccount: google or microsoft"] I --> V["verify_imap_login"] V --> ACC2["MailAccount: imap"] ACC --> S["MailSyncService.sync_account"] ACC2 --> S S --> T["transport.fetch_inbox"] T --> M["MailMessage and MailAttachment cache"] M --> SEND["MailSyncService.send via transport.send_message"]
In prose:
- An administrator registers the OAuth application of a provider under
/oauth-appsand may define mail domains under/domains(src/cluster_erp/modules/mail/providers/routes.py:27-66,src/cluster_erp/modules/mail/domains/routes.py:21-86). - For Google or Microsoft, the user starts the flow, consents at the provider, and returns to the callback. A signed, short-lived state value binds the flow to the user and the provider. The product receives tokens and never the user's provider password (
src/cluster_erp/modules/mail/oauth.py:1-15,src/cluster_erp/modules/mail/accounts/routes.py:66-164). - For IMAP, the user sends the address and an app password; the module performs a real login before saving (
src/cluster_erp/modules/mail/accounts/routes.py:166-183,src/cluster_erp/modules/mail/accounts/services.py:83-160). POST /messages/syncfetches the inbox through the provider transport and upserts it into the cache. A transport error sets the account status toerrorwith the detail; success setsconnectedandlast_sync_at(src/cluster_erp/modules/mail/messages/sync.py:126-203).- Expired access tokens are refreshed with the stored refresh token and written back encrypted (
src/cluster_erp/modules/mail/messages/sync.py:83-124). - Sending builds a message and hands it to the provider transport: IMAP accounts send through SMTP, Google and Microsoft through their mail APIs (
src/cluster_erp/modules/mail/transport.py:1-12,src/cluster_erp/modules/mail/messages/sync.py:205-286).
Screens and metadata¶
get_metadata declares a sidebar group Mail with one settings section, Mail Settings, whose only tab is mail_domain (src/cluster_erp/modules/mail/module.py:119-142). The three entities are mail_domain, mail_account, and mail_message (src/cluster_erp/modules/mail/module.py:143-147). The mail app screen is a custom screen of the web app; see mail and profile screens.
Status and actions¶
The module declares no ActionMeta. An account carries status (default connected) and status_detail (src/cluster_erp/modules/mail/accounts/models.py:48-49). A message lives in a folder, default inbox; drafts use drafts and trash uses trash (src/cluster_erp/modules/mail/messages/models.py:22, src/cluster_erp/modules/mail/messages/sync.py:331-348, src/cluster_erp/modules/mail/messages/sync.py:470-488).
Business rules¶
- Every message and account read is scoped to the signed-in user (
src/cluster_erp/modules/mail/messages/sync.py:52-81,src/cluster_erp/modules/mail/accounts/services.py:169-184). - Discarding a draft sets
deleted_at,deleted_by, and the reasondraft_discardedon the draft and its attachments (src/cluster_erp/modules/mail/messages/sync.py:331-347). - Trash moves a message to the provider's trash and to the local
trashfolder (src/cluster_erp/modules/mail/messages/sync.py:470-488). - Disconnecting an account, deleting a domain, or deleting an OAuth application sets
deleted_at(src/cluster_erp/modules/mail/accounts/services.py:185-191,src/cluster_erp/modules/mail/domains/services.py:55-61,src/cluster_erp/modules/mail/providers/services.py:79-87). - The system mailbox reports
sent=Falsewith the reasonnot_configuredwhen no SMTP is configured andno_recipientwithout a recipient.sent=Truemeans that the SMTP server accepted the message, not that it was delivered (src/cluster_erp/modules/mail/module.py:72-103).
Events and queries¶
The module consumes 3 contracts and produces none, counted as on the fleet module page. The full list is on the contracts reference.
| Direction | Contract class | Handler or emitter | Source |
|---|---|---|---|
| subscribes | SendTransactionalEmail |
_handle_transactional_email |
src/cluster_erp/modules/mail/module.py:47-53, src/cluster_erp/modules/mail/module.py:72-103 |
| subscribes | CollectSubjectData, AnonymiseSubject |
privacy_handlers.py |
src/cluster_erp/modules/mail/module.py:55-70 |
The intranet notification fan-out is one caller of SendTransactionalEmail (src/cluster_erp/modules/intranet/notify.py:91-99). The platform side of system mail is on PDF and system mail.
Permissions¶
| Permission key | Used by | Source |
|---|---|---|
mail_account.read, .connect, .delete |
accounts | src/cluster_erp/modules/mail/accounts/routes.py:33-200 |
mail_domain.read, .create, .update, .delete |
domains | src/cluster_erp/modules/mail/domains/routes.py:24-81 |
mail_provider.read, .update, .delete |
OAuth applications | src/cluster_erp/modules/mail/providers/routes.py:30-60 |
mail_message.read, .create, .update, .delete, .send, .sync |
messages | src/cluster_erp/modules/mail/messages/routes.py:43-246 |
Privacy, retention, and history¶
privacy_handlers.py counts the messages that carry a person's addresses. Anonymisation is an operator decision (include_correspondence); when included, the person's name and address are replaced in sender and recipient fields and inside subject, snippet, and bodies (src/cluster_erp/modules/mail/privacy_handlers.py:1-11). See privacy and data-subject requests. The module has no retention_handlers.py. Changes are captured by change history.
Analytics facts¶
This module has no analytics_facts.py.
Settings and reference data¶
Mail domains and OAuth applications are the module's reference data, kept by administrators (src/cluster_erp/modules/mail/domains/routes.py:21-86, src/cluster_erp/modules/mail/providers/routes.py:27-66). Secret values are never shown: GET /oauth-apps/ returns whether a secret is set (has_secret), not the secret (src/cluster_erp/modules/mail/providers/models.py:9-16, src/cluster_erp/modules/mail/providers/schemas.py:6-15).
Tests¶
Found by name (web-client/_e2e_mail*.mjs) and by a grep for modules.mail in tests/.
| Kind | File | Covers |
|---|---|---|
| e2e | web-client/_e2e_mail.mjs, web-client/_e2e_mailapp.mjs |
mail settings and the mail app |
| e2e | web-client/_e2e_mail_drafts.mjs |
drafts and attachments |
| pytest | tests/test_mail_transport.py |
provider transport parsing and building |
| pytest | tests/test_system_mail.py |
the system mailbox |
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| mail account | MailAccount |
An external mailbox a user connected to the product. | Mail hesabı | src/cluster_erp/modules/mail/accounts/models.py:13 |
| mail domain | MailDomain |
An administrator-defined domain with its provider kind and server coordinates. | Mail alan adı | src/cluster_erp/modules/mail/domains/models.py:9 |
| system mailbox | SendTransactionalEmail |
The single SMTP identity that the application uses for its own mail, which other modules reach through SendTransactionalEmail. |
sistem posta kutusu | src/cluster_erp/modules/mail/module.py:47-53 |
Related pages¶
- Modules index
- Mail and profile screens
- PDF and system mail
- Intranet notifications and delivery
- API reference for this module
Verified against commit 92f8051 on 2026-11-13.