Files
mesh-controller/testdata/beside/mesh-catalog/modules/mailu/module.json.captured
T
jschoubben 816cf96459 Pin mesh-sdk 007b6fd, the release's final sdk, so the controller reads the release's manifests and composes as the group does (issue 774)
The sdk now refuses an offer that says no credential form (6b8c694, ADR 0570). So:
- the catalogue copy in testdata/beside is captured again at the release branch's catalogue 8ae8e88d, whose offers each
  say one, and the sdk's conformance events at 007b6fd, as CAPTURED asks;
- the fixtures of identity_bound_test and issued_test say the form their rows meant: pair for a login of each
  consumer's own, none for a keyless provision. No behaviour of the controller changed.
2026-10-30 00:07:04 +01:00

794 lines
21 KiB
Plaintext

{
"module": "mailu",
"version": "1",
"upgrade": {
"policy": "record",
"why": "people's mail: any change to how its containers are declared recreates them together in one send, and the mail is down for everyone until they are up again — a person chooses the moment (hq ADR 0242, issue 295)"
},
"unhealthy": {
"severity": "urgent",
"why": "while mailu is broken, mail is refused or stuck for everyone with a mailbox here (hq ADR 0355, issue 687)"
},
"capabilities": [
"container-runtime"
],
"requires": [
"postgres-database",
"route",
"secret"
],
"contributes": {
"postgres-database": {
"name": "mailu"
},
"route": {
"web": {
"label": "mail",
"endpoint": "web-tls",
"scheme": "https",
"insecure": true
},
"acme": {
"label": "mail",
"path": "/.well-known/acme-challenge",
"endpoint": "web",
"priority": 100
},
"autoconfig": {
"label": "autoconfig",
"endpoint": "autoconfig"
},
"autodiscover": {
"label": "autodiscover",
"endpoint": "autoconfig"
},
"automx": {
"label": "automx",
"endpoint": "autoconfig"
}
}
},
"binds": {
"postgres-database": "${dir:state}/database.json",
"route": "${dir:state}/route.json"
},
"secrets": {
"postgres-database": "${dir:state}/database.secret",
"secret": {
"secret-key": "${dir:state}/secret-key.secret",
"admin": "${dir:state}/admin.secret",
"api-token": "${dir:state}/api-token.secret"
}
},
"tools": [
"mailu_list_users",
"mailu_create_user",
"mailu_change_password",
"mailu_delete_user",
"mailu_list_aliases",
"mailu_create_alias",
"mailu_delete_alias",
"mailu_list_domains",
"mailu_read_mail",
"mailu_search_mail",
"mailu_health"
],
"emits": [
"user.created",
"user.deleted",
"alias.created",
"alias.deleted"
],
"listens": [
{
"name": "smtp",
"port": 25,
"protocol": "tcp",
"from": "anywhere",
"why": "mail from other mail servers",
"fixed": true
},
{
"name": "pop3",
"port": 110,
"protocol": "tcp",
"from": "anywhere",
"why": "POP3, kept at parity with the predecessor; pruning legacy protocols is its own deliberate change",
"fixed": true
},
{
"name": "imap",
"port": 143,
"protocol": "tcp",
"from": "anywhere",
"why": "IMAP with STARTTLS, kept at parity",
"fixed": true
},
{
"name": "smtps",
"port": 465,
"protocol": "tcp",
"from": "anywhere",
"why": "submission over TLS",
"fixed": true
},
{
"name": "submission",
"port": 587,
"protocol": "tcp",
"from": "anywhere",
"why": "submission; also what the smtp provision serves consumers",
"fixed": true
},
{
"name": "imaps",
"port": 993,
"protocol": "tcp",
"from": "anywhere",
"why": "IMAP over TLS",
"fixed": true
},
{
"name": "pop3s",
"port": 995,
"protocol": "tcp",
"from": "anywhere",
"why": "POP3 over TLS, kept at parity",
"fixed": true
},
{
"name": "web",
"port": 7080,
"protocol": "tcp",
"from": "mesh",
"why": "the web front over http; only the ACME HTTP-01 passthrough is routed here — everything else 301s to https and would loop a proxy"
},
{
"name": "web-tls",
"port": 7443,
"protocol": "tcp",
"from": "mesh",
"why": "the web front over its own TLS (admin, webmail, API); its public name is a route grant reaching it here"
},
{
"name": "autoconfig",
"port": 4243,
"protocol": "tcp",
"from": "mesh",
"why": "automx: mail client autoconfiguration; the autoconfig, autodiscover and automx names are route grants reaching it here"
},
{
"name": "admin-api",
"port": 8080,
"protocol": "tcp",
"from": "machine",
"why": "the admin API, which this module's own code reaches on loopback from the node's runtime now that it runs outside the mailu network"
}
],
"data": {
"own": [
{
"id": "mail",
"path": "${dir:data-mail}",
"class": "valuable",
"why": "every mailbox"
},
{
"id": "dkim",
"path": "${dir:data-dkim}",
"class": "valuable",
"why": "the domain's signing keys; a new one means a new DNS record"
},
{
"id": "data",
"path": "${dir:data-data}",
"class": "valuable",
"why": "the server's own data"
},
{
"id": "dav",
"path": "${dir:data-dav}",
"class": "valuable",
"why": "calendars and contacts"
},
{
"id": "webmail",
"path": "${dir:data-webmail}",
"class": "valuable",
"why": "the webmail's own data: its users' settings and address books"
},
{
"id": "queue",
"path": "${dir:data-mailqueue}",
"class": "valuable",
"why": "mail accepted and not yet delivered, kept nowhere else"
},
{
"id": "filter",
"path": "${dir:data-filter}",
"class": "rebuildable",
"why": "the spam filter's learned statistics; it learns again"
},
{
"id": "certs",
"path": "${dir:data-certs}",
"class": "rebuildable",
"why": "certificates, issued again"
},
{
"id": "fetchmail",
"path": "${dir:data-fetchmail}",
"class": "rebuildable",
"why": "which remote messages were fetched; lost, some are fetched twice"
},
{
"id": "clamav",
"path": "${dir:data-clamav}",
"class": "cache",
"why": "virus signatures, downloaded again"
},
{
"id": "redis",
"path": "${dir:data-redis}",
"class": "cache",
"why": "the filter's working set"
}
],
"consumers": {
"smtp": {
"class": "valuable",
"in": "mail",
"why": "a consumer's mailbox holds the only copy of its mail"
}
}
},
"config": {
"domain": {
"type": "string",
"description": "The mail domain Mailu serves, such as example.org: its first mail domain, the postmaster's and the one consumers send as; the operator's, with no default (novox/hq ADR 0112).",
"trusted": true,
"required": true
},
"proxy-address": {
"type": "string",
"description": "The address of the reverse proxy in front of Mailu, whose X-Real-IP header Mailu believes; the operator's, with no default.",
"trusted": true,
"required": true
},
"sitename": {
"type": "string",
"description": "The name Mailu's web pages show for the mail service; the operator's, with no default.",
"trusted": true,
"required": true
},
"website": {
"type": "string",
"description": "The address of the website Mailu's web pages link to, as a URL; the operator's, with no default.",
"trusted": true,
"required": true
},
"url": {
"type": "string",
"description": "The address the module's tools reach Mailu's API at, as a URL; empty leaves them to MESH_MAILU_URL.",
"trusted": true,
"default": ""
},
"container": {
"type": "string",
"description": "The name of the container holding Mailu's mailboxes, which the module's tools read mail in; empty is mailu-imap.",
"trusted": true,
"default": ""
}
},
"resources": [
{
"id": "mesh-state",
"type": "directory",
"mode": "0700",
"place": "mesh",
"unknown": "program"
},
{
"id": "state",
"type": "directory",
"mode": "0700",
"place": ".",
"unknown": "program"
},
{
"id": "grants",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "config-env",
"type": "file",
"path": "${dir:state}/mailu.env",
"mode": "0644",
"template": "files/mailu.env"
},
{
"id": "secret-env",
"type": "file",
"path": "${dir:state}/secret.env",
"mode": "0600",
"template": "files/secret.env"
},
{
"id": "database-env",
"type": "file",
"path": "${dir:state}/database.env",
"mode": "0600",
"template": "files/database.env"
},
{
"id": "admin-env",
"type": "file",
"path": "${dir:state}/admin.env",
"mode": "0600",
"template": "files/admin.env"
},
{
"id": "data-certs",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-data",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-dkim",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-mail",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-mailqueue",
"type": "directory",
"mode": "0755",
"unknown": "program"
},
{
"id": "data-filter",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-clamav",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-redis",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-webmail",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-dav",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-fetchmail",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-overrides-nginx",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-overrides-dovecot",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-overrides-postfix",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-overrides-rspamd",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "data-overrides-roundcube",
"type": "directory",
"mode": "0700",
"unknown": "program"
},
{
"id": "net",
"type": "network",
"name": "mailu"
},
{
"id": "resolver",
"type": "container",
"name": "mailu-resolver",
"image": "ghcr.io/mailu/unbound@sha256:2c037bc5c988b5c0ab7e1b6e6ffa762e14ba1e45c96281ac87f8b9be8eee8b6e",
"health": {
"kind": "runtime"
},
"network": "mailu",
"env-file": [
"${dir:state}/mailu.env",
"${dir:state}/secret.env"
],
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
"ip": "192.168.203.254"
},
{
"id": "redis",
"type": "container",
"name": "mailu-redis",
"image": "redis@sha256:4bed291aa5efb9f0d77b76ff7d4ab71eee410962965d052552db1fb80576431d",
"network": "mailu",
"volumes": [
"${dir:data-redis}:/data"
]
},
{
"id": "admin",
"type": "container",
"name": "mailu-admin",
"image": "ghcr.io/mailu/admin@sha256:6dbfdadc4a9590dcb7652357b505200115b689b74008653bbf369e4599a3be5a",
"health": {
"kind": "runtime"
},
"network": "mailu",
"ports": [
"8080"
],
"env-file": [
"${dir:state}/mailu.env",
"${dir:state}/secret.env",
"${dir:state}/database.env",
"${dir:state}/admin.env"
],
"volumes": [
"${dir:data-data}:/data",
"${dir:data-dkim}:/dkim"
],
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified"
},
{
"id": "imap",
"type": "container",
"name": "mailu-imap",
"image": "ghcr.io/mailu/dovecot@sha256:7f0ed5db996fbdc00adc5c5e38a08492e04f7eb4a9fbd66a03aa9a28ddf23993",
"health": {
"kind": "runtime"
},
"network": "mailu",
"env-file": [
"${dir:state}/mailu.env"
],
"volumes": [
"${dir:data-mail}:/mail",
"${dir:data-overrides-dovecot}:/overrides:ro"
],
"dns": [
"192.168.203.254"
]
},
{
"id": "smtp",
"type": "container",
"name": "mailu-smtp",
"image": "ghcr.io/mailu/postfix@sha256:e2e49f39e53b80eac9e7a2f18d9df11edeb4914fd62dbba89b3155e8e034f62e",
"health": {
"kind": "tool",
"tool": "mailu_health",
"interval": "60s",
"timeout": "30s",
"looks": 3,
"grace": "120s"
},
"network": "mailu",
"env-file": [
"${dir:state}/mailu.env"
],
"volumes": [
"${dir:data-mailqueue}:/queue",
"${dir:data-overrides-postfix}:/overrides:ro"
],
"dns": [
"192.168.203.254"
]
},
{
"id": "antispam",
"type": "container",
"name": "mailu-antispam",
"image": "ghcr.io/mailu/rspamd@sha256:ff3666d8a61f17d309c5c6f6bcf4d40470b82299ca706ac650301175bb1a079d",
"health": {
"kind": "runtime"
},
"network": "mailu",
"env-file": [
"${dir:state}/mailu.env"
],
"volumes": [
"${dir:data-filter}:/var/lib/rspamd",
"${dir:data-overrides-rspamd}:/etc/rspamd/override.d:ro"
],
"dns": [
"192.168.203.254"
]
},
{
"id": "antivirus",
"type": "container",
"name": "mailu-antivirus",
"image": "clamav/clamav-debian@sha256:b12ef8fefddbba7d88de59bea8a32622f365339154adf02d38fd089112e6745a",
"network": "mailu",
"volumes": [
"${dir:data-clamav}:/var/lib/clamav"
],
"dns": [
"192.168.203.254"
]
},
{
"id": "webmail",
"type": "container",
"name": "mailu-webmail",
"image": "ghcr.io/mailu/webmail@sha256:bdbee44cdb05a4658f0e3b62cc448de55ca8f8aea172279fda594826144c04f6",
"health": {
"kind": "runtime"
},
"network": "mailu",
"env-file": [
"${dir:state}/mailu.env",
"${dir:state}/secret.env"
],
"volumes": [
"${dir:data-webmail}:/data",
"${dir:data-overrides-roundcube}:/overrides:ro"
],
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
"dns": [
"192.168.203.254"
]
},
{
"id": "webdav",
"type": "container",
"name": "mailu-webdav",
"image": "ghcr.io/mailu/radicale@sha256:690ed6edf189dfef100a5a8b37c195ebf5d9241ac5f23f2f44b8b7b75726e3de",
"health": {
"kind": "runtime"
},
"network": "mailu",
"env-file": [
"${dir:state}/mailu.env",
"${dir:state}/secret.env"
],
"volumes": [
"${dir:data-dav}:/data"
],
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
"dns": [
"192.168.203.254"
]
},
{
"id": "fetchmail",
"type": "container",
"name": "mailu-fetchmail",
"image": "ghcr.io/mailu/fetchmail@sha256:f881c8412d3bbe73d638469b48321558d6403a9d45bfa043c1e52c752103d42d",
"health": {
"kind": "runtime"
},
"network": "mailu",
"env-file": [
"${dir:state}/mailu.env",
"${dir:state}/secret.env"
],
"volumes": [
"${dir:data-fetchmail}:/data"
],
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
"dns": [
"192.168.203.254"
]
},
{
"id": "front",
"type": "container",
"name": "mailu-front",
"image": "ghcr.io/mailu/nginx@sha256:36f98897cd1bc9d27628bbb4e04bdf60147af2ec7507d6da77f002c4f256896d",
"health": {
"kind": "exec",
"command": "curl -m3 -skfLo /dev/null http://127.0.0.1:10204/health && kill -0 $(cat /run/dovecot/master.pid) && python3 -c \"import smtplib; s = smtplib.SMTP('127.0.0.1', 25, timeout=5); s.ehlo('health'); s.quit()\" && openssl x509 -checkend 1209600 -noout -in /certs/letsencrypt/live/mailu/nginx-chain.pem",
"interval": "60s",
"timeout": "15s",
"looks": 3,
"grace": "120s"
},
"network": "mailu",
"env-file": [
"${dir:state}/mailu.env"
],
"ports": [
"25",
"110",
"143",
"465",
"587",
"993",
"995",
"7080:80",
"7443:443"
],
"volumes": [
"${dir:data-certs}:/certs",
"${dir:data-overrides-nginx}:/overrides:ro"
],
"dns": [
"192.168.203.254"
],
"logging": "journald"
},
{
"id": "runtime-config",
"type": "file",
"path": "${dir:mesh-state}/config.json",
"mode": "0600",
"template": "files/config.json",
"takes-config": [
"domain",
"proxy-address",
"sitename",
"website",
"url",
"container"
],
"merge": "json"
},
{
"id": "program-automx",
"type": "archive",
"path": "${dir:state}/automx",
"artifact": "automx",
"unknown": "program"
},
{
"id": "automx",
"type": "container",
"name": "mailu-automx",
"artifact": "alpine",
"health": {
"kind": "http",
"endpoint": "autoconfig",
"path": "/",
"status": 200,
"body": "Show Thunderbird-style"
},
"network": "mailu",
"env-file": [
"${dir:state}/mailu.env"
],
"ports": [
"4243"
],
"volumes": [
"${dir:state}/automx:/opt/mesh:ro"
],
"args": [
"/opt/mesh/automx"
],
"restart-on": [
"program-automx",
"config-env"
]
}
],
"build": {
"artifacts": [
{
"name": "code",
"kind": "bundle",
"language": "typescript",
"entrypoints": [
"index.js",
"tools/index.js",
"provisioner/index.js"
],
"loads": [
"index.js",
"tools/index.js",
"provisioner/index.js"
],
"env": {
"MESH_MAILU_URL": "http://127.0.0.1:${port:8080}/api/v1",
"MESH_MAILU_API_KEY_FILE": "${dir:state}/api-token.secret",
"MESH_MAILU_IMAP_CONTAINER": "mailu-imap",
"MESH_MAILU_SMTP_CONTAINER": "mailu-smtp",
"MESH_MAILU_CONFIG_FILE": "${dir:mesh-state}/config.json",
"MESH_RECEIVES": "${dir:grants}/mesh.json"
}
},
{
"name": "automx",
"kind": "bundle",
"language": "go",
"system": "alpine",
"from": "cmd/automx",
"binary": "automx"
},
{
"name": "alpine",
"kind": "upstream",
"from": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b"
},
{
"name": "files",
"kind": "archive",
"from": "files"
}
]
},
"provides": [
{
"name": "smtp",
"scope": "mesh",
"identity": {
"max": 64,
"in": "a mailbox's local part"
},
"credential": {
"form": "pair"
}
}
],
"serves": {
"smtp": {
"port": 587,
"domain": "${config:domain}"
}
},
"receives": {
"smtp": "${dir:grants}/mesh.json"
},
"grants": {
"smtp": "${dir:grants}"
},
"jails": [
{
"name": "mailu-front",
"failregex": "^.*(?:imap|pop3|submission|managesieve)-login: .*\\(auth failed, \\d+ attempts(?: in \\d+ secs)?\\):.*rip=<HOST>(?:,|$)\n ^.*client login failed: \"AUTH not supported\" while in http auth state, client: <HOST>, server: \\S+:25,",
"jail": "backend = systemd\njournalmatch = CONTAINER_NAME=mailu-front\nport = smtp,submission,submissions,imap,imaps,pop3,pop3s\nmaxretry = 3\nfindtime = 1d\nbantime = 1d"
}
]
}