diff --git a/.env.dist b/.env.dist index b839e19..179d27f 100755 --- a/.env.dist +++ b/.env.dist @@ -9,15 +9,14 @@ LDAP_MONITOR_PASSWORD= DOCKER_LDAP_HEALTHCHECK_USERNAME=monitor DOCKER_LDAP_HEALTHCHECK_PASSWORD=PasswordLdapMonitor -MAIL_DATA_DIR= -MAIL_STATE_DIR= +MAILSERVER_DATA_DIR= +MAILSERVER_KEYS_DIR= MAIL_LOGS_DIR= -MAIL_CONFIGS_DIR= + LDAP_DATA_DIR= ACME_HOME_DIR= # web UI (phpldapadmin-ca.crt, phpldapadmin-certificate.key, phpldapadmin-certificate.crt) PHP_LDAP_ADMIN_CERTS_DIR= -SASLAUTHD_SOCKET_FILE= # API keys for acme container CF_API_EMAIL= @@ -29,42 +28,10 @@ LDAP_BASE_DN="dc=example,dc=com" OVERRIDE_HOSTNAME= DOMAIN_NAMES= ACME_COMMAND_ARGUMENTS= -POSTFIX_VIRTUAL_ALIAS_DOMAINS= LDAP_PORT= LDAPS_PORT= PHPLDAPADMIN_SSL_PORT=8080 -# Replication -DOVECOT_REPLICATION_SSL_VOLUME= -DOVECOT_REPLICATION_SERVER= -DOVECOT_REPLICATION_ADM_PASS= -DOVECOT_REPLICATION_SSL_CA_FILE=/etc/ssl/replication/ca.pem -DOVECOT_REPLICATION_SSL_CA_DIR=/etc/ssl/replication/ -DOVECOT_REPLICATION_SSL_CERT_FILE=/etc/ssl/replication/cert.pem -DOVECOT_REPLICATION_SSL_KEY_FILE=/etc/ssl/replication/key.pem - -CRON_MAILFROM= -CRON_MAILTO= -POSTMASTER_ADDRESS= -PFLOGSUMM_RECIPIENT= -LOGWATCH_RECIPIENT= -# The @ must be escaped like \@ -VIRUS_ADMIN_EMAIL= -VIRUS_X_HEADER_LINE= - -# Fail2ban config to allow IPs to make failed attempts -FAIL2BAN_IGNORE_IPS= -FAIL2BAN_DST_EMAIL= -FAIL2BAN_SENDER_EMAIL= -FAIL2BAN_SENDER_NAME= - -# Fail2ban reporting -FAIL2BAN_BLOCKLIST_DE_API_KEY= -FAIL2BAN_BLOCKLIST_DE_EMAIL= -FAIL2BAN_IPTHREAT_API_KEY= -FAIL2BAN_IPTHREAT_SYSTEM_NAME= -FAIL2BAN_ABUSEIPDB_API_KEY= - DNS_SERVER=1.1.1.1 # CrowdSec diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index cb982ce..b539eea 100755 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -2,7 +2,8 @@ name: run full tests on: [push] permissions: - contents: read + contents: read + packages: read jobs: lint: @@ -19,11 +20,19 @@ jobs: - uses: actions/checkout@v4 - name: Docker compose version run: docker compose version + - name: Login to GitHub registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: create temp folder run: make create-temp-env - name: setup for tests run: make setup-test - name: run docker-test run: make run-test + env: + IMAGE_TAG: ghcr.io/datacenters-network/mails/mailserver - name: teardown tests run: make cleanup-test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..8c86d31 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,43 @@ +name: Publish Docker image + +permissions: + contents: read + packages: write + +on: + workflow_dispatch: + release: + types: [published] + +jobs: + push_to_registry: + name: Push Docker image to GitHub Packages + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to GitHub registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build image + run: make docker-build + env: + DOCKER_BUILDKIT: 1 + PLATFORM: linux/amd64 + IMAGE_TAG: ghcr.io/datacenters-network/mails/mailserver + ACTION: push + + - name: Test image + run: make docker-test + env: + IMAGE_TAG: ghcr.io/datacenters-network/mails/mailserver diff --git a/Makefile b/Makefile index caab7ef..a2f5e43 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,21 @@ PLATFORM ?= linux/amd64 ACTION ?= load PROGRESS_MODE ?= plain -.PHONY: docker-test run-test cleanup-test test +.PHONY: docker-build docker-test run-test cleanup-test test -all: docker-test +all: docker-build docker-test + +docker-build: + # https://github.com/docker/buildx#building + docker buildx build \ + --build-arg VCS_REF="$(shell git rev-parse HEAD)" \ + --build-arg BUILD_DATE="$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")" \ + --tag $(IMAGE_TAG) \ + --progress $(PROGRESS_MODE) \ + --platform $(PLATFORM) \ + --pull \ + --$(ACTION) \ + ./docker docker-test: test @@ -34,15 +46,17 @@ cleanup-test: check-env create-temp-env: mktemp -d -t desportes_infra_tests.XXXXXX > /tmp/current-temp-env -setup-test-files: check-env +setup-test-files: set -eu - cp -rv docker-compose.yml dockerl user-patches.sh rspamd $(TEMP_DIR) + cp -rv docker-compose.yml dockerl config.toml docker $(TEMP_DIR) cp tests/.env.test1 $(TEMP_DIR)/.env rm -vf tests/data/acme.sh/*/*.csr rm -vf tests/data/acme.sh/*/*.cer rm -vf tests/data/acme.sh/*/ca.* mkdir $(TEMP_DIR)/tests mkdir -p $(TEMP_DIR)/tests/data/acme.sh/mail.williamdes.eu.org + mkdir -p $(TEMP_DIR)/tests/data/maildata + mkdir $(TEMP_DIR)/tests/data/maildata/queue $(TEMP_DIR)/tests/data/maildata/reports $(TEMP_DIR)/tests/data/maildata/data $(TEMP_DIR)/tests/data/maildata/data/blobs cp tests/make-certs.sh $(TEMP_DIR)/tests/ cp -rp tests/php $(TEMP_DIR)/tests/ cp -rp tests/seeding $(TEMP_DIR)/tests/ diff --git a/config.toml b/config.toml new file mode 100755 index 0000000..6c19507 --- /dev/null +++ b/config.toml @@ -0,0 +1,764 @@ +############################################# +# Server configuration +############################################# + +[server] +hostname = "mail-server.mail.williamdes.eu.org" +max-connections = 8192 + +#[server.run-as] +#user = "stalwart-mail" +#group = "stalwart-mail" + +[server.tls] +enable = true +implicit = false +timeout = "1m" +certificate = "default" +#sni = [{subject = "", certificate = ""}] +#protocols = ["TLSv1.2", "TLSv1.3"] +#ciphers = [ "TLS13_AES_256_GCM_SHA384", "TLS13_AES_128_GCM_SHA256", +# "TLS13_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", +# "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", +# "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", +# "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"] +ignore-client-order = true + +[server.socket] +nodelay = true +reuse-addr = true +#reuse-port = true +backlog = 1024 +#ttl = 3600 +#send-buffer-size = 65535 +#recv-buffer-size = 65535 +#linger = 1 +#tos = 1 + +[global] +shared-map = {shard = 32, capacity = 10} +#thread-pool = 8 + +#[global.tracing] +#method = "log" +#path = "/var/log/mail" +#prefix = "mailserver.log" +#rotate = "daily" +#level = "debug" +#ansi = false + +[global.tracing] +method = "stdout" +level = "debug" + +[certificate."default"] +cert = !SSL_CERT_PATH +private-key = !SSL_KEY_PATH + +############################################# +# Directory configuration +############################################# + +[directory."ldap"] +type = "ldap" +address = !LDAP_SERVER_HOST +base-dn = !LDAP_SEARCH_BASE + +[directory."ldap".bind] +dn = !LDAP_BIND_DN +secret = !LDAP_BIND_PW + +[directory."ldap".cache] +entries = 500 +ttl = {positive = '1h', negative = '10m'} + +[directory."ldap".options] +catch-all = true +#catch-all = { map = "(.+)@(.+)$", to = "info@${2}" } +subaddressing = true +#subaddressing = { map = "^([^.]+)\.([^.]+)@(.+)$", to = "${2}@${3}" } +superuser-group = "superusers" + +[directory."ldap".pool] +max-connections = 10 +min-connections = 0 +max-lifetime = "30m" +idle-timeout = "10m" +connect-timeout = "30s" + + +[directory."ldap".filter] +name = "(&(|(objectClass=PostfixBookMailAccount)(objectClass=posixGroup))(mail=?)(mailEnabled=TRUE))" +email = "(&(|(objectClass=PostfixBookMailAccount)(objectClass=posixGroup))(|(mail=?)(mailAlias=?)(mailList=?))(mailEnabled=TRUE))" +verify = "(&(|(objectClass=PostfixBookMailAccount)(objectClass=posixGroup))(|(mail=*?*)(mailAlias=*?*))(mailEnabled=TRUE))" +expand = "(&(|(objectClass=PostfixBookMailAccount)(objectClass=posixGroup))(mailList=?)(mailEnabled=TRUE))" +domains = "(&(|(objectClass=PostfixBookMailAccount)(objectClass=posixGroup))(|(mail=*@?)(mailAlias=*@?))(mailEnabled=TRUE))" + +[directory."ldap".object-classes] +user = "PostfixBookMailAccount" +group = "posixGroup" + +[directory."ldap".attributes] +name = "uid" +description = ["principalName", "description"] +secret = "userPassword" +groups = ["memberOf", "otherGroups"] +email = "mail" +email-alias = "mailAlias" +quota = "diskQuota" + +[directory."imap"] +type = "imap" +address = "127.0.0.1" +port = 993 + +[directory."imap".pool] +max-connections = 10 +min-connections = 0 +max-lifetime = "30m" +idle-timeout = "10m" +connect-timeout = "30s" + +[directory."imap".tls] +implicit = true +allow-invalid-certs = true + +[directory."imap".cache] +entries = 500 +ttl = {positive = '1h', negative = '10m'} + +[directory."imap".lookup] +domains = ["mail.williamdes.eu.org"] + +[directory."lmtp"] +type = "lmtp" +address = "127.0.0.1" +port = 11200 + +[directory."lmtp".limits] +auth-errors = 3 +rcpt = 5 + +[directory."lmtp".pool] +max-connections = 10 +min-connections = 0 +max-lifetime = "30m" +idle-timeout = "10m" +connect-timeout = "30s" + +[directory."lmtp".tls] +implicit = false +allow-invalid-certs = true + +[directory."lmtp".cache] +entries = 500 +ttl = {positive = '1h', negative = '10m'} + +[directory."lmtp".lookup] +domains = ["mail.williamdes.eu.org"] + +############################################# +# JMAP server configuration +############################################# + +[server.listener."jmap"] +bind = ["[::]:8080"] +url = "https://mail-server.mail.williamdes.eu.org:8080" +protocol = "jmap" + +[store.db] +path = "/opt/stalwart-mail/data/index.sqlite3" + +[store.db.pool] +max-connections = 10 +#workers = 8 + +[store.db.cache] +size = 1000 + +[store.blob] +type = "local" + +[store.blob.local] +path = "/opt/stalwart-mail/data/blobs" + + +[jmap] +directory = "ldap" + +[jmap.http] +#headers = ["Access-Control-Allow-Origin: *", +# "Access-Control-Allow-Methods: POST, GET, HEAD, OPTIONS", +# "Access-Control-Allow-Headers: *"] + +[jmap.encryption] +enable = true +append = false + +[jmap.session.cache] +ttl = "1h" +size = 100 + +[jmap.protocol.get] +max-objects = 500 + +[jmap.protocol.set] +max-objects = 500 + +[jmap.protocol.request] +max-concurrent = 4 +max-size = 10000000 +max-calls = 16 + +[jmap.protocol.query] +max-results = 5000 + +[jmap.protocol.upload] +max-size = 50000000 +max-concurrent = 4 +ttl = "1h" + +[jmap.protocol.upload.quota] +files = 1000 +size = 50000000 + +[jmap.protocol.changes] +max-results = 5000 + +[jmap.rate-limit] +account = "1000/1m" +authentication = "10/1m" +anonymous = "100/1m" +use-forwarded = false + +[jmap.rate-limit.cache] +size = 1024 + +[jmap.mailbox] +max-depth = 10 +max-name-length = 255 + +[jmap.email] +max-attachment-size = 50000000 +max-size = 75000000 + +[jmap.email.parse] +max-items = 10 + +[jmap.principal] +allow-lookups = true + +[jmap.sieve] +disable-capabilities = [] +notification-uris = ["mailto"] +protected-headers = ["Original-Subject", "Original-From", "Received", "Auto-Submitted"] + +[jmap.sieve.limits] +name-length = 512 +max-scripts = 256 +script-size = 102400 +string-length = 4096 +variable-name-length = 32 +variable-size = 4096 +nested-blocks = 15 +nested-tests = 15 +nested-foreverypart = 3 +match-variables = 30 +local-variables = 128 +header-size = 1024 +includes = 3 +nested-includes = 3 +cpu = 5000 +redirects = 1 +received-headers = 10 +outgoing-messages = 3 + +[jmap.sieve.vacation] +default-subject = "Automated reply" +subject-prefix = "Auto: " + +[jmap.sieve.default-expiry] +vacation = "30d" +duplicate = "7d" + +[jmap.event-source] +throttle = "1s" + +[jmap.web-sockets] +throttle = "1s" +timeout = "10m" +heartbeat = "1m" + +[jmap.push] +max-total = 100 +throttle = "1ms" + +[jmap.push.attempts] +interval = "1m" +max = 3 + +[jmap.push.retry] +interval = "1s" + +[jmap.push.timeout] +request = "10s" +verify = "1s" + +[jmap.fts] +default-language = "en" + +[oauth] +key = !OAUTH_KEY + +[oauth.auth] +max-attempts = 3 + +[oauth.expiry] +user-code = "30m" +auth-code = "10m" +token = "1h" +refresh-token = "30d" +refresh-token-renew = "4d" + +[oauth.cache] +size = 128 + +[jmap.purge.schedule] +db = "0 3 *" +blobs = "30 3 *" +sessions = "15 * *" + +############################################# +# IMAP server configuration +############################################# + +[server.listener."imap"] +bind = ["[::]:143"] +protocol = "imap" + +[server.listener."imaptls"] +bind = ["[::]:993"] +protocol = "imap" +tls.implicit = true + +[server.listener."sieve"] +bind = ["[::]:4190"] +protocol = "managesieve" +tls.implicit = true + +[imap.request] +max-size = 52428800 + +[imap.auth] +max-failures = 3 +allow-plain-text = false + +[imap.folders.name] +shared = "Shared Folders" +all = "All Mail" + +[imap.timeout] +authenticated = "30m" +anonymous = "1m" +idle = "30m" + +[imap.rate-limit] +requests = "2000/1m" +concurrent = 4 + +############################################# +# SMTP server configuration +############################################# + +[server.listener."smtp"] +bind = ["[::]:25"] +greeting = !SMTP_GREETING +protocol = "smtp" + +[server.listener."submission"] +bind = ["[::]:587"] +protocol = "smtp" + +[server.listener."submissions"] +bind = ["[::]:465"] +protocol = "smtp" +tls.implicit = true + +[server.listener."management"] +bind = ["0.0.0.0:8080"] +protocol = "http" + +[session] +timeout = "5m" +transfer-limit = 262144000 # 250 MB +duration = "10m" + +[session.connect] +#script = "connect.sieve" + +[session.ehlo] +require = true +reject-non-fqdn = [ { if = "listener", eq = "smtp", then = true}, + { else = false } ] +#script = "ehlo" + +[session.extensions] +pipelining = true +chunking = true +requiretls = true +no-soliciting = "" +dsn = [ { if = "authenticated-as", ne = "", then = true}, + { else = false } ] +expn = [ { if = "authenticated-as", ne = "", then = true}, + { else = false } ] +vrfy = [ { if = "authenticated-as", ne = "", then = true}, + { else = false } ] +future-release = [ { if = "authenticated-as", ne = "", then = "7d"}, + { else = false } ] +deliver-by = [ { if = "authenticated-as", ne = "", then = "15d"}, + { else = false } ] +mt-priority = [ { if = "authenticated-as", ne = "", then = "mixer"}, + { else = false } ] + +[session.auth] +mechanisms = [ { if = "listener", ne = "smtp", then = ["plain", "login"]}, + { else = [] } ] +directory = [ { if = "listener", ne = "smtp", then = "ldap" }, + { else = false } ] +require = [ { if = "listener", ne = "smtp", then = true}, + { else = false } ] +allow-plain-text = false + +[session.auth.errors] +total = 3 +wait = "5s" + +[session.mail] +#script = "mail-from" +#rewrite = [ { all-of = [ { if = "listener", ne = "smtp" }, +# { if = "rcpt", matches = "^([^.]+)@([^.]+)\.(.+)$"}, +# ], then = "${1}@${3}" }, +# { else = false } ] + +[session.rcpt] +#script = "rcpt-to" +relay = [ { if = "authenticated-as", ne = "", then = true }, + { else = false } ] +#rewrite = [ { all-of = [ { if = "rcpt-domain", in-list = "ldap/domains" }, +# { if = "rcpt", matches = "^([^.]+)\.([^.]+)@(.+)$"}, +# ], then = "${1}+${2}@${3}" }, +# { else = false } ] +max-recipients = 25 +directory = "ldap" + +[session.rcpt.errors] +total = 5 +wait = "5s" + +[session.data] +#script = "data" + +#[session.data.milter."rspamd"] +#enable = [ { if = "listener", eq = "smtp", then = true }, +# { else = false } ] +#hostname = "127.0.0.1" +#port = 11332 +#tls = false +#allow-invalid-certs = false + +#[session.data.milter."rspamd".timeout] +#connect = "30s" +#command = "30s" +#data = "60s" + +#[session.data.milter."rspamd".options] +#tempfail-on-error = true +#max-response-size = 52428800 # 50mb +#version = 6 + +#[session.data.pipe."spam-assassin"] +#command = "spamc" +#arguments = [] +#timeout = "10s" + +[session.data.limits] +messages = 10 +size = 104857600 +received-headers = 50 + +[session.data.add-headers] +received = [ { if = "listener", eq = "smtp", then = true }, + { else = false } ] +received-spf = [ { if = "listener", eq = "smtp", then = true }, + { else = false } ] +auth-results = [ { if = "listener", eq = "smtp", then = true }, + { else = false } ] +message-id = [ { if = "listener", eq = "smtp", then = false }, + { else = true } ] +date = [ { if = "listener", eq = "smtp", then = false }, + { else = true } ] +return-path = false + +[[session.throttle]] +#match = {if = "remote-ip", eq = "10.0.0.1"} +key = ["remote-ip"] +concurrency = 5 +#rate = "5/1h" + +[[session.throttle]] +key = ["sender-domain", "rcpt"] +rate = "25/1h" + +[auth.dnsbl] +verify = [ { if = "listener", eq = "smtp", then = ["ip", "iprev", "ehlo", "return-path", "from"] }, + { else = [] } ] + +[auth.dnsbl.lookup] +ip = ["zen.spamhaus.org", "bl.spamcop.net", "b.barracudacentral.org"] +domain = ["dbl.spamhaus.org"] + +[auth.iprev] +verify = [ { if = "listener", eq = "smtp", then = "relaxed" }, + { else = "disable" } ] + +[auth.dkim] +verify = "relaxed" +sign = [ { if = "listener", ne = "smtp", then = ["rsa"] }, + { else = [] } ] + +[auth.spf.verify] +ehlo = [ { if = "listener", eq = "smtp", then = "relaxed" }, + { else = "disable" } ] +mail-from = [ { if = "listener", eq = "smtp", then = "relaxed" }, + { else = "disable" } ] + +[auth.arc] +verify = "relaxed" +seal = ["rsa"] + +[auth.dmarc] +verify = [ { if = "listener", eq = "smtp", then = "relaxed" }, + { else = "disable" } ] + +[queue] +path = "/opt/stalwart-mail/queue" +hash = 64 + +[queue.schedule] +retry = ["2m", "5m", "10m", "15m", "30m", "1h", "2h"] +notify = ["1d", "3d"] +expire = "5d" + +[queue.outbound] +#hostname = "mail-server.mail.williamdes.eu.org" +next-hop = [ { if = "rcpt-domain", in-list = "ldap/domains", then = "local" }, + { else = false } ] +ip-strategy = "ipv4-then-ipv6" + +[queue.outbound.tls] +dane = "optional" +mta-sts = "optional" +starttls = "require" +allow-invalid-certs = true + +#[queue.outbound.source-ip] +#v4 = ["10.0.0.10", "10.0.0.11"] +#v6 = ["a::b", "a::c"] + +[queue.outbound.limits] +mx = 7 +multihomed = 2 + +[queue.outbound.timeouts] +connect = "3m" +greeting = "3m" +tls = "2m" +ehlo = "3m" +mail-from = "3m" +rcpt-to = "3m" +data = "10m" +mta-sts = "2m" + +[[queue.quota]] +#match = {if = "sender-domain", eq = "foobar.org"} +#key = ["rcpt"] +messages = 100000 +size = 10737418240 # 10gb + +[[queue.throttle]] +key = ["rcpt-domain"] +#rate = "100/1h" +concurrency = 5 + +[resolver] +type = "system" +#preserve-intermediates = true +concurrency = 2 +timeout = "5s" +attempts = 2 +try-tcp-on-error = true +public-suffix = ["https://publicsuffix.org/list/public_suffix_list.dat", + "file:///opt/stalwart-mail/etc/lists/public-suffix-list.dat.gz"] + +[resolver.cache] +txt = 2048 +mx = 1024 +ipv4 = 1024 +ipv6 = 1024 +ptr = 1024 +tlsa = 1024 +mta-sts = 1024 + +[report] +path = "/opt/stalwart-mail/reports" +hash = 64 +#submitter = "mail-server.mail.williamdes.eu.org" + +[report.analysis] +addresses = ["dmarc@*", "abuse@*", "postmaster@*"] +forward = true +#store = "/opt/stalwart-mail/incoming" + +[report.dsn] +from-name = "Mail Delivery Subsystem" +from-address = "MAILER-DAEMON@mail.williamdes.eu.org" +sign = ["rsa"] + +[report.dkim] +from-name = "Report Subsystem" +from-address = "noreply-dkim@mail.williamdes.eu.org" +subject = "DKIM Authentication Failure Report" +sign = ["rsa"] +send = "1/1d" + +[report.spf] +from-name = "Report Subsystem" +from-address = "noreply-spf@mail.williamdes.eu.org" +subject = "SPF Authentication Failure Report" +send = "1/1d" +sign = ["rsa"] + +[report.dmarc] +from-name = "Report Subsystem" +from-address = "noreply-dmarc@mail.williamdes.eu.org" +subject = "DMARC Authentication Failure Report" +send = "1/1d" +sign = ["rsa"] + +[report.dmarc.aggregate] +from-name = "DMARC Report" +from-address = "noreply-dmarc@mail.williamdes.eu.org" +org-name = "mail.williamdes.eu.org" +#contact-info = "" +send = "daily" +max-size = 26214400 # 25mb +sign = ["rsa"] + +[report.tls.aggregate] +from-name = "TLS Report" +from-address = "noreply-tls@mail.williamdes.eu.org" +org-name = "mail.williamdes.eu.org" +#contact-info = "" +send = "daily" +max-size = 26214400 # 25 mb +sign = ["rsa"] + + +[signature."rsa"] +#public-key = "file:///opt/stalwart-mail/etc/dkim/mail.williamdes.eu.org.cert" +private-key = "file:///etc/opendkim/keys/mail.williamdes.eu.org/mail.private" +domain = "mail.williamdes.eu.org" +selector = !DKIM_SELECTOR +headers = ["From", "To", "Date", "Subject", "Message-ID"] +algorithm = "rsa-sha256" +canonicalization = "relaxed/relaxed" +#expire = "10d" +#third-party = "" +#third-party-algo = "" +#auid = "" +set-body-length = false +report = true + +[remote."lmtp"] +address = "127.0.0.1" +port = 11200 +protocol = "lmtp" +concurrency = 10 +timeout = "1m" + +[remote."lmtp".tls] +implicit = false +allow-invalid-certs = true + +#[remote."lmtp".auth] +#username = "" +#secret = "" + +[sieve] +from-name = "Automated Message" +from-addr = "no-reply@mail.williamdes.eu.org" +return-path = "" +#hostname = "mail-server.mail.williamdes.eu.org" +sign = ["rsa"] + +[sieve.limits] +redirects = 3 +out-messages = 5 +received-headers = 50 +cpu = 10000 +nested-includes = 5 +duplicate-expiry = "7d" + +[sieve.scripts] +# Note: These scripts are included here for demonstration purposes. +# They should not be used in their current form. +connect = ''' + require ["variables", "extlists", "reject"]; + + if string :list "${env.remote_ip}" "ldap/blocked-ips" { + reject "Your IP '${env.remote_ip}' is not welcomed here."; + } +''' +ehlo = ''' + require ["variables", "extlists", "reject"]; + + if string :list "${env.helo_domain}" "ldap/blocked-domains" { + reject "551 5.1.1 Your domain '${env.helo_domain}' has been blacklisted."; + } +''' +mail = ''' + require ["variables", "envelope", "reject"]; + + if envelope :localpart :is "from" "known_spammer" { + reject "We do not accept SPAM."; + } +''' +rcpt = ''' + require ["variables", "vnd.stalwart.expressions", "envelope", "reject"]; + + set "triplet" "${env.remote_ip}.${envelope.from}.${envelope.to}"; + + if eval "!query('sql', 'SELECT 1 FROM greylist WHERE addr=? LIMIT 1', [triplet])" { + eval "query('sql', 'INSERT INTO greylist (addr) VALUES (?)', [triplet])"; + reject "422 4.2.2 Greylisted, please try again in a few moments."; + } +''' +data = ''' + require ["envelope", "variables", "replace", "mime", "foreverypart", "editheader", "extracttext"]; + + if envelope :domain :is "to" "foobar.net" { + set "counter" "a"; + foreverypart { + if header :mime :contenttype "content-type" "text/html" { + extracttext :upper "text_content"; + replace "${text_content}"; + } + set :length "part_num" "${counter}"; + addheader :last "X-Part-Number" "${part_num}"; + set "counter" "${counter}a"; + } + } +''' + +[management] +directory = "ldap" diff --git a/docker-compose.yml b/docker-compose.yml index 9f6fa7e..fc10877 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: mailserver: - image: mailserver/docker-mailserver:12.1.0 + image: ${IMAGE_TAG:-ghcr.io/datacenters-network/mails/mailserver:latest} mem_limit: 4G mem_reservation: 2G restart: on-failure:40 @@ -23,123 +23,63 @@ services: - smtp.${DOMAIN_NAME} - pop.${DOMAIN_NAME} ports: - - "25:25" # SMTP (explicit TLS => STARTTLS) - - "143:143" # IMAP4 (explicit TLS => STARTTLS) - - "465:465" # ESMTP (implicit TLS) - - "587:587" # ESMTP (explicit TLS => STARTTLS) - - "993:993" # IMAP4 (implicit TLS) - - "110:110" # POP3 - - "995:995" # POP3 (with TLS) - - "4190:4190" # Sieve filters - - "4177:4177" # Dovecot replication - - "11334:11334" # Rspamd UI - - "11332:11332" # Rspamd proxy worker + - "25:25" # -> SMTP (explicit TLS => STARTTLS) + - "143:143" # -> IMAP4 (explicit TLS => STARTTLS) + - "465:465" # -> ESMTP (implicit TLS) + - "587:587" # -> ESMTP (explicit TLS => STARTTLS) + - "993:993" # -> IMAP4 (implicit TLS) + #- "110:110" # POP3 + #- "995:995" # POP3 (with TLS) + - "4190:4190" # -> Sieve filters + - "8080:8080" # JMAP + volumes: - - ${MAIL_DATA_DIR}:/var/mail - - ${MAIL_STATE_DIR}:/var/mail-state + - ${MAILSERVER_DATA_DIR}:/opt/stalwart-mail + - ./config.toml:/etc/config.toml:ro + - ${MAILSERVER_KEYS_DIR}:/etc/opendkim/keys/ + #- ${MAIL_DATA_DIR}:/var/mail + #- ${MAIL_STATE_DIR}:/var/mail-state - ${MAIL_LOGS_DIR}:/var/log/mail - - ${MAIL_CONFIGS_DIR}:/tmp/docker-mailserver/ - - ${USER_PATCHES_FILE:-./user-patches.sh}:/tmp/docker-mailserver/user-patches.sh:ro + #- ${MAIL_CONFIGS_DIR}:/tmp/docker-mailserver/ + # TODO: delete the file + #- ${USER_PATCHES_FILE:-./user-patches.sh}:/tmp/docker-mailserver/user-patches.sh:ro - ${ACME_HOME_DIR}/${DOMAIN_NAME}/fullchain.cer:/etc/ssl/mail-server/server.cer:ro - ${ACME_HOME_DIR}/${DOMAIN_NAME}/${DOMAIN_NAME}.key:/etc/ssl/mail-server/server.key:ro - - ${DOVECOT_REPLICATION_SSL_VOLUME:-./empty-volume}:/etc/ssl/replication/:ro - - ./rspamd/local.d/phishing.conf:/etc/rspamd/local.d/phishing.conf:ro - - ./rspamd/local.d/mx_check.conf:/etc/rspamd/local.d/mx_check.conf:ro + # TODO: delete + #- ${DOVECOT_REPLICATION_SSL_VOLUME:-./empty-volume}:/etc/ssl/replication/:ro + #- ./rspamd/local.d/phishing.conf:/etc/rspamd/local.d/phishing.conf:ro + #- ./rspamd/local.d/mx_check.conf:/etc/rspamd/local.d/mx_check.conf:ro # See: https://github.com/docker-mailserver/docker-mailserver/issues/2636 - - ./rspamd/local.d/dmarc-reports.conf:/etc/rspamd/local.d/dmarc-reports.conf:ro - - ./rspamd/local.d/actions.conf:/etc/rspamd/local.d/actions.conf:ro - - ./rspamd/local.d/milter_headers.conf:/etc/rspamd/local.d/milter_headers.conf:ro - - ./rspamd/local.d/groups.conf:/etc/rspamd/local.d/groups.conf:ro - - ./rspamd/local.d/multimap.conf:/etc/rspamd/local.d/multimap.conf:ro - - ./rspamd/local.d/worker-controller.inc:/etc/rspamd/local.d/worker-controller.inc:ro - - ./rspamd/local.d/dkim_signing.conf:/etc/rspamd/local.d/dkim_signing.conf:ro - - ./rspamd/local.d/history_redis.conf:/etc/rspamd/local.d/history_redis.conf:ro - - ./rspamd/local.d/settings.conf:/etc/rspamd/local.d/settings.conf:ro - - ./rspamd/local.d/rbl.conf:/etc/rspamd/local.d/rbl.conf:ro + #- ./rspamd/local.d/dmarc-reports.conf:/etc/rspamd/local.d/dmarc-reports.conf:ro + #- ./rspamd/local.d/actions.conf:/etc/rspamd/local.d/actions.conf:ro + #- ./rspamd/local.d/milter_headers.conf:/etc/rspamd/local.d/milter_headers.conf:ro + #- ./rspamd/local.d/groups.conf:/etc/rspamd/local.d/groups.conf:ro + #- ./rspamd/local.d/multimap.conf:/etc/rspamd/local.d/multimap.conf:ro + #- ./rspamd/local.d/worker-controller.inc:/etc/rspamd/local.d/worker-controller.inc:ro + #- ./rspamd/local.d/dkim_signing.conf:/etc/rspamd/local.d/dkim_signing.conf:ro + #- ./rspamd/local.d/history_redis.conf:/etc/rspamd/local.d/history_redis.conf:ro + #- ./rspamd/local.d/settings.conf:/etc/rspamd/local.d/settings.conf:ro + #- ./rspamd/local.d/rbl.conf:/etc/rspamd/local.d/rbl.conf:ro # maps - - ./rspamd/maps.d/mx_whitelist.inc:/etc/rspamd/maps.d/mx_whitelist.inc:ro - - ./rspamd/maps.d/local_wl_domain.map:/etc/rspamd/maps.d/local_wl_domain.map:ro - - ./rspamd/maps.d/local_wl_from.map:/etc/rspamd/maps.d/local_wl_from.map:ro - - ./cron/rspamd-learn:/etc/cron.d/rspamd-learn:ro - - ${SASLAUTHD_SOCKET_FILE}:/var/run/saslauthd/mux + #- ./rspamd/maps.d/mx_whitelist.inc:/etc/rspamd/maps.d/mx_whitelist.inc:ro + #- ./rspamd/maps.d/local_wl_domain.map:/etc/rspamd/maps.d/local_wl_domain.map:ro + #- ./rspamd/maps.d/local_wl_from.map:/etc/rspamd/maps.d/local_wl_from.map:ro + #- ./cron/rspamd-learn:/etc/cron.d/rspamd-learn:ro environment: TZ: UTC - OVERRIDE_HOSTNAME: ${OVERRIDE_HOSTNAME} - ENABLE_RSPAMD: "1" - ENABLE_RSPAMD_REDIS: "1" - ENABLE_CLAMAV: "1" - ENABLE_AMAVIS: "1" - ENABLE_FAIL2BAN: "1" - ENABLE_POSTGREY: "0" - ENABLE_POP3: "1" - ENABLE_MANAGESIEVE: "1" - ENABLE_SASLAUTHD: "0" # The docker container manages it - ENABLE_OPENDKIM: "1" - ENABLE_OPENDMARC: "1" - ENABLE_QUOTAS: "1" - SPOOF_PROTECTION: "1" - LOGROTATE_INTERVAL: "daily" - ONE_DIR: "1" - LOG_LEVEL: "${LOG_LEVEL:-warn}" - ACCOUNT_PROVISIONER: "LDAP" - RSPAMD_LEARN: "1" - RSPAMD_GREYLISTING: "1" - MOVE_SPAM_TO_JUNK: "1" - # Now it's up to RSPAMD_GREYLISTING to greylist - # POSTGREY_DELAY: "${POSTGREY_DELAY:-300}" - # POSTGREY_AUTO_WHITELIST_CLIENTS: "1" # Defaults to 5 - LDAP_SERVER_HOST: "${LDAP_PROTOCOL:-ldaps}://ldap.${DOMAIN_NAME}" # Must match the hostname for SSL verification - DOVECOT_URIS: "${LDAP_PROTOCOL:-ldaps}://ldap.${DOMAIN_NAME}" # Must match the hostname for SSL verification + + OAUTH_KEY: "CHANGE-ME" + + SMTP_GREETING: "Datacenters Network emails" + DKIM_SELECTOR: "mail" + + LDAP_SERVER_HOST: "ldap://ldap.${DOMAIN_NAME}" # Must match the hostname for SSL verification LDAP_SEARCH_BASE: "ou=people,${LDAP_BASE_DN}" LDAP_BIND_DN: "cn=admin,${LDAP_BASE_DN}" LDAP_BIND_PW: ${LDAP_ADMIN_PASSWORD} - LDAP_QUERY_FILTER_USER: "(&(mail=%s)(mailEnabled=TRUE))" - LDAP_QUERY_FILTER_GROUP: "(&(mailGroupMember=%s)(mailEnabled=TRUE))" - LDAP_QUERY_FILTER_ALIAS: "(|(&(mailAlias=%s)(objectClass=PostfixBookMailAccount))(&(mailAlias=%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE)))" - LDAP_QUERY_FILTER_DOMAIN: "(|(&(mail=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailGroupMember=*@%s)(objectClass=PostfixBookMailAccount)(mailEnabled=TRUE))(&(mailalias=*@%s)(objectClass=PostfixBookMailAccount)))" - DOVECOT_PASS_FILTER: "(&(objectClass=PostfixBookMailAccount)(mail=%n@%d))" - DOVECOT_USER_FILTER: "(&(objectClass=PostfixBookMailAccount)(mail=%n@%d))" - DOVECOT_DEFAULT_PASS_SCHEME: "SSHA256" - TLS_LEVEL: "intermediate" - POSTFIX_MESSAGE_SIZE_LIMIT: "100000000" - # Replication - DOVECOT_ITERATE_ATTRS: "mail=user" - DOVECOT_ITERATE_FILTER: "(objectClass=PostfixBookMailAccount)" - # Custom ENVS: DOVECOT_REPLICATION_ - DOVECOT_REPLICATION_SERVER: "${DOVECOT_REPLICATION_SERVER}" - DOVECOT_REPLICATION_ADM_PASS: "${DOVECOT_REPLICATION_ADM_PASS}" - DOVECOT_REPLICATION_SSL_CA_FILE: "${DOVECOT_REPLICATION_SSL_CA_FILE}" - DOVECOT_REPLICATION_SSL_CA_DIR: "${DOVECOT_REPLICATION_SSL_CA_DIR}" - DOVECOT_REPLICATION_SSL_CERT_FILE: "${DOVECOT_REPLICATION_SSL_CERT_FILE}" - DOVECOT_REPLICATION_SSL_KEY_FILE: "${DOVECOT_REPLICATION_SSL_KEY_FILE}" - - SSL_TYPE: "manual" - SSL_CERT_PATH: "/etc/ssl/mail-server/server.cer" - SSL_KEY_PATH: "/etc/ssl/mail-server/server.key" - # Daily report & reporting - # See: https://sources.debian.org/src/cron/3.0pl1-163/debian/patches/features/Add-MAILFROM-environment-variable.patch/?hl=93#L93 - MAILFROM: "${CRON_MAILFROM:-$POSTMASTER_ADDRESS}" - MAILTO: "${CRON_MAILTO:-$POSTMASTER_ADDRESS}" - POSTMASTER_ADDRESS: "${POSTMASTER_ADDRESS}" - VIRUS_ADMIN_EMAIL: "${VIRUS_ADMIN_EMAIL}" - VIRUS_X_HEADER_LINE: "${VIRUS_X_HEADER_LINE}" - PFLOGSUMM_RECIPIENT: "${PFLOGSUMM_RECIPIENT}" - PFLOGSUMM_TRIGGER: daily_cron - LOGWATCH_INTERVAL: daily - LOGWATCH_RECIPIENT: "${LOGWATCH_RECIPIENT}" - # Fail2ban config to allow IPs to make failed attempts - FAIL2BAN_IGNORE_IPS: "${FAIL2BAN_IGNORE_IPS}" - FAIL2BAN_DST_EMAIL: "${FAIL2BAN_DST_EMAIL}" - FAIL2BAN_SENDER_EMAIL: "${FAIL2BAN_SENDER_EMAIL}" - FAIL2BAN_SENDER_NAME: "${FAIL2BAN_SENDER_NAME}" - - # Fail2ban reporting - FAIL2BAN_BLOCKLIST_DE_API_KEY: "${FAIL2BAN_BLOCKLIST_DE_API_KEY:-}" - FAIL2BAN_BLOCKLIST_DE_EMAIL: "${FAIL2BAN_BLOCKLIST_DE_EMAIL:-}" - FAIL2BAN_IPTHREAT_API_KEY: "${FAIL2BAN_IPTHREAT_API_KEY:-}" - FAIL2BAN_IPTHREAT_SYSTEM_NAME: "${FAIL2BAN_IPTHREAT_SYSTEM_NAME:-}" - FAIL2BAN_ABUSEIPDB_API_KEY: "${FAIL2BAN_ABUSEIPDB_API_KEY:-}" + SSL_CERT_PATH: "file:///etc/ssl/mail-server/server.cer" + SSL_KEY_PATH: "file:///etc/ssl/mail-server/server.key" healthcheck: test: "ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1" @@ -154,26 +94,26 @@ services: # netcap: https://stackoverflow.com/a/35485119/5155484 # getpcaps # https://book.hacktricks.xyz/linux-hardening/privilege-escalation/linux-capabilities - cap_add: - - NET_ADMIN # fail2ban, spamd - - NET_RAW # spamd - - NET_BIND_SERVICE # dovecot, spamd - - CHOWN # dovecot, spamd - - DAC_OVERRIDE # dovecot, spamd - - SYS_CHROOT # dovecot, spamd - - SETUID # dovecot, spamd - - SETGID # dovecot, spamd - - KILL # dovecot, spamd - - FOWNER # spamd - - FSETID # spamd - - SETPCAP # spamd - - MKNOD # spamd - - AUDIT_WRITE # spamd - - SETFCAP # spamd + #cap_add: + # - NET_ADMIN # fail2ban, spamd + # - NET_RAW # spamd + # - NET_BIND_SERVICE # dovecot, spamd + # - CHOWN # dovecot, spamd + # - DAC_OVERRIDE # dovecot, spamd + # - SYS_CHROOT # dovecot, spamd + # - SETUID # dovecot, spamd + # - SETGID # dovecot, spamd + # - KILL # dovecot, spamd + # - FOWNER # spamd + # - FSETID # spamd + # - SETPCAP # spamd + # - MKNOD # spamd + # - AUDIT_WRITE # spamd + # - SETFCAP # spamd security_opt: - no-new-privileges:true - cap_drop: - - ALL + #cap_drop: + # - ALL openldap: image: docker.io/botsudo/docker-openldap @@ -195,6 +135,8 @@ services: retries: 3 networks: mail_infra_network: + aliases: + - openldap.local environment: # 256 to enable debug # See: https://www.openldap.org/doc/admin24/slapdconf2.html @@ -223,7 +165,8 @@ services: - ${ACME_HOME_DIR}/${DOMAIN_NAME}/ca.cer:/container/service/slapd/assets/certs/ca.cer:ro - ${ACME_HOME_DIR}/${DOMAIN_NAME}/fullchain.cer:/container/service/slapd/assets/certs/server.cer:ro - ${ACME_HOME_DIR}/${DOMAIN_NAME}/${DOMAIN_NAME}.key:/container/service/slapd/assets/certs/server.key:ro - - ${SASLAUTHD_SOCKET_FILE}:/var/run/saslauthd/mux + # TODO: remove it + #- ${SASLAUTHD_SOCKET_FILE}:/var/run/saslauthd/mux ports: - "${LDAP_PORT:-389}:389" - "${LDAPS_PORT:-636}:636" @@ -309,32 +252,32 @@ services: security_opt: - no-new-privileges:true - crowdsec: - image: crowdsecurity/crowdsec:v1.5.4 - dns_search: "" - restart: always - environment: - DISABLE_LOCAL_API: "true" - #DISABLE_SCENARIOS: "crowdsecurity/ssh-bf crowdsecurity/ssh-slow-bf" - AGENT_USERNAME: "${CROWDSEC_AGENT_USERNAME}" - AGENT_PASSWORD: "${CROWDSEC_AGENT_PASSWORD}" - LOCAL_API_URL: "${CROWDSEC_AGENT_LOCAL_API_URL}" - CUSTOM_HOSTNAME: emails - depends_on: - mailserver: - condition: service_healthy - volumes: - - ${CROWDSEC_DB_DIR}:/var/lib/crowdsec/data/ - - ${CROWDSEC_CONFIG_DIR}:/etc/crowdsec/ - - "${MAILS_LOG_DIR}:/var/log/mails:ro" - - ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro - - ./crowdsec/profiles.yaml:/etc/crowdsec/profiles.yaml:ro - - ./crowdsec/scenarios/postfix-dovecot-hackers.yaml:/etc/crowdsec/scenarios/postfix-dovecot-hackers.yaml:ro - - ./crowdsec/parsers/postfix-extended.yaml:/etc/crowdsec/parsers/s01-parse/postfix-extended.yaml:ro - - ./crowdsec/parsers/dovecot-extended.yaml:/etc/crowdsec/parsers/s01-parse/dovecot-extended.yaml:ro - - ./crowdsec/notifications/telegram.yaml:/etc/crowdsec/notifications/http.yaml:ro - networks: - mail_infra_network: + #crowdsec: + # image: crowdsecurity/crowdsec:v1.5.4 + # dns_search: "" + # restart: always + # environment: + # DISABLE_LOCAL_API: "true" + # #DISABLE_SCENARIOS: "crowdsecurity/ssh-bf crowdsecurity/ssh-slow-bf" + # AGENT_USERNAME: "${CROWDSEC_AGENT_USERNAME}" + # AGENT_PASSWORD: "${CROWDSEC_AGENT_PASSWORD}" + # LOCAL_API_URL: "${CROWDSEC_AGENT_LOCAL_API_URL}" + # CUSTOM_HOSTNAME: emails + # depends_on: + # mailserver: + # condition: service_healthy + # volumes: + # - ${CROWDSEC_DB_DIR}:/var/lib/crowdsec/data/ + # - ${CROWDSEC_CONFIG_DIR}:/etc/crowdsec/ + # - "${MAILS_LOG_DIR}:/var/log/mails:ro" + # - ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml:ro + # - ./crowdsec/profiles.yaml:/etc/crowdsec/profiles.yaml:ro + # - ./crowdsec/scenarios/postfix-dovecot-hackers.yaml:/etc/crowdsec/scenarios/postfix-dovecot-hackers.yaml:ro + # - ./crowdsec/parsers/postfix-extended.yaml:/etc/crowdsec/parsers/s01-parse/postfix-extended.yaml:ro + # - ./crowdsec/parsers/dovecot-extended.yaml:/etc/crowdsec/parsers/s01-parse/dovecot-extended.yaml:ro + # - ./crowdsec/notifications/telegram.yaml:/etc/crowdsec/notifications/http.yaml:ro + # networks: + # mail_infra_network: networks: mail_infra_network: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..e82ff05 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,57 @@ +# See: https://github.com/stalwartlabs/mail-server/pull/85 +FROM docker.io/debian:bookworm-slim AS source + +WORKDIR /source + +ARG VERSION="0.3.10" +ADD https://github.com/stalwartlabs/mail-server/archive/refs/tags/v$VERSION.tar.gz /tmp/v$VERSION.tar.gz +RUN tar --strip-components=1 -C /source -xzf /tmp/v$VERSION.tar.gz + +FROM --platform=$BUILDPLATFORM docker.io/lukemathwalker/cargo-chef:latest-rust-slim-bookworm AS chef +WORKDIR /build + +FROM --platform=$BUILDPLATFORM chef AS planner +COPY --from=source /source . +RUN cargo chef prepare --recipe-path /recipe.json + +FROM --platform=$BUILDPLATFORM chef AS builder +ARG TARGETPLATFORM +RUN case "${TARGETPLATFORM}" in \ + "linux/arm64") echo "aarch64-unknown-linux-gnu" > /target.txt && echo "-C linker=aarch64-linux-gnu-gcc" > /flags.txt ;; \ + "linux/amd64") echo "x86_64-unknown-linux-gnu" > /target.txt && echo "-C linker=x86_64-linux-gnu-gcc" > /flags.txt ;; \ + *) exit 1 ;; \ + esac +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt-get update && \ + apt-get install -yq build-essential \ + g++-aarch64-linux-gnu binutils-aarch64-linux-gnu +RUN rustup target add "$(cat /target.txt)" +COPY --from=planner /recipe.json /recipe.json +RUN RUSTFLAGS="$(cat /flags.txt)" cargo chef cook --target "$(cat /target.txt)" --release --recipe-path /recipe.json +COPY --from=source /source . +RUN RUSTFLAGS="$(cat /flags.txt)" cargo build --target "$(cat /target.txt)" --release -p mail-server -p stalwart-cli -p stalwart-install +RUN mv "/build/target/$(cat /target.txt)/release" "/output" + +FROM docker.io/debian:bookworm-slim +ENV STALWART_COMPONENT=all-in-one +WORKDIR /opt/stalwart-mail +RUN export DEBIAN_FRONTEND=noninteractive && \ + apt-get update && \ + apt-get install -yq ca-certificates +COPY --from=builder /output/stalwart-mail /usr/local/bin +COPY --from=builder /output/stalwart-cli /usr/local/bin +COPY --from=builder /output/stalwart-install /usr/local/bin +COPY ./entrypoint.sh /usr/local/bin +COPY ./configure.sh /usr/local/bin +RUN chmod -R 755 /usr/local/bin +CMD ["/usr/local/bin/stalwart-mail"] + +# Get the config +# Run: docker run --rm -it --entrypoint /bin/bash docker-mailserver +# /usr/local/bin/configure.sh +# cat /opt/stalwart-mail/etc/config.toml + +# Supports ENVs in the form !ENV_NAME +# See: https://github.com/stalwartlabs/mail-server/blob/v0.3.10/crates/utils/src/config/parser.rs#L381 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh", "/etc/config.toml"] diff --git a/docker/configure.sh b/docker/configure.sh new file mode 100755 index 0000000..ff72775 --- /dev/null +++ b/docker/configure.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +# shellcheck shell=dash + +set -eu + +exec /usr/local/bin/stalwart-install --docker --component "$STALWART_COMPONENT" --path /opt/stalwart-mail "$@" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..3f823d6 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh +# shellcheck shell=dash + +set -eu + +CONFIG="$1" +shift + +# If the configuration file does not exist wait until it does. +while [ ! -f "${CONFIG}" ]; do + sleep 1 +done + +# If the configuration file exists, start the server. +exec "$@" --config "${CONFIG}" diff --git a/tests/.env.test1 b/tests/.env.test1 index 5f6eac6..bad86de 100755 --- a/tests/.env.test1 +++ b/tests/.env.test1 @@ -8,10 +8,12 @@ LDAP_MONITOR_PASSWORD=PasswordLdapMonitor DOCKER_LDAP_HEALTHCHECK_USERNAME=monitor DOCKER_LDAP_HEALTHCHECK_PASSWORD=PasswordLdapMonitor -MAIL_DATA_DIR=./tests/data/maildata +MAILSERVER_DATA_DIR=./tests/data/maildata MAIL_STATE_DIR=./tests/data/mailstate MAIL_LOGS_DIR=./tests/data/maillogs MAIL_CONFIGS_DIR=./tests/data/mailconfig +MAILSERVER_KEYS_DIR=./tests/data/mailconfig/opendkim/keys/ + LDAP_DATA_DIR=./tests/data/ldapdata ACME_HOME_DIR=./tests/data/acme.sh SASLAUTHD_SOCKET_FILE=./tests/data/saslauthdsocket