Skip to main content

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 TXT record at <dkim_selector>._domainkey.$1 (default selector: platform), with the dkim_public_key value 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 MX record 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.10

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for exordos-mail 0.0.10
File Size Uploaded
exordos_mail-0.0.10.tar.gz 1.1 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for exordos-mail 0.0.10
File Interpreter ABI Platform
exordos_mail-0.0.10-py3-none-any.whl Python 3 none any Details

Total release size: 1.2 MB

Release files / exordos_mail-0.0.10.tar.gz

Download URL exordos_mail-0.0.10.tar.gz
Size 1.1 MB
Tags Source
SHA-256 checksum
How to use checksums
f03784b60d0420f9527fd36bcf81223e250d5f4889654aa5203add7dc64a1449
BLAKE2b-256 checksum
How to use checksums
419deb15e0ce88346a1516777c83475e68b4fdfaf48acf8320d1605d792f4646
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 27, 2026.

Transparency log

Release files / exordos_mail-0.0.10-py3-none-any.whl

Download URL exordos_mail-0.0.10-py3-none-any.whl
Size 47.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cbe5013a80530fb59c4ac794234d7664e926643b78489217b00d44004e775932
BLAKE2b-256 checksum
How to use checksums
439fe380ee0eda8fdb019881bbc7c113c7659919c0e835bcff396ab1909399e6
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 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.10 This release

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page