mailaas: SMTP Relay PaaS Plugin for MetaPaaS
exim4 SMTP submission server packaged as a MetaPaaS plugin,
following the same pattern as metapaas_s3.
Architecture
metapaas-cp
└── mail plugin (exordos_paas_mail)
├── CP: MailInstance + MailAccount models, REST API, migrations
├── infra_builder: CoreInfraBuilder → NodeSet + Config on core
└── paas_builder: MailInstanceBuilder → MailInstanceNode → DP agent
mailaas-dp-<uuid> (VM)
└── exim4 (SMTP 25/465/587 — STARTTLS + auth)
├── /etc/exordos_metapaas/mail.env ← delivered by CP (MAIL_DOMAIN)
├── /etc/exim4/passwd ← managed by DP agent (lsearch auth)
└── exordos-universal-agent ← MailCapabilityDriver
Quick Start
Build
make build
Produces the DP image, the element manifest and the exordos_mail wheel: the
manifest points at that exact wheel as a build artifact, so nothing has to
resolve a version off a pip index.
Install on running metapaas
exordos e e install mailaas # add -v <version> to pin a build
PluginReconciler on metapaas-cp installs the exordos_mail wheel and
activates the /v1/types/mail/ route.
Create Instance
curl -X POST http://metapaas-cp:8080/v1/types/mail/instances/ \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d '{
"name": "mail-prod",
"project_id": "4d657461-0000-0000-0000-000000000002",
"domain": "example.com",
"cpu": 2,
"ram": 2048,
"disk_size": 20,
"version": "/v1/types/mail/versions/<version-uuid>"
}'
Create SMTP Account
# Generate SHA512-crypt hash (compatible with exim4 crypteq)
HASH=$(openssl passwd -6 "mypassword")
curl -X POST http://metapaas-cp:8080/v1/types/mail/instances/<uuid>/accounts/ \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <token>' \
-d "{
\"username\": \"alice\",
\"password_hash\": \"${HASH}\",
\"project_id\": \"4d657461-0000-0000-0000-000000000002\",
\"instance\": \"/v1/types/mail/instances/<uuid>\"
}"
Within seconds the DP agent writes the account to /etc/exim4/passwd and
reloads exim4. The user can then authenticate via SMTP AUTH (PLAIN/LOGIN over
STARTTLS on port 587 or SMTPS on port 465).
Hashes stored with a Dovecot {SHA512-CRYPT} scheme prefix are accepted — the
driver strips the prefix before writing to exim4's passwd file.
Configure DNS
For mail to be accepted by other servers you must publish a few DNS records for
your domain (referred to below as $1, e.g. example.com).
-
DKIM — the key is generated on the node by the configure script. Once the instance is
ACTIVE, read it from the API:curl -s http://metapaas-cp:8080/v1/types/mail/instances/<uuid> \ -H 'Authorization: Bearer <token>' | jq -r '{dkim_selector, dkim_public_key}'
Publish it as a
TXTrecord at<dkim_selector>._domainkey.$1(default selector:platform), with thedkim_public_keyvalue as data. The raw record is also on the node at/etc/exim4/dkim/platform.txt. -
SPF — name:
@(the domain itself), type:TXT, TTL: 3600, data:"v=spf1 ip4:YOUR_SERVER_IP/32 a mx ~all" -
DMARC — name:
_dmarc, type:TXT, TTL: 3600, data:"v=DMARC1; p=none; pct=100; adkim=s; aspf=s" -
PTR — set the reverse record for your server IP to
$1 -
MX — ensure an
MXrecord exists (e.g. name:@, type:MX, data:$1)
API Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /v1/types/mail/instances/ |
Create mail server |
| GET | /v1/types/mail/instances/ |
List instances |
| GET | /v1/types/mail/instances/<uuid> |
Get instance |
| PATCH | /v1/types/mail/instances/<uuid> |
Update (cpu/ram/disk_size) |
| DELETE | /v1/types/mail/instances/<uuid> |
Delete |
| GET | /v1/types/mail/versions/ |
List DP image versions |
| POST | /v1/types/mail/instances/<uuid>/accounts/ |
Create account |
| GET | /v1/types/mail/instances/<uuid>/accounts/ |
List accounts |
| PATCH | /v1/types/mail/instances/<uuid>/accounts/<uuid> |
Update (active, password_hash) |
| DELETE | /v1/types/mail/instances/<uuid>/accounts/<uuid> |
Delete account |
Field permissions
| Field | Create | Read | Update |
|---|---|---|---|
domain |
RW | RO | RO |
status |
— | RO | RO |
ipsv4 |
— | RO | RO |
dkim_public_key |
— | RO | RO |
dkim_selector |
— | RO | RO |
password_hash |
RW | hidden | RW |
username |
RW | RO | RO |
active |
RW | RW | RW |
Repository Layout
.
├── exordos_paas_mail/
│ ├── constants.py # MAIL_ENV_FILE, EXIM4_PASSWD_FILE paths
│ ├── models.py # MailVersion, MailInstance, MailAccount (restalchemy)
│ ├── controllers.py # REST controllers (gcl_iam policy-based)
│ ├── routes.py # Route tree (instances/accounts, versions)
│ ├── definition.py # MailDefinition (PaaSDefinition contract)
│ ├── infra_models.py # MailInstance + infra (NodeSet + Config)
│ ├── infra_builder.py # CoreInfraBuilder (creates VMs + delivers mail.env)
│ ├── paas_models.py # MailInstanceNode (target resource for DP agent)
│ ├── paas_builder.py # MailInstanceBuilder (maps accounts → DP payload)
│ ├── driver.py # MailCapabilityDriver: writes /etc/exim4/passwd
│ ├── utils.py # remove_nested_dm helper
│ ├── migrations/
│ │ ├── 0000-init-mail.py # Creates mail_versions, mail_instances, mail_accounts
│ │ └── 0001-drop-root-password.py
│ └── tests/
│ ├── unit/ # Unit tests (models, driver)
│ └── functional/ # E2E tests (CP API + SMTP auth against a live stand)
├── exordos/
│ ├── exordos.yaml # Build config (deps + elements + DP image)
│ ├── images/
│ │ ├── dp_install.sh # Packer: install exim4 + configure script + agent
│ │ ├── dp_bootstrap.sh # First-boot: persistent disk + start configure service
│ │ └── build_wheel.sh # Builds the wheel shipped as the pypi_package artifact
│ └── manifests/
│ ├── mailaas.yaml.j2 # Element manifest: type reg + IAM + DP version
│ └── example_mail.yaml.j2 # Example consumer element
├── etc/
│ ├── systemd/
│ │ ├── exordos-metapaas-mail-configure.service # Configures exim4 from mail.env
│ │ └── exordos-metapaas-mail-agent.service # Universal agent (MailCapabilityDriver)
│ └── exordos_metapaas/
│ ├── logging.yaml
│ └── metapaas_mail_agent.conf
├── .github/
│ ├── workflows/
│ │ ├── tests.yaml # Lint (ruff) on every push
│ │ └── build.yml # Build + push on a self-hosted runner, then e2e
│ └── scripts/
│ └── wait-for-element.sh # Poll an element until it reports ACTIVE
├── pyproject.toml
├── tox.ini
└── Makefile
Development
Dataplane bootstrap ordering
The image enables the mail agent without starting it. Both the agent and the
mail configure unit use Wants and After for exordos-bootstrap.service:
they wait for its job to finish, but a failed bootstrap does not disable the
agent or prevent it from reporting configuration errors. A mail-image-only
drop-in also orders the base exordos-universal-agent.service after bootstrap:
its RenderAgentDriver delivers mail.env and invokes the configure hook.
MailCapabilityDriver manages accounts and is not the configuration consumer.
The shared base image/repository is unchanged.
Bootstrap prepares /persist, migrates and bind-mounts /var/log, and then
queues the configure service with systemctl --no-block enable --now. A blocking
start here would wait for the bootstrap unit itself and deadlock. The shared
persist_migrate_complete helper writes a marker and returns; it does not reboot.
The configure unit retains the mail.env condition for delayed delivery and
asserts that both /persist and /var/log are mount points. A missing mount
fails the configuration action instead of silently skipping it or configuring
mail on the root filesystem. Once storage is repaired, the agent can retry the
same configuration action without being re-enabled. There is no automatic disk
repair in this change. The shared bootstrap runner's __done file is not a
success gate: that runner currently does not propagate child script failures.
RequiresMountsFor=/persist /var/log also makes configure wait for the generated
mount units on subsequent boots. The mount assertions remain necessary on first
boot, before bootstrap has installed the fstab entries. These hard mount
dependencies apply only to configure, never to either recovery agent.
The bootstrap regression tests run the real dataplane script against isolated command stubs. Unit contracts cover ordering, mount assertions and recovery; these tests do not replace a first-boot test under systemd on an isolated Realm.
Local checks
make test # Unit tests via tox
make lint # ruff check
make format # ruff format
make typecheck # mypy
make functional # E2E tests (needs live stand)
Running functional tests manually
Requires a live exordos_core + exordos_metapaas deployment with mailaas
installed:
EXORDOS_ENDPOINT=http://10.20.0.2:80/api/core \
EXORDOS_USERNAME=admin \
EXORDOS_PASSWORD=<pass> \
EXORDOS_MAIL_CP_URL=http://10.20.0.2:80/api/metapaas \
make functional
To build that environment from scratch — what the build workflow does on a
throwaway runner:
exordos compute hypervisors init --connection-uri qemu+tcp://10.20.0.1/system
exordos bootstrap -i latest -f -m core --pool-agent-placement local \
--admin-password <admin-pass> --cidr 10.20.0.0/22
exordos e e install metapaas
exordos e e install mailaas # add -v <version> to pin a build
Wait for both elements to report ACTIVE
(.github/scripts/wait-for-element.sh mailaas) and the suite has everything it
needs. To test a build of this working tree rather than the published element,
make build it and exordos push it to a repository the core can reach first.
Key differences from metapaas_s3
| Aspect | s3aas | mailaas |
|---|---|---|
| DP software | RustFS | exim4 (SMTP relay) |
| Instance children | Bucket, Policy, User, AccessKey | Account |
| Replicas | 1–16 | Always 1 |
| Config delivered | rustfs.env |
mail.env (domain only) |
| DP auth state | S3 access keys | /etc/exim4/passwd (SHA512-crypt) |
| On-change | systemctl restart rustfs |
systemctl restart mail-configure |
| DP agent state file | s3_meta.json |
mail_meta.json |
| uuid5 name | s3aas |
mailaas |
References
- MetaPaaS design:
../exordos_metapaas/DESIGN.md - How to build new PaaS:
../exordos_s3/HOW_TO_BUILD_NEW_PAAS.md - Working reference:
../exordos_s3/
Metadata
Release files for exordos-mail 0.0.9
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| exordos_mail-0.0.9.tar.gz | 1.1 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| exordos_mail-0.0.9-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.2 MB
Release files / exordos_mail-0.0.9.tar.gz
| Download URL | exordos_mail-0.0.9.tar.gz |
|---|---|
| Size | 1.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d5d718f3d9e2b8c3f627361f29f1b34760670d5ef02377a784b6148e85a92838
|
|
BLAKE2b-256 checksum How to use checksums |
00d2f8c67a29ef39f0ea7ca9a499797257605ba06b94c0f25dda2deeffcb5e8f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logRelease files / exordos_mail-0.0.9-py3-none-any.whl
| Download URL | exordos_mail-0.0.9-py3-none-any.whl |
|---|---|
| Size | 47.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e3ecb67d18c46fdf71a640c796d33330598158e18a79c3f57fc9f91054a54076
|
|
BLAKE2b-256 checksum How to use checksums |
5e728fcf05a77c9c0715e86adb0d45af3837d518872f1f1d4b3bb904d83788c0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency log