diff --git a/dockerfiles/email/base-email/Dockerfile b/dockerfiles/email/base-email/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..d3169d6af5666940b486bd241e8d477a688e5897 --- /dev/null +++ b/dockerfiles/email/base-email/Dockerfile @@ -0,0 +1,15 @@ +FROM debian:jessie + +ENV DEBIAN_FRONTEND noninteractive +RUN apt-get update && \ + apt-get install -q -y \ + python3 \ + mysql-client \ + wget curl && \ + rm -rf /var/lib/apt/lists/* + +COPY editconf.py /opt/editconf.py +COPY mysql-check.sh /opt/mysql-check.sh +RUN chmod u+x /opt/editconf.py && \ + chmod u+x /opt/mysql-check.sh + diff --git a/dockerfiles/email/base-email/editconf.py b/dockerfiles/email/base-email/editconf.py new file mode 100644 index 0000000000000000000000000000000000000000..7bc3d1901eb2ee005f35185bc67a639fcd42ef57 --- /dev/null +++ b/dockerfiles/email/base-email/editconf.py @@ -0,0 +1,127 @@ +#!/usr/bin/python3 +# +# This is a helper tool for editing configuration files during the setup +# process. The tool is given new values for settings as command-line +# arguments. It comments-out existing setting values in the configuration +# file and adds new values either after their former location or at the +# end. +# +# The configuration file has settings that look like: +# +# NAME=VALUE +# +# If the -s option is given, then space becomes the delimiter, i.e.: +# +# NAME VALUE +# +# If the -w option is given, then setting lines continue onto following +# lines while the lines start with whitespace, e.g.: +# +# NAME VAL +# UE + +import sys, re + +# sanity check +if len(sys.argv) < 3: + print("usage: python3 editconf.py /etc/file.conf [-s] [-w] [-t] NAME=VAL [NAME=VAL ...]") + sys.exit(1) + +# parse command line arguments +filename = sys.argv[1] +settings = sys.argv[2:] + +delimiter = "=" +delimiter_re = r"\s*=\s*" +comment_char = "#" +folded_lines = False +testing = False +while settings[0][0] == "-" and settings[0] != "--": + opt = settings.pop(0) + if opt == "-s": + # Space is the delimiter + delimiter = " " + delimiter_re = r"\s+" + elif opt == "-w": + # Line folding is possible in this file. + folded_lines = True + elif opt == "-c": + # Specifies a different comment character. + comment_char = settings.pop(0) + elif opt == "-t": + testing = True + else: + print("Invalid option.") + sys.exit(1) + +# create the new config file in memory + +found = set() +buf = "" +input_lines = list(open(filename)) + +while len(input_lines) > 0: + line = input_lines.pop(0) + + # If this configuration file uses folded lines, append any folded lines + # into our input buffer. + if folded_lines and line[0] not in (comment_char, " ", ""): + while len(input_lines) > 0 and input_lines[0][0] in " \t": + line += input_lines.pop(0) + + # See if this line is for any settings passed on the command line. + for i in range(len(settings)): + # Check that this line contain this setting from the command-line arguments. + name, val = settings[i].split("=", 1) + m = re.match( + "(\s*)" + + "(" + re.escape(comment_char) + "\s*)?" + + re.escape(name) + delimiter_re + "(.*?)\s*$", + line, re.S) + if not m: continue + indent, is_comment, existing_val = m.groups() + + # If this is already the setting, do nothing. + if is_comment is None and existing_val == val: + # It may be that we've already inserted this setting higher + # in the file so check for that first. + if i in found: break + buf += line + found.add(i) + break + + # comment-out the existing line (also comment any folded lines) + if is_comment is None: + buf += comment_char + line.rstrip().replace("\n", "\n" + comment_char) + "\n" + else: + # the line is already commented, pass it through + buf += line + + # if this option oddly appears more than once, don't add the setting again + if i in found: + break + + # add the new setting + buf += indent + name + delimiter + val + "\n" + + # note that we've applied this option + found.add(i) + + break + else: + # If did not match any setting names, pass this line through. + buf += line + +# Put any settings we didn't see at the end of the file. +for i in range(len(settings)): + if i not in found: + name, val = settings[i].split("=", 1) + buf += name + delimiter + val + "\n" + +if not testing: + # Write out the new file. + with open(filename, "w") as f: + f.write(buf) +else: + # Just print the new file to stdout. + print(buf) diff --git a/dockerfiles/email/base-email/mysql-check.sh b/dockerfiles/email/base-email/mysql-check.sh new file mode 100644 index 0000000000000000000000000000000000000000..46a2745629d95c40c467cf020bebe98520b39783 --- /dev/null +++ b/dockerfiles/email/base-email/mysql-check.sh @@ -0,0 +1,23 @@ +#!/bin/bash -eux + +source /etc/environment + +echo "=> Trying to connect to MySQL/MariaDB using:" +echo "========================================================================" +echo " Database Host Address: $DB_HOST" +echo " Database Port number: $DB_PORT" +echo " Database Username: $DB_USER" +echo " Database Password: $DB_PASS" +echo "========================================================================" + +for ((i=0;i<10;i++)) +do + DB_CONNECTABLE=$(mysql -u$DB_USER -p$DB_PASS -h$DB_HOST -P$DB_PORT -e 'status' >/dev/null 2>&1; echo "$?") + if [[ DB_CONNECTABLE -eq 0 ]]; then + exit 0 + fi + sleep 5 +done + +exit 1 + diff --git a/dockerfiles/email/dovecot/99-local-auth.conf b/dockerfiles/email/dovecot/99-local-auth.conf new file mode 100644 index 0000000000000000000000000000000000000000..f0e40bf00decf45f3999a8e3705681f2cc182217 --- /dev/null +++ b/dockerfiles/email/dovecot/99-local-auth.conf @@ -0,0 +1,8 @@ +service auth { + unix_listener /var/spool/postfix/dovecot/auth { + mode = 0666 + user = postfix + group = postfix + } +} + diff --git a/dockerfiles/email/dovecot/99-local-lmtp.conf b/dockerfiles/email/dovecot/99-local-lmtp.conf new file mode 100644 index 0000000000000000000000000000000000000000..14f2d9a0e4d51bc15f5d88e23dea539e23aed34d --- /dev/null +++ b/dockerfiles/email/dovecot/99-local-lmtp.conf @@ -0,0 +1,7 @@ +service lmtp { + unix_listener /var/spool/postfix/dovecot/lmtp { + mode = 0600 + user = postfix + group = postfix + } +} diff --git a/dockerfiles/email/dovecot/Dockerfile b/dockerfiles/email/dovecot/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..73433ab094cbcb9d2c1e9c60dec9d5cf1a472d68 --- /dev/null +++ b/dockerfiles/email/dovecot/Dockerfile @@ -0,0 +1,56 @@ +FROM pierreozoux/base-email + +RUN apt-get update && \ + apt-get install -q -y \ + dovecot-core \ + dovecot-imapd \ + dovecot-lmtpd \ + dovecot-mysql && \ + rm -rf /var/lib/apt/lists/* + +COPY 99-local-lmtp.conf /etc/dovecot/conf.d/99-local-lmtp.conf +COPY auth-sql.conf.ext /etc/dovecot/conf.d/auth-sql.conf.ext +COPY dovecot-sql.conf.ext /etc/dovecot/dovecot-sql.conf.ext +COPY 99-local-auth.conf /etc/dovecot/conf.d/99-local-auth.conf +COPY init.sql /init.sql +COPY startup.sh /startup.sh + +RUN \ + groupadd -r postfix && \ + useradd -r -g postfix postfix && \ + chmod u+x /startup.sh && \ + /opt/editconf.py /etc/dovecot/conf.d/10-master.conf \ + default_process_limit=250 && \ + /opt/editconf.py /etc/sysctl.conf \ + fs.inotify.max_user_instances=1024 && \ + /opt/editconf.py /etc/dovecot/conf.d/10-mail.conf \ + mail_location=maildir:/mail/mailboxes/%d/%n \ + mail_privileged_group=mail \ + first_valid_uid=0 && \ + /opt/editconf.py /etc/dovecot/conf.d/10-auth.conf \ + disable_plaintext_auth=yes \ + 'auth_mechanisms=plain login' && \ + /opt/editconf.py /etc/dovecot/conf.d/10-ssl.conf \ + ssl=required \ + 'ssl_cert=</ssl/ssl_certificate.pem' \ + 'ssl_key=</ssl/ssl_private_key.pem' \ + 'ssl_protocols=!SSLv3 !SSLv2' \ + 'ssl_cipher_list=TLSv1+HIGH !SSLv2 !RC4 !aNULL !eNULL !3DES @STRENGTH' && \ + /opt/editconf.py /etc/dovecot/conf.d/20-imap.conf \ + imap_idle_notify_interval="4 mins" && \ + sed -i "s/#port = 143/port = 0/" /etc/dovecot/conf.d/10-master.conf && \ + sed -i "s/#port = 110/port = 0/" /etc/dovecot/conf.d/10-master.conf && \ + sed -i "s/#*\(\!include auth-system.conf.ext\)/#\1/" /etc/dovecot/conf.d/10-auth.conf && \ + sed -i "s/#\(\!include auth-sql.conf.ext\)/\1/" /etc/dovecot/conf.d/10-auth.conf && \ + mkdir -p /mail/mailboxes && \ + chown -R mail:dovecot /etc/dovecot && \ + chown -R mail.mail /mail/mailboxes && \ + chmod -R o-rwx /etc/dovecot && \ + chmod 0600 /etc/dovecot/dovecot-sql.conf.ext + +ENTRYPOINT ["/startup.sh"] + +VOLUME ["/var/spool/postfix/dovecot"] + +EXPOSE 993 + diff --git a/dockerfiles/email/dovecot/auth-sql.conf.ext b/dockerfiles/email/dovecot/auth-sql.conf.ext new file mode 100644 index 0000000000000000000000000000000000000000..e6104325b5209edfe5fdb61c45684a478c53e7a4 --- /dev/null +++ b/dockerfiles/email/dovecot/auth-sql.conf.ext @@ -0,0 +1,9 @@ +passdb { + driver = sql + args = /etc/dovecot/dovecot-sql.conf.ext +} +userdb { + driver = static + args = uid=mail gid=mail home=/mail/mailboxes/%d/%n +} + diff --git a/dockerfiles/email/dovecot/dovecot-sql.conf.ext b/dockerfiles/email/dovecot/dovecot-sql.conf.ext new file mode 100644 index 0000000000000000000000000000000000000000..21fa681a834233b6047a0cf196a1a52fa72b7ab2 --- /dev/null +++ b/dockerfiles/email/dovecot/dovecot-sql.conf.ext @@ -0,0 +1,5 @@ +driver = mysql +connect = host=##DB_HOST## dbname=servermail user=##DB_USER## password=##DB_PASS## +default_pass_scheme = SHA512-CRYPT +password_query = SELECT email as user, password FROM virtual_users WHERE email='%u'; + diff --git a/dockerfiles/email/dovecot/init.sql b/dockerfiles/email/dovecot/init.sql new file mode 100644 index 0000000000000000000000000000000000000000..c2683730a0d97212dd1ddc644b8beaa33b3acc11 --- /dev/null +++ b/dockerfiles/email/dovecot/init.sql @@ -0,0 +1,27 @@ +USE servermail; +CREATE TABLE `virtual_domains` ( + `id` INT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(50) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `virtual_users` ( + `id` INT NOT NULL AUTO_INCREMENT, + `domain_id` INT NOT NULL, + `password` VARCHAR(106) NOT NULL, + `email` VARCHAR(120) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `virtual_aliases` ( + `id` INT NOT NULL AUTO_INCREMENT, + `domain_id` INT NOT NULL, + `source` varchar(100) NOT NULL, + `destination` varchar(100) NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + diff --git a/dockerfiles/email/dovecot/startup.sh b/dockerfiles/email/dovecot/startup.sh new file mode 100644 index 0000000000000000000000000000000000000000..b9c7673b5124652305a4cfe96dc9d40b790e145e --- /dev/null +++ b/dockerfiles/email/dovecot/startup.sh @@ -0,0 +1,36 @@ +#!/bin/bash -eux + +export DB_PORT=3306 +export DB_HOST=db +export DB_USER=admin +echo $HOSTNAME + +sed -i "s/##DB_HOST##/$DB_HOST/" /etc/dovecot/dovecot-sql.conf.ext +sed -i "s/##DB_USER##/$DB_USER/" /etc/dovecot/dovecot-sql.conf.ext +sed -i "s/##DB_PASS##/$DB_PASS/" /etc/dovecot/dovecot-sql.conf.ext + +/opt/editconf.py /etc/dovecot/conf.d/15-lda.conf postmaster_address=postmaster@$HOSTNAME + +/opt/mysql-check.sh + +DB_EXISTS=$(mysql -u$DB_USER -p$DB_PASS -h$DB_HOST -P$DB_PORT -e "SHOW DATABASES LIKE 'servermail';" 2>&1 |grep servermail > /dev/null ; echo "$?") +if [[ DB_EXISTS -eq 1 ]]; then + echo "=> Creating database servermail" + RET=$(mysql -u$DB_USER -p$DB_PASS -h$DB_HOST -P$DB_PORT -e "CREATE DATABASE servermail") + if [[ RET -ne 0 ]]; then + echo "Cannot create database for emails" + exit RET + fi + echo "=> Loading initial database data to servermail" + RET=$(mysql -u$DB_USER -p$DB_PASS -h$DB_HOST -P$DB_PORT servermail < /init.sql) + if [[ RET -ne 0 ]]; then + echo "Cannot load initial database data for emails" + exit RET + fi + echo "=> Done!" +else + echo "=> Skipped creation of database servermail it already exists." +fi + +dovecot -F + diff --git a/dockerfiles/email/dovecot/user.sql b/dockerfiles/email/dovecot/user.sql new file mode 100644 index 0000000000000000000000000000000000000000..d4384fb63f0b5eec646c54c51db1087cccc698c4 --- /dev/null +++ b/dockerfiles/email/dovecot/user.sql @@ -0,0 +1,17 @@ +INSERT INTO `servermail`.`virtual_domains` + (`id` ,`name`) + VALUES + ('1', 'example.com'), + ('2', 'hostname.example.com'); + +INSERT INTO `servermail`.`virtual_users` + (`id`, `domain_id`, `password` , `email`) + VALUES + ('1', '1', ENCRYPT('firstpassword', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'email1@example.com'), + ('2', '1', ENCRYPT('secondpassword', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), 'email2@example.com'); + +INSERT INTO `servermail`.`virtual_aliases` + (`id`, `domain_id`, `source`, `destination`) + VALUES + ('1', '1', 'alias@example.com', 'email1@example.com'); + diff --git a/tests/dovecot.sh b/tests/dovecot.sh new file mode 100755 index 0000000000000000000000000000000000000000..3508cd9418dbb695c44a6f42f8cc1297f051acd0 --- /dev/null +++ b/tests/dovecot.sh @@ -0,0 +1,28 @@ +#!/bin/bash -eux + +mkdir -p /data/domains/mail/dovecot +mkdir -p /data/domains/mail/TLS +mkdir -p /data/domains/mail/static/www-content +mkdir -p /data/runtime/domains/mail/mysql/db_files +mkdir -p /data/domains/mail/mysql +mkdir /data/domains/mail/nginx +touch /data/domains/mail/nginx/.env + +pass=`echo $RANDOM ${date} | md5sum | base64 | cut -c-10` +echo MYSQL_PASS=$pass > /data/domains/mail/mysql/.env +cat /data/domains/mail/mysql/.env | sed s/MYSQL_PASS/DB_PASS/ > /data/domains/mail/.env; +echo HOSTNAME=pierre-o.fr >> /data/domains/mail/.env +echo APPLICATION=nginx >> /data/domains/mail/.env +echo DOCKER_ARGUMENTS="-v /data/domains/mail/static/www-content:/app" >> /data/domains/mail.env + +openssl genrsa -out /data/domains/mail/TLS/ssl_private_key.pem 2048 +openssl req -new -key /data/domains/mail/TLS/ssl_private_key.pem -out /data/domains/mail/TLS/ssl_cert_sign_req.csr \ + -sha256 -subj "/C=PT/ST=/L=/O=/CN=dovecot.test" +openssl x509 -req -days 365 \ + -in /data/domains/mail/TLS/ssl_cert_sign_req.csr -signkey /data/domains/mail/TLS/ssl_private_key.pem -out /data/domains/mail/TLS/ssl_certificate.pem +openssl dhparam -out /data/domains/mail/TLS/dh2048.pem 2048 + +cp /data/domains/mail/TLS/ssl_certificate.pem /data/domains/mail/TLS/mail.pem + +systemctl start dovecot + diff --git a/unit-files/dovecot.service b/unit-files/dovecot.service new file mode 100644 index 0000000000000000000000000000000000000000..41d05e061b95380a7068af1c3087d222b7fed877 --- /dev/null +++ b/unit-files/dovecot.service @@ -0,0 +1,39 @@ +[Unit] +Description=%p + +# Requirements +Requires=docker.service +Requires=mysql@mail.service +Requires=backup@mail.timer + +# Dependency ordering +After=docker.service +After=mysql@mail.service +Before=backup@mail.timer + +[Service] +Restart=always +RestartSec=10 +TimeoutStartSec=60 +TimeoutStopSec=15 +Type=notify +NotifyAccess=all +ExecStartPre=/usr/bin/docker run --rm -v /opt/bin:/opt/bin ibuildthecloud/systemd-docker +ExecStartPre=-/usr/bin/docker kill dovecot +ExecStartPre=-/usr/bin/docker rm dovecot +ExecStart=/bin/bash -euxc ' \ + /opt/bin/systemd-docker --env run \ + --rm \ + --name dovecot \ + -v /data/domains/mail/TLS:/ssl \ + -v /data/runtime/dev/log:/dev/log \ + --env-file=/data/domains/mail/.env \ + --link mysql-mail:db \ + -p 993:993 \ + pierreozoux/dovecot' +ExecReload=/usr/bin/docker restart dovecot +ExecStop=/usr/bin/docker stop dovecot + +[Install] +WantedBy=multi-user.target +