├── .dockerignore ├── .github └── workflows │ └── testimage.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── ROADMAP.md ├── bld.mk ├── demo ├── .env ├── Makefile ├── ad.mk ├── dkr.mk ├── docker-compose.yml ├── ssl.mk └── ssl │ └── .gitignore ├── hooks └── build ├── src ├── acme │ ├── bin │ │ └── acme-extract.sh │ └── entry.d │ │ ├── 10-acme-common │ │ └── 50-acme-monitor-tlscert ├── docker │ ├── bin │ │ ├── docker-common.sh │ │ ├── docker-config.sh │ │ ├── docker-entrypoint.sh │ │ ├── docker-runfunc.sh │ │ ├── docker-service.sh │ │ └── run │ └── entry.d │ │ ├── 20-docker-print-versions │ │ ├── 50-docker-update-loglevel │ │ └── 80-docker-lock-config ├── dovecot │ └── entry.d │ │ ├── 10-dovecot-common │ │ ├── 50-dovecot-config │ │ └── 61-dovecot-config-late └── postfix │ └── entry.d │ ├── 10-postfix-common │ ├── 30-postfix-migrate │ ├── 40-postfix-config-early │ └── 60-postfix-config-late └── test ├── Makefile ├── acme └── .gitignore ├── ad.mk ├── bin ├── gen-acme-json.sh └── test-smtp.sh ├── bld.mk ├── dkr.mk ├── ssl.mk └── ssl └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .github 4 | .gitattributes 5 | src/notused 6 | local 7 | demo 8 | test 9 | -------------------------------------------------------------------------------- /.github/workflows/testimage.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image Makefile CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install dependencies 16 | run: sudo apt-get install -y ldap-utils 17 | 18 | - name: Build docker images 19 | run: make build-all 20 | 21 | - name: Run tests 22 | shell: 'script -q -e -c "bash {0}"' 23 | run: | 24 | make test-all 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *private 2 | local* 3 | !local.d 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | os: linux 3 | dist: jammy 4 | services: docker 5 | install: make build-all 6 | before_script: sudo apt-get install -y ldap-utils openssl jq 7 | script: 8 | - make test-all 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.6 2 | 3 | - [docker](Makefile) Use alpine:3.21 (postfix:3.9.1 dovecot:2.3.21.1). 4 | 5 | # 1.0.5 6 | 7 | - [docker](Makefile) Use alpine:3.20 (postfix:3.9.0 dovecot:2.3.21.1). 8 | - [github](.github/workflows/testimage.yml) Now use Node.js 20 `actions/checkout@v4`. 9 | - [demo](demo) Fixed target `*-apk_list`. 10 | 11 | # 1.0.4 12 | 13 | - [docker](Makefile) Use alpine:3.19 (postfix:3.8.6 dovecot:2.3.21). 14 | - [demo](demo/docker-compose.yml) Remove obsolete element `version` in docker-compose.yml. 15 | 16 | # 1.0.3 17 | 18 | - [docker](Makefile) Use alpine:3.19 (postfix:3.8.3 dovecot:2.3.21). 19 | 20 | # 1.0.2 21 | 22 | - [docker](Makefile) Use alpine:3.18 (postfix:3.8.3 dovecot:2.3.20). 23 | - [docker](src/docker) Improve debug message in [docker-service.sh](src/docker/bin/docker-service.sh). 24 | - [repo](README.md) Added section on Authentication (SASL) Mechanisms. 25 | 26 | # 1.0.1 27 | 28 | - [docker](Makefile) Use alpine:3.18 (postfix:3.8.1 dovecot:2.3.20). 29 | - [test](test/Makefile) Now use the `mariadb` instead of `mysql` command in MariaDB image. 30 | - [test](demo/Makefile) Now use the `mariadb-show` instead of `mysqlshow` command in MariaDB image. 31 | 32 | # 1.0.0 33 | 34 | - [docker](Makefile) Use alpine:3.18 (postfix:3.8.0 dovecot:2.3.20). 35 | - [github](.github/workflows/testimage.yml) Now use GitHub Actions to test image. 36 | - [demo](demo/Makefile) Now depend on the `docker-compose-plugin`. 37 | - [demo](demo/Makefile) Fix the broken `-diff` target. 38 | - [dovecot](src/dovecot/entry.d/10-dovecot-common) Now support both PLAIN and the legacy LOGIN authentication (SASL) mechanisms. 39 | - [repo](.) Based on [mlan/postfix-amavis](https://github.com/mlan/docker-postfix). 40 | - [test](test) Cleanup tests. 41 | - [test](test/Makefile) Increase sleep time `TST_W8DB` from 40 to 80 for travis-ci. 42 | - [repo](Makefile) Now use functions in `bld.mk`. 43 | - [repo](README.md) Updated the `docker-compose.yml` example. 44 | - [repo](README.md) Added section on Milter support. 45 | - [demo](demo/Makefile) Monitor logs to determine when clamd is activated. 46 | - [test](.travis.yml) Updated dist to jammy. 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG DIST=alpine 2 | ARG REL=latest 3 | 4 | 5 | # 6 | # 7 | # target: base 8 | # 9 | # postfix only 10 | # 11 | # 12 | 13 | FROM $DIST:$REL AS base 14 | LABEL maintainer=mlan 15 | 16 | ENV SVDIR=/etc/service \ 17 | DOCKER_PERSIST_DIR=/srv \ 18 | DOCKER_BIN_DIR=/usr/local/bin \ 19 | DOCKER_ENTRY_DIR=/etc/docker/entry.d \ 20 | DOCKER_SSL_DIR=/etc/ssl \ 21 | DOCKER_SPOOL_DIR=/var/spool/postfix \ 22 | DOCKER_CONF_DIR=/etc/postfix \ 23 | DOCKER_DIST_DIR=/etc/postfix.dist \ 24 | DOCKER_MAIL_LIB=/var/mail \ 25 | DOCKER_IMAP_DIR=/etc/dovecot \ 26 | DOCKER_IMAPDIST_DIR=/etc/dovecot.dist \ 27 | DOCKER_UNLOCK_FILE=/srv/etc/.docker.unlock \ 28 | DOCKER_APPL_RUNAS=postfix \ 29 | DOCKER_IMAP_RUNAS=dovecot \ 30 | ACME_POSTHOOK="postfix reload" \ 31 | SYSLOG_LEVEL=5 \ 32 | SYSLOG_OPTIONS=-SDt 33 | ENV DOCKER_ACME_SSL_DIR=$DOCKER_SSL_DIR/acme \ 34 | DOCKER_APPL_SSL_DIR=$DOCKER_SSL_DIR/postfix \ 35 | DOCKER_IMAP_PASSDB_FILE=$DOCKER_IMAP_DIR/virt-passwd 36 | 37 | # 38 | # Copy utility scripts including docker-entrypoint.sh to image 39 | # 40 | 41 | COPY src/*/bin $DOCKER_BIN_DIR/ 42 | COPY src/*/entry.d $DOCKER_ENTRY_DIR/ 43 | 44 | # 45 | # Install 46 | # 47 | # Arrange persistent directories at /srv. 48 | # Configure Runit, a process manager. 49 | # Make postfix trust smtp clients on the same subnet, 50 | # i.e., containers on the same network. 51 | # 52 | 53 | RUN source docker-common.sh \ 54 | && source docker-config.sh \ 55 | && dc_persist_dirs \ 56 | $DOCKER_APPL_SSL_DIR \ 57 | $DOCKER_AV_DIR \ 58 | $DOCKER_AV_LIB \ 59 | $DOCKER_CONF_DIR \ 60 | $DOCKER_IMAP_DIR \ 61 | $DOCKER_MAIL_LIB \ 62 | $DOCKER_MILT_DIR \ 63 | $DOCKER_MILT_LIB \ 64 | $DOCKER_DB_DIR \ 65 | $DOCKER_DB_LIB \ 66 | $DOCKER_SPOOL_DIR \ 67 | && mkdir -p $DOCKER_ACME_SSL_DIR \ 68 | && apk --no-cache --update add \ 69 | runit \ 70 | postfix \ 71 | postfix-ldap \ 72 | postfix-mysql \ 73 | postsrsd \ 74 | cyrus-sasl-login \ 75 | && cp -rlL $DOCKER_CONF_DIR $DOCKER_DIST_DIR \ 76 | && docker-service.sh \ 77 | "syslogd -nO- -l$SYSLOG_LEVEL $SYSLOG_OPTIONS" \ 78 | "crond -f -c /etc/crontabs" \ 79 | "postfix start-fg" \ 80 | && chown ${DOCKER_APPL_RUNAS}: ${DOCKER_PERSIST_DIR}$DOCKER_MAIL_LIB \ 81 | && mv $DOCKER_CONF_DIR/aliases $DOCKER_CONF_DIR/aliases.dist \ 82 | && postconf -e mynetworks_style=subnet \ 83 | && echo "This file unlocks the configuration, so it will be deleted after initialization." > $DOCKER_UNLOCK_FILE 84 | 85 | # 86 | # state standard smtp, smtps and submission ports 87 | # 88 | 89 | EXPOSE 25 465 587 90 | 91 | # 92 | # Rudimentary healthcheck 93 | # 94 | 95 | HEALTHCHECK CMD sv status ${SVDIR}/* && postfix status 96 | 97 | # 98 | # Entrypoint, how container is run 99 | # 100 | 101 | ENTRYPOINT ["docker-entrypoint.sh"] 102 | 103 | # 104 | # Have runit's runsvdir start all services 105 | # 106 | 107 | CMD runsvdir -P ${SVDIR} 108 | 109 | 110 | # 111 | # 112 | # target: full 113 | # 114 | # Install dovecot, a IMAP server 115 | # 116 | # 117 | 118 | FROM base AS full 119 | 120 | # 121 | # Install 122 | # 123 | # Remove private key that dovecot creates. 124 | # 125 | 126 | RUN apk --no-cache --update add \ 127 | dovecot \ 128 | dovecot-ldap \ 129 | dovecot-mysql \ 130 | dovecot-lmtpd \ 131 | dovecot-pop3d \ 132 | jq \ 133 | && docker-service.sh "dovecot -F" \ 134 | && rm -f /etc/ssl/dovecot/* \ 135 | && mkdir -p $DOCKER_IMAPDIST_DIR \ 136 | && mv -f $DOCKER_IMAP_DIR/* $DOCKER_IMAPDIST_DIR \ 137 | && addgroup $DOCKER_APPL_RUNAS $DOCKER_IMAP_RUNAS \ 138 | && addgroup $DOCKER_IMAP_RUNAS $DOCKER_APPL_RUNAS 139 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 mlan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | # 3 | # build 4 | # 5 | 6 | -include *.mk 7 | 8 | BLD_ARG ?= --build-arg DIST=alpine --build-arg REL=3.21 9 | BLD_REPO ?= mlan/postfix 10 | BLD_VER ?= latest 11 | BLD_TGT ?= full 12 | BLD_TGTS ?= base full 13 | BLD_CMT ?= HEAD 14 | 15 | TST_REPO ?= $(BLD_REPO) 16 | TST_VER ?= $(BLD_VER) 17 | TST_ENV ?= -C test 18 | TST_TGTE ?= $(addprefix test-,all diff down env htop imap logs mail mail-send pop3 sh sv up) 19 | TST_INDX ?= 1 2 3 4 5 20 | TST_TGTI ?= $(addprefix test_,$(TST_INDX)) $(addprefix test-up_,0 $(TST_INDX)) 21 | 22 | export TST_REPO TST_VER 23 | 24 | push: 25 | # 26 | # PLEASE REVIEW THESE IMAGES WHICH ARE ABOUT TO BE PUSHED TO THE REGISTRY 27 | # 28 | @docker image ls $(BLD_REPO) 29 | # 30 | # ARE YOU SURE YOU WANT TO PUSH THESE IMAGES TO THE REGISTRY? [yN] 31 | @read input; [ "$${input}" = "y" ] 32 | docker push --all-tags $(BLD_REPO) 33 | 34 | build-all: $(addprefix build_,$(BLD_TGTS)) 35 | 36 | build: build_$(BLD_TGT) 37 | 38 | build_%: Dockerfile 39 | docker build $(BLD_ARG) --target $* \ 40 | $(addprefix --tag $(BLD_REPO):,$(call bld_tags,$*,$(BLD_VER))) . 41 | 42 | variables: 43 | make -pn | grep -A1 "^# makefile"| grep -v "^#\|^--" | sort | uniq 44 | 45 | ps: 46 | docker ps -a 47 | 48 | prune: 49 | docker image prune -f 50 | 51 | clean: 52 | docker images | grep $(BLD_REPO) | awk '{print $$1 ":" $$2}' | uniq | xargs docker rmi 53 | 54 | $(TST_TGTE): 55 | ${MAKE} $(TST_ENV) $@ 56 | 57 | $(TST_TGTI): 58 | ${MAKE} $(TST_ENV) $@ 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The `mlan/postfix` repository 2 | 3 | ![github action ci](https://img.shields.io/github/actions/workflow/status/mlan/docker-postfix/testimage.yml?label=build&style=flat-square&logo=github) 4 | ![docker version](https://img.shields.io/docker/v/mlan/postfix?logo=docker&style=flat-square) 5 | ![image size](https://img.shields.io/docker/image-size/mlan/postfix/latest.svg?label=size&style=flat-square&logo=docker) 6 | ![docker pulls](https://img.shields.io/docker/pulls/mlan/postfix.svg?label=pulls&style=flat-square&logo=docker) 7 | ![docker stars](https://img.shields.io/docker/stars/mlan/postfix.svg?label=stars&style=flat-square&logo=docker) 8 | ![github stars](https://img.shields.io/github/stars/mlan/docker-postfix.svg?label=stars&style=flat-square&logo=github) 9 | 10 | This (non official) repository provides dockerized (MTA) [Mail Transfer Agent](https://en.wikipedia.org/wiki/Message_transfer_agent) (SMTP) service using [Postfix](http://www.postfix.org/) and [Dovecot](https://www.dovecot.org/). 11 | 12 | ## Features 13 | 14 | - MTA (SMTP) server and client [Postfix](http://www.postfix.org/) 15 | - [SMTP client authentication](#incoming-smtps-and-submission-client-authentication) on the SMTPS (port 465) and submission (port 587) using [Dovecot](https://www.dovecot.org/) 16 | - [PostSRSd](#forwarding-rewrite), sender rewriting scheme 17 | - Hooks for integrating [Let’s Encrypt](#lets-encrypt-lts-certificates-using-traefik) LTS certificates using the reverse proxy [Traefik](https://docs.traefik.io/) 18 | - Consolidated configuration and run data under `/srv` to facilitate [persistent storage](#persistent-storage) 19 | - Simplified configuration of [passwd file](#table-mailbox-lookup) authentication, mailbox lookup using environment variables 20 | - Simplified configuration of [LDAP](#ldap-mailbox-lookup) authentication, mailbox and alias lookup using environment variables 21 | - Simplified configuration of [MySQL](#mysql-mailbox-lookup) authentication, mailbox and alias lookup using environment variables 22 | - Simplified configuration of [remote IMAP](#imap-sasl-client-authentication-smtpd_sasl_imaphost) authentication using environment variables 23 | - Simplified configuration of [SMTP relay](#outgoing-smtp-relay) using environment variables 24 | - Simplified configuration of secure SMTP, [IMAP](#mail-delivery-imap-imaps-pop3-and-pop3s) and [POP3](#mail-delivery-imap-imaps-pop3-and-pop3s) [TLS](#incoming-tls-support) using environment variables 25 | - Simplified generation of Diffie-Hellman parameters needed for [EDH](https://en.wikipedia.org/wiki/Diffie–Hellman_key_exchange) using utility script 26 | - Multi-staged build providing the images  `base` and `full` 27 | - Configuration using [environment variables](#environment-variables) 28 | - [Log](#logging-syslog_level-log_level-sa_debug) directed to docker daemon with configurable level 29 | - Built in utility script `run` helping configuring Postfix and Dovecot 30 | - Makefile which can build images and do some management and testing 31 | - Health check 32 | - Small image size based on [Alpine Linux](https://alpinelinux.org/) 33 | - [Demo](#demo) based on `docker-compose.yml` and `Makefile` files 34 | 35 | ## Tags 36 | 37 | The MAJOR.MINOR.PATCH [SemVer](https://semver.org/) is 38 | used. In addition to the three number version number you can use two or 39 | one number versions numbers, which refers to the latest version of the 40 | sub series. The tag `latest` references the build based on the latest commit to the repository. 41 | 42 | The `mlan/postfix` repository contains a multi staged built. You select which build using the appropriate tag from `base` and `full`. The image `base` only contain Postfix. The image built with the tag `full` extend `base` to include [Dovecot](https://www.dovecot.org/), which provides mail delivery via IMAP and POP3 and SMTP client authentication as well as integration of [Let’s Encrypt](https://letsencrypt.org/) TLS certificates using [Traefik](https://docs.traefik.io/). 43 | 44 | To exemplify the usage of the tags, lets assume that the latest version is `1.0.0`. In this case `latest`, `1.0.0`, `1.0`, `1`, `full`, `full-1.0.0`, `full-1.0` and `full-1` all identify the same image. 45 | 46 | # Usage 47 | 48 | Often you want to configure Postfix and its components. There are different methods available to achieve this. Many aspects can be configured using [environment variables](#environment-variables) described below. These environment variables can be explicitly given on the command line when creating the container. They can also be given in an `docker-compose.yml` file, see the [docker compose example](#docker-compose-example) below. Moreover docker volumes or host directories with desired configuration files can be mounted in the container. And finally you can `docker exec` into a running container and modify configuration files directly. 49 | 50 | You can start a `mlan/postfix` container using the destination domain `example.com` and table mail boxes for info@example.com and abuse@example.com by issuing the shell command below. 51 | 52 | ```bash 53 | docker run -d --name mta --hostname mx1.example.com -e MAIL_BOXES="info@example.com abuse@example.com" -p 127.0.0.1:25:25 mlan/postfix 54 | ``` 55 | 56 | One convenient way to test the image is to clone the [github](https://github.com/mlan/docker-postfix) repository and run the [demo](#demo) therein, see below. 57 | 58 | ## Docker compose example 59 | 60 | An example of how to configure an web mail server using docker compose is given below. It defines 5 services, `app`, `mta`, `filt`, `db` and `auth`, which are the web mail server, the mail transfer agent, the SQL database and LDAP authentication respectively. 61 | 62 | ```yaml 63 | name: demo 64 | 65 | services: 66 | app: 67 | image: mlan/kopano 68 | networks: 69 | - backend 70 | ports: 71 | - "127.0.0.1:8008:80" # WebApp & EAS (alt. HTTP) 72 | - "127.0.0.1:143:143" # IMAP (not needed if all devices can use EAS) 73 | - "127.0.0.1:110:110" # POP3 (not needed if all devices can use EAS) 74 | - "127.0.0.1:8080:8080" # ICAL (not needed if all devices can use EAS) 75 | - "127.0.0.1:993:993" # IMAPS (not needed if all devices can use EAS) 76 | - "127.0.0.1:995:995" # POP3S (not needed if all devices can use EAS) 77 | - "127.0.0.1:8443:8443" # ICALS (not needed if all devices can use EAS) 78 | depends_on: 79 | - auth 80 | - db 81 | - mta 82 | environment: # Virgin config, ignored on restarts unless FORCE_CONFIG given. 83 | - USER_PLUGIN=ldap 84 | - LDAP_URI=ldap://auth:389/ 85 | - MYSQL_HOST=db 86 | - SMTP_SERVER=mta 87 | - LDAP_SEARCH_BASE=${AD_BASE-dc=example,dc=com} 88 | - LDAP_USER_TYPE_ATTRIBUTE_VALUE=${AD_USR_OB-kopano-user} 89 | - LDAP_GROUP_TYPE_ATTRIBUTE_VALUE=${AD_GRP_OB-kopano-group} 90 | - LDAP_GROUPMEMBERS_ATTRIBUTE_TYPE=dn 91 | - LDAP_PROPMAP= 92 | - DAGENT_PLUGINS=movetopublicldap 93 | - MYSQL_DATABASE=${MYSQL_DATABASE-kopano} 94 | - MYSQL_USER=${MYSQL_USER-kopano} 95 | - MYSQL_PASSWORD=${MYSQL_PASSWORD-secret} 96 | - IMAP_LISTEN=*:143 # also listen to eth0 97 | - POP3_LISTEN=*:110 # also listen to eth0 98 | - ICAL_LISTEN=*:8080 # also listen to eth0 99 | - IMAPS_LISTEN=*:993 # enable TLS 100 | - POP3S_LISTEN=*:995 # enable TLS 101 | - ICALS_LISTEN=*:8443 # enable TLS 102 | - PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME=true 103 | - SYSLOG_LEVEL=${SYSLOG_LEVEL-3} 104 | - LOG_LEVEL=${LOG_LEVEL-3} 105 | volumes: 106 | - app-conf:/etc/kopano 107 | - app-atch:/var/lib/kopano/attachments 108 | - app-sync:/var/lib/z-push 109 | - app-spam:/var/lib/kopano/spamd # kopano-spamd integration 110 | - /etc/localtime:/etc/localtime:ro # Use host timezone 111 | cap_add: # helps debugging by allowing strace 112 | - sys_ptrace 113 | 114 | mta: 115 | image: mlan/postfix 116 | hostname: ${MAIL_SRV-mx}.${MAIL_DOMAIN-example.com} 117 | networks: 118 | - backend 119 | ports: 120 | - "127.0.0.1:25:25" # SMTP 121 | - "127.0.0.1:465:465" # SMTPS authentication required 122 | depends_on: 123 | - auth 124 | environment: # Virgin config, ignored on restarts unless FORCE_CONFIG given. 125 | - MESSAGE_SIZE_LIMIT=${MESSAGE_SIZE_LIMIT-25600000} 126 | - LDAP_HOST=auth 127 | - VIRTUAL_TRANSPORT=lmtp:app:2003 128 | - SMTPD_MILTERS=inet:flt:11332 129 | - MILTER_DEFAULT_ACTION=accept 130 | - SMTP_RELAY_HOSTAUTH=${SMTP_RELAY_HOSTAUTH-} 131 | - SMTP_TLS_SECURITY_LEVEL=${SMTP_TLS_SECURITY_LEVEL-} 132 | - SMTP_TLS_WRAPPERMODE=${SMTP_TLS_WRAPPERMODE-no} 133 | - SMTPD_USE_TLS=yes 134 | - LDAP_USER_BASE=ou=${AD_USR_OU-users},${AD_BASE-dc=example,dc=com} 135 | - LDAP_QUERY_FILTER_USER=(&(objectclass=${AD_USR_OB-kopano-user})(mail=%s)) 136 | - LDAP_QUERY_FILTER_ALIAS=(&(objectclass=${AD_USR_OB-kopano-user})(kopanoAliases=%s)) 137 | - LDAP_QUERY_ATTRS_PASS=uid=user 138 | - REGEX_ALIAS=${REGEX_ALIAS-} 139 | volumes: 140 | - mta:/srv 141 | - app-spam:/var/lib/kopano/spamd # kopano-spamd integration 142 | - /etc/localtime:/etc/localtime:ro # Use host timezone 143 | cap_add: # helps debugging by allowing strace 144 | - sys_ptrace 145 | 146 | flt: 147 | image: mlan/rspamd 148 | networks: 149 | - backend 150 | ports: 151 | - "127.0.0.1:11334:11334" # HTML rspamd WebGui 152 | depends_on: 153 | - mta 154 | environment: # Virgin config, ignored on restarts unless FORCE_CONFIG given. 155 | - WORKER_CONTROLLER=enable_password="${FLT_PASSWD-secret}"; 156 | - METRICS=${FLT_METRIC} 157 | - CLASSIFIER_BAYES=${FLT_BAYES} 158 | - MILTER_HEADERS=${FLT_HEADERS} 159 | - DKIM_DOMAIN=${MAIL_DOMAIN-example.com} 160 | - DKIM_SELECTOR=${DKIM_SELECTOR-default} 161 | - SYSLOG_LEVEL=${SYSLOG_LEVEL-} 162 | - LOGGING=level="${FLT_LOGGING-error}"; 163 | volumes: 164 | - flt:/srv 165 | - app-spam:/var/lib/kopano/spamd # kopano-spamd integration 166 | - /etc/localtime:/etc/localtime:ro # Use host timezone 167 | cap_add: # helps debugging by allowing strace 168 | - sys_ptrace 169 | 170 | db: 171 | image: mariadb 172 | command: ['--log_warnings=1'] 173 | networks: 174 | - backend 175 | environment: 176 | - LANG=C.UTF-8 177 | - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD-secret} 178 | - MYSQL_DATABASE=${MYSQL_DATABASE-kopano} 179 | - MYSQL_USER=${MYSQL_USER-kopano} 180 | - MYSQL_PASSWORD=${MYSQL_PASSWORD-secret} 181 | volumes: 182 | - db:/var/lib/mysql 183 | - /etc/localtime:/etc/localtime:ro # Use host timezone 184 | 185 | auth: 186 | image: mlan/openldap 187 | networks: 188 | - backend 189 | command: --root-cn ${AD_ROOT_CN-admin} --root-pw ${AD_ROOT_PW-secret} 190 | environment: 191 | - LDAPBASE=${AD_BASE-dc=example,dc=com} 192 | - LDAPDEBUG=${AD_DEBUG-parse} 193 | volumes: 194 | - auth:/srv 195 | - /etc/localtime:/etc/localtime:ro # Use host timezone 196 | 197 | networks: 198 | backend: 199 | 200 | volumes: 201 | app-atch: 202 | app-conf: 203 | app-spam: 204 | app-sync: 205 | auth: 206 | db: 207 | mta: 208 | flt: 209 | ``` 210 | 211 | ## Demo 212 | 213 | This repository contains a [demo](demo) directory which hold the [docker-compose.yml](demo/docker-compose.yml) file as well as a [Makefile](demo/Makefile) which might come handy. Start with cloning the [github](https://github.com/mlan/docker-postfix) repository. 214 | 215 | ```bash 216 | git clone https://github.com/mlan/docker-postfix.git 217 | ``` 218 | 219 | From within the [demo](demo) directory you can start the containers by typing: 220 | 221 | ```bash 222 | make init 223 | ``` 224 | 225 | Then you can assess WebApp on the URL [`http://localhost:8008`](http://localhost:8008) and log in with the user name `demo` and password `demo` . 226 | 227 | ```bash 228 | make web 229 | ``` 230 | 231 | You can send yourself a test email by typing: 232 | 233 | ```bash 234 | make test 235 | ``` 236 | 237 | When you are done testing you can destroy the test containers by typing 238 | 239 | ```bash 240 | make destroy 241 | ``` 242 | 243 | ## Persistent storage 244 | 245 | By default, docker will store the configuration and run data within the container. This has the drawback that the configurations and queued and quarantined mail are lost together with the container should it be deleted. It can therefore be a good idea to use docker volumes and mount the run directories and/or the configuration directories there so that the data will survive a container deletion. 246 | 247 | To facilitate such approach, to achieve persistent storage, the configuration and run directories of the services has been consolidated to `/srv/etc` and `/srv/var` respectively. So if you to have chosen to use both persistent configuration and run data you can run the container like this: 248 | 249 | ``` 250 | docker run -d --name mta -v mta:/srv -p 127.0.0.1:25:25 mlan/postfix 251 | ``` 252 | 253 | When you start a container which creates a new volume, as above, and the container has files or directories in the directory to be mounted (such as `/srv/` above), the directory’s contents are copied into the volume. The container then mounts and uses the volume, and other containers which use the volume also have access to the pre-populated content. More details [here](https://docs.docker.com/storage/volumes/#populate-a-volume-using-a-container). 254 | 255 | ## Configuration / seeding procedure 256 | 257 | The `mlan/postfix` image contains an elaborate configuration / seeding procedure. The configuration is controlled by environment variables, described below. 258 | 259 | The seeding procedure will leave any existing configuration untouched. This is achieved by the using an unlock file: `DOCKER_UNLOCK_FILE=/srv/etc/.docker.unlock`. 260 | During the image build this file is created. When the the container is started the configuration / seeding procedure will be executed if the `DOCKER_UNLOCK_FILE` can be found. Once the procedure completes the unlock file is deleted preventing the configuration / seeding procedure to run when the container is restarted. 261 | 262 | The unlock file approach was selected since it is difficult to accidentally _create_ a file. 263 | 264 | In the rare event that want to modify the configuration of an existing container you can override the default behavior by setting `FORCE_CONFIG=OVERWRITE` to a no-empty string. 265 | 266 | ## Environment variables 267 | 268 | When you create the `mlan/postfix` container, you can configure the services by passing one or more environment variables or arguments on the docker run command line. Once the services has been configured a lock file is created, to avoid repeating the configuration procedure when the container is restated. 269 | 270 | To see all available Postfix configuration variables you can run `postconf` within the container, for example like this: 271 | 272 | ```bash 273 | docker-compose exec mta postconf 274 | ``` 275 | 276 | If you do, you will notice that configuration variable names are all lower case, but they will be matched with all uppercase environment variables by the container initialization scripts. 277 | 278 | Similarly Dovecot configuration variables can be set. One difference is that, to avoid name clashes, the variables are prefixed by `DOVECOT_PREFIX=DOVECOT_`. You can list all Dovecot variables by typing: 279 | 280 | ```sh 281 | docker-compose exec mta doveconf 282 | ``` 283 | 284 | ## Milter support 285 | 286 | Postfix communicates with external applications like mail filters (Milters), providing spam filtering, using the Milter protocol, which is similar to SMTP. 287 | 288 | [Rspamd](https://rspamd.com/) is a fast, free and open-source spam filtering system, which has been tested with `mlan/postfix`. The [docker-postfix](https://github.com/mlan/docker-rspamd) repository provides a dockerized version of the Rspamd mail filter. 289 | 290 | #### `SMTPD_MILTERS` 291 | 292 | Communication with the [Rspamd](https://rspamd.com/) milter is configured by setting `SMTPD_MILTERS=inet:flt:11332`, which assumes that a Rspamd container, named `flt`, is reachable on the custom network. 293 | 294 | #### `MILTER_DEFAULT_ACTION` 295 | 296 | The [milter_default_action](http://www.postfix.org/postconf.5.html#milter_default_action) parameter specifies how Postfix handles Milter application errors. You can set `MILTER_DEFAULT_ACTION=accept` to proceed as if the mail filter was not present, when there are errors. 297 | 298 | ## Outgoing SMTP relay 299 | 300 | Sometimes you want outgoing email to be sent to a SMTP relay and _not_ directly to its destination. This could for instance be when your ISP is blocking port 25 or perhaps if you have a dynamic IP and are afraid of that mail servers will drop your outgoing emails because of that. 301 | 302 | #### `SMTP_RELAY_HOSTAUTH` 303 | This environment variable simplify a SMTP relay configuration. The SMTP relay host might require SASL authentication in which case user name and password can also be given in variable. The format is `"host:port user:passwd"`. Example: `SMTP_RELAY_HOSTAUTH="[example.relay.com]:587 e863ac2bc1e90d2b05a47b2e5c69895d:b35266f99c75d79d302b3adb42f3c75f"` 304 | 305 | #### `SMTP_TLS_SECURITY_LEVEL` 306 | 307 | You can enforce the use of TLS, so that the Postfix SMTP server announces STARTTLS and accepts no 308 | mail without TLS encryption, by setting `SMTP_TLS_SECURITY_LEVEL=encrypt`. Default: `SMTP_TLS_SECURITY_LEVEL=none`. 309 | 310 | #### `SMTP_TLS_WRAPPERMODE` 311 | 312 | To configure the Postfix SMTP client connecting using the legacy SMTPS protocol instead of using the STARTTLS command, set `SMTP_TLS_WRAPPERMODE=yes`. This mode requires `SMTP_TLS_SECURITY_LEVEL=encrypt` or stronger. Default: `SMTP_TLS_WRAPPERMODE=no` 313 | 314 | ## Forwarding rewrite 315 | 316 | [PostSRSd](https://github.com/roehling/postsrsd), implementing a sender rewriting scheme (SRS), offer optional forwarding rewrite to avoid receiving servers flagging messages as spam. 317 | 318 | ## Incoming SMTPS and submission client authentication 319 | 320 | Postfix achieves client authentication using SASL provided by [Dovecot](https://dovecot.org/). Client authentication is the mechanism that is used on SMTP relay using SASL authentication, see the [`SMTP_RELAY_HOSTAUTH`](#smtp_relay_hostauth). Here the client authentication is arranged on the [smtps](https://en.wikipedia.org/wiki/SMTPS) port: 465 and [submission](https://en.wikipedia.org/wiki/Message_submission_agent) port: 587. 321 | 322 | To avoid the risk of being an open relay the SMTPS and submission ([MSA](https://en.wikipedia.org/wiki/Message_submission_agent)) services are only activated when at least one SASL method has activated. Four methods are supported; LDAP, MySQL, IMAP and password file. Any combination of methods can simultaneously be active. If more than one method is active, all authentication methods are attempted one after another. 323 | 324 | A method is activated when its required variables has been defined. For LDAP, `LDAP_QUERY_ATTRS_PASS` is needed in addition to the LDAP variables discussed in [LDAP mailbox lookup](#ldap-mailbox-lookup). MySQL needs `MYSQL_QUERY_PASS` in addition to the MySQL variables discussed in [MySQL mailbox lookup](#mysql-mailbox-lookup). And IMAP needs the [`SMTPD_SASL_IMAPHOST`](#imap-sasl-client-authentication-smtpd_sasl_imaphost) variable and password file require [`SMTPD_SASL_CLIENTAUTH`](#password-file-sasl-client-authentication-smtpd_sasl_clientauth). 325 | 326 | Additionally clients are required to authenticate using TLS to avoid password being sent in the clear. The configuration of the services are the similar with the exception that the SMTPS service uses the legacy SMTPS protocol; `SMTPD_TLS_WRAPPERMODE=yes`, whereas the submission service uses the STARTTLS protocol. 327 | 328 | ### Authentication (SASL) Mechanisms 329 | 330 | There are unavoidable limitations with [non-plaintext authentication mechanisms](https://doc.dovecot.org/configuration_manual/authentication/password_schemes/#non-plaintext-authentication-mechanisms) and password storage schemes. If more than one non-plaintext authentication mechanism, e.g. CRAM-MD5, are enabled then the passwords must be stored in plain text. 331 | 332 | ### Password file SASL client authentication `SMTPD_SASL_CLIENTAUTH` 333 | 334 | You can list clients and their passwords in a space separated string using the format: `"username:{scheme}passwd"`. Example: `SMTPD_SASL_CLIENTAUTH="client1:{plain}passwd1 client2:{plain}passwd2"`. For security you might want to use encrypted passwords. One way to encrypt a password (`{plain}secret`) is by running 335 | 336 | ```bash 337 | docker exec -it mta doveadm pw -p secret 338 | 339 | {CRYPT}$2y$05$Osj5ebALV/bXo18H4BKLa.J8Izn23ilI8TNA/lIHz92TuQFbZ/egK 340 | ``` 341 | 342 | for use in `SMTPD_SASL_CLIENTAUTH`. 343 | 344 | ### LDAP SASL client authentication `LDAP_QUERY_ATTRS_PASS` 345 | 346 | Using [LDAP with authentication binds](https://wiki.dovecot.org/AuthDatabase/LDAP/AuthBinds), Dovecot, binds, using the SMTPS client credentials, to the LDAP server which that verifies the them. See [LDAP](https://doc.dovecot.org/configuration_manual/authentication/ldap/) for more details. 347 | 348 | The LDAP client configurations described in [LDAP mailbox lookup](#ldap-mailbox-lookup) are also used here. In addition to these, the binding `` attribute needs to be specified using `LDAP_QUERY_ATTRS_PASS`. The `` attribute is defined in this way `LDAP_QUERY_ATTRS_PASS==user`. To exemplify, if `uid` is the desired `` attribute define `LDAP_QUERY_ATTRS_PASS=uid=user`. 349 | 350 | #### `LDAP_QUERY_FILTER_PASS` 351 | 352 | Dovecot sends a LDAP request defined by `LDAP_QUERY_FILTER_PASS` to lookup the DN that will be used for the authentication bind. Example: `LDAP_QUERY_FILTER_PASS=(&(objectclass=posixAccount)(uid=%u))`. 353 | 354 | `LDAP_QUERY_FILTER_PASS` can be omitted in which case the filter is being reconstructed from `LDAP_QUERY_FILTER_USER`. The reconstruction tries to replace the string `(mail=%s)` in `LDAP_QUERY_FILTER_USER` with `(=%u),` where `` is taken from `LDAP_QUERY_ATTRS_PASS`. Example: `LDAP_QUERY_FILTER_USER=(&(objectclass=posixAccount)(mail=%s))` and `LDAP_QUERY_ATTRS_PASS=uid=user` will result in this filter `(&(objectclass=posixAccount)(uid=%u))`. 355 | 356 | ### IMAP SASL client authentication `SMTPD_SASL_IMAPHOST` 357 | 358 | Dovecot, can authenticate users against a remote IMAP server (RIMAP). For this to work it is sufficient to provide the address of the IMAP host, by using `SMTPD_SASL_IMAPHOST`. Examples `SMTPD_SASL_IMAPHOST=app`, `SASL_IMAP_HOST=192.168.1.123:143`. 359 | 360 | For more details see [Authentication via remote IMAP server](https://doc.dovecot.org/configuration_manual/protocols/imap). 361 | 362 | ## Incoming destination domain 363 | 364 | Postfix is configured to be 365 | the final destination of the virtual/hosted domains defined by the environment variable `MAIL_DOMAIN`. If the domains are not properly configured Postfix will be rejecting the emails. When multiple domains are used the first domain in the list is considered to be the primary one. 366 | 367 | #### `MAIL_DOMAIN` 368 | 369 | The default value of `MAIL_DOMAIN=$(hostname -d)` is to 370 | use the host name of the container minus the first component. So you can either use the environment variable `MAIL_DOMAIN` or the argument `--hostname`. So for example, `--hostname mx1.example.com` or `-e MAIL_DOMAIN="example.com secondary.com" `. 371 | 372 | ## Incoming TLS support 373 | 374 | Transport Layer Security (TLS, formerly called SSL) provides certificate-based authentication and encrypted sessions. An encrypted session protects the information that is transmitted with SMTP mail or with SASL authentication. 375 | 376 | Here TLS is activated for inbound messages when either `SMTPD_TLS_CHAIN_FILES` or `SMTPD_TLS_CERT_FILE` (or its [DSA](https://en.wikipedia.org/wiki/Digital_Signature_Algorithm) and [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) counterparts) is not empty or `SMTPD_USE_TLS=yes`. The Postfix SMTP server generally needs a certificate and a private key to provide TLS. Both must be in [PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) format. The private key must not be encrypted, meaning: the key must be accessible without a password. The [RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) certificate and a private key files are identified by `SMTPD_TLS_CERT_FILE` and `SMTPD_TLS_KEY_FILE`. 377 | 378 | #### `SMTPD_USE_TLS=yes` 379 | 380 | If `SMTPD_USE_TLS=yes` is explicitly defined but there are no certificate files defined, a self-signed certificate will be generated when the container is created. 381 | 382 | #### `SMTPD_TLS_CERT_FILE` 383 | 384 | Specifies the [RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem)) [PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) certificate file within the container to be used with incoming TLS connections. The certificate file need to be made available in the container by some means. Example `SMTPD_TLS_CERT_FILE=cert.pem`. Additionally there are the [DSA](https://en.wikipedia.org/wiki/Digital_Signature_Algorithm), [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) or chain counterparts; `SMTPD_TLS_DCERT_FILE`, `SMTPD_TLS_ECCERT_FILE` and `SMTPD_TLS_CHAIN_FILES`. 385 | 386 | #### `SMTPD_TLS_KEY_FILE` 387 | 388 | Specifies the RSA PEM private key file within the container to be used with incoming TLS connections. The private key file need to be made available in the container by some means. Example `SMTPD_TLS_KEY_FILE=key.pem`. Additionally there are the DSA, ECDSA or chain counterparts; `SMTPD_TLS_DKEY_FILE`, `SMTPD_TLS_ECKEY_FILE` and `SMTPD_TLS_CHAIN_FILES`. 389 | 390 | ### TLS forward secrecy 391 | 392 | The term "Forward Secrecy" (or sometimes "Perfect Forward Secrecy") is used to describe security protocols in which the confidentiality of past traffic is not compromised when long-term keys used by either or both sides are later disclosed. 393 | 394 | Forward secrecy is accomplished by negotiating session keys using per-session cryptographically-strong random numbers that are not saved, and signing the exchange with long-term authentication keys. Later disclosure of the long-term keys allows impersonation of the key holder from that point on, but not recovery of prior traffic, since with forward secrecy, the discarded random key agreement inputs are not available to the attacker. 395 | 396 | The built in utility script `run` can be used to generate the Diffie-Hellman parameters needed for forward secrecy. 397 | 398 | ```bash 399 | docker exec -it mta run postfix_update_dhparam 400 | ``` 401 | 402 | ### Let’s Encrypt LTS certificates using Traefik 403 | 404 | [Let’s Encrypt](https://letsencrypt.org/) provide free, automated, authorized certificates when you can demonstrate control over your domain. Automatic Certificate Management Environment (ACME) is the protocol used for such demonstration. There are many agents and applications that supports ACME, e.g., [certbot](https://certbot.eff.org/). The reverse proxy [Traefik](https://docs.traefik.io/) also supports ACME. 405 | 406 | #### `ACME_FILE`, `ACME_POSTHOOK` 407 | 408 | The `mlan/postfix` image looks for a file `ACME_FILE=/acme/acme.json` at container startup and every time this file changes certificates within this file are extracted. If the host or domain name of one of those certificates matches `HOSTNAME=$(hostname)` or `DOMAIN=${HOSTNAME#*.}` it will be used for TLS support. 409 | 410 | Once the certificates and keys have been updated, we run the command in the environment variable `ACME_POSTHOOK="postfix reload"`. Postfix's parameters needs to be reloaded to update the LTS parameters. If such automatic reloading is not desired, set `ACME_POSTHOOK=` to empty. 411 | 412 | So reusing certificates from Traefik will work out of the box if the `/acme` directory in the Traefik container is also mounted in the `mlan/postfix` container. 413 | 414 | ```bash 415 | docker run -d -name mta -v proxy-acme:/acme:ro mlan/postfix 416 | ``` 417 | 418 | Note, if the target certificate Common Name (CN) or Subject Alternate Name (SAN) is changed the container needs to be restarted. 419 | 420 | Moreover, do not set `SMTPD_TLS_CERT_FILE` and/or `SMTPD_TLS_KEY_FILE` when using `ACME_FILE`. 421 | 422 | ## Mailbox maps and authentication 423 | 424 | When Postfix receives an message it uses mailbox maps to lookup the recipient's mailbox-path/username. If successful the message is accepted. Whether what the lookup returns is used as a mailbox-path or a username depends on if the messages will be delivered to a local mailbox or is transported for delivery elsewhere. See [delivery transport and mail boxes](#delivery-transport-and-mail-boxes) for an overview on delivery methods. 425 | 426 | So one can imagine situations where Postfix is set up to lookup and pass on a username that is different from what dovecot is expecting when performing authentication. Using `DOVECOT_AUTH_USERNAME_FORMAT=%Ln` Dovecot can be made to drop the domain part, if present, from the supplied username, see [Dovecot core settings](https://doc.dovecot.org/settings/core/?highlight=auth_username_format) for details. 427 | 428 | ## Table mailbox lookup 429 | 430 | Postfix can use a table as a source for any of its lookups including virtual mailbox and aliases. The `mlan/postfix` image provides a simple way to generate virtual mailbox lookup using the `MAIL_BOXES` and `MAIL_ALIASES` environment variables. 431 | 432 | #### `MAIL_BOXES` 433 | 434 | The `MAIL_BOXES` environment variable (empty by default) hold a space separated list of addresses and their mailboxes using the following syntax: `MAIL_BOXES="address address:mailbox"`. The mailbox will have the same name as the address if it is not explicitly given. 435 | 436 | Using the `MAIL_BOXES` environment variable you simply provide a space separated list with all email addresses that Postfix should accept incoming mail to. For example: `MAIL_BOXES="receiver@example.com info@example.com"`. 437 | 438 | The mailbox path is separated from the address by a colon `:`, like so; `MAIL_BOXES="receiver@example.com:receiver/inbox info@example.com:example.com/info/"`. 439 | 440 | Mail is stored either in [mbox or maildir format](https://wiki1.dovecot.org/MailboxFormat). The mbox format is used unless the mailbox path ends with `/` in which case maildir format is used. 441 | 442 | #### `MAIL_ALIASES` 443 | 444 | Using the `MAIL_ALIASES` environment variable you simply provide a space separated list with email alias addresses that Postfix should accept incoming mail to, using the following syntax: `MAIL_ALIASES="alias:address alias:address,address"`. For example: `MAIL_ALIASES="root:info,info@example.com postmaster:root"`. The default value is empty. 445 | 446 | ## LDAP mailbox lookup 447 | 448 | Postfix can use an [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) directory as a source for any of its lookups including virtual mailbox and aliases. 449 | 450 | For LDAP mailbox lookup to work `LDAP_HOST`, `LDAP_USER_BASE` and `LDAP_QUERY_FILTER_USER` need to be configured. LDAP can also be used for alias lookup, in which case use `LDAP_QUERY_FILTER_ALIAS`. In addition LDAP can be used to lookup mail groups using `LDAP_QUERY_FILTER_GROUP` and `LDAP_QUERY_FILTER_EXPAND`. For detailed explanation see [LDAP client configuration](http://www.postfix.org/ldap_table.5.html). 451 | 452 | If the LDAP server is not configured to allow anonymous queries, you use `LDAP_BIND_DN` and `LDAP_BIND_PW` to provide LDAP user and password to be used for the queries. 453 | 454 | ### Required LDAP parameters 455 | 456 | #### `LDAP_HOST` 457 | 458 | Use `LDAP_HOST` to configure the connection to the LDAP server. When the default port (389) is used just providing the server name is often sufficient. You can also use full URL or part thereof, for example: `LDAP_HOST=auth`, `LDAP_HOST=auth:389`, `LDAP_HOST=ldap://ldap.example.com:1444`. 459 | 460 | #### `LDAP_USER_BASE` 461 | 462 | The `LDAP_USER_BASE`, is the base DNs at which to conduct the searches for users. Example: `LDAP_USER_BASE=ou=people,dc=example,dc=com`. 463 | 464 | #### `LDAP_QUERY_FILTER_USER` 465 | 466 | This is the filter used to search the directory, where `%s` is a 467 | substitute for the address Postfix is trying to resolve. Example, only consider the email address of users who also have `objectclass=posixAccount`; `LDAP_QUERY_FILTER_USER=(&(objectclass=posixAccount)(mail=%s))`. 468 | 469 | ### Optional LDAP parameters 470 | 471 | #### `LDAP_QUERY_ATTRS_USER` 472 | 473 | As mentioned in [mailbox maps and authentication](#mailbox-maps-and-authentication) what the LDAP lookup returns can be used as a mailbox-path or a username depending on if the messages will be delivered to a local mailbox or is transported for delivery elsewhere. The default attribute to return is `LDAP_QUERY_ATTRS_USER=mail`. Use this variable if another attribute is to be returned. 474 | 475 | #### `LDAP_GROUP_BASE` 476 | 477 | The `LDAP_GROUP_BASE` is the base DNs at which to conduct the searches for groups. Example: `LDAP_GROUP_BASE=ou=groups,dc=example,dc=com`. 478 | 479 | #### `LDAP_QUERY_FILTER_ALIAS` 480 | 481 | This is the filter used to search the directory, where `%s` is a 482 | substitute for the address Postfix is trying to resolve. Example, only consider email aliases of users who also have `objectclass=posixAccount`; `LDAP_QUERY_FILTER_ALIAS=(&(objectclass=posixAccount)(aliases=%s))`. 483 | 484 | #### `LDAP_QUERY_FILTER_GROUP`, `LDAP_QUERY_FILTER_EXPAND` 485 | 486 | To deliver mails to a member of a group the email addresses of the individual must be resolved. For resolving group members use `LDAP_QUERY_FILTER_GROUP` and to expand group members’ mail into `uid` use `LDAP_QUERY_FILTER_EXPAND`. 487 | 488 | Example, only consider group mail from group who is of `objectclass=group`: `LDAP_QUERY_FILTER_GROUP=(&(objectclass=group)(mail=%s))` and then only consider user with matching `uid` who is of `objectclass=posixAccount`; `LDAP_QUERY_FILTER_EXPAND=(&(objectclass=posixAccount)(uid=%s))`. 489 | 490 | #### `LDAP_BIND_DN`, `LDAP_BIND_PW` 491 | 492 | The defaults for these environment variables are empty. If you do have to bind, do it with this distinguished name and password. Example: `LDAP_BIND_DN=uid=admin,dc=example,dc=com`, `LDAP_BIND_PW=secret`. 493 | 494 | ## MySQL mailbox lookup 495 | 496 | Postfix can use an [MySQL](https://en.wikipedia.org/wiki/MySQL) database as a source for any of its lookups including virtual mailbox and aliases. 497 | 498 | For MySQL mailbox lookup to work `MYSQL_HOST`, `MYSQL_DATABASE` and `MYSQL_QUERY_USER` need to be configured. MySQL can also be used for alias lookup, in which case use `MYSQL_QUERY_ALIAS`. For detailed explanation see [MySQL client configuration](http://www.postfix.org/mysql_table.5.html). 499 | 500 | If the MySQL server is not configured to allow password less queries, you use `MYSQL_USER` and `MYSQL_PASSWORD` to provide authentication credentials for the queries. 501 | 502 | ### Required MySQL parameters 503 | 504 | #### `MYSQL_HOST` 505 | 506 | Use `MYSQL_HOST` to configure the connection to the MySQL server. When the default port (3306) is used just providing the server name is often sufficient. You can also use full URL or part thereof, for example: `MYSQL_HOST=db` or `MYSQL_HOST=db:3306`. 507 | 508 | #### `MYSQL_DATABASE` 509 | 510 | The `MYSQL_DATABASE`, is the database on which to conduct the searches for users. Example: `MYSQL_DATABASE=postfix`. 511 | 512 | #### `MYSQL_QUERY_USER` 513 | 514 | The `MYSQL_QUERY_USER` query is used to lookup the recipient, 515 | where `%s` is a substitute for the address Postfix is trying to resolve. 516 | To exemplify, lets assume that the table `users` within the database `postfix` is structured like this: 517 | 518 | ```mysql 519 | +----+----------+---------------------------------------------+----------------------+ 520 | | id | userid | password | mail | 521 | +----+----------+---------------------------------------------+----------------------+ 522 | | 1 | receiver | {PLAIN-MD5}5ebe2294ecd0e0f08eab7690d2a6ee69 | receiver@example.com | 523 | | 2 | office1 | {PLAIN-MD5}7c6a180b36896a0a8c02787eeafb0e4c | NULL | 524 | +----+----------+---------------------------------------------+----------------------+ 525 | ``` 526 | 527 | We can use the following query to find the recipient that matches the mail address being resolved: 528 | `MYSQL_QUERY_USER="select mail from users where mail='%s' limit 1;"`. 529 | 530 | ### Optional MySQL parameters 531 | 532 | #### `MYSQL_QUERY_ALIAS` 533 | 534 | The `MYSQL_QUERY_ALIAS` query is used to retrieve aliases from the database, where `%s` is a 535 | substitute for the address Postfix is trying to resolve. 536 | 537 | #### `MYSQL_USER`, `MYSQL_PASSWORD` 538 | 539 | Use `MYSQL_USER` and `MYSQL_PASSWORD` to provide authentication credentials for MySQL queries. 540 | Example: `MYSQL_USER=admin`, `MYSQL_PASSWORD=secret`. These environment variables are empty by fault. 541 | 542 | #### `MYSQL_QUERY_PASS` 543 | 544 | As mentioned in [incoming SMTPS and submission client authentication](#incoming-smtps-and-submission-client-authentication) Dovecot needs the `MYSQL_QUERY_PASS` to be defined to be able to lookup the user and password when performing authentication. The following would work with the `users` table shown above `MYSQL_QUERY_PASS="select password, userid as user from $(SQL_TAB) where userid = '%u'"`. See [Dovecot MySQL authentication](https://doc.dovecot.org/configuration_manual/authentication/sql/#mysql) for details. 545 | 546 | ## Rewrite recipient email address `REGEX_ALIAS` 547 | 548 | The recipient email address can be rewritten using [regular expressions](https://en.wikipedia.org/wiki/Regular_expression) in `REGEX_ALIAS`. This can be useful in some situations. 549 | 550 | For example, assume you want email addresses like `user+info@domain.com` and `user-news@domain.com` to be forwarded to `user@domain.com`. This can be achieved by setting `REGEX_ALIAS='/([^+]+)[+-].*@(domain.com)/ $1@$2'`. The user can now, with the mail client, arrange filters to sort email into sub folders. 551 | 552 | ## Delivery transport and mail boxes 553 | 554 | The `mlan/postfix` image is designed primarily to work with companion software, like [Kolab](https://hub.docker.com/r/kvaps/kolab), [Kopano](https://cloud.docker.com/u/mlan/repository/docker/mlan/kopano) or [Zimbra](https://hub.docker.com/r/jorgedlcruz/zimbra/) which will hold the mail boxes. That is, often received messages are transported for final delivery. [Local Mail Transfer Protocol (LMTP)](https://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol) is one such transport mechanism. Nonetheless, if no transport mechanism is specified messages will be delivered to local mail boxes. 555 | 556 | #### `VIRTUAL_TRANSPORT` 557 | 558 | The environment variable `VIRTUAL_TRANSPORT` specifies how messages will be transported for final delivery. Frequently the server taking final delivery listen to LMTP. Assuming it does so on port 2003 it is sufficient to set `VIRTUAL_TRANSPORT=lmtp:app:2003` to arrange the transport. 559 | 560 | If `VIRTUAL_TRANSPORT` is not defined local mail boxes will be managed by Postfix directly. The local mail boxes will be created in the directory `/var/mail`. For example `/var/mail/user@example.com`. See [`MAIL_BOXES`](#mail-boxes) for details on mailbox paths. 561 | 562 | The `mlan/postfix` image include the [Dovecot, a secure IMAP server](https://dovecot.org/), which can also manage mail boxes. Setting `VIRTUAL_TRANSPORT=lmtp:unix:private/transport` will transport messages to dovecot which will arrange local mailboxes. The environment variable `DOVECOT_MAIL_LOCATION` can be used to set the [mailbox location template](https://doc.dovecot.org/configuration_manual/mail_location/). Since Dovecot serves both IMAP and POP3 these mailboxes can be accessed by remote mail clients if desired. 563 | 564 | The table below is provided to give an overview of the options discussed here. 565 | 566 | | `VIRTUAL_TRANSPORT` | Final delivery | 567 | | ------------------------------ | ------------------------------------------------------------ | 568 | | `=` | Postfix local mailbox `/var/mail/user@example.com` | 569 | | `=lmtp:app:2003` | External LMTP host `app` take delivery | 570 | | `=lmtp:unix:private/transport` | Dovecot local mailbox `/var/mail/user/inbox`, with IMAP and POP3 access | 571 | 572 | ## Mail delivery, IMAP, IMAPS, POP3 and POP3S 573 | 574 | When [Dovecot](https://dovecot.org/) manages the mail boxes, see [`VIRTUAL_TRANSPORT`](#virtual-transport), mail clients can retrieve messages using both the [IMAP](https://www.atmail.com/blog/imap-commands/) and POP3 protocols. Dovecot will use TLS certificates that have been made available to Postfix, in which case IMAPS and POP3S connections will be possible, see [Incoming TLS support](#incoming-tls-support). 575 | 576 | By default Dovecot refuses plain text authentication unless within a secure TLS connection. Sometimes, perhaps for testing, you want to enable plain text authentication for non-secure IMAP or POP3 connections. if so set `DOVECOT_DISABLE_PLAINTEXT_AUTH=no`. 577 | 578 | ## Message size limit `MESSAGE_SIZE_LIMIT` 579 | 580 | The maximal size in bytes of a message, including envelope information. Default: `MESSAGE_SIZE_LIMIT=10240000` ~10MB. Many mail servers are configured with maximal size of 10MB, 20MB or 25MB. 581 | 582 | ## Logging `SYSLOG_LEVEL` 583 | 584 | The level of output for logging is in the range from 0 to 7. The default is: `SYSLOG_LEVEL=5` 585 | 586 | | emerg | alert | crit | err | warning | notice | info | debug | 587 | | ----- | ----- | ---- | ---- | ------- | ------ | ---- | ----- | 588 | | 0 | 1 | 2 | 3 | 4 | **5** | 6 | 7 | 589 | 590 | # Knowledge base 591 | 592 | Here some topics relevant for arranging a mail server are presented. 593 | 594 | ## DNS records 595 | 596 | The [Domain Name System](https://en.wikipedia.org/wiki/Domain_Name_System) (DNS) is a [hierarchical](https://en.wikipedia.org/wiki/Hierarchical) and [decentralized](https://en.wikipedia.org/wiki/Decentralised_system) naming system for computers, services, or other resources connected to the [Internet](https://en.wikipedia.org/wiki/Internet) or a private network. 597 | 598 | ### MX record 599 | 600 | A mail exchange record (MX record) specifies the [mail server](https://en.wikipedia.org/wiki/Mail_server) responsible for accepting [email](https://en.wikipedia.org/wiki/Email) messages on behalf of a domain name. It is a [resource record](https://en.wikipedia.org/wiki/Resource_record) in the DNS. The MX record should correspond to the host name of the image. 601 | 602 | # Implementation 603 | 604 | Here some implementation details are presented. 605 | 606 | ## Container init scheme 607 | 608 | The container use [runit](http://smarden.org/runit/), providing an init scheme and service supervision, allowing multiple services to be started. There is a Gentoo Linux [runit wiki](https://wiki.gentoo.org/wiki/Runit). 609 | 610 | When the container is started, execution is handed over to the script [`docker-entrypoint.sh`](src/docker/bin/docker-entrypoint.sh). It has 4 stages; 0) *register* the SIGTERM [signal (IPC)](https://en.wikipedia.org/wiki/Signal_(IPC)) handler, which is programmed to run all exit scripts in `/etc/docker/exit.d/` and terminate all services, 1) *run* all entry scripts in `/etc/docker/entry.d/`, 2) *start* services registered in `SVDIR=/etc/service/`, 3) *wait* forever, allowing the signal handler to catch the SIGTERM and run the exit scripts and terminate all services. 611 | 612 | The entry scripts are responsible for tasks like, seeding configurations, register services and reading state files. These scripts are run before the services are started. 613 | 614 | There is also exit script that take care of tasks like, writing state files. These scripts are run when docker sends the SIGTERM signal to the main process in the container. Both `docker stop` and `docker kill --signal=TERM` sends SIGTERM. 615 | 616 | ## Build assembly 617 | 618 | The entry and exit scripts, discussed above, as well as other utility scrips are copied to the image during the build phase. The source file tree was designed to facilitate simple scanning, using wild-card matching, of source-module directories for files that should be copied to image. Directory names indicate its file types so they can be copied to the correct locations. The code snippet in the `Dockerfile` which achieves this is show below. 619 | 620 | ```dockerfile 621 | COPY src/*/bin $DOCKER_BIN_DIR/ 622 | COPY src/*/entry.d $DOCKER_ENTRY_DIR/ 623 | ``` 624 | 625 | There is also a mechanism for excluding files from being copied to the image from some source-module directories. Source-module directories to be excluded are listed in the file [`.dockerignore`](https://docs.docker.com/engine/reference/builder/#dockerignore-file). Since we don't want files from the module `notused` we list it in the `.dockerignore` file: 626 | 627 | ```sh 628 | src/notused 629 | ``` 630 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Road map 2 | 3 | ## Postfix configuration 4 | ### TLS Forward Secrecy 5 | 6 | The built-in default Postfix FFDHE group is a 2048-bit group as of Postfix 3.1. You can optionally generate non-default Postfix SMTP server FFDHE parameters for possibly improved security against pre-computation attacks, but this is not necessary or recommended. Just leave "smtpd_tls_dh1024_param_file" at its default empty value. [TLS Forward Secrecy in Postfix](https://www.postfix.org/FORWARD_SECRECY_README.html) 7 | 8 | ```sh 9 | /etc/postfix/main.cf: support for parameter "smtpd_tls_dh1024_param_file" will be removed; instead, do not specify (leave at default) 10 | ``` 11 | ### Enable TLS 12 | 13 | Dont use `smtpd_use_tls` anymore. `smtpd_tls_security_level=may` is sufficient. 14 | 15 | ```sh 16 | /etc/postfix/main.cf: support for parameter "smtpd_use_tls" will be removed; instead, specify "smtpd_tls_security_level" 17 | ``` 18 | 19 | ## PostSRSd 20 | 21 | Arrange optional configuration of the [PostSRSd](https://github.com/roehling/postsrsd) Sender Rewriting Scheme (SRS) via TCP-based lookup tables for Postfix. 22 | 23 | ```sh 24 | dd if=/dev/urandom bs=18 count=1 | base64 > /etc/postsrsd/postsrsd.secret 25 | ``` 26 | 27 | ## ACME 28 | 29 | Don't make DOCKER_ACME_SSL_DIR=/etc/ssl/acme persistent. We will remove all old certs and keys on updates anyway. 30 | 31 | ## Runit 32 | 33 | Need to fix runit script for postfix. It does not kill all children. 34 | the reason is that we don't let `runsvdir` become pid=1 and `postfix startup-fg` 35 | checks for pid=1 and since it isn't start `master -s` instead of `exec master -i` 36 | , see `/usr/libexec/postfix/postfix-script`. 37 | -------------------------------------------------------------------------------- /bld.mk: -------------------------------------------------------------------------------- 1 | # bld.mk 2 | # 3 | # Docker build make-functions 4 | # 5 | 6 | BLD_VER ?= latest 7 | BLD_TGT ?= full 8 | BLD_CMT ?= HEAD 9 | 10 | # 11 | # $(call bld_tags,mini,) -> mini mini-1.2.3 mini-1.2 mini-1 12 | # $(call bld_tags,full,) -> latest full 1.2.3 1.2 1 full-1.2.3 full-1.2 full-1 13 | # $(call bld_tags,,) -> latest 1.2.3 1.2 1 14 | # 15 | # $(call bld_tags,mini,something) -> mini-something 16 | # $(call bld_tags,full,something) -> something full-something 17 | # $(call bld_tags,,something) -> something 18 | # 19 | # $(call bld_tags,mini,latest) -> mini 20 | # $(call bld_tags,full,latest) -> latest full 21 | # $(call bld_tags,,latest) -> latest 22 | # 23 | bld_tags = $(if $(2),\ 24 | $(call bld_ver,$(1),$(2)),\ 25 | $(call bld_ver,$(1),latest) $(call bld_ver,$(1),$(call bld_gittags))) 26 | 27 | # 28 | # $(call bld_ver,mini,something) -> mini-something 29 | # $(call bld_ver,full,something) -> something full-something 30 | # $(call bld_ver,,something) -> something 31 | # 32 | # $(call bld_ver,mini,latest) -> mini 33 | # $(call bld_ver,full,latest) -> latest full 34 | # $(call bld_ver,,latest) -> latest 35 | # 36 | bld_ver = $(if $(1),\ 37 | $(if $(findstring $(BLD_TGT),$(1)),\ 38 | $(if $(findstring latest,$(2)),latest $(1),$(2) $(addprefix $(1)-,$(2))),\ 39 | $(if $(findstring latest,$(2)),$(1),$(addprefix $(1)-,$(2)))),\ 40 | $(2)) 41 | 42 | # 43 | # $(call bld_tag,full,) -> full 44 | # $(call bld_tag,,) -> latest 45 | # 46 | # $(call bld_tag,full,something) -> full-something 47 | # $(call bld_tag,,something) -> something 48 | # 49 | # $(call bld_tag,full,latest) -> full 50 | # $(call bld_tag,,latest) -> latest 51 | # 52 | bld_tag = $(strip $(if $(1),\ 53 | $(if $(2),$(if $(findstring latest,$(2)),$(1),$(1)-$(2)),$(1)),\ 54 | $(if $(2),$(2),latest))) 55 | 56 | # 57 | # $(call bld_gittags,HEAD) -> 1.2.3 1.2 1 58 | # 59 | bld_gittags = $(subst v,,$(shell git tag --points-at $(BLD_CMT))) 60 | -------------------------------------------------------------------------------- /demo/.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=demo 2 | SYSLOG_LEVEL=7 3 | LOG_LEVEL=6 4 | FLT_LOGGING=silent 5 | AD_DEBUG=stats 6 | AD_ADM_CN=admin 7 | AD_ADM_PW=admin 8 | AD_ADM_TEL=555-540-9637 9 | AD_ADM_TIT=System Admin 10 | AD_BASE=dc=example,dc=com 11 | AD_GRP_CN=team 12 | AD_GRP_OB=kopano-group 13 | AD_GRP_OU=groups 14 | AD_PUB_CN=public 15 | AD_ROOT_CN=admin 16 | AD_ROOT_PW=secret 17 | AD_SHR_CN=shared 18 | AD_USR_AS=trial 19 | AD_USR_CN=demo 20 | AD_USR_OB=kopano-user 21 | AD_USR_OU=users 22 | AD_USR_PW=demo 23 | AD_USR_TEL=555-439-2736 24 | AD_USR_TIT=First User 25 | DKIM_SELECTOR=selector 26 | MAIL_DOMAIN=example.com 27 | MAIL_SRV=mx 28 | MYSQL_DATABASE=kopano 29 | MYSQL_PASSWORD=secret 30 | MYSQL_ROOT_PASSWORD=secret 31 | MYSQL_USER=kopano 32 | RAZOR_REGISTRATION= 33 | REGEX_ALIAS='/([^+]+)[+-].*@(example.com)/ $1@$2' 34 | #FLT_PASSWD=demo 35 | FLT_PASSWD='$2$ozw7cijr195thspohzjw7qwex5jn88xb$powk4ez7zf4hgxbxxgx51aquhozfyz5okpako7cx41axq17k6bdy' 36 | FLT_METRIC='actions {add_header=6;reject=15;}' 37 | FLT_BAYES='autolearn=[0,6]; min_learns=1; per_user=true; users_enabled=true; allow_learn=true;' 38 | FLT_HEADERS='skip_local=false; use=["x-spam-status"];' -------------------------------------------------------------------------------- /demo/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | # 3 | # demo 4 | # 5 | 6 | -include *.mk .env .init.env 7 | 8 | SRV_LIST ?= auth app db mta flt 9 | 10 | AD_DOM ?= $(call ad_sub_dot, $(AD_BASE)) 11 | AD_DC ?= $(call ad_cut_dot, 1, 1, $(AD_DOM)) 12 | 13 | SSL_O = $(MAIL_DOMAIN) 14 | SSL_MAIL = auto 15 | SSL_PASS = $(AD_USR_PW) 16 | #SSL_TRST = $(SSL_SMIME) 17 | 18 | NET_NAME ?= $(COMPOSE_PROJECT_NAME)_backend 19 | CURL_OPT ?= -s -v 20 | TSSL_CMD ?= docker run -i --rm --network $(NET_NAME) drwetter/testssl.sh 21 | CURL_CMD ?= curl 22 | webb_avl := $(shell command -v browse 2> /dev/null || command -v firefox 2> /dev/null) 23 | webb_cmd ?= $(webb_avl) $(1) & 24 | APP_NAME = app 25 | AUT_NAME = auth 26 | AUW_NAME = auth-web 27 | DB_NAME = db 28 | DBW_NAME = db-web 29 | MTA_NAME = mta 30 | FLT_NAME = flt 31 | APP_FQDN ?= $(call dkr_srv_ip,$(APP_NAME)) 32 | AUT_FQDN ?= $(call dkr_srv_ip,$(AUT_NAME)) 33 | AUW_FQDN ?= $(call dkr_cnt_ip,$(AUW_NAME)) 34 | DB_FQDN ?= $(call dkr_srv_ip,$(DB_NAME)) 35 | DBW_FQDN ?= $(call dkr_cnt_ip,$(DBW_NAME)) 36 | MTA_FQDN ?= $(call dkr_srv_ip,$(MTA_NAME)) 37 | FLT_FQDN ?= $(call dkr_srv_ip,$(FLT_NAME)):11334 38 | 39 | MAIL_FROM ?= test@$(MAIL_DOMAIN) #test@my-domain.biz 40 | MAIL_TYPE ?= text/plain 41 | 42 | HAM_URL ?= https://www2.aueb.gr/users/ion/data/enron-spam/raw/ham/beck-s.tar.gz 43 | SPAM_URL ?= https://www2.aueb.gr/users/ion/data/enron-spam/raw/spam/BG.tar.gz 44 | 45 | variables: 46 | make -pn | grep -A1 "^# makefile"| grep -v "^#\|^--" | sort | uniq 47 | 48 | test: all-test_quiet mta-test_smtp 49 | 50 | init: up auth-init db-init mta-init app-init 51 | 52 | #init: up auth-init db-init app-restart mta-init wait_99 app-init 53 | 54 | ps: 55 | docker compose ps 56 | 57 | up: 58 | docker compose up -d 59 | 60 | down: 61 | docker compose down 62 | 63 | destroy: auth-web-down db-web-down all-destroy_smime 64 | docker compose down -v 65 | 66 | config: 67 | docker compose config 68 | 69 | logs: 70 | docker compose logs --tail 10 71 | 72 | images: 73 | docker compose images 74 | 75 | $(addsuffix -up,$(SRV_LIST)): 76 | docker compose up -d $(patsubst %-up,%,$@) 77 | 78 | $(addsuffix -down,$(SRV_LIST)): 79 | docker compose rm -sf $(patsubst %-down,%,$@) 80 | 81 | $(addsuffix -restart,$(SRV_LIST)): 82 | docker compose restart $(patsubst %-restart,%,$@) 83 | 84 | $(addsuffix -renew,$(SRV_LIST)): 85 | docker compose rm -s $(patsubst %-renew,%,$@) 86 | docker compose up -d $(patsubst %-renew,%,$@) 87 | 88 | $(addsuffix -top,$(SRV_LIST)): 89 | docker compose top $(patsubst %-top,%,$@) 90 | 91 | $(addsuffix -logs,$(SRV_LIST)): 92 | docker compose logs $(patsubst %-logs,%,$@) 93 | 94 | $(addsuffix -pull,$(SRV_LIST)): 95 | docker compose pull $(patsubst %-pull,%,$@) 96 | 97 | $(addsuffix -sh,$(SRV_LIST)): 98 | docker compose exec $(patsubst %-sh,%,$@) sh -c 'exec $$(getent passwd root | sed "s/.*://g")' 99 | 100 | $(addsuffix -env,$(SRV_LIST)): 101 | docker compose exec $(patsubst %-env,%,$@) env 102 | 103 | $(addsuffix -sv,$(SRV_LIST)): 104 | docker compose exec $(patsubst %-sv,%,$@) sh -c 'sv status $$SVDIR/*' 105 | 106 | $(addsuffix -apk_list,$(SRV_LIST)): 107 | docker compose exec $(patsubst %-apk_list,%,$@) sh -c 'apk info -sq $$(apk info -q) | sed -r "N;N;s/([^ ]+) installed size:\n([^ ]+) (.).*/\2\3\t\1/" | sort -h' 108 | 109 | $(addsuffix -diff,$(SRV_LIST)): 110 | docker container diff $(call dkr_srv_cnt,$(patsubst %-diff,%,$@)) 111 | 112 | $(addsuffix -hostaddr,$(SRV_LIST)): 113 | $(eval myhost := $(call dkr_srv_ip,$(patsubst %-hostaddr,%,$@))) 114 | 115 | wait_%: 116 | sleep $* 117 | 118 | web: app-web 119 | 120 | auth-init: wait_3 auth-mod_conf auth-add_schema auth-add_data 121 | 122 | export define LDIF_MOD_CONF 123 | dn: olcDatabase={-1}frontend,cn=config 124 | changetype: modify 125 | add: olcPasswordHash 126 | olcPasswordHash: {CRYPT} 127 | 128 | dn: cn=config 129 | changetype: modify 130 | add: olcPasswordCryptSaltFormat 131 | olcPasswordCryptSaltFormat: $$6$$%.16s 132 | 133 | dn: olcDatabase={1}mdb,cn=config 134 | changetype: modify 135 | add: olcDbIndex 136 | olcDbIndex: cn,ou,uid,mail eq 137 | endef 138 | 139 | export define LDIF_ADD_DATA 140 | dn: $(AD_BASE) 141 | objectClass: organization 142 | objectClass: dcObject 143 | dc: $(AD_DC) 144 | o: $(AD_DOM) 145 | 146 | dn: ou=$(AD_USR_OU),$(AD_BASE) 147 | ou: $(AD_USR_OU) 148 | objectClass: organizationalUnit 149 | 150 | dn: ou=$(AD_GRP_OU),$(AD_BASE) 151 | ou: $(AD_GRP_OU) 152 | objectClass: organizationalUnit 153 | 154 | dn: cn=$(AD_GRP_CN),ou=$(AD_GRP_OU),$(AD_BASE) 155 | cn: $(AD_GRP_CN) 156 | objectClass: groupOfNames 157 | objectClass: kopano-group 158 | member: uid=$(AD_ADM_CN),ou=$(AD_USR_OU),$(AD_BASE) 159 | member: uid=$(AD_USR_CN),ou=$(AD_USR_OU),$(AD_BASE) 160 | mail: $(AD_GRP_CN)@$(MAIL_DOMAIN) 161 | 162 | dn: uid=$(AD_ADM_CN),ou=$(AD_USR_OU),$(AD_BASE) 163 | changetype: add 164 | cn: $(AD_ADM_CN) 165 | objectClass: inetOrgPerson 166 | objectClass: kopano-user 167 | sn: $(AD_ADM_CN) 168 | uid: $(AD_ADM_CN) 169 | mail: $(AD_ADM_CN)@$(MAIL_DOMAIN) 170 | userPassword: $(AD_ADM_PW) 171 | telephoneNumber: $(AD_ADM_TEL) 172 | title: $(AD_ADM_TIT) 173 | kopanoAccount: 1 174 | kopanoAdmin: 1 175 | kopanoEnabledFeatures: imap 176 | kopanoEnabledFeatures: pop3 177 | 178 | dn: uid=$(AD_USR_CN),ou=$(AD_USR_OU),$(AD_BASE) 179 | changetype: add 180 | cn: $(AD_USR_CN) 181 | objectClass: inetOrgPerson 182 | objectClass: kopano-user 183 | sn: $(AD_USR_CN) 184 | uid: $(AD_USR_CN) 185 | mail: $(AD_USR_CN)@$(MAIL_DOMAIN) 186 | userPassword: $(AD_USR_PW) 187 | telephoneNumber: $(AD_USR_TEL) 188 | title: $(AD_USR_TIT) 189 | kopanoAccount: 1 190 | kopanoAliases: $(AD_USR_AS)@$(MAIL_DOMAIN) 191 | kopanoEnabledFeatures: imap 192 | kopanoEnabledFeatures: pop3 193 | 194 | dn: uid=$(AD_SHR_CN),ou=$(AD_USR_OU),$(AD_BASE) 195 | cn: $(AD_SHR_CN) 196 | objectClass: inetOrgPerson 197 | objectClass: kopano-user 198 | sn: $(AD_SHR_CN) 199 | uid: $(AD_SHR_CN) 200 | mail: $(AD_SHR_CN)@$(MAIL_DOMAIN) 201 | kopanoAccount: 1 202 | kopanoSharedStoreOnly: 1 203 | 204 | dn: uid=$(AD_PUB_CN),ou=$(AD_USR_OU),$(AD_BASE) 205 | cn: $(AD_PUB_CN) 206 | objectClass: inetOrgPerson 207 | objectClass: kopano-user 208 | sn: $(AD_PUB_CN) 209 | uid: $(AD_PUB_CN) 210 | mail: $(AD_PUB_CN)@$(MAIL_DOMAIN) 211 | kopanoAccount: 1 212 | kopanoHidden: 1 213 | kopanoSharedStoreOnly: 1 214 | kopanoResourceType: publicFolder:Public Stores/public 215 | endef 216 | 217 | PLAIN_SUBJ = Test message. 218 | PLAIN_MESS = Great news! You can receive email. 219 | GTUBE_SUBJ = GTUBE spam message. 220 | GTUBE_MESS = XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X 221 | EICAR_SUBJ = EICAR virus message. 222 | EICAR_MESS = X5O!P%@AP[4\PZX54(P^)7CC)7}\$$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!\$$H+H* 223 | EICAR_TYPE = audio/basic 224 | RANDS_SUBJ = $(shell shuf -n 3 /usr/share/dict/words | tr '\n' ' ') 225 | RANDS_MESS = $(shell shuf -n 200 /usr/share/dict/words | tr '\n' ' ') 226 | 227 | define messid 228 | $(shell echo $$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c8)@dot.com) 229 | endef 230 | 231 | define head_mail 232 | @echo -e "From: <$(1)>\nTo: <$(2)>\nDate: $$(date -R)\nMessage-ID: <$(messid)>\nMIME-Version: 1.0\nContent-type: $(if $(5),$(5),$(MAIL_TYPE))\nSubject: $(3)\n\n$(if $(4),$(4),$(PLAIN_MESS))\n" | tee /dev/tty 233 | endef 234 | 235 | define smtp_mail 236 | $(call head_mail,$(2),$(3),$(4),$(5),$(6)) \ 237 | | $(CURL_CMD) $(1) -T - --mail-from $(2) --mail-rcpt $(3) $(CURL_OPT) 238 | endef 239 | 240 | define lmtp_mail 241 | printf "LHLO mx\nMAIL FROM: <$(2)>\nRCPT TO: <$(3)>\nDATA\ 242 | \nFrom: <$(2)>\nTo: <$(3)>\nDate: $$(date -R)\nSubject: $(4)\ 243 | \n\nGreat news! You can receive email.\n.\nQUIT\n" | tee /dev/tty \ 244 | | $(CURL_CMD) $(1) -T - $(CURL_OPT) 245 | endef 246 | 247 | export define MAKE_UTILS_CONTAINER 248 | CURL_CMD ?= docker run -i --rm --network $(NET_NAME) curlimages/curl 249 | webb_cmd ?= docker run -d --rm --network $(NET_NAME) \ 250 | -e DISPLAY=$$$$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix \ 251 | -v /etc/localtime:/etc/localtime:ro -v $$$$(pwd)/ssl:/ssl \ 252 | kennethkl/firefox $$(1) 253 | APP_FQDN ?= $(APP_NAME) 254 | AUT_FQDN ?= $(AUT_NAME) 255 | AUW_FQDN ?= $(AUW_NAME) 256 | DB_FQDN ?= $(DB_NAME) 257 | DBW_FQDN ?= $(DBW_NAME) 258 | MTA_FQDN ?= $(MTA_NAME) 259 | FLT_FQDN ?= $(FLT_NAME) 260 | endef 261 | 262 | utils-container: 263 | echo "$$MAKE_UTILS_CONTAINER" > utils-container.mk 264 | 265 | utils-default: 266 | rm -f utils-container.mk 267 | 268 | auth-mod_conf: 269 | echo "$$LDIF_MOD_CONF" | docker compose exec -T auth ldapmodify -Q 270 | 271 | auth-add_data: 272 | echo "$$LDIF_ADD_DATA" | docker compose exec -T auth ldapadd -Q 273 | 274 | auth-add_schema: 275 | docker compose exec app zcat /usr/share/doc/kopano/kopano.ldif.gz \ 276 | | docker compose exec -T auth ldapadd -Q 277 | 278 | auth-show_conf: 279 | docker compose exec auth ldapsearch -QLLLb cn=config "(cn=config)" 280 | docker compose exec auth ldapsearch -QLLLb cn=config olcDatabase={-1}frontend 281 | docker compose exec auth ldapsearch -QLLLb cn=config olcDatabase={1}mdb 282 | 283 | auth-show_data: 284 | docker compose exec auth ldapsearch -QLLL 285 | 286 | auth-show_cat0: 287 | docker compose exec auth slapcat -n0 288 | 289 | auth-show_cat1: 290 | docker compose exec auth slapcat -n1 291 | 292 | auth-web: auth-web-up 293 | sleep 2 294 | $(call webb_cmd,http://$(AUW_FQDN)) 295 | 296 | auth-web-up: 297 | docker run -d --name $(AUW_NAME) --network $(NET_NAME) \ 298 | -e PHPLDAPADMIN_LDAP_HOSTS=auth -e PHPLDAPADMIN_HTTPS=false \ 299 | osixia/phpldapadmin || true 300 | 301 | auth-web-down: 302 | docker rm -f $(AUW_NAME) || true 303 | 304 | mta-init: 305 | 306 | mta-edh: 307 | docker compose exec mta run postfix_update_dhparam 308 | 309 | mta-test_smtp: 310 | $(call smtp_mail,smtp://$(MTA_FQDN),$(MAIL_FROM),$(AD_USR_CN)@$(MAIL_DOMAIN),$(PLAIN_SUBJ)) 311 | 312 | mta-test_rand: 313 | $(call smtp_mail,smtp://$(MTA_FQDN),$(MAIL_FROM),$(AD_USR_CN)@$(MAIL_DOMAIN),$(RANDS_SUBJ),$(RANDS_MESS)) 314 | 315 | mta-test_gtube: 316 | $(call smtp_mail,smtp://$(MTA_FQDN),$(MAIL_FROM),$(AD_USR_CN)@$(MAIL_DOMAIN),$(GTUBE_SUBJ),$(GTUBE_MESS)) 317 | 318 | mta-test_eicar: 319 | $(call smtp_mail,smtp://$(MTA_FQDN),$(MAIL_FROM),$(AD_USR_CN)@$(MAIL_DOMAIN),$(EICAR_SUBJ),$(EICAR_MESS),$(EICAR_TYPE)) 320 | 321 | mta-test_regexp: 322 | $(call smtp_mail,smtp://$(MTA_FQDN),$(MAIL_FROM),$(AD_USR_CN)+info@$(MAIL_DOMAIN),A regexp SMTP test message.) 323 | 324 | mta-test_smtps: 325 | $(call smtp_mail,smtps://$(MTA_FQDN),$(MAIL_FROM),$(AD_USR_CN)@$(MAIL_DOMAIN),A secure SMTPS test message.) \ 326 | -k --login-option "AUTH=PLAIN" -u $(AD_USR_CN):$(AD_USR_PW) 327 | 328 | mta-test_shared: all-test_quiet 329 | $(call smtp_mail,smtp://$(MTA_FQDN),$(MAIL_FROM),$(AD_SHR_CN)@$(MAIL_DOMAIN),A shared SMTP test message.) 330 | 331 | mta-test_public: all-test_quiet 332 | $(call smtp_mail,smtp://$(MTA_FQDN),$(MAIL_FROM),$(AD_PUB_CN)@$(MAIL_DOMAIN),A public SMTP test message.) 333 | 334 | mta-tools: 335 | docker compose exec mta apk --no-cache --update add \ 336 | nano less lsof htop openldap-clients bind-tools iputils strace iproute2 337 | 338 | mta-htop: mta-tools 339 | docker compose exec mta htop 340 | 341 | mta-encrypt: 342 | $(eval secret := $(shell whiptail --backtitle "doveadm pw" --title "encrypt password" --inputbox "password" 8 78 secret 3>&1 1>&2 2>&3)) 343 | docker compose exec mta doveadm pw -p $(secret) 344 | 345 | mta-show_doveconf: 346 | docker compose exec mta doveconf -n 347 | 348 | mta-show_postconf: 349 | docker compose exec mta postconf -n 350 | 351 | mta-show_mailq: 352 | docker compose exec mta mailq 353 | 354 | mta-flush_mailq: 355 | docker compose exec mta postqueue -f 356 | 357 | mta-test_auth: 358 | docker compose exec mta doveadm auth test $(AD_USR_CN) $(AD_USR_PW) 359 | 360 | mta-man: 361 | docker compose exec mta apk --no-cache --update add \ 362 | man-db man-pages apk-tools-doc postfix-doc cyrus-sasl-doc dovecot-doc 363 | 364 | flt-init: 365 | docker compose exec flt sh -c 'rspamadm configwizard; sv restart rspamd' 366 | 367 | flt-clamdtop: 368 | docker compose exec flt clamdtop 369 | 370 | flt-reload: 371 | docker compose exec flt /bin/sh -c 'echo RELOAD | nc localhost 3310' 372 | 373 | flt-ping: 374 | docker compose exec flt /bin/sh -c 'echo PING | nc localhost 3310' 375 | 376 | flt-tools: 377 | docker compose exec flt apk --no-cache --update add \ 378 | nano less lsof htop openldap-clients bind-tools iputils strace iproute2 curl 379 | 380 | flt-man: 381 | docker compose exec flt apk --no-cache --update add \ 382 | man-db man-pages apk-tools-doc clamav-doc rspamd-doc 383 | 384 | flt-web: 385 | $(call webb_cmd,http://$(FLT_FQDN)) 386 | 387 | flt-test: flt-test_plain flt-test_gtube flt-test_eicar 388 | 389 | flt-test_plain: 390 | $(call head_mail,$(MAIL_FROM),$(AD_USR_CN)@$(MAIL_DOMAIN),$(PLAIN_SUBJ)) \ 391 | | docker compose exec -T flt rspamc --hostname localhost 392 | 393 | flt-test_gtube: 394 | $(call head_mail,$(MAIL_FROM),$(AD_USR_CN)@$(MAIL_DOMAIN),$(GTUBE_SUBJ),$(GTUBE_MESS)) \ 395 | | docker compose exec -T flt rspamc --hostname localhost 396 | 397 | flt-test_eicar: 398 | $(call head_mail,$(MAIL_FROM),$(AD_USR_CN)@$(MAIL_DOMAIN),$(EICAR_SUBJ),$(EICAR_MESS),$(EICAR_TYPE)) \ 399 | | docker compose exec -T flt rspamc --hostname localhost 400 | 401 | flt-passwd: 402 | docker compose exec flt rspamadm pw 403 | 404 | flt-bayes_init: 405 | docker compose exec flt sh -c 'mkdir -p /tmp/ham && wget --no-check-certificate -O - $(HAM_URL) | tar -xzC /tmp/ham && rspamc learn_ham /tmp/ham' 406 | docker compose exec flt sh -c 'mkdir -p /tmp/spam && wget --no-check-certificate -O - $(SPAM_URL) | tar -xzC /tmp/spam && rspamc learn_spam /tmp/spam' 407 | 408 | flt-stat: 409 | docker compose exec flt rspamc stat 410 | 411 | flt-config: 412 | docker compose exec flt rspamadm configdump 413 | 414 | flt-config_%: 415 | docker compose exec flt rspamadm configdump $* 416 | 417 | $(addprefix flt-config_,actions antivirus classifier dkim dkim_signing greylist group.statistics logging metric milter_headers options redis worker): 418 | 419 | db-init: 420 | 421 | db-test: 422 | docker compose exec db mariadb-show -u $(MYSQL_USER) $(MYSQL_DATABASE) -p$(MYSQL_PASSWORD) 423 | 424 | db-web: db-web-up 425 | sleep 2 426 | $(call webb_cmd,http://$(DBW_FQDN)) 427 | 428 | db-web-up: 429 | docker run -d --name $(DBW_NAME) --network $(NET_NAME) \ 430 | -e PMA_HOST=db phpmyadmin/phpmyadmin || true 431 | 432 | db-web-down: 433 | docker rm -f $(DBW_NAME) || true 434 | 435 | app-init: app-wait app-public_store app-create_smime 436 | 437 | app-wait: 438 | # 439 | # Waiting for kopano-server to initialize. 440 | # 441 | time docker compose logs -f app | sed -n '/Startup succeeded/{p;q}' 442 | # 443 | # kopano-server ready. 444 | # 445 | 446 | app-tools: 447 | docker compose exec app apt update 448 | docker compose exec app apt install --yes \ 449 | less nano ldap-utils htop net-tools lsof iputils-ping dnsutils strace 450 | 451 | app-htop: app-tools 452 | docker compose exec app htop 453 | 454 | app-test_smtp: mta-test_smtp 455 | 456 | app-test_lmtp: 457 | $(call lmtp_mail,telnet://$(APP_FQDN):2003,$(MAIL_FROM),$(AD_USR_CN)@$(MAIL_DOMAIN),A LMTP test message.) 458 | 459 | app-test_all: all-test_muted $(addprefix app-test_,imap pop3 ical imaps pop3s icals) 460 | 461 | app-test_imap: 462 | $(CURL_CMD) imap://$(APP_FQDN) -u $(AD_USR_CN):$(AD_USR_PW) $(CURL_OPT) 463 | 464 | app-test_imaps: 465 | $(CURL_CMD) imaps://$(APP_FQDN) -k -u $(AD_USR_CN):$(AD_USR_PW) $(CURL_OPT) 466 | 467 | app-test_pop3: 468 | $(CURL_CMD) pop3://$(APP_FQDN) -u $(AD_USR_CN):$(AD_USR_PW) $(CURL_OPT) 469 | 470 | app-test_pop3s: 471 | $(CURL_CMD) pop3s://$(APP_FQDN) -k -u $(AD_USR_CN):$(AD_USR_PW) $(CURL_OPT) 472 | 473 | app-test_ical: 474 | $(CURL_CMD) http://$(APP_FQDN):8080 -u $(AD_USR_CN):$(AD_USR_PW) $(CURL_OPT) 475 | 476 | app-test_icals: 477 | $(CURL_CMD) https://$(APP_FQDN):8443 -k -u $(AD_USR_CN):$(AD_USR_PW) $(CURL_OPT) 478 | 479 | app-test_tls: 480 | $(TSSL_CMD) $(APP_FQDN):993 || true 481 | 482 | app-test_oof1: 483 | docker compose exec app kopano-oof -u $(AD_USR_CN) -m 1 -t "Dunno when I return" 484 | 485 | app-test_oof0: 486 | docker compose exec app kopano-oof -u $(AD_USR_CN) -m 0 487 | 488 | app-stats_server: 489 | docker compose exec app kopano-stats --system 490 | 491 | app-show_server: 492 | docker compose exec app kopano-stats --top 493 | 494 | app-show_user1: 495 | docker compose exec app kopano-admin --details $(AD_USR_CN) 496 | 497 | app-show_user2: app-tools 498 | docker compose exec app ldapsearch -H ldap://auth:389 -xLLL -b $(AD_BASE) '*' 499 | 500 | app-show_sync: 501 | docker compose exec app z-push-top 502 | 503 | app-create_store: 504 | docker compose exec app kopano-admin --create-store $(AD_USR_CN) 505 | 506 | app-public_store: 507 | docker compose exec app kopano-storeadm -P 508 | 509 | #app-add_user: 510 | # docker compose exec app kopano-admin -c $(AD_USR_CN) -p $(AD_USR_PW) \ 511 | # -e $(AD_USR_CN)@$(MAIL_DOMAIN) -f $(AD_USR_CN) -a 1 512 | 513 | $(addprefix app-parms_,archiver dagent gateway ical ldap search server spamd spooler): 514 | docker compose exec app run list_parms $(patsubst app-parms_%,%,$@) 515 | 516 | app-create_smime: all-create_smime 517 | docker cp ssl/ca.crt $(call dkr_srv_cnt,app):/usr/local/share/ca-certificates/$(MAIL_DOMAIN)_CA.crt 518 | docker compose exec app update-ca-certificates 519 | 520 | app-web: 521 | $(call webb_cmd,http://$(APP_FQDN)) 522 | 523 | all-test_quiet: 524 | $(eval CURL_OPT := -s -S ) 525 | 526 | all-test_muted: 527 | $(eval CURL_OPT := -s -S >/dev/null || true) 528 | 529 | all-create_smime: ssl/$(AD_USR_CN).p12 530 | 531 | all-destroy_smime: ssl-destroy 532 | -------------------------------------------------------------------------------- /demo/ad.mk: -------------------------------------------------------------------------------- 1 | ../test/ad.mk -------------------------------------------------------------------------------- /demo/dkr.mk: -------------------------------------------------------------------------------- 1 | ../test/dkr.mk -------------------------------------------------------------------------------- /demo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: demo 2 | 3 | services: 4 | app: 5 | image: mlan/kopano 6 | networks: 7 | - backend 8 | ports: 9 | - "127.0.0.1:8008:80" # WebApp & EAS (alt. HTTP) 10 | - "127.0.0.1:143:143" # IMAP (not needed if all devices can use EAS) 11 | - "127.0.0.1:110:110" # POP3 (not needed if all devices can use EAS) 12 | - "127.0.0.1:8080:8080" # ICAL (not needed if all devices can use EAS) 13 | - "127.0.0.1:993:993" # IMAPS (not needed if all devices can use EAS) 14 | - "127.0.0.1:995:995" # POP3S (not needed if all devices can use EAS) 15 | - "127.0.0.1:8443:8443" # ICALS (not needed if all devices can use EAS) 16 | depends_on: 17 | - auth 18 | - db 19 | - mta 20 | environment: # Virgin config, ignored on restarts unless FORCE_CONFIG given. 21 | - USER_PLUGIN=ldap 22 | - LDAP_URI=ldap://auth:389/ 23 | - MYSQL_HOST=db 24 | - SMTP_SERVER=mta 25 | - LDAP_SEARCH_BASE=${AD_BASE-dc=example,dc=com} 26 | - LDAP_USER_TYPE_ATTRIBUTE_VALUE=${AD_USR_OB-kopano-user} 27 | - LDAP_GROUP_TYPE_ATTRIBUTE_VALUE=${AD_GRP_OB-kopano-group} 28 | - LDAP_GROUPMEMBERS_ATTRIBUTE_TYPE=dn 29 | - LDAP_PROPMAP= 30 | - DAGENT_PLUGINS=movetopublicldap 31 | - MYSQL_DATABASE=${MYSQL_DATABASE-kopano} 32 | - MYSQL_USER=${MYSQL_USER-kopano} 33 | - MYSQL_PASSWORD=${MYSQL_PASSWORD-secret} 34 | - IMAP_LISTEN=*:143 # also listen to eth0 35 | - POP3_LISTEN=*:110 # also listen to eth0 36 | - ICAL_LISTEN=*:8080 # also listen to eth0 37 | - IMAPS_LISTEN=*:993 # enable TLS 38 | - POP3S_LISTEN=*:995 # enable TLS 39 | - ICALS_LISTEN=*:8443 # enable TLS 40 | - PLUGIN_SMIME_USER_DEFAULT_ENABLE_SMIME=true 41 | - SYSLOG_LEVEL=${SYSLOG_LEVEL-3} 42 | - LOG_LEVEL=${LOG_LEVEL-3} 43 | volumes: 44 | - app-conf:/etc/kopano 45 | - app-atch:/var/lib/kopano/attachments 46 | - app-sync:/var/lib/z-push 47 | - app-spam:/var/lib/kopano/spamd # kopano-spamd integration 48 | - /etc/localtime:/etc/localtime:ro # Use host timezone 49 | cap_add: # helps debugging by allowing strace 50 | - sys_ptrace 51 | 52 | mta: 53 | image: mlan/postfix 54 | hostname: ${MAIL_SRV-mx}.${MAIL_DOMAIN-example.com} 55 | networks: 56 | - backend 57 | ports: 58 | - "127.0.0.1:25:25" # SMTP 59 | - "127.0.0.1:465:465" # SMTPS authentication required 60 | depends_on: 61 | - auth 62 | environment: # Virgin config, ignored on restarts unless FORCE_CONFIG given. 63 | - MESSAGE_SIZE_LIMIT=${MESSAGE_SIZE_LIMIT-25600000} 64 | - LDAP_HOST=auth 65 | - VIRTUAL_TRANSPORT=lmtp:app:2003 66 | - SMTPD_MILTERS=inet:flt:11332 67 | - MILTER_DEFAULT_ACTION=accept 68 | - SMTP_RELAY_HOSTAUTH=${SMTP_RELAY_HOSTAUTH-} 69 | - SMTP_TLS_SECURITY_LEVEL=${SMTP_TLS_SECURITY_LEVEL-} 70 | - SMTP_TLS_WRAPPERMODE=${SMTP_TLS_WRAPPERMODE-no} 71 | - SMTPD_USE_TLS=yes 72 | - LDAP_USER_BASE=ou=${AD_USR_OU-users},${AD_BASE-dc=example,dc=com} 73 | - LDAP_QUERY_FILTER_USER=(&(objectclass=${AD_USR_OB-kopano-user})(mail=%s)) 74 | - LDAP_QUERY_FILTER_ALIAS=(&(objectclass=${AD_USR_OB-kopano-user})(kopanoAliases=%s)) 75 | - LDAP_QUERY_ATTRS_PASS=uid=user 76 | - REGEX_ALIAS=${REGEX_ALIAS-} 77 | volumes: 78 | - mta:/srv 79 | - app-spam:/var/lib/kopano/spamd # kopano-spamd integration 80 | - /etc/localtime:/etc/localtime:ro # Use host timezone 81 | cap_add: # helps debugging by allowing strace 82 | - sys_ptrace 83 | 84 | flt: 85 | image: mlan/rspamd 86 | networks: 87 | - backend 88 | ports: 89 | - "127.0.0.1:11334:11334" # HTML rspamd WebGui 90 | depends_on: 91 | - mta 92 | environment: # Virgin config, ignored on restarts unless FORCE_CONFIG given. 93 | - WORKER_CONTROLLER=enable_password="${FLT_PASSWD-secret}"; 94 | - METRICS=${FLT_METRIC} 95 | - CLASSIFIER_BAYES=${FLT_BAYES} 96 | - MILTER_HEADERS=${FLT_HEADERS} 97 | - DKIM_DOMAIN=${MAIL_DOMAIN-example.com} 98 | - DKIM_SELECTOR=${DKIM_SELECTOR-default} 99 | - SYSLOG_LEVEL=${SYSLOG_LEVEL-} 100 | - LOGGING=level="${FLT_LOGGING-error}"; 101 | volumes: 102 | - flt:/srv 103 | - app-spam:/var/lib/kopano/spamd # kopano-spamd integration 104 | - /etc/localtime:/etc/localtime:ro # Use host timezone 105 | cap_add: # helps debugging by allowing strace 106 | - sys_ptrace 107 | 108 | db: 109 | image: mariadb 110 | command: ['--log_warnings=1'] 111 | networks: 112 | - backend 113 | environment: 114 | - LANG=C.UTF-8 115 | - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD-secret} 116 | - MYSQL_DATABASE=${MYSQL_DATABASE-kopano} 117 | - MYSQL_USER=${MYSQL_USER-kopano} 118 | - MYSQL_PASSWORD=${MYSQL_PASSWORD-secret} 119 | volumes: 120 | - db:/var/lib/mysql 121 | - /etc/localtime:/etc/localtime:ro # Use host timezone 122 | 123 | auth: 124 | image: mlan/openldap 125 | networks: 126 | - backend 127 | command: --root-cn ${AD_ROOT_CN-admin} --root-pw ${AD_ROOT_PW-secret} 128 | environment: 129 | - LDAPBASE=${AD_BASE-dc=example,dc=com} 130 | - LDAPDEBUG=${AD_DEBUG-parse} 131 | volumes: 132 | - auth:/srv 133 | - /etc/localtime:/etc/localtime:ro # Use host timezone 134 | 135 | networks: 136 | backend: 137 | 138 | volumes: 139 | app-atch: 140 | app-conf: 141 | app-spam: 142 | app-sync: 143 | auth: 144 | db: 145 | mta: 146 | flt: 147 | -------------------------------------------------------------------------------- /demo/ssl.mk: -------------------------------------------------------------------------------- 1 | ../test/ssl.mk -------------------------------------------------------------------------------- /demo/ssl/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /hooks/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # hooks/build 3 | # $DOCKER_REPO and $DOCKER_TAG are injected into the build environment 4 | 5 | echo "hooks/build called with IMAGE_NAME=${DOCKER_REPO}:${DOCKER_TAG}, so we will run:" 6 | #printenv 7 | 8 | case "${DOCKER_TAG}" in 9 | mini*) bld_target=mini ;; 10 | base*) bld_target=base ;; 11 | full*|latest*|[0-9]*) bld_target=full ;; 12 | *) 13 | echo "NOTHING since we do not know how to build with tag=${DOCKER_TAG}" 14 | exit 1 15 | ;; 16 | esac 17 | 18 | echo "docker build --target $bld_target -t ${DOCKER_REPO}:${DOCKER_TAG} ." 19 | docker build --target $bld_target -t ${DOCKER_REPO}:${DOCKER_TAG} . 20 | -------------------------------------------------------------------------------- /src/acme/bin/acme-extract.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # Copyright (c) 2017 Brian 'redbeard' Harrington 3 | # 4 | # acme-export.sh - A simple utility to explode a Traefik acme.json file into a 5 | # directory of certificates and a private key 6 | # 7 | # Usage - acme-export.sh /etc/traefik/acme.json /etc/ssl/ 8 | # 9 | # Dependencies - 10 | # util-linux 11 | # openssl 12 | # jq 13 | # The MIT License (MIT) 14 | # 15 | # Permission is hereby granted, free of charge, to any person obtaining a copy 16 | # of this software and associated documentation files (the "Software"), to deal 17 | # in the Software without restriction, including without limitation the rights 18 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | # copies of the Software, and to permit persons to whom the Software is 20 | # furnished to do so, subject to the following conditions: 21 | # 22 | # The above copyright notice and this permission notice shall be included in 23 | # all copies or substantial portions of the Software. 24 | # 25 | 26 | # Exit codes: 27 | # 1 - A component is missing or could not be read 28 | # 2 - There was a problem reading acme.json 29 | # 4 - The destination certificate directory does not exist 30 | # 8 - Missing private key 31 | 32 | #set -o errexit 33 | ##set -o pipefail 34 | #set -o nounset 35 | #set -o verbose 36 | 37 | 38 | # 39 | # configuration 40 | # 41 | . docker-common.sh 42 | 43 | DOCKER_ACME_SSL_DIR=${DOCKER_ACME_SSL_DIR-/etc/ssl/acme} 44 | ACME_FILE=${ACME_FILE-/acme/acme.json} 45 | 46 | CMD_DECODE_BASE64="base64 -d" 47 | 48 | # 49 | # functions 50 | # 51 | usage() { echo "$(basename $0) " ;} 52 | 53 | test_args() { 54 | # when called by inotifyd the first argument is the single character 55 | # event descriptor, lets drop it 56 | dc_log 7 "Called with args $@" 57 | [ $# -ge 0 ] && [ ${#1} -eq 1 ] && shift 58 | readonly acmefile="${1-$ACME_FILE}" 59 | readonly certdir="${2-$DOCKER_ACME_SSL_DIR}" 60 | } 61 | 62 | test_dependencies() { 63 | # Allow us to exit on a missing jq binary 64 | if dc_is_installed jq; then 65 | dc_log 7 "The package jq is installed." 66 | else 67 | dc_log 4 "You must have the binary jq to use this." 68 | exit 1 69 | fi 70 | } 71 | 72 | test_acmefile() { 73 | if [ ! -r "${acmefile}" ]; then 74 | dc_log 4 "There was a problem reading from (${acmefile}). We need to read this file to explode the JSON bundle... exiting." 75 | exit 2 76 | fi 77 | } 78 | 79 | test_certdir() { 80 | if [ ! -d "${certdir}" ]; then 81 | dc_log 4 "Path ${certdir} does not seem to be a directory. We need a directory in which to explode the JSON bundle... exiting." 82 | exit 4 83 | fi 84 | } 85 | 86 | make_certdirs() { 87 | # If they do not exist, create the needed subdirectories for our assets 88 | # and place each in a variable for later use, normalizing the path 89 | mkdir -p "${certdir}/certs" "${certdir}/private" 90 | pdir="${certdir}/private" 91 | cdir="${certdir}/certs" 92 | } 93 | 94 | bad_acme() { 95 | dc_log 4 "There was a problem parsing your acme.json file." 96 | exit 2 97 | } 98 | 99 | read_letsencryptkey() { 100 | # look for key assuming acme v2 format 101 | priv=$(jq -e -r '.[].Account.PrivateKey' "${acmefile}" 2>/dev/null) 102 | if [ $? -eq 0 ]; then 103 | acmeversion=2 104 | dc_log 7 "Using acme v2 format, the PrivateKey was found in ${acmefile}" 105 | else 106 | # look for key assuming acme v1 format 107 | priv=$(jq -e -r '.Account.PrivateKey' "${acmefile}" 2>/dev/null) 108 | if [ $? -eq 0 ]; then 109 | acmeversion=1 110 | dc_log 7 "Using acme v1 format, the PrivateKey was found in ${acmefile}" 111 | else 112 | dc_log 4 "There didn't seem to be a private key in ${acmefile}. Please ensure that there is a key in this file and try again." 113 | exit 2 114 | fi 115 | fi 116 | } 117 | 118 | save_letsencryptkey() { 119 | local keyfile=${pdir}/letsencrypt.key 120 | printf -- \ 121 | "-----BEGIN RSA PRIVATE KEY-----\n%s\n-----END RSA PRIVATE KEY-----\n" \ 122 | ${priv} | fold -w 65 | \ 123 | openssl rsa -inform pem -out $keyfile 2>/dev/null 124 | if [ -e $keyfile ]; then 125 | dc_log 7 "PrivateKey is valid and saved in $keyfile" 126 | else 127 | dc_log 4 "PrivateKey appers NOT to be valid" 128 | exit 2 129 | fi 130 | } 131 | 132 | read_domains() { 133 | # Process the certificates for each of the domains in acme.json 134 | case $acmeversion in 135 | 1) jq_filter='.Certificates[].Domain.Main' ;; 136 | 2) jq_filter='.[].Certificates[].domain.main' ;; 137 | esac 138 | domains=$(jq -r $jq_filter $acmefile) 139 | if [ -n "$domains" ]; then 140 | dc_log 7 "Extracting private key and cert bundle for domains $domains." 141 | else 142 | dc_log 4 "Unable to find any domains in $acmefile." 143 | exit 2 144 | fi 145 | } 146 | 147 | # 148 | # Traefik stores a cert bundle for each domain. Within this cert 149 | # bundle there is both proper the certificate and the Let's Encrypt CA 150 | # 151 | save_certs() { 152 | dc_log 5 "Extracting private keys and cert bundles in ${acmefile}" 153 | case $acmeversion in 154 | 1) 155 | jq_crtfilter='.Certificates[] | select (.Domain.Main == $domain )| .Certificate' 156 | jq_keyfilter='.Certificates[] | select (.Domain.Main == $domain )| .Key' 157 | ;; 158 | 2) 159 | jq_crtfilter='.[].Certificates[] | select (.domain.main == $domain )| .certificate' 160 | jq_keyfilter='.[].Certificates[] | select (.domain.main == $domain )| .key' 161 | ;; 162 | esac 163 | for domain in $domains; do 164 | crt=$(jq -e -r --arg domain "$domain" "$jq_crtfilter" $acmefile) || bad_acme 165 | echo "${crt}" | ${CMD_DECODE_BASE64} > "${cdir}/${domain}.crt" 166 | key=$(jq -e -r --arg domain "$domain" "$jq_keyfilter" $acmefile) || bad_acme 167 | echo "${key}" | ${CMD_DECODE_BASE64} > "${pdir}/${domain}.key" 168 | done 169 | } 170 | 171 | # 172 | # Run command in ACME_POSTHOOK if it contain a valid command and runsvdir is running. 173 | # 174 | run_posthook() { 175 | if (pidof runsvdir >/dev/null && [ -n "$ACME_POSTHOOK" ] && command -v $ACME_POSTHOOK >/dev/null); then 176 | local out=$(eval "$ACME_POSTHOOK" 2>&1) 177 | [ -n "$out" ] && dc_log 7 "$ACME_POSTHOOK : $out" 178 | fi 179 | } 180 | 181 | # 182 | # run 183 | # 184 | test_args $@ 185 | test_dependencies 186 | test_acmefile 187 | test_certdir 188 | read_letsencryptkey 189 | make_certdirs 190 | save_letsencryptkey 191 | read_domains 192 | save_certs 193 | run_posthook 194 | -------------------------------------------------------------------------------- /src/acme/entry.d/10-acme-common: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 10-acme-common 4 | # 5 | # Define variables and functions used during container initialization. 6 | # 7 | # Variables defined in Dockerfile 8 | # DOCKER_ACME_SSL_DIR DOCKER_APPL_SSL_DIR 9 | # 10 | ACME_FILE=${ACME_FILE-/acme/acme.json} 11 | HOSTNAME=${HOSTNAME-$(hostname)} 12 | DOMAIN=${HOSTNAME#*.} 13 | DOCKER_APPL_SSL_CERT=${DOCKER_APPL_SSL_CERT-$DOCKER_APPL_SSL_DIR/cert.pem} 14 | DOCKER_APPL_SSL_KEY=${DOCKER_APPL_SSL_KEY-$DOCKER_APPL_SSL_DIR/priv_key.pem} 15 | DOCKER_ACME_SSL_H_CERT=$DOCKER_ACME_SSL_DIR/certs/${HOSTNAME}.crt 16 | DOCKER_ACME_SSL_H_KEY=$DOCKER_ACME_SSL_DIR/private/${HOSTNAME}.key 17 | DOCKER_ACME_SSL_D_CERT=$DOCKER_ACME_SSL_DIR/certs/${DOMAIN}.crt 18 | DOCKER_ACME_SSL_D_KEY=$DOCKER_ACME_SSL_DIR/private/${DOMAIN}.key 19 | 20 | # 21 | # Setup monitoring of ACME_FILE 22 | # 23 | acme_monitor_tls_cert() { 24 | if (dc_is_installed jq && (dc_is_command inotifyd || dc_is_command inotifywait)); then 25 | if [ -s $ACME_FILE ]; then 26 | # run acme-extract.sh on cnt creation (and every time the json file changes) 27 | dc_log 5 "Setup ACME TLS certificate monitoring" 28 | if dc_is_command inotifyd; then 29 | docker-service.sh "-n acme $(which inotifyd) $(which acme-extract.sh) $ACME_FILE:c" 30 | else 31 | docker-service.sh "-n acme sh -c \"while $(which inotifywait) -e close_write $ACME_FILE; do $(which acme-extract.sh); done\"" 32 | fi 33 | # acme-extract.sh reports to logger but it is yet to be started so this run will be quiet 34 | acme-extract.sh $ACME_FILE $DOCKER_ACME_SSL_DIR 35 | fi 36 | else 37 | dc_log 5 "Not all required pkgs installed so cannot setup ACME TLS certificate monitoring" 38 | fi 39 | } 40 | 41 | # 42 | # Arrange sym-links to support both host and domain certificates. 43 | # 44 | acme_symlink_tls_cert() { 45 | if ([ -r $DOCKER_ACME_SSL_H_CERT ] && [ -r $DOCKER_ACME_SSL_H_KEY ]); then 46 | dc_log 5 "Setting up ACME TLS certificate for host $HOSTNAME" 47 | mkdir -p $DOCKER_APPL_SSL_DIR 48 | ln -sf $DOCKER_ACME_SSL_H_CERT $DOCKER_APPL_SSL_CERT 49 | ln -sf $DOCKER_ACME_SSL_H_KEY $DOCKER_APPL_SSL_KEY 50 | else 51 | if ([ -r $DOCKER_ACME_SSL_D_CERT ] && [ -r $DOCKER_ACME_SSL_D_KEY ]); then 52 | dc_log 5 "Setting up ACME TLS certificate for domain $DOMAIN" 53 | mkdir -p $DOCKER_APPL_SSL_DIR 54 | ln -sf $DOCKER_ACME_SSL_D_CERT $DOCKER_APPL_SSL_CERT 55 | ln -sf $DOCKER_ACME_SSL_D_KEY $DOCKER_APPL_SSL_KEY 56 | fi 57 | fi 58 | } 59 | -------------------------------------------------------------------------------- /src/acme/entry.d/50-acme-monitor-tlscert: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 50-acme-monitor-tlscert 4 | # 5 | # Functions defined in: 6 | # 10-acme-common 7 | # 8 | 9 | # 10 | # Setup ACME monitor and arrange certificate symbolic links 11 | # 12 | acme_monitor_tls_cert 13 | acme_symlink_tls_cert 14 | -------------------------------------------------------------------------------- /src/docker/bin/docker-common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # docker-common.sh 4 | # 5 | # Defines common functions. Source this file from other scripts. 6 | # 7 | DOCKER_LOGLEVEL=${DOCKER_LOGLEVEL-5} 8 | DOCKER_LOGENTRY=${DOCKER_LOGENTRY-docker-entrypoint.sh} 9 | DOCKER_LOGUSAGE=${DOCKER_LOGUSAGE-usage} 10 | 11 | # 12 | # Write messages to console if interactive or syslog if not. 13 | # Usage: inform priority message 14 | # The priority may be specified numerically or as a facility.level pair. 15 | # Example user.notice, or 1.6 level is one of: 16 | # 0|emerg|1|alert|2|crit|3|err|4|warning|5|notice|6|info|7|debug 17 | # 18 | dc_log() { 19 | local script=$(basename $0) 20 | local stamp="$(dc_log_stamp)" 21 | local prio=$1 22 | local level=${prio#*.} 23 | local logtag="${script}[${$}]" 24 | local ttytag="$(dc_log_stamp)$(dc_log_tag $level $logtag):" 25 | shift 26 | # Assume interactive if we have stdout open and print usage message if needed. 27 | if [ -t 1 ]; then 28 | echo "$@" 29 | case "$level" in 30 | 0|emerg|1|alert|2|crit|3|err) $DOCKER_LOGUSAGE 2>/dev/null ;; 31 | esac 32 | else 33 | # If we have /dev/log socket send message to logger otherwise to stdout. 34 | if [ -S /dev/log ]; then 35 | logger -t "$logtag" -p "$prio" "$@" 36 | else 37 | if dc_log_level "$level"; then 38 | echo "$ttytag $@" 39 | fi 40 | fi 41 | fi 42 | } 43 | 44 | # 45 | # Color log output. Used if the syslogd daemon is not running. 46 | # 47 | dc_log_tag() { 48 | local level=$1 49 | local string=$2 50 | local c l 51 | case $level in 52 | 0|emerg) c=91; l=EMERG ;; 53 | 1|alert) c=91; l=ALERT ;; 54 | 2|crit) c=91; l=CRIT ;; 55 | 3|err) c=91; l=ERROR ;; 56 | 4|warning) c=93; l=WARN ;; 57 | 5|notice) c=92; l=NOTE ;; 58 | 6|info) c=92; l=INFO ;; 59 | 7|debug) c=92; l=DEBUG ;; 60 | esac 61 | printf "\e[%sm%s %s\e[0m\n" $c $string $l 62 | } 63 | 64 | # 65 | # Use $DOCKER_LOGLEVEL during image build phase. Assume we are in build phase if 66 | # $DOCKER_LOGENTRY is not running. 67 | # 68 | dc_log_level() { 69 | local level=$1 70 | if pidof $DOCKER_LOGENTRY >/dev/null; then 71 | [ "$level" -le "$SYSLOG_LEVEL" ] 72 | else 73 | [ "$level" -le "$DOCKER_LOGLEVEL" ] 74 | fi 75 | } 76 | 77 | # 78 | # Don't add time stamp during image build phase. Assume we are in build phase if 79 | # $DOCKER_LOGENTRY is not running. 80 | # 81 | dc_log_stamp() { 82 | if grep -q $DOCKER_LOGENTRY /proc/1/cmdline; then 83 | date +'%b %e %X ' 84 | fi 85 | } 86 | 87 | # 88 | # Tests if command is in the path 89 | # 90 | dc_is_command() { [ -x "$(command -v $1)" ] ;} 91 | 92 | # 93 | # Tests if pkgs are installed 94 | # 95 | dc_is_installed() { 96 | if dc_is_command apk; then 97 | ver_cmd="apk -e info" 98 | elif dc_is_command dpkg; then 99 | ver_cmd="dpkg -s" 100 | else 101 | dc_log 5 "No package manager found among: apk dpkg" 102 | fi 103 | for cmd in $@; do 104 | $ver_cmd $cmd > /dev/null 2>&1 || return 1 105 | done 106 | } 107 | 108 | # 109 | # Update loglevel 110 | # 111 | dc_update_loglevel() { 112 | loglevel=${1-$SYSLOG_LEVEL} 113 | if [ -n "$loglevel" ]; then 114 | dc_log 5 "Setting syslogd level=$loglevel." 115 | docker-service.sh "syslogd -nO- -l$loglevel $SYSLOG_OPTIONS" 116 | [ -n "$DOCKER_RUNFUNC" ] && sv restart syslogd 117 | fi 118 | } 119 | 120 | # 121 | # Print package versions 122 | # 123 | dc_pkg_versions() { 124 | local pkgs="$@" 125 | local len=$(echo $pkgs | tr " " "\n" | wc -L) 126 | local ver ver_cmd sed_flt 127 | local os=$(sed -rn 's/PRETTY_NAME="(.*)"/\1/p' /etc/os-release) 128 | local kern=$(uname -r) 129 | local host=$(uname -n) 130 | dc_log 5 $host $os $kern 131 | if dc_is_command apk; then 132 | ver_cmd="apk info -s" 133 | sed_flt="s/.*-(.*)-.*/\1/p" 134 | elif dc_is_command dpkg; then 135 | ver_cmd="dpkg -s" 136 | sed_flt="s/Version: ([[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+).*/\1/p" 137 | else 138 | dc_log 5 "No package manager found among: apk dpkg" 139 | fi 140 | for pkg in $pkgs; do 141 | ver=$($ver_cmd $pkg 2> /dev/null | sed -rn "$sed_flt") 142 | if [ -n "$ver" ]; then 143 | printf "\t%-${len}s\t%s\n" $pkg $ver 144 | fi 145 | done 146 | } 147 | -------------------------------------------------------------------------------- /src/docker/bin/docker-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # docker-config.sh 4 | # 5 | # Defines common functions. Source this file from other scripts. 6 | # 7 | # Defined in Dockerfile: 8 | # DOCKER_UNLOCK_FILE 9 | # 10 | HOSTNAME=${HOSTNAME-$(hostname)} 11 | DOMAIN=${HOSTNAME#*.} 12 | TLS_KEYBITS=${TLS_KEYBITS-2048} 13 | TLS_CERTDAYS=${TLS_CERTDAYS-30} 14 | DOCKER_CRONTAB_FILE=${DOCKER_CRONTAB_FILE-/etc/crontab} 15 | DOCKER_CRONTAB_ENV=${DOCKER_CRONTAB_ENV-CRONTAB_ENTRY} 16 | 17 | # 18 | # general file manipulation commands, used both during build and run time 19 | # 20 | 21 | _escape() { echo "$@" | sed 's|/|\\\/|g' | sed 's|;|\\\;|g' | sed 's|\$|\\\$|g' | sed "s/""'""/\\\x27/g" ;} 22 | 23 | dc_modify() { 24 | local cfg_file=$1 25 | shift 26 | local lhs="$1" 27 | shift 28 | local eq= 29 | local rhs= 30 | if [ "$1" = "=" ]; then 31 | eq="$1" 32 | shift 33 | rhs="$(_escape $@)" 34 | else 35 | rhs="$(_escape $@)" 36 | fi 37 | dc_log 7 's/.*('"$lhs"'\s*'"$eq"'\s*)[^#]+(.*)/\1'"$rhs"' \2/g' $cfg_file 38 | sed -ri 's/.*('"$lhs"'\s*'"$eq"'\s*)[^#]+(.*)/\1'"$rhs"' \2/g' $cfg_file 39 | } 40 | 41 | dc_replace() { 42 | local cfg_file=$1 43 | local old="$(_escape $2)" 44 | local new="$(_escape $3)" 45 | dc_log 7 's/'"$old"'/'"$new"'/g' $cfg_file 46 | sed -i 's/'"$old"'/'"$new"'/g' $cfg_file 47 | } 48 | 49 | dc_addafter() { 50 | local cfg_file=$1 51 | local startline="$(_escape $2)" 52 | local new="$(_escape $3)" 53 | dc_log 7 '/'"$startline"'/!{p;d;}; $!N;s/\n\s*$/\n'"$new"'\n/g' $cfg_file 54 | sed -i '/'"$startline"'/!{p;d;}; $!N;s/\n\s*$/\n'"$new"'\n/g' $cfg_file 55 | } 56 | 57 | dc_comment() { 58 | local cfg_file=$1 59 | local string="$2" 60 | dc_log 7 '/^'"$string"'/s/^/#/g' $cfg_file 61 | sed -i '/^'"$string"'/s/^/#/g' $cfg_file 62 | } 63 | 64 | dc_uncommentsection() { 65 | local cfg_file=$1 66 | local startline="$(_escape $2)" 67 | dc_log 7 '/^'"$startline"'$/,/^\s*$/s/^#*//g' $cfg_file 68 | sed -i '/^'"$startline"'$/,/^\s*$/s/^#*//g' $cfg_file 69 | } 70 | 71 | dc_removeline() { 72 | local cfg_file=$1 73 | local string="$2" 74 | dc_log 7 '/'"$string"'.*/d' $cfg_file 75 | sed -i '/'"$string"'.*/d' $cfg_file 76 | } 77 | 78 | dc_uniquelines() { 79 | local cfg_file=$1 80 | dc_log 7 '$!N; /^(.*)\n\1$/!P; D' $cfg_file 81 | sed -ri '$!N; /^(.*)\n\1$/!P; D' $cfg_file 82 | } 83 | 84 | 85 | # 86 | # Persist dirs 87 | # 88 | 89 | # 90 | # Make sure that we have the required directory structure in place under 91 | # DOCKER_PERSIST_DIR. 92 | # 93 | dc_persist_mkdirs() { 94 | local dirs=$@ 95 | for dir in $dirs; do 96 | mkdir -p ${DOCKER_PERSIST_DIR}${dir} 97 | done 98 | } 99 | 100 | # 101 | # Make sure that we have the required directory structure in place under 102 | # DOCKER_PERSIST_DIR. 103 | # 104 | dc_persist_dirs() { 105 | local srcdirs="$@" 106 | local dstdir 107 | if [ -n "$DOCKER_PERSIST_DIR" ]; then 108 | for srcdir in $srcdirs; do 109 | mkdir -p "$srcdir" 110 | dstdir="${DOCKER_PERSIST_DIR}${srcdir}" 111 | mkdir -p "$(dirname $dstdir)" 112 | mv -f "$srcdir" "$(dirname $dstdir)" 113 | ln -sf "$dstdir" "$srcdir" 114 | dc_log 5 "Moving $srcdir to $dstdir" 115 | done 116 | fi 117 | } 118 | 119 | # 120 | # mv dir to persist location and leave a link to it 121 | # 122 | dc_persist_mvdirs() { 123 | local srcdirs="$@" 124 | if [ -n "$DOCKER_PERSIST_DIR" ]; then 125 | for srcdir in $srcdirs; do 126 | if [ -e "$srcdir" ]; then 127 | local dstdir="${DOCKER_PERSIST_DIR}${srcdir}" 128 | local dsthome="$(dirname $dstdir)" 129 | if [ ! -d "$dstdir" ]; then 130 | dc_log 5 "Moving $srcdir to $dstdir" 131 | mkdir -p "$dsthome" 132 | mv "$srcdir" "$dsthome" 133 | ln -sf "$dstdir" "$srcdir" 134 | else 135 | dc_log 4 "$srcdir already moved to $dstdir" 136 | fi 137 | else 138 | dc_log 4 "Cannot find $srcdir" 139 | fi 140 | done 141 | fi 142 | } 143 | 144 | # 145 | # Conditionally change owner of files. 146 | # -a all 147 | # -r readable 148 | # -w writable 149 | # -x executable 150 | # 151 | dc_cond_chown() { 152 | dc_log 7 "Called with args: $@" 153 | OPTIND=1 154 | local find_opts="! -perm -404" 155 | while getopts ":arwx" opts; do 156 | case "${opts}" in 157 | a) find_opts="";; 158 | r) find_opts="! -perm -404";; 159 | w) find_opts="! -perm -606";; 160 | x) find_opts="! -perm -505";; 161 | esac 162 | done 163 | shift $((OPTIND -1)) 164 | local user=$1 165 | shift 166 | if id $user > /dev/null 2>&1; then 167 | for dir in $@; do 168 | if [ -n "$(find $dir ! -user $user $find_opts -print -exec chown -h $user: {} \;)" ]; then 169 | dc_log 5 "Changed owner to $user for some files in $dir" 170 | fi 171 | done 172 | else 173 | dc_log 3 "User $user is unknown." 174 | fi 175 | } 176 | 177 | # 178 | # Append entry if it is not already there. If mode is -i then append before last line. 179 | # 180 | dc_cond_append() { 181 | local mode filename lineraw lineesc 182 | case $1 in 183 | -i) mode=i; shift;; 184 | -a) mode=a; shift;; 185 | *) mode=a;; 186 | esac 187 | filename=$1 188 | shift 189 | lineraw=$@ 190 | lineesc="$(echo $lineraw | sed 's/[\";/*]/\\&/g')" 191 | if [ -e "$filename" ]; then 192 | if [ -z "$(sed -n '/'"$lineesc"'/p' $filename)" ]; then 193 | dc_log 7 "dc_cond_append append: $mode $filename $lineraw" 194 | case $mode in 195 | a) echo "$lineraw" >> $filename;; 196 | i) sed -i "$ i\\$lineesc" $filename;; 197 | esac 198 | else 199 | dc_log 4 "Avoiding duplication: $filename $lineraw" 200 | fi 201 | else 202 | dc_log 7 "dc_cond_append create: $mode $filename $lineraw" 203 | echo "$lineraw" >> $filename 204 | fi 205 | } 206 | 207 | dc_cpfile() { 208 | local suffix=$1 209 | shift 210 | local cfs=$@ 211 | for cf in $cfs; do 212 | cp "$cf" "$cf.$suffix" 213 | done 214 | } 215 | 216 | dc_mvfile() { 217 | local suffix=$1 218 | shift 219 | local cfs=$@ 220 | for cf in $cfs; do 221 | mv "$cf" "$cf.$suffix" 222 | done 223 | } 224 | 225 | # 226 | # Prune PID files 227 | # 228 | dc_prune_pidfiles() { 229 | local dirs=$@ 230 | for dir in $dirs; do 231 | if [ -n "$(find -H $dir -type f -name "*.pid" -exec rm {} \; 2>/dev/null)" ]; then 232 | dc_log 5 "Removed orphan pid files in $dir" 233 | fi 234 | done 235 | } 236 | 237 | # 238 | # Setup crontab entries 239 | # 240 | dc_crontab_entries() { 241 | local entries="$(eval echo \${!$DOCKER_CRONTAB_ENV*})" 242 | for entry in $entries; do 243 | [ -z "${changed+x}" ] && local changed= && sed -i '/^\s*[0-9*]/d' $DOCKER_CRONTAB_FILE 244 | echo "${!entry}" >> $DOCKER_CRONTAB_FILE 245 | dc_log 5 "Added entry ${!entry} in $DOCKER_CRONTAB_FILE" 246 | done 247 | } 248 | 249 | # 250 | # TLS/SSL Certificates [openssl] 251 | # 252 | dc_tls_setup_selfsigned_cert() { 253 | local cert=$1 254 | local key=$2 255 | if ([ ! -s $cert ] || [ ! -s $key ]); then 256 | dc_log 5 "Setup self-signed TLS certificate for host $HOSTNAME" 257 | openssl genrsa -out $key $TLS_KEYBITS 258 | openssl req -x509 -utf8 -new -batch -subj "/CN=$HOSTNAME" \ 259 | -days $TLS_CERTDAYS -key $key -out $cert 260 | fi 261 | } 262 | 263 | # 264 | # Configuration Lock 265 | # 266 | dc_lock_config() { 267 | if [ -f "$DOCKER_UNLOCK_FILE" ]; then 268 | rm $DOCKER_UNLOCK_FILE 269 | dc_log 5 "Removing unlock file, locking the configuration." 270 | elif [ -n "$FORCE_CONFIG" ]; then 271 | dc_log 5 "Configuration update was forced, since we got FORCE_CONFIG=$FORCE_CONFIG" 272 | else 273 | dc_log 5 "No unlock file found, so not touching configuration." 274 | fi 275 | } 276 | 277 | # 278 | # true if there is no unlock file or FORCE_CONFIG is not empty 279 | # 280 | dc_is_unlocked() { [ -f "$DOCKER_UNLOCK_FILE" ] || [ -n "$FORCE_CONFIG" ] ;} 281 | -------------------------------------------------------------------------------- /src/docker/bin/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # set -x 3 | # 4 | # This script need to run as PID 1 allowing it to receive signals from docker 5 | # 6 | # Usage: add the folowing lines in Dockerfile 7 | # ENTRYPOINT ["docker-entrypoint.sh"] 8 | # CMD runsvdir -P ${SVDIR} 9 | # 10 | 11 | # 12 | # Variables 13 | # 14 | DOCKER_ENTRY_DIR=${DOCKER_ENTRY_DIR-/etc/docker/entry.d} 15 | DOCKER_EXIT_DIR=${DOCKER_EXIT_DIR-/etc/docker/exit.d} 16 | SVDIR=${SVDIR-/etc/service} 17 | 18 | # 19 | # Source common functions. 20 | # 21 | . docker-common.sh 22 | . docker-config.sh 23 | 24 | # 25 | # Functions 26 | # 27 | 28 | # 29 | # run_parts dir 30 | # Read and execute commands from files in the _current_ shell environment 31 | # 32 | run_parts() { 33 | for file in $(find $1 -type f -executable 2>/dev/null|sort); do 34 | dc_log 7 run_parts: executing $file 35 | . $file 36 | done 37 | } 38 | 39 | # 40 | # If the service is running, send it the TERM signal, and the CONT signal. 41 | # If both files ./run and ./finish exits, execute ./finish. 42 | # After it stops, do not restart the service. 43 | # 44 | sv_down() { sv down ${SVDIR}/* ;} 45 | 46 | # 47 | # SIGTERM handler 48 | # docker stop first sends SIGTERM, and after a grace period, SIGKILL. 49 | # use exit code 143 = 128 + 15 -- SIGTERM 50 | # 51 | term_trap() { 52 | dc_log 4 "Got SIGTERM, so shutting down." 53 | run_parts "$DOCKER_EXIT_DIR" 54 | sv_down 55 | exit 143 56 | } 57 | 58 | 59 | # 60 | # Stage 0) Register signal handlers and redirect stderr 61 | # 62 | 63 | exec 2>&1 64 | trap 'kill $!; term_trap' SIGTERM 65 | 66 | # 67 | # Stage 1) run all entry scripts in $DOCKER_ENTRY_DIR 68 | # 69 | 70 | run_parts "$DOCKER_ENTRY_DIR" 71 | 72 | # 73 | # Stage 2) run provided arguments in the background 74 | # Start services with: runsvdir -P ${SVDIR} 75 | # 76 | 77 | "$@" & 78 | 79 | # 80 | # Stage 3) wait forever so we can catch the SIGTERM 81 | # 82 | while true; do 83 | tail -f /dev/null & wait $! 84 | done 85 | -------------------------------------------------------------------------------- /src/docker/bin/docker-runfunc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # docker-runfunc.sh 4 | # 5 | # Allow functions to be accessed from the command line. 6 | # 7 | 8 | # 9 | # Source common functions. 10 | # 11 | . docker-common.sh 12 | . docker-config.sh 13 | 14 | # 15 | # dr_docker_call_func "$@" 16 | # 17 | dr_docker_call_func() { 18 | export DOCKER_RUNFUNC="$@" 19 | local cmd=$1 20 | shift 21 | # dc_log 7 "CMD:$cmd ARG:$@" 22 | $cmd "$@" 23 | exit 0 24 | } 25 | 26 | # 27 | # dr_docker_run_parts dir name 28 | # Read and execute commands from files in the _current_ shell environment. 29 | # 30 | dr_docker_run_parts() { 31 | for file in $(find $1 -type f -name "$2" -executable 2>/dev/null|sort); do 32 | # dc_log 7 run_parts: executing $file 33 | . $file 34 | done 35 | } 36 | 37 | # 38 | # Source files with function definitions. 39 | # 40 | dr_docker_run_parts "$DOCKER_ENTRY_DIR" "1*" 41 | 42 | # 43 | # Call function. 44 | # 45 | dr_docker_call_func "$@" 46 | -------------------------------------------------------------------------------- /src/docker/bin/docker-service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # docker-service.sh 4 | # 5 | . docker-common.sh 6 | 7 | # use /etc/service if $SVDIR not already defined 8 | SVDIR=${SVDIR-/etc/service} 9 | DOCKER_SVLOG_DIR=${DOCKER_SVLOG_DIR-/var/log/sv} 10 | DOCKER_RUN_DIR=${DOCKER_RUN_DIR-/var/run} 11 | 12 | # 13 | # Define helpers 14 | # 15 | usage() { 16 | cat <<-!cat 17 | NAME 18 | docker-service.sh 19 | 20 | SYNOPSIS 21 | docker-service.sh [-d] [-f] [-h] [-l] [-n name] [-s file] [-q] command [args] 22 | 23 | OPTIONS 24 | -d default down 25 | -f remove lingering pid file at start up 26 | -h print this text and exit 27 | -l activate logging (svlogd) 28 | -n name use this name instead of command 29 | -s file source file 30 | -u user run command as this user 31 | -q send stdout and stderr to /dev/null 32 | 33 | EXAMPLES 34 | docker-service.sh "kopano-dagent -l" "-d kopano-grapi serve" 35 | "-q -s /etc/apache2/envvars apache2 -DFOREGROUND -DNO_DETACH -k start" 36 | 37 | !cat 38 | } 39 | 40 | base_name() { local base=${1##*/}; echo ${base%%.*} ;} 41 | 42 | pid_name() { 43 | local dir_name=${1%%-*} 44 | local pid_name=${1##*-} 45 | echo "${DOCKER_RUN_DIR}/${dir_name}/${pid_name}.pid" 46 | } 47 | 48 | add_opt() { 49 | if [ -z "$options" ]; then 50 | options=$1 51 | else 52 | options="$options,$1" 53 | fi 54 | } 55 | 56 | # 57 | # Define main function 58 | # 59 | 60 | init_service() { 61 | local redirstd= 62 | local clearpid= 63 | local sourcefile= 64 | local setuser= 65 | local sv_name cmd runsv_dir svlog_dir sv_log sv_down sv_force options 66 | dc_log 7 "Called with args $@" 67 | OPTIND=1 68 | while getopts ":dfhln:s:u:q" opts; do 69 | case "${opts}" in 70 | d) sv_down="down"; add_opt "down";; 71 | f) sv_force="force"; add_opt "force";; 72 | h) usage; exit;; 73 | l) sv_log="log"; add_opt "log";; 74 | n) sv_name="${OPTARG}"; add_opt "name";; 75 | s) sourcefile=". ${OPTARG}"; add_opt "source";; 76 | u) sv_user="${OPTARG}"; add_opt "user";; 77 | q) redirstd="exec >/dev/null"; add_opt "quiet";; 78 | esac 79 | done 80 | shift $((OPTIND -1)) 81 | cmd=$1 82 | cmd_path=$(which "$cmd") 83 | sv_name=${sv_name-$(base_name $cmd)} 84 | runsv_dir=$SVDIR/$sv_name 85 | svlog_dir=$DOCKER_SVLOG_DIR/$sv_name 86 | if [ -n "$sv_force" ]; then 87 | forcepid="$(echo rm -f $(pid_name $sv_name)*)" 88 | fi 89 | if [ -n "$sv_user" ]; then 90 | setuser="chpst -u $sv_name" 91 | fi 92 | shift 93 | if [ ! -z "$cmd_path" ]; then 94 | dc_log 5 "Setting up ($sv_name) options ($options) cmd ($cmd_path) args ($@)" 95 | mkdir -p $runsv_dir 96 | cat <<-!cat > $runsv_dir/run 97 | #!/bin/sh -e 98 | exec 2>&1 99 | $forcepid 100 | $redirstd 101 | $sourcefile 102 | exec $setuser $cmd_path $@ 103 | !cat 104 | chmod +x $runsv_dir/run 105 | if [ -n "$sv_down" ]; then 106 | touch $runsv_dir/down 107 | fi 108 | if [ -n "$sv_log" ]; then 109 | mkdir -p $runsv_dir/log $svlog_dir 110 | cat <<-!cat > $runsv_dir/log/run 111 | #!/bin/sh 112 | exec svlogd -tt $svlog_dir 113 | !cat 114 | chmod +x $runsv_dir/log/run 115 | fi 116 | else 117 | dc_log 4 "Cannot find command: $cmd" 118 | fi 119 | } 120 | 121 | # 122 | # run 123 | # 124 | 125 | for args in "$@" ; do 126 | init_service $args 127 | done 128 | -------------------------------------------------------------------------------- /src/docker/bin/run: -------------------------------------------------------------------------------- 1 | docker-runfunc.sh -------------------------------------------------------------------------------- /src/docker/entry.d/20-docker-print-versions: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 20-docker-print-versions 4 | # 5 | dc_pkg_versions postfix dovecot 6 | -------------------------------------------------------------------------------- /src/docker/entry.d/50-docker-update-loglevel: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 50-docker-update-loglevel 4 | # 5 | # If SYSLOG_LEVEL is not empty update syslog level 6 | # 7 | dc_update_loglevel 8 | -------------------------------------------------------------------------------- /src/docker/entry.d/80-docker-lock-config: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 80-docker-lock-config 4 | # 5 | # Functions defined in: 6 | # docker-config.sh 7 | # 8 | # 9 | dc_lock_config 10 | -------------------------------------------------------------------------------- /src/dovecot/entry.d/10-dovecot-common: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 10-dovecot-common 4 | # 5 | # Define variables and functions used during container initialization. 6 | # 7 | # Defined in Dockerfile: 8 | # DOCKER_IMAP_DIR DOCKER_IMAPDIST_DIR DOCKER_IMAP_PASSDB_FILE 9 | # 10 | DOVECOT_CF=${DOVECOT_CF-$DOCKER_IMAP_DIR/dovecot.conf} 11 | DOVECOT_CD=${DOVECOT_CD-$DOCKER_IMAP_DIR/conf.d} 12 | DOVECOT_PREFIX=${DOVECOT_PREFIX-DOVECOT_} 13 | 14 | # 15 | # Setup dovecot sasl auth for smtps and submission. 16 | # 17 | dovecot_setup_postfix() { 18 | # dovecot need to be installed 19 | if dc_is_installed dovecot; then 20 | dovecot_setup_conf 21 | dovecot_setup_master 22 | dovecot_setup_auth_file 23 | dovecot_setup_auth_imap 24 | dovecot_setup_auth_ldap 25 | dovecot_setup_auth_mysql 26 | if [ -n "$setup_auth" ]; then 27 | dovecot_setup_smtpd_sasl 28 | fi 29 | dovecot_apply_envvars 30 | fi 31 | } 32 | 33 | # 34 | # Configure postfix sasl auth to use dovecot. 35 | # 36 | dovecot_setup_smtpd_sasl() { 37 | dc_log 5 "[postfix] Enabling secure smtps and subm with client SASL auth-dovecot." 38 | # 39 | # enable sasl auth on the submission port 40 | # 41 | postconf -M "submission/inet=submission inet n - n - - smtpd" 42 | postconf -P "submission/inet/syslog_name=postfix/submission" 43 | postconf -P "submission/inet/smtpd_sasl_auth_enable=yes" 44 | postconf -P "submission/inet/smtpd_sasl_type=dovecot" 45 | postconf -P "submission/inet/smtpd_sasl_path=private/auth" 46 | postconf -P "submission/inet/smtpd_sasl_security_options=noanonymous" 47 | postconf -P "submission/inet/smtpd_tls_security_level=encrypt" 48 | postconf -P "submission/inet/smtpd_tls_auth_only=yes" 49 | postconf -P "submission/inet/smtpd_reject_unlisted_recipient=no" 50 | postconf -P "submission/inet/smtpd_client_restrictions=permit_sasl_authenticated,reject" 51 | # 52 | # enable sasl auth on the smtps port 53 | # 54 | postconf -M "smtps/inet=smtps inet n - n - - smtpd" 55 | postconf -P "smtps/inet/syslog_name=postfix/smtps" 56 | postconf -P "smtps/inet/smtpd_sasl_auth_enable=yes" 57 | postconf -P "smtps/inet/smtpd_sasl_type=dovecot" 58 | postconf -P "smtps/inet/smtpd_sasl_path=private/auth" 59 | postconf -P "smtps/inet/smtpd_sasl_security_options=noanonymous" 60 | postconf -P "smtps/inet/smtpd_tls_security_level=encrypt" 61 | postconf -P "smtps/inet/smtpd_tls_auth_only=yes" 62 | postconf -P "smtps/inet/smtpd_reject_unlisted_recipient=no" 63 | postconf -P "smtps/inet/smtpd_tls_wrappermode=yes" 64 | postconf -P "smtps/inet/smtpd_client_restrictions=permit_sasl_authenticated,reject" 65 | if dc_is_installed amavis; then 66 | postconf -P "submission/inet/cleanup_service_name=pre-cleanup" 67 | postconf -P "smtps/inet/cleanup_service_name=pre-cleanup" 68 | fi 69 | } 70 | 71 | # 72 | # Configure dovecot local config. 73 | # 74 | dovecot_setup_conf() { 75 | rm -rf $DOCKER_IMAP_DIR/* 76 | mkdir -p $DOVECOT_CD 77 | cat <<-!cat > $DOVECOT_CF 78 | !include conf.d/*.conf 79 | !cat 80 | cat <<-!cat > $DOCKER_IMAP_DIR/README 81 | You can find dovecot example config files here: $DOCKER_IMAPDIST_DIR 82 | !cat 83 | } 84 | 85 | # 86 | # Configure dovecot auth service. 87 | # postconf virtual_transport=lmtp:unix:private/transport 88 | # https://doc.dovecot.org/settings/core/ 89 | # 90 | dovecot_setup_master() { 91 | cat <<-!cat > $DOVECOT_CD/10-master.conf 92 | protocols = imap lmtp pop3 93 | mail_location = mbox:/var/mail/%Lu 94 | first_valid_uid = 1 95 | mail_uid = $DOCKER_APPL_RUNAS 96 | mail_gid = $DOCKER_APPL_RUNAS 97 | service auth { 98 | unix_listener /var/spool/postfix/private/auth { 99 | mode = 0660 100 | user = $DOCKER_APPL_RUNAS 101 | group = $DOCKER_APPL_RUNAS 102 | } 103 | } 104 | service lmtp { 105 | unix_listener /var/spool/postfix/private/transport { 106 | mode = 0660 107 | user = $DOCKER_APPL_RUNAS 108 | group = $DOCKER_APPL_RUNAS 109 | } 110 | } 111 | !cat 112 | cat <<-!cat > $DOVECOT_CD/10-auth.conf 113 | auth_mechanisms = plain login 114 | !cat 115 | } 116 | 117 | # 118 | # Configure dovecot to use passwd-file 119 | # 120 | dovecot_setup_auth_file() { 121 | local clientauth=${1-$SMTPD_SASL_CLIENTAUTH} 122 | if [ -n "$clientauth" ]; then 123 | setup_auth=file 124 | dc_log 5 "[dovecot] Setup authentication with passwd-file." 125 | cat <<-!cat > $DOVECOT_CD/10-auth-file.conf 126 | passdb { 127 | driver = passwd-file 128 | args = $DOCKER_IMAP_PASSDB_FILE 129 | } 130 | !cat 131 | # create client passwd file used for authentication 132 | for entry in $clientauth; do 133 | echo $entry >> $DOCKER_IMAP_PASSDB_FILE 134 | done 135 | fi 136 | } 137 | 138 | # 139 | # Configure dovecot to use remote imap server. 140 | # 141 | dovecot_setup_auth_imap() { 142 | local imaphost=${1-$SMTPD_SASL_IMAPHOST} 143 | if [ -n "$imaphost" ]; then 144 | setup_auth=imap 145 | dc_log 5 "[dovecot] Setup authentication with remote-imap-host: $imaphost." 146 | cat <<-!cat > $DOVECOT_CD/10-auth-imap.conf 147 | passdb { 148 | driver = imap 149 | args = host=$imaphost 150 | } 151 | # Enable some workarounds for Thunderbird 152 | imap_client_workarounds = tb-extra-mailbox-sep tb-lsub-flags 153 | !cat 154 | fi 155 | } 156 | 157 | # 158 | # Configure dovecot to use ldap. 159 | # 160 | # Try to reuse LDAP_QUERY_FILTER_USER which might contain (mail=%s). If so 161 | # replace is with (=%u) where is taken from LDAP_QUERY_ATTRS_PASS. 162 | # 163 | dovecot_setup_auth_ldap() { 164 | local pass_filter_1=${LDAP_QUERY_ATTRS_PASS/=user*/} 165 | local pass_filter_2=${LDAP_QUERY_FILTER_USER/mail=%s/$pass_filter_1=%u} 166 | local pass_filter=${LDAP_QUERY_FILTER_PASS-$pass_filter_2} 167 | local passdb_ldap_cf=$DOVECOT_CD/10-auth-ldap.conf 168 | local passdb_ldap_arg=$DOVECOT_CD/auth-args-ldap.conf.ext 169 | if ([ -n "$LDAP_HOST" ] && [ -n "$LDAP_USER_BASE" ] && [ -n "$pass_filter" ] && [ -n "$LDAP_QUERY_ATTRS_PASS" ]); then 170 | setup_auth=ldap 171 | dc_log 5 "[dovecot] Setup authentication with ldap-host: $LDAP_HOST." 172 | cat <<-!cat > $passdb_ldap_cf 173 | passdb { 174 | driver = ldap 175 | args = $passdb_ldap_arg 176 | } 177 | !cat 178 | cat <<-!cat > $passdb_ldap_arg 179 | auth_bind = yes 180 | hosts = $LDAP_HOST 181 | base = $LDAP_USER_BASE 182 | ldap_version = 3 183 | scope = subtree 184 | pass_attrs = $LDAP_QUERY_ATTRS_PASS 185 | pass_filter = $pass_filter 186 | !cat 187 | fi 188 | } 189 | 190 | # 191 | # Configure dovecot to use mysql. 192 | # 193 | dovecot_setup_auth_mysql() { 194 | local passdb_mysql_cf=$DOVECOT_CD/10-auth-mysql.conf 195 | local passdb_mysql_arg=$DOVECOT_CD/auth-args-mysql.conf.ext 196 | if ([ -n "$MYSQL_HOST" ] && [ -n "$MYSQL_DATABASE" ] && [ -n "$MYSQL_QUERY_PASS" ]); then 197 | setup_auth=mysql 198 | dc_log 5 "[dovecot] Setup authentication with mysql-host: $MYSQL_HOST." 199 | cat <<-!cat > $passdb_mysql_cf 200 | passdb { 201 | driver = sql 202 | args = $passdb_mysql_arg 203 | } 204 | !cat 205 | local passdb_mysql_con="host=$MYSQL_HOST dbname=$MYSQL_DATABASE" 206 | if [ -n "$MYSQL_USER" ]; then 207 | passdb_mysql_con="$passdb_mysql_con user=$MYSQL_USER" 208 | fi 209 | if [ -n "$MYSQL_PASSWORD" ]; then 210 | passdb_mysql_con="$passdb_mysql_con password=$MYSQL_PASSWORD" 211 | fi 212 | cat <<-!cat > $passdb_mysql_arg 213 | driver = mysql 214 | connect = $passdb_mysql_con 215 | password_query = $MYSQL_QUERY_PASS 216 | !cat 217 | fi 218 | } 219 | 220 | # 221 | # 222 | # Activate TLS if we have relevant envvars defined. 223 | # 224 | dovecot_activate_tls_cert() { 225 | local ssl_cf=$DOVECOT_CD/10-ssl.conf 226 | local ssl_crt=$(postconf -h smtpd_tls_cert_file) 227 | local ssl_key=$(postconf -h smtpd_tls_key_file) 228 | if ([ -n "$ssl_crt" ] && [ -n "$ssl_key" ]); then 229 | dc_log 5 "[dovecot] Setup ssl/tls certificate: $ssl_crt." 230 | cat <<-!cat > $ssl_cf 231 | ssl = yes 232 | ssl_cert = <$ssl_crt 233 | ssl_key = <$ssl_key 234 | !cat 235 | fi 236 | } 237 | 238 | # 239 | # Apply envvars 240 | # Dovecot parameter names are prefixed with ${DOVECOT_PREFIX} to form envvars. 241 | # doveconf -m auth -m login | sed -rn 's/^([^ ]+) =.*/\1/p' 242 | # 243 | dovecot_apply_envvars() { 244 | local env_vars="$(export -p | sed -nr 's/export '${DOVECOT_PREFIX}'([^=]+).*/\1/p')" 245 | local lcase_var env_val 246 | dc_log 7 "[dovecot] apply_envvars with prefix: ${DOVECOT_PREFIX}" 247 | for env_var in $env_vars; do 248 | lcase_var="$(echo $env_var | tr '[:upper:]' '[:lower:]')" 249 | if [ -n "$(doveconf -h $lcase_var 2>/dev/null | tr '\n' '\')" ]; then 250 | env_val="$(eval echo \$${DOVECOT_PREFIX}$env_var)" 251 | dc_log 5 "[dovecot] Setting parameter: $lcase_var = $env_val" 252 | echo "$lcase_var = $env_val" >> $DOVECOT_CD/50-envvars.conf 253 | fi 254 | done 255 | } 256 | 257 | # 258 | # Generate encrypted password. 259 | # 260 | doveadm_pw() { doveadm pw -p $1 ;} 261 | -------------------------------------------------------------------------------- /src/dovecot/entry.d/50-dovecot-config: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 50-dovecot-config 4 | # 5 | # Functions defined in: 6 | # 10-dovecot-common 7 | # 8 | # 9 | 10 | # 11 | # Run 12 | # 13 | if dc_is_unlocked; then 14 | dovecot_setup_postfix 15 | fi 16 | -------------------------------------------------------------------------------- /src/dovecot/entry.d/61-dovecot-config-late: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 61-dovecot-config-late 4 | # 5 | # Functions defined in: 6 | # 10-dovecot-common 7 | # 8 | # 9 | 10 | # 11 | # Run 12 | # 13 | if dc_is_unlocked; then 14 | dovecot_activate_tls_cert 15 | fi 16 | -------------------------------------------------------------------------------- /src/postfix/entry.d/10-postfix-common: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 10-postfix-common 4 | # 5 | # Define variables and functions used during container initialization. 6 | # 7 | # Defined in Dockerfile: 8 | # DOCKER_APPL_RUNAS DOCKER_CONF_DIR DOCKER_MAIL_LIB 9 | # 10 | DOCKER_DEFAULT_DOMAIN=${DOCKER_DEFAULT_DOMAIN-example.com} 11 | POSTFIX_VIRT_DOMAIN=${POSTFIX_VIRT_DOMAIN-$DOCKER_CONF_DIR/virt-domains} 12 | POSTFIX_VIRT_MAILBOX=${POSTFIX_VIRT_MAILBOX-$DOCKER_CONF_DIR/virt-users} 13 | POSTFIX_VIRT_ALIASES=${POSTFIX_VIRT_ALIASES-$DOCKER_CONF_DIR/virt-aliases} 14 | POSTFIX_ALIASES=${POSTFIX_ALIASES-$DOCKER_CONF_DIR/aliases} 15 | POSTFIX_REGEXP_ALIASES=${POSTFIX_REGEXP_ALIASES-$DOCKER_CONF_DIR/regexp-aliases} 16 | POSTFIX_SASL_PASSWD=${POSTFIX_SASL_PASSWD-$DOCKER_CONF_DIR/sasl-passwords} 17 | POSTFIX_LDAP_USERS_CF=${POSTFIX_LDAP_USERS_CF-$DOCKER_CONF_DIR/ldap-users.cf} 18 | POSTFIX_LDAP_ALIAS_CF=${POSTFIX_LDAP_ALIAS_CF-$DOCKER_CONF_DIR/ldap-aliases.cf} 19 | POSTFIX_LDAP_GROUPS_CF=${POSTFIX_LDAP_GROUPS_CF-$DOCKER_CONF_DIR/ldap-groups.cf} 20 | POSTFIX_LDAP_EXPAND_CF=${POSTFIX_LDAP_EXPAND_CF-$DOCKER_CONF_DIR/ldap-groups-expand.cf} 21 | POSTFIX_MYSQL_USERS_CF=${POSTFIX_MYSQL_USERS_CF-$DOCKER_CONF_DIR/mysql-users.cf} 22 | POSTFIX_MYSQL_ALIAS_CF=${POSTFIX_MYSQL_ALIAS_CF-$DOCKER_CONF_DIR/mysql-aliases.cf} 23 | LDAP_QUERY_ATTRS_USER=${LDAP_QUERY_ATTRS_USER-mail} 24 | 25 | # 26 | # Apply envvars 27 | # Some postfix parameters start with a digit and may contain dash "-" 28 | # and so are not legal variable names 29 | # 30 | postfix_apply_envvars() { 31 | local env_vars="$(export -p | sed -r 's/export ([^=]+).*/\1/g')" 32 | local lcase_var env_val 33 | for env_var in $env_vars; do 34 | lcase_var="$(echo $env_var | tr '[:upper:]' '[:lower:]')" 35 | if [ "$(postconf -H $lcase_var 2>/dev/null)" = "$lcase_var" ]; then 36 | env_val="$(eval echo \$$env_var)" 37 | dc_log 5 "[postfix] Setting parameter: $lcase_var = $env_val" 38 | postconf $lcase_var="$env_val" 39 | fi 40 | done 41 | } 42 | 43 | # 44 | # Copy image version of postfix install files to persistent volume if they 45 | # are different. 46 | # 47 | postfix_install_files() { 48 | for file in postfix-files dynamicmaps.cf.d; do 49 | if [ -e "$DOCKER_DIST_DIR/$file" ] && ! diff -q $DOCKER_CONF_DIR/$file $DOCKER_DIST_DIR/$file >/dev/null; then 50 | rm -rf $DOCKER_CONF_DIR/$file 51 | cp -rL $DOCKER_DIST_DIR/$file $DOCKER_CONF_DIR 52 | dc_log 5 "[postfix] Updating install file: $file" 53 | fi 54 | done 55 | } 56 | 57 | postfix_backup_file() { mv -f $1 $1.bak 2>/dev/null ;} 58 | 59 | # 60 | # run early to make sure MAIL_DOMAIN is not empty 61 | # 62 | postfix_default_domains() { 63 | local domains=${MAIL_DOMAIN-$(hostname -d)} 64 | if [ -z "$domains" ]; then 65 | export MAIL_DOMAIN=$DOCKER_DEFAULT_DOMAIN 66 | dc_log 4 "[postfix] No MAIL_DOMAIN, non FQDN HOSTNAME, so using: $MAIL_DOMAIN" 67 | fi 68 | } 69 | 70 | # 71 | # configure domains if we have recipients 72 | # 73 | postfix_setup_domains() { 74 | local domains=${MAIL_DOMAIN-$(hostname -d)} 75 | if [ -n "$domains" ] && ([ -n "$MAIL_BOXES" ] || \ 76 | ([ -n "$LDAP_HOST" ] && [ -n "$LDAP_USER_BASE" ] && [ -n "$LDAP_QUERY_FILTER_USER" ]) || \ 77 | ([ -n "$MYSQL_HOST" ] && [ -n "$MYSQL_DATABASE" ] && [ -n "$MYSQL_QUERY_USER" ])); then 78 | dc_log 5 "[postfix] Configuring for domains $domains" 79 | if [ $(echo $domains | wc -w) -gt 1 ]; then 80 | postfix_backup_file $POSTFIX_VIRT_DOMAIN 81 | for domain in $domains; do 82 | echo "$domain #domain" >> $POSTFIX_VIRT_DOMAIN 83 | done 84 | postmap lmdb:$POSTFIX_VIRT_DOMAIN 85 | postconf virtual_mailbox_domains=lmdb:$POSTFIX_VIRT_DOMAIN 86 | else 87 | postconf mydomain=$domains 88 | postconf virtual_mailbox_domains='$mydomain' 89 | fi 90 | fi 91 | } 92 | 93 | # 94 | # Set default postfix alias maps 95 | # 96 | postfix_default_maps() { 97 | postconf alias_maps= 98 | postconf alias_database= 99 | postconf virtual_lmdb_alias_maps= 100 | postconf virtual_ldap_alias_maps= 101 | postconf virtual_mysql_alias_maps= 102 | postconf virtual_regexp_alias_maps= 103 | postconf 'virtual_alias_maps=$virtual_lmdb_alias_maps $virtual_ldap_alias_maps $virtual_mysql_alias_maps $virtual_regexp_alias_maps' 104 | postconf virtual_lmdb_mailbox_maps= 105 | postconf virtual_ldap_mailbox_maps= 106 | postconf virtual_mysql_mailbox_maps= 107 | postconf 'virtual_mailbox_maps=$virtual_lmdb_mailbox_maps $virtual_ldap_mailbox_maps $virtual_mysql_mailbox_maps' 108 | } 109 | 110 | # 111 | # Setup lmdb mailboxes 112 | # MAIL_BOXES="address address:mailbox" 113 | # We use virtual, so table format is: address mailbox 114 | # Postfix need help with creating path root 115 | # 116 | postfix_setup_mailbox_lmdb() { 117 | local mboxmaps="${1-$MAIL_BOXES}" 118 | if [ -n "$mboxmaps" ]; then 119 | dc_log 5 "[postfix] Configuring virtual mailboxes." 120 | postfix_backup_file $POSTFIX_VIRT_MAILBOX 121 | for mboxmap in $mboxmaps; do 122 | echo "$mboxmap" | sed '/:/!s/.*/& &/g;s/:/ /g' >> $POSTFIX_VIRT_MAILBOX 123 | for mboxbase in $(echo "$mboxmap" | sed '/:/!d;s/^.*://g;/\//!d;s/\/[^/]*\/*$//g'); do 124 | mkdir -p ${DOCKER_MAIL_LIB}/${mboxbase} 125 | chown -LR ${DOCKER_APPL_RUNAS}: ${DOCKER_MAIL_LIB} 126 | done 127 | done 128 | postmap lmdb:$POSTFIX_VIRT_MAILBOX 129 | postconf virtual_lmdb_mailbox_maps=lmdb:$POSTFIX_VIRT_MAILBOX 130 | fi 131 | } 132 | 133 | # 134 | # Setup postfix aliases 135 | # MAIL_ALIASES="alias:address alias:address,address" 136 | # We use virtual, so table format is: alias address, address 137 | # 138 | postfix_setup_alias_lmdb() { 139 | local aliasmaps="${1-$MAIL_ALIASES}" 140 | if [ -n "$aliasmaps" ]; then 141 | dc_log 5 "[postfix] Configuring virtual aliases." 142 | postfix_backup_file $POSTFIX_VIRT_ALIASES 143 | for aliasmap in $aliasmaps; do 144 | echo "$aliasmap" | sed 's/:/ /g;s/[,]/& /g' >> $POSTFIX_VIRT_ALIASES 145 | done 146 | postmap lmdb:$POSTFIX_VIRT_ALIASES 147 | postconf virtual_lmdb_alias_maps=lmdb:$POSTFIX_VIRT_ALIASES 148 | fi 149 | } 150 | 151 | # 152 | # Allow recipient email address to be rewritten using regexp in REGEX_ALIAS 153 | # 154 | postfix_setup_alias_regex() { 155 | if [ -n "$REGEX_ALIAS" ]; then 156 | dc_log 5 "[postfix] Configuring recipient address rewrite using regexp: $REGEX_ALIAS" 157 | echo "$REGEX_ALIAS" > $POSTFIX_REGEXP_ALIASES 158 | postmap lmdb:$POSTFIX_REGEXP_ALIASES 159 | postconf "virtual_regexp_alias_maps=regexp:$POSTFIX_REGEXP_ALIASES" 160 | fi 161 | } 162 | 163 | # 164 | # Setup SMTP auth 165 | # SMTP_RELAY_HOSTAUTH="[relay_fqdn]:587 user:password" 166 | # 167 | postfix_setup_smtp_auth() { 168 | local hostauth=${1-$SMTP_RELAY_HOSTAUTH} 169 | local host=${hostauth% *} 170 | local auth=${hostauth#* } 171 | if [ -n "$host" ]; then 172 | dc_log 5 "[postfix] Configuring SMTP relay: $host" 173 | postconf -e relayhost=$host 174 | if [ -n "$auth" ]; then 175 | postconf -e smtp_sasl_auth_enable=yes 176 | postconf -e smtp_sasl_password_maps=lmdb:$POSTFIX_SASL_PASSWD 177 | postconf -e smtp_sasl_security_options=noanonymous 178 | echo "$hostauth" > $POSTFIX_SASL_PASSWD 179 | postmap lmdb:$POSTFIX_SASL_PASSWD 180 | fi 181 | else 182 | dc_log 7 "[postfix] No SMTP relay defined." 183 | fi 184 | } 185 | 186 | # 187 | # Setup ldap mailboxes 188 | # 189 | postfix_setup_mailbox_ldap() { 190 | if ([ -n "$LDAP_HOST" ] && [ -n "$LDAP_USER_BASE" ] && [ -n "$LDAP_QUERY_FILTER_USER" ]); then 191 | dc_log 5 "[postfix] Configuring ldap lookup with ldap-host: $LDAP_HOST" 192 | _postfix_generate_ldapmap "$LDAP_USER_BASE" "$LDAP_QUERY_ATTRS_USER" "$LDAP_QUERY_FILTER_USER" > $POSTFIX_LDAP_USERS_CF 193 | postconf virtual_ldap_mailbox_maps=ldap:$POSTFIX_LDAP_USERS_CF 194 | if [ -n "$LDAP_QUERY_FILTER_ALIAS" ]; then 195 | _postfix_generate_ldapmap "$LDAP_USER_BASE" "$LDAP_QUERY_ATTRS_USER" "$LDAP_QUERY_FILTER_ALIAS" > $POSTFIX_LDAP_ALIAS_CF 196 | if [ -n "$LDAP_GROUP_BASE" -a -n "$LDAP_QUERY_FILTER_GROUP" -a -n "$LDAP_QUERY_FILTER_EXPAND" ]; then 197 | _postfix_generate_ldapmap "$LDAP_GROUP_BASE" memberUid "$LDAP_QUERY_FILTER_GROUP" > $POSTFIX_LDAP_GROUPS_CF 198 | _postfix_generate_ldapmap "$LDAP_GROUP_BASE" "$LDAP_QUERY_ATTRS_USER" "$LDAP_QUERY_FILTER_EXPAND" > $POSTFIX_LDAP_EXPAND_CF 199 | postconf "virtual_ldap_alias_maps=ldap:$POSTFIX_LDAP_ALIAS_CF ldap:$POSTFIX_LDAP_GROUPS_CF ldap:$POSTFIX_LDAP_EXPAND_CF" 200 | else 201 | postconf "virtual_ldap_alias_maps=ldap:$POSTFIX_LDAP_ALIAS_CF" 202 | fi 203 | fi 204 | fi 205 | } 206 | 207 | _postfix_generate_ldapmap() { 208 | local server_host="$LDAP_HOST" 209 | local search_base="$1" 210 | local result_attribute="$2" 211 | local query_filter="$3" 212 | local bind_dn="$LDAP_BIND_DN" 213 | local bind_pw="$LDAP_BIND_PW" 214 | cat <<-!cat 215 | server_host = $server_host 216 | search_base = $search_base 217 | version = 3 218 | scope = sub 219 | result_attribute = $result_attribute 220 | query_filter = $query_filter 221 | !cat 222 | if [ -n "$bind_dn" ]; then 223 | cat <<-!cat 224 | bind = yes 225 | bind_dn = $bind_dn 226 | bind_pw = $bind_pw 227 | !cat 228 | fi 229 | } 230 | 231 | 232 | # 233 | # Setup mysql mailboxes 234 | # 235 | postfix_setup_mailbox_mysql() { 236 | if ([ -n "$MYSQL_HOST" ] && [ -n "$MYSQL_DATABASE" ]); then 237 | if [ -n "$MYSQL_QUERY_USER" ]; then 238 | dc_log 5 "[postfix] Configuring mysql mailbox lookup with mysql-host: $MYSQL_HOST" 239 | _postfix_generate_mysqlmap "$MYSQL_QUERY_USER" > $POSTFIX_MYSQL_USERS_CF 240 | postconf virtual_mysql_mailbox_maps=mysql:$POSTFIX_MYSQL_USERS_CF 241 | fi 242 | if [ -n "$MYSQL_QUERY_ALIAS" ]; then 243 | dc_log 5 "[postfix] Configuring mysql alias lookup with mysql-host: $MYSQL_HOST" 244 | _postfix_generate_mysqlmap "$MYSQL_QUERY_ALIAS" > $POSTFIX_MYSQL_ALIAS_CF 245 | postconf virtual_mysql_alias_maps=mysql:$POSTFIX_MYSQL_ALIAS_CF 246 | fi 247 | fi 248 | } 249 | 250 | _postfix_generate_mysqlmap() { 251 | local query="$@" 252 | local hosts="$MYSQL_HOST" 253 | local dbname="$MYSQL_DATABASE" 254 | local user="$MYSQL_USER" 255 | local password="$MYSQL_PASSWORD" 256 | cat <<-!cat 257 | hosts = $hosts 258 | dbname = $dbname 259 | query = $query 260 | !cat 261 | if [ -n "$user" ]; then 262 | cat <<-!cat 263 | user = $user 264 | password = $password 265 | !cat 266 | fi 267 | } 268 | 269 | # 270 | # Setup local mailboxes 271 | # 272 | postfix_setup_mailbox_local() { 273 | if [ -z "$VIRTUAL_TRANSPORT" ]; then # need local mail boxes 274 | dc_log 5 "[postfix] No VIRTUAL_TRANSPORT so arranging local mboxes: $DOCKER_MAIL_LIB" 275 | mkdir -p $DOCKER_MAIL_LIB 276 | dc_cond_chown $DOCKER_APPL_RUNAS $DOCKER_MAIL_LIB 277 | postconf virtual_mailbox_base=$DOCKER_MAIL_LIB 278 | postconf virtual_uid_maps=static:$(id -u $DOCKER_APPL_RUNAS) 279 | postconf virtual_gid_maps=static:$(id -g $DOCKER_APPL_RUNAS) 280 | fi 281 | } 282 | 283 | # 284 | # Update SMTPD_TLS_CERT_FILE and SMTPD_TLS_KEY_FILE. 285 | # Variables defined in 30-acme-common 286 | # DOCKER_APPL_SSL_CERT 287 | # DOCKER_APPL_SSL_KEY 288 | # 289 | postfix_export_tls_cert() { 290 | if ([ -f $DOCKER_APPL_SSL_CERT ] && [ -f $DOCKER_APPL_SSL_KEY ]); then 291 | export SMTPD_TLS_CERT_FILE=${SMTPD_TLS_CERT_FILE-$DOCKER_APPL_SSL_CERT} 292 | export SMTPD_TLS_KEY_FILE=${SMTPD_TLS_KEY_FILE-$DOCKER_APPL_SSL_KEY} 293 | fi 294 | } 295 | 296 | # 297 | # Generate self signed certificate if SMTPD_USE_TLS=yes but no certificates 298 | # are given. 299 | # 300 | postfix_generate_tls_cert() { 301 | if ([ -z "$SMTPD_TLS_CERT_FILE" ] && [ -z "$SMTPD_TLS_ECCERT_FILE" ] && \ 302 | [ -z "$SMTPD_TLS_DCERT_FILE" ] && [ -z "$SMTPD_TLS_CHAIN_FILES" ] && \ 303 | [ "$SMTPD_USE_TLS" = "yes" ] && dc_is_installed openssl); then 304 | dc_log 4 "[postfix] SMTPD_USE_TLS=yes but no certs given, so generating self-signed cert for host: $HOSTNAME" 305 | dc_tls_setup_selfsigned_cert $DOCKER_APPL_SSL_CERT $DOCKER_APPL_SSL_KEY 306 | fi 307 | } 308 | 309 | # 310 | # Activate TLS if we have relevant envvars defined. 311 | # 312 | postfix_activate_tls_cert() { 313 | if ([ -n "$SMTPD_TLS_CERT_FILE" ] || [ -n "$SMTPD_TLS_ECCERT_FILE" ] || \ 314 | [ -n "$SMTPD_TLS_DCERT_FILE" ] || [ -n "$SMTPD_TLS_CHAIN_FILES" ]); then 315 | dc_log 5 "[postfix] Activating incoming tls." 316 | # postconf -e smtpd_use_tls=yes # use only smtpd_tls_security_level 317 | postconf -e smtpd_tls_security_level=may 318 | postconf -e smtpd_tls_auth_only=yes 319 | fi 320 | } 321 | 322 | # 323 | # Optionally generate non-default Postfix SMTP server EDH parameters for improved security. 324 | # Note, since 2015, 512 bit export ciphers are no longer used this takes a long time. 325 | # Run this manually once the container is up by: 326 | # run postfix_update_dhparam 327 | # 328 | postfix_update_dhparam() { 329 | local bits=${1-2048} 330 | if dc_is_installed openssl; then 331 | dc_log 5 "[postfix] Regenerating edh $bits bit parameters." 332 | mkdir -p $DOCKER_APPL_SSL_DIR 333 | openssl dhparam -out $DOCKER_APPL_SSL_DIR/dh$bits.pem $bits 334 | postconf smtpd_tls_dh1024_param_file=$DOCKER_APPL_SSL_DIR/dh$bits.pem 335 | else 336 | dc_log 4 "[postfix] Cannot regenerate edh since openssl is not installed." 337 | fi 338 | } 339 | -------------------------------------------------------------------------------- /src/postfix/entry.d/30-postfix-migrate: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 30-postfix-migrate 4 | # 5 | # Try to make configs compatible with new version if MIGRATE_CONFIG is defined. 6 | # Set MIGRATE_CONFIG=1 2 3 to list of fixes or MIGRATE_CONFIG=all to attempt all fixes. 7 | # 8 | postfix_apply_migrate_fixes() { 9 | local applied 10 | if [ -n "$MIGRATE_CONFIG" ]; then 11 | for fix in ${MIGRATE_CONFIG/all/}; do # list all fixes here 12 | case $fix in 13 | *) fix= ;; 14 | esac 15 | if [ -n "$fix" ]; then 16 | applied="$applied $fix" 17 | fi 18 | done 19 | if [ -n "$applied" ]; then 20 | dc_log 5 "Applied fixes;$applied to configuration since MIGRATE_CONFIG=$MIGRATE_CONFIG" 21 | fi 22 | fi 23 | } 24 | 25 | postfix_notify_compat_issues() { 26 | local compat_bdb="$(postconf -n | grep -E 'hash:|btree:' | tr -d ' ')" 27 | for issue in $compat_bdb; do 28 | dc_log 4 "[postfix] Incompatible hash|btree, use FORCE_CONFIG to migrate to lmdb: $issue" 29 | done 30 | } 31 | 32 | # 33 | # run 34 | # 35 | postfix_apply_migrate_fixes 36 | postfix_notify_compat_issues 37 | -------------------------------------------------------------------------------- /src/postfix/entry.d/40-postfix-config-early: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 40-postfix-config-early 4 | # 5 | # Functions defined in: 6 | # 10-postfix-common 7 | # 8 | # 9 | 10 | # 11 | # Run early. 12 | # 13 | dc_prune_pidfiles /run $DOCKER_SPOOL_DIR/pid 14 | 15 | if dc_is_unlocked; then 16 | postfix_default_domains 17 | postfix_install_files 18 | fi 19 | -------------------------------------------------------------------------------- /src/postfix/entry.d/60-postfix-config-late: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # 60-postfix-config-late 4 | # 5 | # Functions defined in: 6 | # docker-config.sh 10-postfix-common 7 | # 8 | # 9 | 10 | # 11 | # Run late. 12 | # 13 | if dc_is_unlocked; then 14 | postfix_setup_domains 15 | postfix_default_maps 16 | postfix_setup_smtp_auth 17 | postfix_setup_mailbox_lmdb 18 | postfix_setup_mailbox_ldap 19 | postfix_setup_mailbox_mysql 20 | postfix_setup_mailbox_local 21 | postfix_setup_alias_lmdb 22 | # postfix_setup_aliasmap 23 | postfix_setup_alias_regex 24 | postfix_export_tls_cert 25 | postfix_generate_tls_cert 26 | postfix_export_tls_cert 27 | postfix_activate_tls_cert 28 | postfix_apply_envvars 29 | fi 30 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | # 3 | # test 4 | # 5 | 6 | -include *.mk 7 | 8 | TST_REPO ?= mlan/postfix 9 | TST_VER ?= latest 10 | 11 | MTA_LIST ?= srv cli 12 | CNT_LIST ?= $(MTA_LIST) auth db 13 | TST_NAME ?= test 14 | 15 | NET_NAME ?= $(TST_NAME)-net 16 | NET_ENV ?= --network $(NET_NAME) 17 | 18 | AD_DOM ?= example.com 19 | AD_BASE ?= $(call ad_sub_dc,$(AD_DOM)) 20 | AD_DC ?= $(call ad_cut_dot, 1, 1, $(AD_DOM)) 21 | AD_ROOT_CN ?= admin 22 | AD_ROOT_PW ?= secret 23 | AD_GRP_OU ?= groups 24 | AD_USR_OB ?= inetOrgPerson 25 | AD_USR_OU ?= users 26 | AD_USR_CN ?= receiver 27 | AD_USR_PW ?= secret 28 | AD_USR_AD ?= $(AD_USR_CN)@$(AD_DOM) 29 | AD_USR_BX ?= $(AD_USR_CN) 30 | AD_USR_AL ?= my-alias 31 | AD_MX_CN ?= office1 32 | AD_MX_PW ?= password1 33 | AD_FLT_US ?= "(&(objectclass=$(AD_USR_OB))(mail=%s))" 34 | AD_FLT_PW ?= "(&(objectclass=$(AD_USR_OB))(uid=%u))" 35 | AD_ATT_PW ?= uid=user 36 | 37 | SQL_DB ?= postfix 38 | SQL_TAB ?= users 39 | SQL_ROOT_PW ?= secret 40 | SQL_BD_CN ?= admin 41 | SQL_BD_PW ?= secret 42 | SQL_Q_CN ?= "select mail from $(SQL_TAB) where mail='%s' limit 1;" 43 | SQL_Q_PW ?= "select password, userid as user from $(SQL_TAB) where userid = '%u'" 44 | 45 | EX_DOM ?= my-domain.org 46 | EX_USR_CN ?= sender 47 | EX_MX_CN ?= office2 48 | EX_MX_PW ?= password2 49 | AD_SND_AD ?= $(EX_USR_CN)@$(AD_DOM) 50 | EX_USR_AD ?= $(AD_USR_CN)@$(EX_DOM) 51 | EX_USR_BX ?= $(EX_DOM)/$(AD_USR_CN) 52 | 53 | MAIL_SUB ?= ~~~test~subject~~~ 54 | MAIL_MSG ?= ~~~test~message~~~ 55 | 56 | TST_SLOG ?= 7 57 | TST_ALOG ?= 6 58 | TST_SBUG ?= 0 59 | TST_STAG ?= -99 60 | 61 | AUT_NAME ?= $(TST_NAME)-auth 62 | AUT_IMG ?= mlan/openldap 63 | AUT_FQDN ?= $(AUT_NAME).$(AD_DOM) 64 | AUT_VOL ?= 65 | AUT_ENV ?= $(NET_ENV) \ 66 | --name $(AUT_NAME) \ 67 | --hostname $(AUT_FQDN) \ 68 | -e LDAPBASE=$(AD_BASE) \ 69 | -e LDAPROOT_CN=$(AD_ROOT_CN) \ 70 | -e LDAPROOT_PW=$(AD_ROOT_PW) 71 | 72 | DB_NAME ?= $(TST_NAME)-db 73 | DB_IMG ?= mariadb 74 | DB_FQDN ?= $(DB_NAME).$(AD_DOM) 75 | DB_VOL ?= 76 | SQL_ENV ?= \ 77 | -e MYSQL_ROOT_PASSWORD=$(SQL_ROOT_PW) \ 78 | -e MYSQL_DATABASE=$(SQL_DB) \ 79 | -e MYSQL_USER=$(SQL_BD_CN) \ 80 | -e MYSQL_PASSWORD=$(SQL_BD_PW) 81 | DB_ENV ?= $(NET_ENV) $(SQL_ENV) \ 82 | --name $(DB_NAME) \ 83 | --hostname $(DB_FQDN) 84 | 85 | TST_ENV ?= $(NET_ENV) \ 86 | -e MYORIGIN=$(AD_DOM) \ 87 | -e SYSLOG_LEVEL=$(TST_SLOG) \ 88 | -e SA_TAG_LEVEL_DEFLT=$(TST_STAG) \ 89 | -e SA_DEBUG=$(TST_SBUG) \ 90 | -e LOG_LEVEL=$(TST_ALOG) 91 | 92 | CLT_NAME ?= $(TST_NAME)-cli 93 | CLT_FQDN ?= $(CLT_NAME).$(AD_DOM) 94 | CLT_ENV ?= $(TST_ENV) \ 95 | --name $(CLT_NAME) \ 96 | --hostname $(CLT_FQDN) 97 | CLTV_ENV ?= -v $(CLT_NAME):/srv 98 | SRV_NAME ?= $(TST_NAME)-srv 99 | SRV_FQDN ?= $(SRV_NAME).$(AD_DOM) 100 | SRV_CERT ?= ssl/$(SRV_FQDN).crt 101 | SRV_KEY ?= ssl/$(SRV_FQDN).key 102 | SRV_ENV ?= $(TST_ENV) \ 103 | --name $(SRV_NAME) \ 104 | --hostname $(SRV_FQDN) 105 | SRVV_ENV ?= -v $(SRV_NAME):/srv 106 | ADBA_ENV ?= \ 107 | -e LDAP_HOST=$(AUT_NAME) \ 108 | -e LDAP_USER_BASE=ou=$(AD_USR_OU),$(AD_BASE) 109 | ADLU_ENV ?= $(ADBA_ENV) \ 110 | -e LDAP_QUERY_FILTER_USER=$(AD_FLT_US) 111 | ADAU_ENV ?= $(ADBA_ENV) \ 112 | -e LDAP_QUERY_ATTRS_PASS=$(AD_ATT_PW) \ 113 | -e LDAP_QUERY_FILTER_PASS=$(AD_FLT_PW) 114 | ADAL_ENV ?= $(ADLU_ENV) \ 115 | -e LDAP_QUERY_ATTRS_PASS=$(AD_ATT_PW) 116 | DBLU_ENV ?= $(SQL_ENV) \ 117 | -e MYSQL_HOST=$(DB_NAME) \ 118 | -e MYSQL_QUERY_USER=$(SQL_Q_CN) \ 119 | -e MYSQL_QUERY_PASS=$(SQL_Q_PW) 120 | 121 | DOVE_ENV ?= \ 122 | -e VIRTUAL_TRANSPORT=lmtp:unix:private/transport \ 123 | -e DOVECOT_AUTH_USERNAME_FORMAT=%Ln 124 | #-e DOVECOT_DISABLE_PLAINTEXT_AUTH=no \ 125 | 126 | TLS_ENV ?= \ 127 | -v $(shell pwd)/ssl:/etc/ssl/postfix \ 128 | -e SMTPD_TLS_CERT_FILE=/etc/ssl/postfix/$(notdir $(SRV_CERT)) \ 129 | -e SMTPD_TLS_KEY_FILE=/etc/ssl/postfix/$(notdir $(SRV_KEY)) 130 | ACME_ENV ?= \ 131 | -v $(shell pwd)/acme:/acme \ 132 | -e ACME_FILE=/acme/acme.json 133 | 134 | IMAP_ENV ?= \ 135 | -e VIRTUAL_TRANSPORT=lmtp:unix:private/transport 136 | 137 | TST_BOX ?= $(AD_USR_AD):$(AD_USR_BX) $(AD_SND_AD) 138 | TST_BOX2 ?= $(AD_USR_AD):$(AD_USR_BX) $(EX_USR_AD):$(EX_USR_BX) 139 | TST_ALS ?= $(AD_USR_AL):$(AD_USR_AD) postmaster:$(AD_USR_AD),$(AD_USR_CN) 140 | TST_DK_S ?= default 141 | 142 | CURL_OPT ?= -s -v 143 | CURL_IMG ?= curlimages/curl 144 | CURL_ENV ?= $(NET_ENV) \ 145 | -i --rm 146 | 147 | GREP_ENV ?= 148 | 149 | TST_W8S1 ?= 1 150 | TST_W8AU ?= 3 151 | TST_W8DB ?= 5 152 | TST_W8S2 ?= 80 153 | TST_W8L1 ?= 20 154 | TST_W8L2 ?= 120 155 | 156 | export define LDIF_ADD_DATA 157 | dn: $(AD_BASE) 158 | objectClass: organization 159 | objectClass: dcObject 160 | dc: $(AD_DC) 161 | o: $(AD_DOM) 162 | 163 | dn: ou=$(AD_USR_OU),$(AD_BASE) 164 | objectClass: organizationalUnit 165 | ou: $(AD_USR_OU) 166 | 167 | dn: ou=$(AD_GRP_OU),$(AD_BASE) 168 | objectClass: organizationalUnit 169 | ou: $(AD_GRP_OU) 170 | 171 | dn: uid=$(AD_USR_CN),ou=$(AD_USR_OU),$(AD_BASE) 172 | objectClass: $(AD_USR_OB) 173 | cn: $(AD_USR_CN) 174 | sn: $(AD_USR_CN) 175 | uid: $(AD_USR_CN) 176 | mail: $(AD_USR_AD) 177 | userPassword: $(AD_USR_PW) 178 | 179 | dn: uid=$(AD_MX_CN),ou=$(AD_USR_OU),$(AD_BASE) 180 | objectClass: $(AD_USR_OB) 181 | cn: $(AD_MX_CN) 182 | sn: $(AD_MX_CN) 183 | uid: $(AD_MX_CN) 184 | userPassword: $(AD_MX_PW) 185 | endef 186 | 187 | export define SQL_ADD_DATA 188 | create table $(SQL_TAB)( 189 | id int not null auto_increment, 190 | userid varchar(128) not null, 191 | password varchar(64) not null, 192 | mail varchar(128), 193 | primary key ( id ) 194 | ); 195 | insert into $(SQL_TAB) (userid, password, mail) values ("$(AD_USR_CN)", concat("{PLAIN-MD5}",md5("$(AD_USR_PW)")), "$(AD_USR_AD)"); 196 | insert into $(SQL_TAB) (userid, password) values ("$(AD_MX_CN)", concat("{PLAIN-MD5}",md5("$(AD_MX_PW)"))); 197 | endef 198 | 199 | variables: 200 | make -pn | grep -A1 "^# makefile"| grep -v "^#\|^--" | sort | uniq 201 | 202 | ps: 203 | docker ps -a 204 | 205 | test-all: test-up_0 $(addprefix test_,1 2 3 4 5) 206 | 207 | 208 | test_%: test-up_% test-waitl_% test-logs_% test-mail_% test-down_% 209 | 210 | 211 | test-up_0: 212 | # 213 | # 214 | # 215 | # test (0) run without envvars (is there smoke?) 216 | # 217 | # run containers see if there are logs and stop. 218 | # 219 | # 220 | docker run -d --name $(SRV_NAME) $(TST_REPO):$(call bld_tag,base,$(TST_VER)) 221 | sleep $(TST_W8L1) 222 | docker container logs $(SRV_NAME) | grep 'docker-entrypoint.sh' 223 | docker rm -f $(SRV_NAME) 224 | sleep $(TST_W8S1) 225 | docker run -d --name $(SRV_NAME) $(TST_REPO):$(call bld_tag,full,$(TST_VER)) 226 | sleep $(TST_W8L1) 227 | docker container logs $(SRV_NAME) | grep 'docker-entrypoint.sh' 228 | docker rm -f $(SRV_NAME) 229 | sleep $(TST_W8S1) 230 | # 231 | # 232 | # test (0) success ☺ 233 | # 234 | # 235 | # 236 | 237 | test-up_1: test-up-net 238 | # 239 | # 240 | # 241 | # test (1) basic mta function and virtual alias lookup 242 | # 243 | # send: curl smtp://clt -> clt smtp://srv -> srv postfix mbox:$(AD_USR_BX) 244 | # recv: cat srv mbox:$(AD_USR_BX) 245 | # 246 | # 247 | docker run -d $(SRV_ENV) \ 248 | -e MAIL_BOXES="$(TST_BOX)" -e MAIL_ALIASES="$(TST_ALS)" \ 249 | $(TST_REPO):$(call bld_tag,base,$(TST_VER)) 250 | docker run -d $(CLT_ENV) \ 251 | -e RELAYHOST=[$(SRV_NAME)] -e MYDESTINATION= \ 252 | $(TST_REPO):$(call bld_tag,base,$(TST_VER)) 253 | 254 | test-up_2: test-up-net test-up-auth_2 255 | # 256 | # 257 | # 258 | # test (2) basic mta function and ldap lookup 259 | # 260 | # send: curl smtp://clt -> clt smtp://srv -> srv postfix mbox:$(AD_USR_AD) 261 | # recv: cat srv mbox:$(AD_USR_AD) 262 | # 263 | # 264 | docker run -d $(SRV_ENV) $(ADLU_ENV) \ 265 | $(TST_REPO):$(call bld_tag,base,$(TST_VER)) 266 | docker run -d $(CLT_ENV) -e RELAYHOST=[$(SRV_NAME)] -e MYDESTINATION= \ 267 | $(TST_REPO):$(call bld_tag,base,$(TST_VER)) 268 | 269 | test-up_3: test-up-net test-up-db_3 acme/acme.json 270 | # 271 | # 272 | # 273 | # test (3) basic mta function and sql lookup and imap 274 | # 275 | # send: curl smtp://clt -> clt smtp://srv -> srv dovecot mbox 276 | # recv: imaps://srv/inbox 277 | # 278 | # 279 | docker run -d $(SRV_ENV) $(DBLU_ENV) $(TLS_ENV) $(DOVE_ENV) \ 280 | $(TST_REPO):$(call bld_tag,full,$(TST_VER)) 281 | docker run -d $(CLT_ENV) -e RELAYHOST=[$(SRV_NAME)] -e MYDESTINATION= \ 282 | $(TST_REPO):$(call bld_tag,base,$(TST_VER)) 283 | 284 | test-up_4: test-up-net test-up-auth_4 acme/acme.json 285 | # 286 | # 287 | # 288 | # test (4) ldap sasl, basic and selfsigned tls over smtps, subm w ldap lookup 289 | # 290 | # send: curl smtps://clt -> clt smtps://srv:587 -> srv dovecot mbox 291 | # recv: imaps://srv/inbox 292 | # 293 | # 294 | docker run -d $(SRV_ENV) $(ADAL_ENV) $(TLS_ENV) $(DOVE_ENV) \ 295 | $(TST_REPO):$(call bld_tag,full,$(TST_VER)) 296 | docker run -d $(CLT_ENV) $(ADAU_ENV) -e SMTPD_USE_TLS=yes \ 297 | -e SMTP_RELAY_HOSTAUTH="[$(SRV_NAME)]:587 $(AD_MX_CN):$(AD_MX_PW)" \ 298 | -e MYDESTINATION= -e SMTP_TLS_SECURITY_LEVEL=encrypt \ 299 | $(TST_REPO):$(call bld_tag,full,$(TST_VER)) 300 | 301 | test-up_5: test-up-net acme/acme.json 302 | # 303 | # 304 | # 305 | # test (5) passwd-file sasl and acme tls over subm 306 | # 307 | # send: curl smtp://clt -> clt smtps://srv:587 -> srv postfix mbox:$(AD_USR_BX) 308 | # recv: cat srv mbox:$(AD_USR_BX) 309 | # 310 | # 311 | docker run -d $(SRV_ENV) $(ACME_ENV) \ 312 | -e MAIL_BOXES="$(TST_BOX)" \ 313 | -e SMTPD_SASL_CLIENTAUTH="$(AD_USR_CN):{plain}$(AD_USR_PW) $(EX_MX_CN):{plain}$(EX_MX_PW)" \ 314 | $(TST_REPO):$(call bld_tag,full,$(TST_VER)) 315 | docker run -d $(CLT_ENV) \ 316 | -e SMTP_RELAY_HOSTAUTH="[$(SRV_NAME)]:587 $(EX_MX_CN):$(EX_MX_PW)" \ 317 | -e MYDESTINATION= -e SMTP_TLS_SECURITY_LEVEL=encrypt \ 318 | $(TST_REPO):$(call bld_tag,base,$(TST_VER)) 319 | 320 | test-mail: test-mail_0 321 | test-mail-send: test-mail-send_0 322 | 323 | test-mail_%: test-mail-send_% test-waits_% test-mail-read_% 324 | # 325 | # 326 | # test ($*) success ☺ 327 | # 328 | # 329 | # 330 | 331 | test-logs_%: 332 | docker container logs $(SRV_NAME) 333 | 334 | test-waits_%: 335 | case $* in [0-5]) sleep $(TST_W8S1);; *) sleep $(TST_W8S2);; esac 336 | 337 | test-waitl_%: 338 | case $* in [0-5]) sleep $(TST_W8L1);; *) sleep $(TST_W8L2);; esac 339 | 340 | test-up-net: 341 | docker network create $(NET_NAME) 2>/dev/null || true 342 | 343 | test-down-net: 344 | docker network rm $(NET_NAME) 2>/dev/null || true 345 | 346 | test-down-vol: 347 | docker volume rm $(SRV_NAME) $(CLT_NAME) 2>/dev/null || true 348 | 349 | test-down: test-down_0 test-down-net test-down-vol acme-destroy 350 | 351 | test-down_%: 352 | docker rm -f $(CLT_NAME) $(SRV_NAME) $(AUT_NAME) $(DB_NAME) 2>/dev/null || true 353 | if [ $* -ge 0 ]; then sleep $(TST_W8S1); fi 354 | 355 | test-up-auth_%: 356 | docker run -d $(AUT_ENV) $(AUT_VOL) $(AUT_IMG) 357 | $(call dkr_cnt_wait_log,$(AUT_NAME),OpenLDAP) 358 | sleep $(TST_W8AU) 359 | echo "$$LDIF_ADD_DATA" | docker exec -i $(AUT_NAME) ldapadd -Q 360 | 361 | test-up-db_%: 362 | docker run -d $(DB_ENV) $(DB_VOL) $(DB_IMG) 363 | $(call dkr_cnt_wait_log,$(DB_NAME),ready for connections) 364 | sleep $(TST_W8DB) 365 | echo "$$SQL_ADD_DATA" | docker exec -i $(DB_NAME) mariadb -u$(SQL_BD_CN) -p$(SQL_BD_PW) $(SQL_DB) 366 | 367 | test-conf_%: 368 | ${eval tst_ad := ${shell case $* in \ 369 | 1) echo $(AD_USR_AL);; [7-9]) echo $(EX_USR_AD);; *) echo $(AD_USR_AD);; esac}} 370 | ${eval tst_bx := ${shell case $* in \ 371 | 2) echo $(AD_USR_AD);; [7-9]) echo $(EX_USR_BX);; *) echo $(AD_USR_BX);; esac}} 372 | ${eval tst_spro := ${shell case $* in 4) echo smtps;; *) echo smtp;; esac}} 373 | ${eval tst_str := ${shell case $* in 7) echo DKIM-Signature;; *) echo $(MAIL_SUB)$*;; esac}} 374 | 375 | test-mail-send_%: test-conf_% 376 | printf "From: <$(AD_SND_AD)>\nTo: <$(tst_ad)>\nDate: $$(date -R)\nSubject:$(MAIL_SUB)$*\n$(MAIL_MSG)$*\n" \ 377 | | tee /dev/tty | docker run $(CURL_ENV) $(CURL_IMG) $(CURL_OPT) -T - \ 378 | --mail-from $(AD_SND_AD) --mail-rcpt $(tst_ad) \ 379 | --url $(tst_spro)://$(CLT_NAME) -u $(AD_USR_CN):$(AD_USR_PW) --ssl --anyauth -k 380 | 381 | test-mail-read_%: test-conf_% all-test_quiet 382 | case $* in [3-4]) ${MAKE} srv-imap;; *) ${MAKE} test-mail-cat_$*;; esac | grep $(tst_str) 383 | 384 | test-mail-cat_%: test-conf_% 385 | docker exec -it $(SRV_NAME) cat /var/mail/$(tst_bx) 386 | 387 | $(addprefix test-,diff env htop imap logs pop3 sh sv): 388 | ${MAKE} $(patsubst test-%,srv-%,$@) 389 | 390 | $(addsuffix -sh,$(CNT_LIST)): 391 | docker exec -it $(patsubst %-sh,$(TST_NAME)-%,$@) sh -c 'exec $$(getent passwd root | sed "s/.*://g")' 392 | 393 | $(addsuffix -env,$(CNT_LIST)): 394 | docker exec -it $(patsubst %-env,$(TST_NAME)-%,$@) env 395 | 396 | $(addsuffix -logs,$(CNT_LIST)): 397 | docker container logs $(patsubst %-logs,$(TST_NAME)-%,$@) 398 | 399 | $(addsuffix -diff,$(CNT_LIST)): 400 | docker container diff $(patsubst %-diff,$(TST_NAME)-%,$@) 401 | 402 | $(addsuffix -tools,$(CNT_LIST)): 403 | docker exec -it $(patsubst %-tools,$(TST_NAME)-%,$@) \ 404 | apk --no-cache --update add \ 405 | nano less lsof htop openldap-clients bind-tools iputils mariadb-client 406 | 407 | $(addsuffix -htop,$(CNT_LIST)): 408 | docker exec -it $(patsubst %-htop,$(TST_NAME)-%,$@) htop 409 | 410 | $(addsuffix -imap,$(MTA_LIST)): 411 | docker run $(CURL_ENV) $(CURL_IMG) $(CURL_OPT) \ 412 | imap://$(patsubst %-imap,$(TST_NAME)-%,$@)/inbox \ 413 | -X "fetch * all" --ssl --anyauth -k -u $(AD_USR_CN):$(AD_USR_PW) 414 | 415 | $(addsuffix -pop3,$(MTA_LIST)): 416 | docker run $(CURL_ENV) $(CURL_IMG) $(CURL_OPT) \ 417 | pop3://$(patsubst %-pop3,$(TST_NAME)-%,$@)/1 \ 418 | --ssl --anyauth -k -u $(AD_USR_CN):$(AD_USR_PW) 419 | 420 | $(addsuffix -sv,$(MTA_LIST)): 421 | docker exec -it $(patsubst %-sv,$(TST_NAME)-%,$@) sh -c 'sv status $$SVDIR/*' 422 | 423 | $(addsuffix -userdb,$(MTA_LIST)): 424 | docker exec -it $(patsubst %-userdb,$(TST_NAME)-%,$@) doveadm user $(AD_USR_CN) 425 | 426 | $(addsuffix -doveconf,$(MTA_LIST)): 427 | docker exec -it $(patsubst %-doveconf,$(TST_NAME)-%,$@) doveconf -NP 428 | 429 | $(addsuffix -regen-edh,$(MTA_LIST)): 430 | docker exec -it $(patsubst %-regen-edh,$(TST_NAME)-%,$@) conf update_postfix_dhparam 431 | 432 | $(addsuffix -dkim-key,$(MTA_LIST)): 433 | docker exec -it $(patsubst %-dkim-key,$(TST_NAME)-%,$@) amavisd testkeys 434 | 435 | $(addsuffix -spam-learn,$(MTA_LIST)): 436 | docker exec -it $(patsubst %-spam-learn,$(TST_NAME)-%,$@) amavis-learn.sh a 437 | 438 | $(addsuffix -bayes-status,$(MTA_LIST)): 439 | docker exec -it $(patsubst %-bayes-status,$(TST_NAME)-%,$@) sa-learn --dump magic \ 440 | | sed -r 's/[^ ]+\s+[^ ]+\s+([^ ]+).*non-token data: (.*)/\1\@\2/g' \ 441 | | sed -r '/atime/s/(.*)@(.*)/echo $$(date --date=@\1 +%Y%b%d-%T)@\2/eg' \ 442 | | column -t -s @ 443 | 444 | auth-test: 445 | docker exec $(AUT_NAME) ldapsearch 446 | 447 | db-table: 448 | echo "select * from $(SQL_TAB)" | \ 449 | docker exec -i $(DB_NAME) mariadb -t -u$(SQL_BD_CN) -p$(SQL_BD_PW) $(SQL_DB) 450 | 451 | db-auth: 452 | echo "select password, userid as user from $(SQL_TAB) where userid = \"$(AD_MX_CN)\"" | \ 453 | docker exec -i $(DB_NAME) mariadb -t -u$(SQL_BD_CN) -p$(SQL_BD_PW) $(SQL_DB) 454 | 455 | $(addprefix test-tls_,25 465 587): 456 | test-tls_%: 457 | $(eval tst_starttls := $(shell if [ $* != 465 ]; then echo --starttls smtp; fi )) 458 | docker run --rm -it --network $(NET_NAME) drwetter/testssl.sh $(tst_starttls) $(SRV_NAME):$* || true 459 | 460 | all-test_quiet: 461 | $(eval CURL_OPT := -s -S ) 462 | 463 | acme-destroy: ssl-destroy 464 | rm -f acme/* 465 | 466 | acme/acme.json: $(SRV_CERT) 467 | bin/gen-acme-json.sh $(AD_USR_AD) $(SRV_FQDN) $(SRV_KEY) $(SRV_CERT) > $@ 468 | -------------------------------------------------------------------------------- /test/acme/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /test/ad.mk: -------------------------------------------------------------------------------- 1 | # ad.mk 2 | # 3 | # AD and LDAP make-functions 4 | # 5 | 6 | # 7 | # chars 8 | # 9 | char_null := 10 | char_space := $(char_null) # 11 | char_comma := , 12 | char_dot := . 13 | char_colon := : 14 | 15 | # 16 | # $(call ad_sub_dc,example.com) -> dc=example,dc=com 17 | # 18 | ad_sub_dc = $(subst $(char_space),$(char_comma),$(addprefix dc=, $(subst ., ,$(1)))) 19 | # 20 | # $(call ad_sub_dot,dc=example,dc=com) -> example.com 21 | # 22 | ad_sub_dot = $(subst $(char_comma)dc=,$(char_dot),$(patsubst dc=%,%,$(1))) 23 | # 24 | # $(call ad_cat_dn,admin,dc=example,dc=com) -> cn=admin,dc=example,dc=com 25 | # 26 | ad_cat_dn = cn=$(1),$(2) 27 | # 28 | # $(call ad_cut_dot,1,1,example.com) -> example 29 | # 30 | ad_cut_dot = $(subst $(char_space),$(char_dot),$(wordlist $(1), $(2), $(subst $(char_dot),$(char_space),$(3)))) 31 | # 32 | # $(call ad_rootdc,2,9,adm.dom.org:secret) -> dom.org 33 | # 34 | ad_rootdc = $(subst $(char_space),$(char_dot),$(wordlist $(1), $(2), $(subst $(char_dot),$(char_space),$(firstword $(subst $(char_colon),$(char_space),$(3)))))) 35 | # 36 | # $(call ad_rootpw,adm.dom.org:secret) -> secret 37 | # 38 | ad_rootpw = $(lastword $(subst $(char_colon),$(char_space),$(1))) 39 | -------------------------------------------------------------------------------- /test/bin/gen-acme-json.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # args: email hostname keyfile certfile 3 | mail=$1 4 | host=$2 5 | keyfile=$3 6 | certfile=$4 7 | 8 | # 9 | # The "PrivateKey": attribute needs a PKCS#1 key without tags and line breaks 10 | # "openssl req -newkey rsa" generates a key stored in PKCS#8 so needs conversion 11 | # 12 | #acme_strip_tag() { openssl rsa -in $1 | sed '/^-----/d' | sed ':a;N;$!ba;s/\n//g' ;} 13 | acme_strip_tag() { sed '/^-----/d' $1 | sed ':a;N;$!ba;s/\n//g' ;} 14 | 15 | cat <<-!cat 16 | { 17 | "Account": { 18 | "Email": "$mail", 19 | "Registration": { 20 | "body": { 21 | "status": "valid", 22 | "contact": [ 23 | "mailto:$mail" 24 | ] 25 | }, 26 | "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/$RANDOM" 27 | }, 28 | "PrivateKey": "$(acme_strip_tag $keyfile)", 29 | "KeyType": "2048" 30 | }, 31 | "Certificates": [ 32 | { 33 | "Domain": { 34 | "Main": "$host", 35 | "SANs": null 36 | }, 37 | "Certificate": "$(base64 -w 0 $certfile)", 38 | "Key": "$(base64 -w 0 $keyfile)" 39 | } 40 | ], 41 | "HTTPChallenges": {}, 42 | "TLSChallenges": {} 43 | } 44 | !cat 45 | -------------------------------------------------------------------------------- /test/bin/test-smtp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TESTSMTP_SMTPHOST=localhost 4 | TESTSMTP_SMTPPORT=25 5 | TESTSMTP_MAILFROM=postmaster@example.com 6 | TESTSMTP_MAILTO=postmaster@localhost 7 | 8 | # 9 | # define function 10 | # 11 | 12 | test_smtp() { 13 | local host=${1-$TESTSMTP_SMTPHOST} 14 | local port=${2-$TESTSMTP_SMTPPORT} 15 | local from=${3-$TESTSMTP_MAILFROM} 16 | local to=${4-$TESTSMTP_MAILTO} 17 | local from_name=${from%@*} 18 | local to_name=${to%@*} 19 | local domain=${from#*@} 20 | nc -C $host $port <<-!nc 21 | EHLO $domain 22 | MAIL FROM:<$from> 23 | RCPT TO:<$to> 24 | DATA 25 | From: $from_name <$from> 26 | To: $to_name <$to> 27 | Subject: A test message generated on $(hostname) 28 | Hello $name 29 | Local time is now: $(date) 30 | . 31 | QUIT 32 | !nc 33 | } 34 | 35 | # 36 | # run 37 | # 38 | 39 | test_smtp "$@" 40 | -------------------------------------------------------------------------------- /test/bld.mk: -------------------------------------------------------------------------------- 1 | ../bld.mk -------------------------------------------------------------------------------- /test/dkr.mk: -------------------------------------------------------------------------------- 1 | # dkr.mk 2 | # 3 | # Container make-functions 4 | # 5 | 6 | # 7 | # $(call dkr_srv_cnt,app) -> d03dda046e0b90c... 8 | # 9 | dkr_srv_cnt = $(shell docker compose ps -q $(1) | head -n1) 10 | # 11 | # $(call dkr_cnt_ip,demo-app-1) -> 172.28.0.3 12 | # 13 | dkr_cnt_ip = $(shell docker inspect -f \ 14 | '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ 15 | $(1) | head -n1) 16 | # 17 | # $(call dkr_srv_ip,app) -> 172.28.0.3 18 | # 19 | dkr_srv_ip = $(shell docker inspect -f \ 20 | '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ 21 | $$(docker compose ps -q $(1)) | head -n1) 22 | # 23 | # $(call dkr_cnt_pid,demo-app-1) -> 9755 24 | # 25 | dkr_cnt_pid = $(shell docker inspect --format '{{.State.Pid}}' $(1)) 26 | # 27 | #cnt_ip_old = $(shell docker inspect -f \ 28 | # '{{range .NetworkSettings.Networks}}{{println .IPAddress}}{{end}}' \ 29 | # $(1) | head -n1) 30 | 31 | # 32 | # $(call dkr_img_env,image,envvar) -> value 33 | # 34 | dkr_img_env = $(shell docker inspect -f \ 35 | '{{range .Config.Env}}{{println .}}{{end}}' $(1) | grep -P "^$(2)=" | sed 's/[^=]*=//' 36 | 37 | # 38 | # $(call dkr_cnt_state,demo-app-1) -> docker inspect -f '{{.State.Status}}' demo-app-1 39 | # 40 | dkr_cnt_state = docker inspect -f '{{.State.Status}}' $(1) 41 | 42 | # 43 | # $(call dkr_cnt_wait_run,test-db,180) -> i=0; time while ! [ "$(docker inspect -f '{{.State.Status}}' test-db)" = "running" ]; do sleep 1; i=$((i+1)); if [[ $i > 180 ]]; then echo test-db timeout with state: $(docker inspect -f '{{.State.Status}}' test-db); break; fi; done 44 | # 45 | dkr_cnt_wait_run = i=0; time while ! [ "$$($(call dkr_cnt_state, $(1)))" = "running" ]; do sleep 1; i=$$((i+1)); if [[ $$i > $(2) ]]; then echo $(1) timeout with state: $$($(call dkr_cnt_state, $(1))); break; fi; done 46 | 47 | # 48 | # $(call dkr_srv_wait_run,180,app) -> wait up to 180s for app to enter state running 49 | # 50 | dkr_srv_wait_run = $(call dkr_cnt_wait_run,$(call dkr_srv_cnt $(1)),$(2)) 51 | 52 | # 53 | # $(call dkr_cnt_wait_log,app,ready for connections) -> time docker logs -f app | sed -n '/ready for connections/{p;q}' 54 | # 55 | dkr_cnt_wait_log = time docker logs -f $(1) 2>&1 | sed -n '/$(2)/{p;q}' 56 | 57 | # 58 | # $(call dkr_pull_missing,mariadb:latest) -> if ! docker image inspect mariadb:latest &>/dev/null; then docker pull mariadb:latest; fi 59 | # 60 | dkr_pull_missing = if ! docker image inspect $(1) &>/dev/null; then docker pull $(1); fi 61 | 62 | # 63 | # List IPs of containers 64 | # 65 | ip-list: 66 | @for srv in $$(docker ps --format "{{.Names}}"); do \ 67 | echo $$srv $$(docker inspect -f \ 68 | '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $$srv); \ 69 | done | column -t 70 | -------------------------------------------------------------------------------- /test/ssl.mk: -------------------------------------------------------------------------------- 1 | # ssl.mk 2 | # 3 | # SSL and TLS make-functions 4 | # 5 | 6 | SSL_O ?= example.com 7 | SSL_KEY ?= rsa:2048 # rsa:2048 rsa:4096 8 | SSL_MAIL ?= 9 | SSL_PASS ?= secret 10 | SSL_SAN ?= 11 | SSL_TRST ?= 12 | 13 | # 14 | # Usage: OpenLDAP 15 | # 16 | #SSL_O = $(AD_DOM) 17 | #target: ssl/auth.crt ssl/demo.crt 18 | 19 | # 20 | # Usage: SMIME 21 | # 22 | #SSL_O = $(MAIL_DOMAIN) 23 | #SSL_MAIL = auto 24 | #SSL_PASS = $(AD_USR_PW) 25 | ##SSL_TRST = $(SSL_SMIME) 26 | #target: ssl/$(AD_USR_CN)@$(MAIL_DOMAIN).p12 27 | SSL_SMIME = -setalias "Self Signed SMIME" -addtrust emailProtection \ 28 | -addreject clientAuth -addreject serverAuth 29 | 30 | # 31 | # Usage: SUbject Alternate Name SAN 32 | # 33 | #SSL_O = example.com 34 | #SSL_SAN = "subjectAltName=DNS:auth,DNS:*.docker" 35 | #target: ssl/auth.crt 36 | 37 | 38 | # 39 | # $(call ssl_subj,root,example.com,) -> -subj "/CN=root/O=example.com" 40 | # $(call ssl_subj,root,example.com,auto) -> -subj "/CN=root/O=example.com/emailAddress=root@example.com" 41 | # $(call ssl_subj,root,example.com,admin@my.org) -> -subj "/CN=root/O=example.com/emailAddress=admin@my.org" 42 | # 43 | ssl_subj = -subj "/CN=$(1)/O=$(2)$(if $(3),/emailAddress=$(if $(findstring @,$(3)),$(3),$(1)@$(2)),)" 44 | 45 | # 46 | # $(call ssl_extfile,"subjectAltName=DNS:auth") -> -extfile <(printf "subjectAltName=DNS:auth") 47 | # 48 | ssl_extfile = $(if $(1),-extfile <(printf $(1)),) 49 | 50 | 51 | .PRECIOUS: %.crt %.csr %.key 52 | SHELL = /bin/bash 53 | 54 | # 55 | # Personal information exchange file PKCS#12 56 | # 57 | %.p12: %.crt 58 | openssl pkcs12 -export -in $< -inkey $*.key -out $@ \ 59 | -passout pass:$(SSL_PASS) 60 | 61 | # 62 | # Certificate PEM 63 | # 64 | %.crt: %.csr ssl/ca.crt 65 | openssl x509 -req -in $< -CA $(@D)/ca.crt -CAkey $(@D)/ca.key -out $@ \ 66 | $(call ssl_extfile,$(SSL_SAN)) $(SSL_TRST) -CAcreateserial 67 | 68 | # 69 | # Certificate signing request PEM 70 | # 71 | %.csr: ssl 72 | openssl req -new -newkey $(SSL_KEY) -nodes -keyout $*.key -out $@ \ 73 | $(call ssl_subj,$(*F),$(SSL_O),$(SSL_MAIL)) 74 | 75 | # 76 | # Certificate authority certificate PEM 77 | # 78 | ssl/ca.crt: ssl 79 | openssl req -x509 -new -newkey $(SSL_KEY) -nodes -keyout ssl/ca.key -out $@ \ 80 | $(call ssl_subj,root,$(SSL_O),$(SSL_MAIL)) 81 | 82 | # 83 | # SSL directory 84 | # 85 | ssl: 86 | mkdir -p $@ 87 | 88 | # 89 | # Remove all files in SSL directory 90 | # 91 | ssl-destroy: 92 | rm -f ssl/* 93 | 94 | # 95 | # Inspect all files in SSL directory 96 | # 97 | ssl-list: 98 | @for file in $$(ls ssl/*); do \ 99 | case $$file in \ 100 | *.crt) \ 101 | printf "\e[33;1m%s\e[0m\n" $$file; \ 102 | openssl x509 -noout -issuer -subject -ext basicConstraints,keyUsage,extendedKeyUsage,subjectAltName -in $$file;; \ 103 | *.csr) \ 104 | printf "\e[33;1m%s\e[0m\n" $$file; \ 105 | openssl req -noout -subject -in $$file;; \ 106 | *.key) \ 107 | printf "\e[33;1m%s\e[0m\n" $$file; \ 108 | openssl rsa -text -noout -in $$file | head -n 1;; \ 109 | esac \ 110 | done 111 | 112 | ssl-inspect: 113 | @for file in $$(ls ssl/*); do \ 114 | case $$file in \ 115 | *.crt) \ 116 | printf "\e[33;1m%s\e[0m " $$file; \ 117 | openssl x509 -text -noout -certopt no_sigdump,no_pubkey -in $$file;; \ 118 | *.csr) \ 119 | printf "\e[33;1m%s\e[0m " $$file; \ 120 | openssl req -text -noout -reqopt no_sigdump,no_pubkey,ext_default -in $$file;; \ 121 | *.key) \ 122 | printf "\e[33;1m%s\e[0m " $$file; \ 123 | openssl rsa -text -noout -in $$file | head -n 1;; \ 124 | esac \ 125 | done 126 | -------------------------------------------------------------------------------- /test/ssl/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | --------------------------------------------------------------------------------