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.9

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.9
File Size Uploaded
exordos_mail-0.0.9.tar.gz 1.1 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for exordos-mail 0.0.9
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

0.0.10

2 release files

This release

0.0.9 This release

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