{ "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": "automx", "path": "${dir:data-automx}", "class": "rebuildable", "why": "client autoconfiguration, written 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" }, { "id": "state", "type": "directory", "mode": "0700", "place": "." }, { "id": "grants", "type": "directory", "mode": "0700" }, { "id": "data-automx", "type": "directory", "mode": "0700" }, { "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" }, { "id": "data-data", "type": "directory", "mode": "0700" }, { "id": "data-dkim", "type": "directory", "mode": "0700" }, { "id": "data-mail", "type": "directory", "mode": "0700" }, { "id": "data-mailqueue", "type": "directory", "mode": "0755" }, { "id": "data-filter", "type": "directory", "mode": "0700" }, { "id": "data-clamav", "type": "directory", "mode": "0700" }, { "id": "data-redis", "type": "directory", "mode": "0700" }, { "id": "data-webmail", "type": "directory", "mode": "0700" }, { "id": "data-dav", "type": "directory", "mode": "0700" }, { "id": "data-fetchmail", "type": "directory", "mode": "0700" }, { "id": "data-overrides-nginx", "type": "directory", "mode": "0700" }, { "id": "data-overrides-dovecot", "type": "directory", "mode": "0700" }, { "id": "data-overrides-postfix", "type": "directory", "mode": "0700" }, { "id": "data-overrides-rspamd", "type": "directory", "mode": "0700" }, { "id": "data-overrides-roundcube", "type": "directory", "mode": "0700" }, { "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": "automx", "type": "container", "name": "mailu-automx", "artifact": "automx", "network": "mailu", "env-file": [ "${dir:state}/mailu.env" ], "ports": [ "4243" ], "volumes": [ "${dir:data-automx}:/data" ] } ], "build": { "on": [ { "arg": "PYTHON_BASE", "image": "python@sha256:25f3cfeaceca14921366af4d1240b56457ef46273bdb508c7b0e8f469f6fd228" } ], "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": "image", "from": "automx/Dockerfile" }, { "name": "files", "kind": "archive", "from": "files" } ] }, "provides": [ { "name": "smtp", "scope": "mesh", "identity": { "max": 64, "in": "a mailbox's local part" } } ], "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=(?:,|$)\n ^.*client login failed: \"AUTH not supported\" while in http auth state, client: , 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" } ] }