From ca26264d0119261da6c37f95bce5ad3572de02cd Mon Sep 17 00:00:00 2001 From: hoellen Date: Fri, 29 Jun 2018 13:47:55 +0200 Subject: [PATCH 001/144] Dont flag spam as ham if moved to trash (fix #474) --- core/dovecot/sieve/report-ham.sieve | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/dovecot/sieve/report-ham.sieve b/core/dovecot/sieve/report-ham.sieve index 89962067..1ad8abdf 100644 --- a/core/dovecot/sieve/report-ham.sieve +++ b/core/dovecot/sieve/report-ham.sieve @@ -1,3 +1,11 @@ -require "vnd.dovecot.execute"; +require ["vnd.dovecot.execute", "copy", "imapsieve", "environment", "variables"]; + +if environment :matches "imap.mailbox" "*" { + set "mailbox" "${1}"; +} + +if string "${mailbox}" "Trash" { + stop; +} execute :pipe "mailtrain" "ham"; From f5e7751835764a819678f58be0098cd7a62cb691 Mon Sep 17 00:00:00 2001 From: Michal Prihoda Date: Fri, 22 Jun 2018 11:47:18 +0200 Subject: [PATCH 002/144] Return correct status codes from auth rate limiter failure. --- core/admin/mailu/internal/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/admin/mailu/internal/__init__.py b/core/admin/mailu/internal/__init__.py index 45084fe5..6419ad10 100644 --- a/core/admin/mailu/internal/__init__.py +++ b/core/admin/mailu/internal/__init__.py @@ -1,3 +1,5 @@ +from flask_limiter import RateLimitExceeded + from mailu import limiter import socket @@ -6,6 +8,14 @@ import flask internal = flask.Blueprint('internal', __name__) +@internal.app_errorhandler(RateLimitExceeded) +def rate_limit_handler(e): + response = flask.Response() + response.headers['Auth-Status'] = 'Authentication rate limit from one source exceeded' + response.headers['Auth-Error-Code'] = '451 4.3.2' + if int(flask.request.headers['Auth-Login-Attempt']) < 10: + response.headers['Auth-Wait'] = '3' + return response @limiter.request_filter def whitelist_webmail(): From 147a1359cdd028ae1709d4ec48e0916f2e00566e Mon Sep 17 00:00:00 2001 From: Michal Prihoda Date: Mon, 2 Jul 2018 21:39:31 +0200 Subject: [PATCH 003/144] Fixed libpng12-dev dependency, called libpng-dev now. --- webmails/roundcube/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile index c779e71a..a3fe4e24 100644 --- a/webmails/roundcube/Dockerfile +++ b/webmails/roundcube/Dockerfile @@ -4,7 +4,7 @@ RUN apt-get update && apt-get install -y \ libfreetype6-dev \ libjpeg62-turbo-dev \ libmcrypt-dev \ - libpng12-dev \ + libpng-dev \ && docker-php-ext-install pdo_mysql mcrypt zip ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.6/roundcubemail-1.3.6-complete.tar.gz From 262e82a36777e296811a0447b99c78381e36bb2a Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Tue, 3 Jul 2018 20:13:00 +0200 Subject: [PATCH 004/144] Add a postfix socketmap to http proxy --- core/postfix/conf/postproxy.py | 175 +++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100755 core/postfix/conf/postproxy.py diff --git a/core/postfix/conf/postproxy.py b/core/postfix/conf/postproxy.py new file mode 100755 index 00000000..83126353 --- /dev/null +++ b/core/postfix/conf/postproxy.py @@ -0,0 +1,175 @@ +#!/usr/bin/python3 + +# Postfix socketmap proxy +# +# This script provides a proxy from Postfix socketmap to a variety of backends. +# For now, only HTTP backends are supported. + +import asyncio +import aiohttp +import logging +import urllib +import argparse + + +class NetstringProtocol(asyncio.Protocol): + """ Netstring asyncio protocol implementation. + + For protocol details, see https://cr.yp.to/proto/netstrings.txt + """ + + # Length of the smallest allocated buffer, larger buffers will be + # allocated dynamically + BASE_BUFFER = 1024 + + # Maximum length of a buffer, will crash when exceeded + MAX_BUFFER = 65535 + + def __init__(self): + super(NetstringProtocol, self).__init__() + self.init_buffer() + + def init_buffer(self): + self.len = None # None when waiting for a length to be sent) + self.separator = -1 # -1 when not yet detected (str.find) + self.index = 0 # relative to the buffer + self.buffer = bytearray(NetstringProtocol.BASE_BUFFER) + + def data_received(self, data): + # Manage the buffer + missing = len(data) - len(self.buffer) + self.index + if missing > 0: + if len(self.buffer) + missing > NetstringProtocol.MAX_BUFFER: + raise IOError("Not enough space when decoding netstring") + self.buffer.append(bytearray(missing + 1)) + new_index = self.index + len(data) + self.buffer[self.index:new_index] = data + self.index = new_index + # Try to detect a length at the beginning of the string + if self.len is None: + self.separator = self.buffer.find(0x3a) + if self.separator != -1 and self.buffer[:self.separator].isdigit(): + self.len = int(self.buffer[:self.separator], 10) + # Then get the complete string + if self.len is not None: + if self.index - self.separator == self.len + 2: + string = self.buffer[self.separator + 1:self.index - 1] + self.init_buffer() + self.string_received(string) + + def string_received(self, string): + pass + + def send_string(self, string): + """ Send a netstring + """ + self.transport.write(str(len(string)).encode('ascii')) + self.transport.write(b':') + self.transport.write(string) + self.transport.write(b',') + + +class SocketmapProtocol(NetstringProtocol): + """ TCP protocol to answer Postfix socketmap and proxify lookups to + an outside object. + + See http://www.postfix.org/socketmap_table.5.html for details on the + protocol. + + A table map must be provided as a dictionary to lookup tables. + """ + + def __init__(self, table_map): + self.table_map = table_map + super(SocketmapProtocol, self).__init__() + + def connection_made(self, transport): + logging.info('Connect {}'.format(transport.get_extra_info('peername'))) + self.transport = transport + + def string_received(self, string): + space = string.find(0x20) + if space != -1: + name = string[:space].decode('ascii') + key = string[space+1:].decode('utf8') + return asyncio.async(self.process_request(name, key)) + + def send_string(self, string): + logging.debug("Send {}".format(string)) + super(SocketmapProtocol, self).send_string(string) + + async def process_request(self, name, key): + """ Process a request by querying the provided map. + """ + logging.debug("Request {}/{}".format(name, key)) + try: + table = self.table_map.get(name) + except KeyError: + return self.send_string(b'TEMP no such map') + try: + result = await table.get(key) + return self.send_string(b'OK ' + result.encode('utf8')) + except KeyError: + return self.send_string(b'NOTFOUND ') + except Exception: + logging.exception("Error when processing request") + return self.send_string(b'TEMP unknown error') + + @classmethod + def factory(cls, table_map): + """ Provide a protocol factory for a given map instance. + """ + return lambda: cls(table_map) + + +class UrlTable(object): + """ Resolve an entry by querying a parametrized GET URL. + """ + + def __init__(self, url_pattern): + """ url_pattern must contain a format ``{}`` so the key is injected in + the url before the query, the ``§`` character will be replaced with + ``{}`` for easier setup. + """ + self.url_pattern = url_pattern.replace('§', '{}') + + async def get(self, key): + async with aiohttp.ClientSession() as session: + async with session.get(self.url_pattern.format(key)) as request: + if request.status == 200: + result = await request.text() + return result + + +def main(): + """ Run the asyncio loop. + """ + parser = argparse.ArgumentParser("Postfix Socketmap proxy") + parser.add_argument("--bind", help="address to bind to", required=True) + parser.add_argument("--port", type=int, help="port to bind to", required=True) + parser.add_argument("--name", help="name of the table", action="append") + parser.add_argument("--type", help="type of the table", action="append") + parser.add_argument("--param", help="table parameter", action="append") + args = parser.parse_args() + # Prepare the maps + table_types = dict(url=UrlTable) + table_map = {name: table_types[table_type](param) + for name, table_type, param + in zip(args.name, args.type, args.param)} + # Run the main loop + logging.basicConfig(level=logging.DEBUG) + loop = asyncio.get_event_loop() + server = loop.run_until_complete(loop.create_server( + SocketmapProtocol.factory(table_map), args.bind, args.port + )) + try: + loop.run_forever() + except KeyboardInterrupt: + pass + server.close() + loop.run_until_complete(server.wait_closed()) + loop.close() + + +if __name__ == "__main__": + main() From 109842502aa19cf8478e95de9376121c6fc389a2 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Fri, 13 Jul 2018 15:11:40 +0100 Subject: [PATCH 005/144] Fix typo --- docs/compose/setup.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compose/setup.rst b/docs/compose/setup.rst index 64e2fa22..8759e2f1 100644 --- a/docs/compose/setup.rst +++ b/docs/compose/setup.rst @@ -92,7 +92,7 @@ setting. The configuration option must be one of the following: - ``none`` disables antivirus checks; - ``clamav`` is the default values, the popular ClamAV antivirus is enabled. -Make sure that you have at least 1GB or memory for ClamAV to load its signature +Make sure that you have at least 1GB of memory for ClamAV to load its signature database. If you run Mailu behind a reverse proxy you can use ``REAL_IP_HEADER`` and From 70175f8c28767de6cda5a3d4f49db256731166bd Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Sun, 15 Jul 2018 15:30:16 +0200 Subject: [PATCH 006/144] Add postproxy support for Dovecot dict protocol --- core/postfix/conf/postproxy.py | 94 ++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/core/postfix/conf/postproxy.py b/core/postfix/conf/postproxy.py index 83126353..234d8c34 100755 --- a/core/postfix/conf/postproxy.py +++ b/core/postfix/conf/postproxy.py @@ -70,7 +70,7 @@ class NetstringProtocol(asyncio.Protocol): class SocketmapProtocol(NetstringProtocol): - """ TCP protocol to answer Postfix socketmap and proxify lookups to + """ Protocol to answer Postfix socketmap and proxify lookups to an outside object. See http://www.postfix.org/socketmap_table.5.html for details on the @@ -122,6 +122,79 @@ class SocketmapProtocol(NetstringProtocol): return lambda: cls(table_map) +class DictProtocol(asyncio.Protocol): + """ Protocol to answer Dovecot dict requests, as implemented in Dict proxy. + + There is very little documentation about the protocol, most of it was + reverse-engineered from : + + https://github.com/dovecot/core/blob/master/src/dict/dict-connection.c + https://github.com/dovecot/core/blob/master/src/dict/dict-commands.c + https://github.com/dovecot/core/blob/master/src/lib-dict/dict-client.h + """ + + DATA_TYPES = {0: str, 1: int} + + def __init__(self, table_map): + self.table_map = table_map + self.major_version = None + self.minor_version = None + self.dict = None + super(DictProtocol, self).__init__() + + def connection_made(self, transport): + logging.info('Connect {}'.format(transport.get_extra_info('peername'))) + self.transport = transport + + def data_received(self, data): + logging.debug("Received {}".format(data)) + for line in data.split(b"\n"): + if len(line) < 2: + continue + command = DictProtocol.COMMANDS.get(line[0]) + if command is None: + logging.warning('Unknown command {}'.format(line[0])) + return self.transport.abort() + args = line[1:].strip().split(b"\t") + try: + command(self, *args) + except Exception: + logging.exception("Error when processing request") + return self.transport.abort() + + def process_hello(self, major, minor, value_type, user, dict_name): + self.major, self.minor = int(major), int(minor) + logging.debug('Client version {}.{}'.format(self.major, self.minor)) + assert self.major == 2 + self.value_type = DictProtocol.DATA_TYPES[int(value_type)] + self.user = user + self.dict = self.table_map[dict_name.decode("ascii")] + logging.debug("Value type {}, user {}, dict {}".format( + self.value_type, self.user, dict_name)) + + def process_lookup(self, key): + logging.debug("Looking up {}".format(key)) + self.reply(b"O", json.dumps({})) + + def reply(self, command, *args): + logging.debug("Replying {} with {}".format(command, args)) + self.transport.write(command) + for arg in args: + self.transport.write("b\t" + arg.replace(b"\t", b"\t\t")) + self.transport.write("\n") + + @classmethod + def factory(cls, table_map): + """ Provide a protocol factory for a given map instance. + """ + return lambda: cls(table_map) + + COMMANDS = { + ord("H"): process_hello, + ord("L"): process_lookup + } + + class UrlTable(object): """ Resolve an entry by querying a parametrized GET URL. """ @@ -144,23 +217,26 @@ class UrlTable(object): def main(): """ Run the asyncio loop. """ - parser = argparse.ArgumentParser("Postfix Socketmap proxy") - parser.add_argument("--bind", help="address to bind to", required=True) - parser.add_argument("--port", type=int, help="port to bind to", required=True) + # Reference tables + server_types = dict(postfix=SocketmapProtocol, dovecot=DictProtocol) + table_types = dict(url=UrlTable) + # Argument parsing + parser = argparse.ArgumentParser("Postfix and Dovecot map proxy") + parser.add_argument("--socket", help="path to a socket", required=True) + parser.add_argument("--mode", choices=server_types.keys(), required=True) parser.add_argument("--name", help="name of the table", action="append") - parser.add_argument("--type", help="type of the table", action="append") + parser.add_argument("--type", choices=table_types.keys(), action="append") parser.add_argument("--param", help="table parameter", action="append") args = parser.parse_args() # Prepare the maps - table_types = dict(url=UrlTable) table_map = {name: table_types[table_type](param) for name, table_type, param - in zip(args.name, args.type, args.param)} + in zip(args.name, args.type, args.param)} if args.name else {} # Run the main loop logging.basicConfig(level=logging.DEBUG) loop = asyncio.get_event_loop() - server = loop.run_until_complete(loop.create_server( - SocketmapProtocol.factory(table_map), args.bind, args.port + server = loop.run_until_complete(loop.create_unix_server( + server_types[args.mode].factory(table_map), args.socket )) try: loop.run_forever() From 2b2ab864d19cc226459056f2006fb2ad110155ca Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Sun, 15 Jul 2018 15:35:35 +0200 Subject: [PATCH 007/144] Add support for querying the table in Dovecot proxy --- core/postfix/conf/postproxy.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/postfix/conf/postproxy.py b/core/postfix/conf/postproxy.py index 234d8c34..29b616ae 100755 --- a/core/postfix/conf/postproxy.py +++ b/core/postfix/conf/postproxy.py @@ -10,6 +10,7 @@ import aiohttp import logging import urllib import argparse +import json class NetstringProtocol(asyncio.Protocol): @@ -108,7 +109,7 @@ class SocketmapProtocol(NetstringProtocol): return self.send_string(b'TEMP no such map') try: result = await table.get(key) - return self.send_string(b'OK ' + result.encode('utf8')) + return self.send_string(b'OK ' + str(result).encode('utf8')) except KeyError: return self.send_string(b'NOTFOUND ') except Exception: @@ -157,7 +158,7 @@ class DictProtocol(asyncio.Protocol): return self.transport.abort() args = line[1:].strip().split(b"\t") try: - command(self, *args) + return command(self, *args) except Exception: logging.exception("Error when processing request") return self.transport.abort() @@ -172,9 +173,11 @@ class DictProtocol(asyncio.Protocol): logging.debug("Value type {}, user {}, dict {}".format( self.value_type, self.user, dict_name)) - def process_lookup(self, key): + async def process_lookup(self, key): logging.debug("Looking up {}".format(key)) - self.reply(b"O", json.dumps({})) + result = await self.dict.get(key) + response = result if type(result) is str else json.dumps(result) + return self.reply(b"O", response) def reply(self, command, *args): logging.debug("Replying {} with {}".format(command, args)) @@ -210,7 +213,7 @@ class UrlTable(object): async with aiohttp.ClientSession() as session: async with session.get(self.url_pattern.format(key)) as request: if request.status == 200: - result = await request.text() + result = await request.json() return result From 76617a3c972060394a7406b55fbeea28a98c134c Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Thu, 26 Jul 2018 21:38:21 +0200 Subject: [PATCH 008/144] Store the quota status in database --- core/admin/mailu/models.py | 26 ++++++++++++++--- .../migrations/versions/25fd6c7bcb4a_.py | 28 +++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 core/admin/migrations/versions/25fd6c7bcb4a_.py diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 71c5f4d7..7e13682b 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -1,11 +1,11 @@ -from mailu import app, db, dkim, login_manager, quota +from mailu import app, db, dkim, login_manager from sqlalchemy.ext import declarative from passlib import context, hash from datetime import datetime, date from email.mime import text - +import sqlalchemy import re import time import os @@ -235,6 +235,7 @@ class User(Base, Email): backref=db.backref('users', cascade='all, delete-orphan')) password = db.Column(db.String(255), nullable=False) quota_bytes = db.Column(db.Integer(), nullable=False, default=10**9) + quota_bytes_used = db.Column(db.Integer(), nullable=False, default=0) global_admin = db.Column(db.Boolean(), nullable=False, default=False) enabled = db.Column(db.Boolean(), nullable=False, default=True) @@ -266,8 +267,14 @@ class User(Base, Email): return self.email @property - def quota_bytes_used(self): - return quota.get(self.email + "/quota/storage") or 0 + def destination(self): + if self.foward_enabled: + result = self.self.forward_destination + if self.forward_keep: + result += ',' + self.email + return result + else: + return self.email scheme_dict = {'SHA512-CRYPT': "sha512_crypt", 'SHA256-CRYPT': "sha256_crypt", @@ -329,6 +336,17 @@ class Alias(Base, Email): wildcard = db.Column(db.Boolean(), nullable=False, default=False) destination = db.Column(CommaSeparatedList, nullable=False, default=[]) + @classmethod + def resolve(cls, localpart, domain_name): + return cls.query.filter( + sqlalchemy._and(cls.domain_name == domain_name, + sqlalchemy._or( + cls.localpart == localpart, + cls.wildcard.like(localpart) + ) + ) + ) + class Token(Base): """ A token is an application password for a given user. diff --git a/core/admin/migrations/versions/25fd6c7bcb4a_.py b/core/admin/migrations/versions/25fd6c7bcb4a_.py new file mode 100644 index 00000000..cf9a0fc7 --- /dev/null +++ b/core/admin/migrations/versions/25fd6c7bcb4a_.py @@ -0,0 +1,28 @@ +""" Add a column for used quota + +Revision ID: 25fd6c7bcb4a +Revises: 049fed905da7 +Create Date: 2018-07-25 21:56:09.729153 + +""" + +# revision identifiers, used by Alembic. +revision = '25fd6c7bcb4a' +down_revision = '049fed905da7' + +from alembic import op +import sqlalchemy as sa + + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + with op.batch_alter_table('user') as batch: + batch.add_column(sa.Column('quota_bytes_used', sa.Integer(), nullable=False, server_default='0')) + + +def downgrade(): + with op.batch_alter_table('user') as batch: + batch.drop_column('user', 'quota_bytes_used') From 28001213d48f5fb917e1063c6130dd025f83d9bc Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Thu, 26 Jul 2018 21:39:30 +0200 Subject: [PATCH 009/144] Remove the redis-based quota code --- core/admin/mailu/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index a3f0353c..d77420e6 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -11,7 +11,6 @@ import os import docker import socket import uuid -import redis from werkzeug.contrib import fixers @@ -89,9 +88,6 @@ manager.add_command('db', flask_migrate.MigrateCommand) babel = flask_babel.Babel(app) translations = list(map(str, babel.list_translations())) -# Quota manager -quota = redis.Redis.from_url(app.config.get("QUOTA_STORAGE_URL")) - @babel.localeselector def get_locale(): return flask.request.accept_languages.best_match(translations) From 809fe78f82250591b2b06052e6eec3872745472b Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Thu, 26 Jul 2018 21:40:44 +0200 Subject: [PATCH 010/144] Add dovecot views to the internal API --- core/admin/mailu/internal/views.py | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/core/admin/mailu/internal/views.py b/core/admin/mailu/internal/views.py index b97d329e..b5b1da77 100644 --- a/core/admin/mailu/internal/views.py +++ b/core/admin/mailu/internal/views.py @@ -1,5 +1,6 @@ from mailu import db, models, app, limiter from mailu.internal import internal, nginx +from sqlalchemy import or_ import flask import flask_login @@ -49,3 +50,47 @@ def basic_authentication(): response = flask.Response(status=401) response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"' return response + + +@internal.route("/postfix/alias/") +def postfix_alias_map(alias): + localpart, domain = alias.split('@', 1) if '@' in alias else (None, alias) + alternative = models.Alternative.query.get(domain) + if alternative: + domain = alternative.domain_name + email = '{}@{}'.format(localpart, domain) + if localpart is None: + return domain + else: + alias_obj = models.Alias.resolve(localpart, domain) + if alias_obj: + return alias_obj.destination + user_obj = models.User.query.get(email) + if user_obj: + return user_obj.destination + flask.abort(404) + + +@internal.route("/dovecot/auth/passdb/") +def dovecot_passdb_dict(user_email): + user = models.User.query.get(user_email) or flask.abort(403) + return flask.jsonify({ + "password": user.password, + }) + + +@internal.route("/dovecot/auth/userdb/") +def dovecot_userdb_dict(user_email): + user = models.User.query.get(user_email) or flask.abort(403) + return flask.jsonify({ + "quota_rule": "*:bytes={}".format(user.quota_bytes) + }) + + +@internal.route("/dovecot/quota/quota//", methods=["POST"]) +def dovecot_quota(ns, user_email): + user = models.User.query.get(user_email) or flask.abort(403) + if ns == "storage": + user.quota_bytes_used = flask.request.get_json() + db.session.commit() + return flask.jsonify(None) From b5d6b938693d30aa176b456739495b494c668f42 Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Thu, 26 Jul 2018 21:41:11 +0200 Subject: [PATCH 011/144] Switch to using Podop in Dovecot --- core/dovecot/Dockerfile | 8 ++-- core/dovecot/conf/auth.conf | 5 +++ core/dovecot/conf/dovecot-sql.conf.ext | 18 --------- core/dovecot/conf/dovecot.conf | 51 ++++++------------------- core/dovecot/conf/pigeonhole-sieve.dict | 43 --------------------- core/dovecot/start.py | 20 +++++++--- 6 files changed, 36 insertions(+), 109 deletions(-) create mode 100644 core/dovecot/conf/auth.conf delete mode 100644 core/dovecot/conf/dovecot-sql.conf.ext delete mode 100644 core/dovecot/conf/pigeonhole-sieve.dict diff --git a/core/dovecot/Dockerfile b/core/dovecot/Dockerfile index cacfe354..484ab1d6 100644 --- a/core/dovecot/Dockerfile +++ b/core/dovecot/Dockerfile @@ -1,9 +1,9 @@ FROM alpine:edge -RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && apk add --no-cache \ - dovecot dovecot-sqlite dovecot-pigeonhole-plugin dovecot-pigeonhole-plugin-extdata \ - rspamd-client@testing python py-jinja2 +RUN apk add --no-cache \ + dovecot dovecot-pop3d dovecot-lmtpd dovecot-pigeonhole-plugin rspamd-client \ + python3 py3-pip \ + && pip3 install jinja2 podop COPY conf /conf COPY sieve /var/lib/dovecot diff --git a/core/dovecot/conf/auth.conf b/core/dovecot/conf/auth.conf new file mode 100644 index 00000000..44a874ba --- /dev/null +++ b/core/dovecot/conf/auth.conf @@ -0,0 +1,5 @@ +uri = proxy:/tmp/podop.socket:auth +iterate_disable = yes +default_pass_scheme = plain +password_key = passdb/%u +user_key = userdb/%u diff --git a/core/dovecot/conf/dovecot-sql.conf.ext b/core/dovecot/conf/dovecot-sql.conf.ext deleted file mode 100644 index b55a5557..00000000 --- a/core/dovecot/conf/dovecot-sql.conf.ext +++ /dev/null @@ -1,18 +0,0 @@ -driver = sqlite -connect = /data/main.db - -# Return the user hashed password -password_query = \ - SELECT NULL as password, 'Y' as nopassword, '{% if POD_ADDRESS_RANGE %}{{ POD_ADDRESS_RANGE }}{% else %}{{ FRONT_ADDRESS }}{% if WEBMAIL_ADDRESS %},{{ WEBMAIL_ADDRESS }}{% endif %}{% endif %}' as allow_nets \ - FROM user \ - WHERE user.email = '%u' - -# Mostly get the user quota -user_query = \ - SELECT '*:bytes=' || user.quota_bytes AS quota_rule \ - FROM user \ - WHERE user.email = '%u' - -# For using doveadm -A: -iterate_query = \ - SELECT user.email AS user FROM user diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index 94c43901..d6aee60c 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -7,17 +7,6 @@ postmaster_address = {{ POSTMASTER }}@{{ DOMAIN }} hostname = {{ HOSTNAMES.split(",")[0] }} submission_host = {{ FRONT_ADDRESS }} -service dict { - unix_listener dict { - group = mail - mode = 0660 - } -} - -dict { - sieve = sqlite:/etc/dovecot/pigeonhole-sieve.dict -} - ############### # Mailboxes ############### @@ -36,28 +25,18 @@ mail_plugins = $mail_plugins quota quota_clone namespace inbox { inbox = yes - mailbox Trash { + {% for mailbox in ("Trash", "Drafts", "Sent", "Junk") %} + mailbox {{ mailbox }} { auto = subscribe - special_use = \Trash - } - mailbox Drafts { - auto = subscribe - special_use = \Drafts - } - mailbox Sent { - auto = subscribe - special_use = \Sent - } - mailbox Junk { - auto = subscribe - special_use = \Junk + special_use = \{{ mailbox }} } + {% endfor %} } plugin { quota = count:User quota quota_vsizes = yes - quota_clone_dict = redis:host={{ REDIS_ADDRESS }}:port=6379:db=1 + quota_clone_dict = proxy:/tmp/podop.socket:quota } ############### @@ -65,16 +44,15 @@ plugin { ############### auth_mechanisms = plain login disable_plaintext_auth = no -ssl_protocols = !SSLv3 passdb { - driver = sql - args = /etc/dovecot/dovecot-sql.conf.ext + driver = dict + args = /etc/dovecot/auth.conf } userdb { - driver = sql - args = /etc/dovecot/dovecot-sql.conf.ext + driver = dict + args = /etc/dovecot/auth.conf } service auth { @@ -95,7 +73,6 @@ service auth-worker { ############### # IMAP & POP ############### - protocol imap { mail_plugins = $mail_plugins imap_quota imap_sieve } @@ -113,7 +90,6 @@ service imap-login { ############### # Delivery ############### - protocol lmtp { mail_plugins = $mail_plugins sieve recipient_delimiter = {{ RECIPIENT_DELIMITER }} @@ -125,11 +101,9 @@ service lmtp { } } - ############### # Filtering ############### - service managesieve-login { inet_listener sieve { port = 4190 @@ -140,13 +114,12 @@ service managesieve { } plugin { - sieve = file:~/sieve;active=~/.dovecot.sieve - sieve_plugins = sieve_extdata sieve_imapsieve sieve_extprograms - sieve_global_extensions = +vnd.dovecot.extdata +spamtest +spamtestplus +vnd.dovecot.execute +editheader + sieve = dict:proxy:/tmp/podop.socket:sieve + sieve_plugins = sieve_imapsieve sieve_extprograms + sieve_global_extensions = +spamtest +spamtestplus +vnd.dovecot.execute +editheader sieve_before = /var/lib/dovecot/before.sieve sieve_default = /var/lib/dovecot/default.sieve sieve_after = /var/lib/dovecot/after.sieve - sieve_extdata_dict_uri = proxy::sieve # Sieve execute sieve_execute_bin_dir = /var/lib/dovecot/bin diff --git a/core/dovecot/conf/pigeonhole-sieve.dict b/core/dovecot/conf/pigeonhole-sieve.dict deleted file mode 100644 index 917fce83..00000000 --- a/core/dovecot/conf/pigeonhole-sieve.dict +++ /dev/null @@ -1,43 +0,0 @@ -connect = /data/main.db - -map { - pattern = priv/spam_enabled - table = user - username_field = email - value_field = spam_enabled -} - -map { - pattern = priv/spam_threshold - table = user - username_field = email - value_field = spam_threshold -} - -map { - pattern = priv/reply_enabled - table = user - username_field = email - value_field = reply_enabled -} - -map { - pattern = priv/reply_subject - table = user - username_field = email - value_field = reply_subject -} - -map { - pattern = priv/reply_body - table = user - username_field = email - value_field = reply_body -} - -map { - pattern = priv/reply_enddate - table = user - username_field = email - value_field = reply_enddate -} diff --git a/core/dovecot/start.py b/core/dovecot/start.py index 83f91fab..48e9377c 100755 --- a/core/dovecot/start.py +++ b/core/dovecot/start.py @@ -1,21 +1,31 @@ -#!/usr/bin/python +#!/usr/bin/python3 import jinja2 import os import socket import glob +import multiprocessing + +from podop import run_server + + +def start_podop(): + os.setuid(8) + run_server(40, "dovecot", "/tmp/podop.socket", [ + ("quota", "url", "http://admin/internal/dovecot/quota/§"), + ("auth", "url", "http://admin/internal/dovecot/auth/§"), + ("sieve", "url", "http://admin/internal/dovecot/sieve/§"), + ]) convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) # Actual startup script os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front")) -os.environ["REDIS_ADDRESS"] = socket.gethostbyname(os.environ.get("REDIS_ADDRESS", "redis")) -if os.environ["WEBMAIL"] != "none": - os.environ["WEBMAIL_ADDRESS"] = socket.gethostbyname(os.environ.get("WEBMAIL_ADDRESS", "webmail")) for dovecot_file in glob.glob("/conf/*"): convert(dovecot_file, os.path.join("/etc/dovecot", os.path.basename(dovecot_file))) -# Run postfix +# Run Podop, then postfix +multiprocessing.Process(target=start_podop).start() os.system("chown -R mail:mail /mail /var/lib/dovecot") os.execv("/usr/sbin/dovecot", ["dovecot", "-c", "/etc/dovecot/dovecot.conf", "-F"]) From 82e738cc533f1de83af0bb4fa8250a5034536733 Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Thu, 26 Jul 2018 21:45:33 +0200 Subject: [PATCH 012/144] Remove the old code of postproxy --- core/postfix/conf/postproxy.py | 254 --------------------------------- 1 file changed, 254 deletions(-) delete mode 100755 core/postfix/conf/postproxy.py diff --git a/core/postfix/conf/postproxy.py b/core/postfix/conf/postproxy.py deleted file mode 100755 index 29b616ae..00000000 --- a/core/postfix/conf/postproxy.py +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/python3 - -# Postfix socketmap proxy -# -# This script provides a proxy from Postfix socketmap to a variety of backends. -# For now, only HTTP backends are supported. - -import asyncio -import aiohttp -import logging -import urllib -import argparse -import json - - -class NetstringProtocol(asyncio.Protocol): - """ Netstring asyncio protocol implementation. - - For protocol details, see https://cr.yp.to/proto/netstrings.txt - """ - - # Length of the smallest allocated buffer, larger buffers will be - # allocated dynamically - BASE_BUFFER = 1024 - - # Maximum length of a buffer, will crash when exceeded - MAX_BUFFER = 65535 - - def __init__(self): - super(NetstringProtocol, self).__init__() - self.init_buffer() - - def init_buffer(self): - self.len = None # None when waiting for a length to be sent) - self.separator = -1 # -1 when not yet detected (str.find) - self.index = 0 # relative to the buffer - self.buffer = bytearray(NetstringProtocol.BASE_BUFFER) - - def data_received(self, data): - # Manage the buffer - missing = len(data) - len(self.buffer) + self.index - if missing > 0: - if len(self.buffer) + missing > NetstringProtocol.MAX_BUFFER: - raise IOError("Not enough space when decoding netstring") - self.buffer.append(bytearray(missing + 1)) - new_index = self.index + len(data) - self.buffer[self.index:new_index] = data - self.index = new_index - # Try to detect a length at the beginning of the string - if self.len is None: - self.separator = self.buffer.find(0x3a) - if self.separator != -1 and self.buffer[:self.separator].isdigit(): - self.len = int(self.buffer[:self.separator], 10) - # Then get the complete string - if self.len is not None: - if self.index - self.separator == self.len + 2: - string = self.buffer[self.separator + 1:self.index - 1] - self.init_buffer() - self.string_received(string) - - def string_received(self, string): - pass - - def send_string(self, string): - """ Send a netstring - """ - self.transport.write(str(len(string)).encode('ascii')) - self.transport.write(b':') - self.transport.write(string) - self.transport.write(b',') - - -class SocketmapProtocol(NetstringProtocol): - """ Protocol to answer Postfix socketmap and proxify lookups to - an outside object. - - See http://www.postfix.org/socketmap_table.5.html for details on the - protocol. - - A table map must be provided as a dictionary to lookup tables. - """ - - def __init__(self, table_map): - self.table_map = table_map - super(SocketmapProtocol, self).__init__() - - def connection_made(self, transport): - logging.info('Connect {}'.format(transport.get_extra_info('peername'))) - self.transport = transport - - def string_received(self, string): - space = string.find(0x20) - if space != -1: - name = string[:space].decode('ascii') - key = string[space+1:].decode('utf8') - return asyncio.async(self.process_request(name, key)) - - def send_string(self, string): - logging.debug("Send {}".format(string)) - super(SocketmapProtocol, self).send_string(string) - - async def process_request(self, name, key): - """ Process a request by querying the provided map. - """ - logging.debug("Request {}/{}".format(name, key)) - try: - table = self.table_map.get(name) - except KeyError: - return self.send_string(b'TEMP no such map') - try: - result = await table.get(key) - return self.send_string(b'OK ' + str(result).encode('utf8')) - except KeyError: - return self.send_string(b'NOTFOUND ') - except Exception: - logging.exception("Error when processing request") - return self.send_string(b'TEMP unknown error') - - @classmethod - def factory(cls, table_map): - """ Provide a protocol factory for a given map instance. - """ - return lambda: cls(table_map) - - -class DictProtocol(asyncio.Protocol): - """ Protocol to answer Dovecot dict requests, as implemented in Dict proxy. - - There is very little documentation about the protocol, most of it was - reverse-engineered from : - - https://github.com/dovecot/core/blob/master/src/dict/dict-connection.c - https://github.com/dovecot/core/blob/master/src/dict/dict-commands.c - https://github.com/dovecot/core/blob/master/src/lib-dict/dict-client.h - """ - - DATA_TYPES = {0: str, 1: int} - - def __init__(self, table_map): - self.table_map = table_map - self.major_version = None - self.minor_version = None - self.dict = None - super(DictProtocol, self).__init__() - - def connection_made(self, transport): - logging.info('Connect {}'.format(transport.get_extra_info('peername'))) - self.transport = transport - - def data_received(self, data): - logging.debug("Received {}".format(data)) - for line in data.split(b"\n"): - if len(line) < 2: - continue - command = DictProtocol.COMMANDS.get(line[0]) - if command is None: - logging.warning('Unknown command {}'.format(line[0])) - return self.transport.abort() - args = line[1:].strip().split(b"\t") - try: - return command(self, *args) - except Exception: - logging.exception("Error when processing request") - return self.transport.abort() - - def process_hello(self, major, minor, value_type, user, dict_name): - self.major, self.minor = int(major), int(minor) - logging.debug('Client version {}.{}'.format(self.major, self.minor)) - assert self.major == 2 - self.value_type = DictProtocol.DATA_TYPES[int(value_type)] - self.user = user - self.dict = self.table_map[dict_name.decode("ascii")] - logging.debug("Value type {}, user {}, dict {}".format( - self.value_type, self.user, dict_name)) - - async def process_lookup(self, key): - logging.debug("Looking up {}".format(key)) - result = await self.dict.get(key) - response = result if type(result) is str else json.dumps(result) - return self.reply(b"O", response) - - def reply(self, command, *args): - logging.debug("Replying {} with {}".format(command, args)) - self.transport.write(command) - for arg in args: - self.transport.write("b\t" + arg.replace(b"\t", b"\t\t")) - self.transport.write("\n") - - @classmethod - def factory(cls, table_map): - """ Provide a protocol factory for a given map instance. - """ - return lambda: cls(table_map) - - COMMANDS = { - ord("H"): process_hello, - ord("L"): process_lookup - } - - -class UrlTable(object): - """ Resolve an entry by querying a parametrized GET URL. - """ - - def __init__(self, url_pattern): - """ url_pattern must contain a format ``{}`` so the key is injected in - the url before the query, the ``§`` character will be replaced with - ``{}`` for easier setup. - """ - self.url_pattern = url_pattern.replace('§', '{}') - - async def get(self, key): - async with aiohttp.ClientSession() as session: - async with session.get(self.url_pattern.format(key)) as request: - if request.status == 200: - result = await request.json() - return result - - -def main(): - """ Run the asyncio loop. - """ - # Reference tables - server_types = dict(postfix=SocketmapProtocol, dovecot=DictProtocol) - table_types = dict(url=UrlTable) - # Argument parsing - parser = argparse.ArgumentParser("Postfix and Dovecot map proxy") - parser.add_argument("--socket", help="path to a socket", required=True) - parser.add_argument("--mode", choices=server_types.keys(), required=True) - parser.add_argument("--name", help="name of the table", action="append") - parser.add_argument("--type", choices=table_types.keys(), action="append") - parser.add_argument("--param", help="table parameter", action="append") - args = parser.parse_args() - # Prepare the maps - table_map = {name: table_types[table_type](param) - for name, table_type, param - in zip(args.name, args.type, args.param)} if args.name else {} - # Run the main loop - logging.basicConfig(level=logging.DEBUG) - loop = asyncio.get_event_loop() - server = loop.run_until_complete(loop.create_unix_server( - server_types[args.mode].factory(table_map), args.socket - )) - try: - loop.run_forever() - except KeyboardInterrupt: - pass - server.close() - loop.run_until_complete(server.wait_closed()) - loop.close() - - -if __name__ == "__main__": - main() From bb73933e1e7ed99970dfafb80d9d0721a04e67ea Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Thu, 26 Jul 2018 21:57:21 +0200 Subject: [PATCH 013/144] Switch postfix to Podop --- core/postfix/Dockerfile | 4 +++- core/postfix/conf/main.cf | 16 ++++++++-------- core/postfix/start.py | 21 ++++++++++++++++++--- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/core/postfix/Dockerfile b/core/postfix/Dockerfile index bb5831a2..81ffc95b 100644 --- a/core/postfix/Dockerfile +++ b/core/postfix/Dockerfile @@ -1,6 +1,8 @@ FROM alpine -RUN apk add --no-cache postfix postfix-sqlite postfix-pcre rsyslog python py-jinja2 +RUN apk add --no-cache postfix postfix-pcre rsyslog \ + python3 py3-pip \ + && pip3 install jinja2 podop COPY conf /conf COPY start.py /start.py diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 2f2c6990..19c2d0c2 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -19,8 +19,8 @@ mynetworks = 127.0.0.1/32 [::1]/128 {{ RELAYNETS }} # Empty alias list to override the configuration variable and disable NIS alias_maps = -# SQLite configuration -sql = sqlite:${config_directory}/ +# Podop configuration +podop = socketmap:unix:/tmp/podop.socket: # Only accept virtual emails mydestination = @@ -56,13 +56,13 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache # The alias map actually returns both aliases and local mailboxes, which is # required for reject_unlisted_sender to work properly -virtual_alias_maps = ${sql}sqlite-virtual_alias_maps.cf -virtual_mailbox_domains = ${sql}sqlite-virtual_mailbox_domains.cf -virtual_mailbox_maps = $virtual_alias_maps +virtual_alias_maps = ${podop}alias +virtual_mailbox_domains = ${podop}domains +virtual_mailbox_maps = ${podop}mailbox # Mails are transported if required, then forwarded to Dovecot for delivery -relay_domains = ${sql}sqlite-transport.cf -transport_maps = ${sql}sqlite-transport.cf +relay_domains = ${podop}transport +transport_maps = ${podop}transport virtual_transport = lmtp:inet:{{ HOST_LMTP }} # In order to prevent Postfix from running DNS query, enforce the use of the @@ -84,7 +84,7 @@ smtpd_helo_required = yes smtpd_recipient_restrictions = permit_mynetworks, - check_sender_access ${sql}sqlite-reject-spoofed.cf, + check_sender_access ${podop}spoofed reject_non_fqdn_sender, reject_unknown_sender_domain, reject_unknown_recipient_domain, diff --git a/core/postfix/start.py b/core/postfix/start.py index 4dbf2206..38905224 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -1,11 +1,25 @@ -#!/usr/bin/python +#!/usr/bin/python3 import jinja2 import os import socket import glob import shutil - +import multiprocessing + +from podop import run_server + + +def start_podop(): + os.setuid(100) + run_server(40, "postfix", "/tmp/podop.socket", [ + ("transport", "url", "http://admin/internal/postfix/transport/§"), + ("alias", "url", "http://admin/internal/postfix/alias/§"), + ("domains", "url", "http://admin/internal/postfix/domains/§"), + ("mailbox", "url", "http://admin/internal/postfix/mailbox/§"), + ("spoofed", "url", "http://admin/internal/postfix/spoofed/§"), + ]) + convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) # Actual startup script @@ -32,7 +46,8 @@ for map_file in glob.glob("/overrides/*.map"): convert("/conf/rsyslog.conf", "/etc/rsyslog.conf") -# Run postfix +# Run Podop and Postfix +multiprocessing.Process(target=start_podop).start() if os.path.exists("/var/run/rsyslogd.pid"): os.remove("/var/run/rsyslogd.pid") os.system("/usr/lib/postfix/post-install meta_directory=/etc/postfix create-missing") From c04e58498de43c7bf6549808b75c495cd31cadac Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Thu, 26 Jul 2018 21:57:48 +0200 Subject: [PATCH 014/144] Remove unused postfix sqlite files --- core/postfix/conf/sqlite-reject-spoofed.cf | 5 ---- core/postfix/conf/sqlite-transport.cf | 3 --- .../postfix/conf/sqlite-virtual_alias_maps.cf | 23 ------------------- .../conf/sqlite-virtual_mailbox_domains.cf | 5 ---- 4 files changed, 36 deletions(-) delete mode 100644 core/postfix/conf/sqlite-reject-spoofed.cf delete mode 100644 core/postfix/conf/sqlite-transport.cf delete mode 100644 core/postfix/conf/sqlite-virtual_alias_maps.cf delete mode 100644 core/postfix/conf/sqlite-virtual_mailbox_domains.cf diff --git a/core/postfix/conf/sqlite-reject-spoofed.cf b/core/postfix/conf/sqlite-reject-spoofed.cf deleted file mode 100644 index 9cdd6c45..00000000 --- a/core/postfix/conf/sqlite-reject-spoofed.cf +++ /dev/null @@ -1,5 +0,0 @@ -dbpath = /data/main.db -query = - SELECT 'REJECT' FROM domain WHERE name='%s' - UNION - SELECT 'REJECT' FROM alternative WHERE name='%s' diff --git a/core/postfix/conf/sqlite-transport.cf b/core/postfix/conf/sqlite-transport.cf deleted file mode 100644 index 6295523b..00000000 --- a/core/postfix/conf/sqlite-transport.cf +++ /dev/null @@ -1,3 +0,0 @@ -dbpath = /data/main.db -query = - SELECT 'smtp:['||smtp||']' FROM relay WHERE name='%s' diff --git a/core/postfix/conf/sqlite-virtual_alias_maps.cf b/core/postfix/conf/sqlite-virtual_alias_maps.cf deleted file mode 100644 index dcf0a0c6..00000000 --- a/core/postfix/conf/sqlite-virtual_alias_maps.cf +++ /dev/null @@ -1,23 +0,0 @@ -dbpath = /data/main.db -query = - SELECT destination - FROM - (SELECT destination, email, wildcard, localpart, localpart||'@'||alternative.name AS alt_email FROM alias LEFT JOIN alternative ON alias.domain_name = alternative.domain_name - UNION - SELECT (CASE WHEN forward_enabled=1 THEN (CASE WHEN forward_keep=1 THEN email||',' ELSE '' END)||forward_destination ELSE email END) AS destination, email, 0 as wildcard, localpart, localpart||'@'||alternative.name as alt_email FROM user LEFT JOIN alternative ON user.domain_name = alternative.domain_name - UNION - SELECT '@'||domain_name as destination, '@'||name as email, 0 as wildcard, '' as localpart, NULL AS alt_email FROM alternative) - WHERE - ( - wildcard = 0 - AND - (email = '%s' OR alt_email = '%s') - ) OR ( - wildcard = 1 - AND - '%s' LIKE email - ) - ORDER BY - wildcard ASC, - length(localpart) DESC - LIMIT 1 diff --git a/core/postfix/conf/sqlite-virtual_mailbox_domains.cf b/core/postfix/conf/sqlite-virtual_mailbox_domains.cf deleted file mode 100644 index af453bce..00000000 --- a/core/postfix/conf/sqlite-virtual_mailbox_domains.cf +++ /dev/null @@ -1,5 +0,0 @@ -dbpath = /data/main.db -query = - SELECT name FROM domain WHERE name='%s' - UNION - SELECT name FROM alternative WHERE name='%s' From 0085b6f1e61347b22330364b0348fef6c4b24817 Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Thu, 26 Jul 2018 21:58:50 +0200 Subject: [PATCH 015/144] Remove the data mount where unused --- docs/compose/docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/compose/docker-compose.yml b/docs/compose/docker-compose.yml index 740a5ffc..1a171af5 100644 --- a/docs/compose/docker-compose.yml +++ b/docs/compose/docker-compose.yml @@ -39,7 +39,6 @@ services: restart: always env_file: .env volumes: - - "$ROOT/data:/data" - "$ROOT/mail:/mail" - "$ROOT/overrides:/overrides" depends_on: @@ -50,7 +49,6 @@ services: restart: always env_file: .env volumes: - - "$ROOT/data:/data" - "$ROOT/overrides:/overrides" depends_on: - front From 14a6cfb5c6b327baf161a524bcd3c8ba091796b2 Mon Sep 17 00:00:00 2001 From: d-fens Date: Sat, 28 Jul 2018 21:46:33 +0100 Subject: [PATCH 016/144] [Security] Update Roundcube to 1.3.7 https://github.com/roundcube/roundcubemail/releases/tag/1.3.7 --- webmails/roundcube/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile index c779e71a..c411226c 100644 --- a/webmails/roundcube/Dockerfile +++ b/webmails/roundcube/Dockerfile @@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y \ libpng12-dev \ && docker-php-ext-install pdo_mysql mcrypt zip -ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.6/roundcubemail-1.3.6-complete.tar.gz +ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.7/roundcubemail-1.3.7-complete.tar.gz RUN echo date.timezone=UTC > /usr/local/etc/php/conf.d/timezone.ini From 9e24064e354a881e60b0d4750fc959cffc2877dd Mon Sep 17 00:00:00 2001 From: hoellen Date: Tue, 31 Jul 2018 00:07:11 +0200 Subject: [PATCH 017/144] update rainloop to 1.12.1 --- webmails/rainloop/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webmails/rainloop/Dockerfile b/webmails/rainloop/Dockerfile index 714390d8..dfc6c83e 100644 --- a/webmails/rainloop/Dockerfile +++ b/webmails/rainloop/Dockerfile @@ -3,7 +3,7 @@ FROM php:5-apache RUN apt-get update && apt-get install -y \ unzip python3 python3-jinja2 -ENV RAINLOOP_URL https://github.com/RainLoop/rainloop-webmail/releases/download/v1.12.0/rainloop-community-1.12.0.zip +ENV RAINLOOP_URL https://github.com/RainLoop/rainloop-webmail/releases/download/v1.12.1/rainloop-community-1.12.1.zip RUN rm -rf /var/www/html/ \ && mkdir /var/www/html \ From eb9649db4e1daa59271d51cc34ea4ee478cc2294 Mon Sep 17 00:00:00 2001 From: hacor Date: Tue, 31 Jul 2018 17:15:25 +0200 Subject: [PATCH 018/144] Added a new release for Kubernetes Signed-off-by: hacor --- docs/kubernetes/1.6/README.md | 118 ++++++++++++++ docs/kubernetes/1.6/mailu/admin.yaml | 64 ++++++++ docs/kubernetes/1.6/mailu/configmap.yaml | 153 ++++++++++++++++++ docs/kubernetes/1.6/mailu/fetchmail.yaml | 39 +++++ docs/kubernetes/1.6/mailu/front.yaml | 129 +++++++++++++++ docs/kubernetes/1.6/mailu/imap.yaml | 80 +++++++++ docs/kubernetes/1.6/mailu/ingress-ssl.yaml | 32 ++++ docs/kubernetes/1.6/mailu/pvc.yaml | 27 ++++ docs/kubernetes/1.6/mailu/rbac.yaml | 4 + docs/kubernetes/1.6/mailu/redis.yaml | 56 +++++++ docs/kubernetes/1.6/mailu/security.yaml | 110 +++++++++++++ docs/kubernetes/1.6/mailu/smtp.yaml | 80 +++++++++ docs/kubernetes/1.6/mailu/static-ips.yaml | 0 docs/kubernetes/1.6/mailu/webdav.yaml | 63 ++++++++ docs/kubernetes/1.6/mailu/webmail.yaml | 59 +++++++ .../1.6/nginx/default-http-backend.yaml | 55 +++++++ docs/kubernetes/1.6/nginx/nginx-ingress.yaml | 139 ++++++++++++++++ docs/kubernetes/1.6/nginx/rbac.yaml | 129 +++++++++++++++ docs/kubernetes/{ => stable}/index.rst | 0 .../{ => stable}/kubernetes-mailu.yaml | 0 .../kubernetes-nginx-ingress-controller.yaml | 0 21 files changed, 1337 insertions(+) create mode 100644 docs/kubernetes/1.6/README.md create mode 100644 docs/kubernetes/1.6/mailu/admin.yaml create mode 100644 docs/kubernetes/1.6/mailu/configmap.yaml create mode 100644 docs/kubernetes/1.6/mailu/fetchmail.yaml create mode 100644 docs/kubernetes/1.6/mailu/front.yaml create mode 100644 docs/kubernetes/1.6/mailu/imap.yaml create mode 100644 docs/kubernetes/1.6/mailu/ingress-ssl.yaml create mode 100644 docs/kubernetes/1.6/mailu/pvc.yaml create mode 100644 docs/kubernetes/1.6/mailu/rbac.yaml create mode 100644 docs/kubernetes/1.6/mailu/redis.yaml create mode 100644 docs/kubernetes/1.6/mailu/security.yaml create mode 100644 docs/kubernetes/1.6/mailu/smtp.yaml create mode 100644 docs/kubernetes/1.6/mailu/static-ips.yaml create mode 100644 docs/kubernetes/1.6/mailu/webdav.yaml create mode 100644 docs/kubernetes/1.6/mailu/webmail.yaml create mode 100644 docs/kubernetes/1.6/nginx/default-http-backend.yaml create mode 100644 docs/kubernetes/1.6/nginx/nginx-ingress.yaml create mode 100644 docs/kubernetes/1.6/nginx/rbac.yaml rename docs/kubernetes/{ => stable}/index.rst (100%) rename docs/kubernetes/{ => stable}/kubernetes-mailu.yaml (100%) rename docs/kubernetes/{ => stable}/kubernetes-nginx-ingress-controller.yaml (100%) diff --git a/docs/kubernetes/1.6/README.md b/docs/kubernetes/1.6/README.md new file mode 100644 index 00000000..345609f9 --- /dev/null +++ b/docs/kubernetes/1.6/README.md @@ -0,0 +1,118 @@ +# Install Mailu master on kubernetes + +## Prequisites + +### Structure + +There's chosen to have a double NGINX stack for Mailu, this way the main ingress can still be used to access other websites/domains on your cluster. This is the current structure: + +- `NGINX Ingress controller`: Listens to the nodes ports 80 & 443 and directly forwards all TCP traffic on the E-amail ports (993,143,25,587,...). This is because this `DaemonSet` already consumes ports 80 & 443 and uses `hostNetwork: true` +- `Cert manager`: Creates automatic Lets Encrypt certificates based on an `Ingress`-objects domain name. +- `Mailu NGINX Front container`: This container receives all the mail traffic forwarded from the ingress controller. The web traffic is also forwarded based on an ingress +- `Mailu components`: All Mailu components are split into separate files to make them more + +### What you need +- A working Kubernetes cluster (tested with 1.10.5) +- A working [cert-manager](https://github.com/jetstack/cert-manager) installation +- A working nginx-ingress controller needed for the lets-encrypt certificates. You can find those files in the `nginx` subfolder + +#### Cert manager + +The `Cert-manager` is quite easy to deploy using Helm when reading the [docs](https://cert-manager.readthedocs.io/en/latest/getting-started/2-installing.html). +After booting the `Cert-manager` you'll need a `ClusterIssuer` which takes care of all required certificates through `Ingress` items. An example: + +```yaml +apiVersion: certmanager.k8s.io/v1alpha1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + email: something@example.com + http01: {} + privateKeySecretRef: + key: "" + name: letsencrypt-stage + server: https://acme-v02.api.letsencrypt.org/directory +``` + +### Things to change +- All services run in the same namespace, currently `mailu-mailserver`. So if you want to use a different one, change the `namespace` value in **every** file +- Check the `storage-class` field in the `pvc.yaml` file, you can also change the sizes to your liking. Note that you need `RWX` (read-write-many) and `RWO` (read-write-once) storageclasses. +- Check the `configmap.yaml` and adapt it to your needs. Be sure to check the kubernetes DNS values at the end (if you use a different namespace) +- Check the `ingress-ssl.yaml` and change it to the domain you want (this is for the kubernetes ingress controller, it will forward to `mailu/nginx` a.k.a. the `front` pod) + +## Installation +First run the command to start Mailu: + +```bash +kubectl create -f rbac.yaml +kubectl create -f configmap.yaml +kubectl create -f pvc.yaml +kubectl create -f ingress-ssl.yaml +kubectl create -f redis.yaml +kubectl create -f front.yaml +kubectl create -f webmail.yaml +kubectl create -f imap.yaml +kubectl create -f security.yaml +kubectl create -f smtp.yaml +kubectl create -f fetchmail.yaml +kubectl create -f admin.yaml +kubectl create -f webdav.yaml +``` + +## Create the first admin account + +When the cluster is online you need to create you master user to access `https://mail.example.com/admin`. +Enter the main `admin` pod to create the root account: + +```bash +kubectl -n mailu-mailserver get po +kubectl -n mailu-mailserver exec -it mailu-admin-.... /bin/sh +``` + +And in the pod run the following command. The command uses following entries: +- `admin` Make it an admin user +- `root` The first part of the e-mail adres (ROOT@example.com) +- `example.com` the domain appendix +- `password` the chosen password for the user + +```bash +python manage.py admin root example.com password +``` + +Now you should be able to login on the mail account: `https://mail.example.com/admin` + +## Adaptations + +I noticed you need an override for the `postfix` server in order to be able to send mail. I noticed Google wasn't able to deliver mail to my account and it had to do with the `smtpd_authorized_xclient_hosts` value in the config file. The config can be read [here](https://github.com/hacor/Mailu/blob/master/core/postfix/conf/main.cf#L35) and is pointing to a single IP of the service. But the requests come from the host IPs (the NGINX Ingress proxy) and they don't use the service specific IP. + +Enter the `postfix` pod: + +```bash +kubectl -n mailu-mailserver get po +kubectl -n mailu-mailserver exec -it mailu-smtp-.... /bin/sh +``` + +Now you're in the pod, create an override file like so: + +```bash +vi /overrides/postfix.cf +``` + +And give it the following contents, off course replacing `10.2.0.0/16` with the CIDR of your pod range. This way the NGINX pods can also restart and your mail server will still operate + +```bash +not_needed = true +smtpd_authorized_xclient_hosts = 10.2.0.0/16 +``` + +The first line seems stupid, but is needed because its pasted after a #, so from the second line we're really in action. +Save and close the file and exit. Now you need to delete the pod in order to recreate the config file. + +```bash +kubectl -n mailu-mailserver delete po/mailu-smtp-.... +``` + +Wait for the pod to recreate and you're online! +Happy mailing! \ No newline at end of file diff --git a/docs/kubernetes/1.6/mailu/admin.yaml b/docs/kubernetes/1.6/mailu/admin.yaml new file mode 100644 index 00000000..b36760a2 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/admin.yaml @@ -0,0 +1,64 @@ + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mailu-admin + namespace: mailu-mailserver +spec: + replicas: 1 + template: + metadata: + labels: + app: mailu-admin + role: mail + tier: backend + spec: + containers: + - name: admin + image: mailu/admin:master + imagePullPolicy: Always + envFrom: + - configMapRef: + name: mailu-config + volumeMounts: + - name: maildata + mountPath: /data + subPath: maildata + - name: maildata + mountPath: /dkim + subPath: dkim + ports: + - name: http + containerPort: 80 + protocol: TCP + resources: + requests: + memory: 500Mi + cpu: 500m + limits: + memory: 500Mi + cpu: 500m + volumes: + - name: maildata + persistentVolumeClaim: + claimName: mail-storage +--- + +apiVersion: v1 +kind: Service +metadata: + name: admin + namespace: mailu-mailserver + labels: + app: mailu-admin + role: mail + tier: backend +spec: + selector: + app: mailu-admin + role: mail + tier: backend + ports: + - name: http + port: 80 + protocol: TCP \ No newline at end of file diff --git a/docs/kubernetes/1.6/mailu/configmap.yaml b/docs/kubernetes/1.6/mailu/configmap.yaml new file mode 100644 index 00000000..9ebce8b1 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/configmap.yaml @@ -0,0 +1,153 @@ + apiVersion: v1 + kind: ConfigMap + metadata: + name: mailu-config + namespace: mailu-mailserver + data: + # Mailu main configuration file + # + # Most configuration variables can be modified through the Web interface, + # these few settings must however be configured before starting the mail + # server and require a restart upon change. + + ################################### + # Common configuration variables + ################################### + + # Set this to the path where Mailu data and configuration is stored + ROOT: "/mailu" + + # Mailu version to run (1.0, 1.1, etc. or master) + VERSION: "master" + + # Set to a randomly generated 16 bytes string + SECRET_KEY: "YourKeyHere" + + # Address where listening ports should bind + BIND_ADDRESS4: "127.0.0.1" + #BIND_ADDRESS6: "::1" + + # Main mail domain + DOMAIN: "example.com" + + # Hostnames for this server, separated with comas + HOSTNAMES: "mail.example.com" + + # Postmaster local part (will append the main mail domain) + POSTMASTER: "admin" + + # Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt) + TLS_FLAVOR: "cert" + + # Authentication rate limit (per source IP address) + AUTH_RATELIMIT: "10/minute;1000/hour" + + # Opt-out of statistics, replace with "True" to opt out + DISABLE_STATISTICS: "False" + + ################################### + # Optional features + ################################### + + # Expose the admin interface (value: true, false) + ADMIN: "true" + # Run the admin interface in debug mode + #DEBUG: "True" + + # Choose which webmail to run if any (values: roundcube, rainloop, none) + WEBMAIL: "roundcube" + + # Dav server implementation (value: radicale, none) + WEBDAV: "radicale" + + # Antivirus solution (value: clamav, none) + ANTIVIRUS: "clamav" + + ################################### + # Mail settings + ################################### + + # Message size limit in bytes + # Default: accept messages up to 50MB + MESSAGE_SIZE_LIMIT: "50000000" + + # Networks granted relay permissions, make sure that you include your Docker + # internal network (default to 172.17.0.0/16) + # For kubernetes this is the CIDR of the pod network + RELAYNETS: "10.2.0.0/16" + POD_ADDRESS_RANGE: "10.2.0.0/16" + + + # Will relay all outgoing mails if configured + #RELAYHOST= + + # This part is needed for the XCLIENT login for postfix. This should be the POD ADDRESS range + FRONT_ADDRESS: "front.mailu-mailserver.svc.cluster.local" + + # Fetchmail delay + FETCHMAIL_DELAY: "600" + + # Recipient delimiter, character used to delimiter localpart from custom address part + # e.g. localpart+custom@domain;tld + RECIPIENT_DELIMITER: "+" + + # DMARC rua and ruf email + DMARC_RUA: "root" + DMARC_RUF: "root" + + # Welcome email, enable and set a topic and body if you wish to send welcome + # emails to all users. + WELCOME: "false" + WELCOME_SUBJECT: "Welcome to your new email account" + WELCOME_BODY: "Welcome to your new email account, if you can read this, then it is configured properly!" + + ################################### + # Web settings + ################################### + + # Path to the admin interface if enabled + WEB_ADMIN: "/admin" + + # Path to the webmail if enabled + WEB_WEBMAIL: "/webmail" + + # Website name + SITENAME: "AppSynth" + + # Linked Website URL + WEBSITE: "https://example.com" + + # Registration reCaptcha settings (warning, this has some privacy impact) + # RECAPTCHA_PUBLIC_KEY= + # RECAPTCHA_PRIVATE_KEY= + + # Domain registration, uncomment to enable + # DOMAIN_REGISTRATION=true + + ################################### + # Advanced settings + ################################### + + # Docker-compose project name, this will prepended to containers names. + COMPOSE_PROJECT_NAME: "mailu" + + # Default password scheme used for newly created accounts and changed passwords + # (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT) + PASSWORD_SCHEME: "SHA512-CRYPT" + + # Header to take the real ip from + #REAL_IP_HEADER: + + # IPs for nginx set_real_ip_from (CIDR list separated by commas) + #REAL_IP_FROM: + + # Host settings + HOST_IMAP: "imap.mailu-mailserver.svc.cluster.local" + HOST_POP3: "imap.mailu-mailserver.svc.cluster.local" + HOST_SMTP: "smtp.mailu-mailserver.svc.cluster.local" + HOST_AUTHSMTP: "smtp.mailu-mailserver.svc.cluster.local" + HOST_WEBMAIL: "webmail.mailu-mailserver.svc.cluster.local" + HOST_ADMIN: "admin.mailu-mailserver.svc.cluster.local" + HOST_WEBDAV: "webdav.mailu-mailserver.svc.cluster.local:5232" + HOST_ANTISPAM: "antispam.mailu-mailserver.svc.cluster.local:11332" + HOST_REDIS: "redis.mailu-mailserver.svc.cluster.local" diff --git a/docs/kubernetes/1.6/mailu/fetchmail.yaml b/docs/kubernetes/1.6/mailu/fetchmail.yaml new file mode 100644 index 00000000..cf3271e7 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/fetchmail.yaml @@ -0,0 +1,39 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mailu-fetchmail + namespace: mailu-mailserver +spec: + replicas: 1 + template: + metadata: + labels: + app: mailu-fetchmail + role: mail + tier: backend + spec: + containers: + - name: fetchmail + image: mailu/fetchmail:master + imagePullPolicy: Always + envFrom: + - configMapRef: + name: mailu-config + volumeMounts: + - name: maildata + mountPath: /data + subPath: maildata + ports: + - containerPort: 5232 + - containerPort: 80 + resources: + requests: + memory: 100Mi + cpu: 100m + limits: + memory: 100Mi + cpu: 100m + volumes: + - name: maildata + persistentVolumeClaim: + claimName: mail-storage \ No newline at end of file diff --git a/docs/kubernetes/1.6/mailu/front.yaml b/docs/kubernetes/1.6/mailu/front.yaml new file mode 100644 index 00000000..e25ac828 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/front.yaml @@ -0,0 +1,129 @@ + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mailu-front + namespace: mailu-mailserver +spec: + replicas: 1 + template: + metadata: + labels: + app: mailu-front + role: mail + tier: backend + spec: + restartPolicy: Always + terminationGracePeriodSeconds: 60 + containers: + - name: front + image: mailu/nginx:latest + imagePullPolicy: Always + envFrom: + - configMapRef: + name: mailu-config + volumeMounts: + - name: certs + mountPath: /certs + ports: + - name: http + containerPort: 80 + protocol: TCP + - name: https + containerPort: 443 + protocol: TCP + - name: pop3 + containerPort: 110 + protocol: TCP + - name: pop3s + containerPort: 995 + protocol: TCP + - name: imap + containerPort: 143 + protocol: TCP + - name: imaps + containerPort: 993 + protocol: TCP + - name: smtp + containerPort: 25 + protocol: TCP + - name: smtp-auth + containerPort: 10025 + protocol: TCP + - name: imap-auth + containerPort: 10143 + protocol: TCP + - name: smtps + containerPort: 465 + protocol: TCP + - name: smtpd + containerPort: 587 + protocol: TCP + - name: auth + containerPort: 8000 + protocol: TCP + resources: + requests: + memory: 100Mi + cpu: 100m + limits: + memory: 200Mi + cpu: 200m + volumes: + - name: certs + secret: + items: + - key: tls.crt + path: cert.pem + - key: tls.key + path: key.pem + secretName: letsencrypt-certs-all +--- +apiVersion: v1 +kind: Service +metadata: + name: front + namespace: mailu-mailserver + labels: + app: mailu-admin + role: mail + tier: backend +spec: + selector: + app: mailu-front + role: mail + tier: backend + ports: + - name: http + port: 80 + protocol: TCP + - name: https + port: 443 + protocol: TCP + - name: pop3 + port: 110 + protocol: TCP + - name: pop3s + port: 995 + protocol: TCP + - name: imap + port: 143 + protocol: TCP + - name: imaps + port: 993 + protocol: TCP + - name: smtp + port: 25 + protocol: TCP + - name: smtps + port: 465 + protocol: TCP + - name: smtpd + port: 587 + protocol: TCP + - name: smtp-auth + port: 10025 + protocol: TCP + - name: imap-auth + port: 10143 + protocol: TCP diff --git a/docs/kubernetes/1.6/mailu/imap.yaml b/docs/kubernetes/1.6/mailu/imap.yaml new file mode 100644 index 00000000..069b7730 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/imap.yaml @@ -0,0 +1,80 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mailu-imap + namespace: mailu-mailserver +spec: + replicas: 1 + template: + metadata: + labels: + app: mailu-imap + role: mail + tier: backend + spec: + containers: + - name: imap + image: mailu/dovecot:master + imagePullPolicy: Always + envFrom: + - configMapRef: + name: mailu-config + volumeMounts: + - mountPath: /data + name: maildata + subPath: maildata + - mountPath: /mail + name: maildata + subPath: mailstate + - mountPath: /overrides + name: maildata + subPath: overrides + ports: + - containerPort: 2102 + - containerPort: 2525 + - containerPort: 143 + - containerPort: 993 + - containerPort: 4190 + resources: + requests: + memory: 500Mi + cpu: 500m + limits: + memory: 1Gi + cpu: 1000m + volumes: + - name: maildata + persistentVolumeClaim: + claimName: mail-storage +--- +apiVersion: v1 +kind: Service +metadata: + name: imap + namespace: mailu-mailserver + labels: + app: mailu + role: mail + tier: backend +spec: + selector: + app: mailu-imap + role: mail + tier: backend + ports: + ports: + - name: imap-auth + port: 2102 + protocol: TCP + - name: imap-transport + port: 2525 + protocol: TCP + - name: imap-default + port: 143 + protocol: TCP + - name: imap-ssl + port: 993 + protocol: TCP + - name: sieve + port: 4190 + protocol: TCP \ No newline at end of file diff --git a/docs/kubernetes/1.6/mailu/ingress-ssl.yaml b/docs/kubernetes/1.6/mailu/ingress-ssl.yaml new file mode 100644 index 00000000..61ae3cf7 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/ingress-ssl.yaml @@ -0,0 +1,32 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: mailu-ssl-ingress + namespace: mailu-mailserver + annotations: + kubernetes.io/ingress.class: tectonic + kubernetes.io/tls-acme: "true" + nginx.ingress.kubernetes.io/proxy-body-size: "0" + ingress.kubernetes.io/ssl-redirect: "true" + # Replace letsencrypt-prod with the name of the certificate issuer + certmanager.k8s.io/cluster-issuer: letsencrypt-prod + #ingress.kubernetes.io/rewrite-target: "/" + #ingress.kubernetes.io/app-root: "/ui" + #ingress.kubernetes.io/follow-redirects: "true" + labels: + app: mailu + role: mail + tier: backend +spec: + tls: + - hosts: + - "mail.example.com" + secretName: letsencrypt-certs-all # If unsure how to generate these, check out https://github.com/ployst/docker-letsencrypt + rules: + - host: "mail.example.com" + http: + paths: + - path: "/" + backend: + serviceName: front + servicePort: 80 \ No newline at end of file diff --git a/docs/kubernetes/1.6/mailu/pvc.yaml b/docs/kubernetes/1.6/mailu/pvc.yaml new file mode 100644 index 00000000..0ec2852f --- /dev/null +++ b/docs/kubernetes/1.6/mailu/pvc.yaml @@ -0,0 +1,27 @@ +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: redis-hdd + namespace: mailu-mailserver + annotations: + volume.beta.kubernetes.io/storage-class: "glusterblock-hdd" +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: mail-storage + namespace: mailu-mailserver + annotations: + volume.beta.kubernetes.io/storage-class: "gluster-heketi-hdd" +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 100Gi diff --git a/docs/kubernetes/1.6/mailu/rbac.yaml b/docs/kubernetes/1.6/mailu/rbac.yaml new file mode 100644 index 00000000..33255130 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/rbac.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: mailu-mailserver \ No newline at end of file diff --git a/docs/kubernetes/1.6/mailu/redis.yaml b/docs/kubernetes/1.6/mailu/redis.yaml new file mode 100644 index 00000000..d6bb1eb8 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/redis.yaml @@ -0,0 +1,56 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mailu-redis + namespace: mailu-mailserver +spec: + replicas: 1 + template: + metadata: + labels: + app: mailu-redis + role: mail + tier: backend + spec: + containers: + - name: redis + image: redis:4.0-alpine + imagePullPolicy: Always + volumeMounts: + - mountPath: /data + name: redisdata + ports: + - containerPort: 6379 + name: redis + protocol: TCP + resources: + requests: + memory: 200Mi + cpu: 100m + limits: + memory: 300Mi + cpu: 200m + volumes: + - name: redisdata + persistentVolumeClaim: + claimName: redis-hdd +--- + +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: mailu-mailserver + labels: + app: mailu-redis + role: mail + tier: backend +spec: + selector: + app: mailu-redis + role: mail + tier: backend + ports: + - name: redis + port: 6379 + protocol: TCP \ No newline at end of file diff --git a/docs/kubernetes/1.6/mailu/security.yaml b/docs/kubernetes/1.6/mailu/security.yaml new file mode 100644 index 00000000..c1c1ac0b --- /dev/null +++ b/docs/kubernetes/1.6/mailu/security.yaml @@ -0,0 +1,110 @@ + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mailu-security + namespace: mailu-mailserver +spec: + replicas: 1 + template: + metadata: + labels: + app: mailu-security + role: mail + tier: backend + spec: + containers: + - name: antispam + image: mailu/rspamd:master + imagePullPolicy: Always + envFrom: + - configMapRef: + name: mailu-config + resources: + requests: + memory: 100Mi + cpu: 100m + limits: + memory: 200Mi + cpu: 200m + ports: + - name: antispam + containerPort: 11332 + protocol: TCP + volumeMounts: + - name: filter + subPath: filter + mountPath: /var/lib/rspamd + - name: filter + mountPath: /dkim + subPath: dkim + - name: filter + mountPath: /etc/rspamd/override.d + subPath: rspamd-overrides + - name: antivirus + image: mailu/clamav:master + imagePullPolicy: Always + resources: + requests: + memory: 1Gi + cpu: 1000m + limits: + memory: 2Gi + cpu: 1000m + envFrom: + - configMapRef: + name: mailu-config + ports: + - name: antivirus + containerPort: 3310 + protocol: TCP + volumeMounts: + - name: filter + subPath: filter + mountPath: /data + volumes: + - name: filter + persistentVolumeClaim: + claimName: mail-storage + +--- + +apiVersion: v1 +kind: Service +metadata: + name: antispam + namespace: mailu-mailserver + labels: + app: mailu-antispam + role: mail + tier: backend +spec: + selector: + app: mailu-security + role: mail + tier: backend + ports: + - name: antispam + port: 11332 + protocol: TCP + +--- + +apiVersion: v1 +kind: Service +metadata: + name: antivirus + namespace: mailu-mailserver + labels: + app: mailu-antivirus + role: mail + tier: backend +spec: + selector: + app: mailu-security + role: mail + tier: backend + ports: + - name: antivirus + port: 3310 + protocol: TCP \ No newline at end of file diff --git a/docs/kubernetes/1.6/mailu/smtp.yaml b/docs/kubernetes/1.6/mailu/smtp.yaml new file mode 100644 index 00000000..454b8ed7 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/smtp.yaml @@ -0,0 +1,80 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mailu-smtp + namespace: mailu-mailserver +spec: + replicas: 1 + template: + metadata: + labels: + app: mailu-smtp + role: mail + tier: backend + spec: + containers: + - name: smtp + image: mailu/postfix:master + imagePullPolicy: Always + envFrom: + - configMapRef: + name: mailu-config + resources: + requests: + memory: 500Mi + cpu: 200m + limits: + memory: 1Gi + cpu: 500m + volumeMounts: + - mountPath: /data + name: maildata + subPath: maildata + - mountPath: /overrides + name: maildata + subPath: overrides + ports: + - name: smtp + containerPort: 25 + protocol: TCP + - name: smtp-ssl + containerPort: 465 + protocol: TCP + - name: smtp-starttls + containerPort: 587 + protocol: TCP + - name: smtp-auth + containerPort: 10025 + protocol: TCP + volumes: + - name: maildata + persistentVolumeClaim: + claimName: mail-storage +--- +apiVersion: v1 +kind: Service +metadata: + name: smtp + namespace: mailu-mailserver + labels: + app: mailu + role: mail + tier: backend +spec: + selector: + app: mailu-smtp + role: mail + tier: backend + ports: + - name: smtp + port: 25 + protocol: TCP + - name: smtp-ssl + port: 465 + protocol: TCP + - name: smtp-starttls + port: 587 + protocol: TCP + - name: smtp-auth + port: 10025 + protocol: TCP diff --git a/docs/kubernetes/1.6/mailu/static-ips.yaml b/docs/kubernetes/1.6/mailu/static-ips.yaml new file mode 100644 index 00000000..e69de29b diff --git a/docs/kubernetes/1.6/mailu/webdav.yaml b/docs/kubernetes/1.6/mailu/webdav.yaml new file mode 100644 index 00000000..07b7733c --- /dev/null +++ b/docs/kubernetes/1.6/mailu/webdav.yaml @@ -0,0 +1,63 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mailu-webdav + namespace: mailu-mailserver +spec: + replicas: 1 + template: + metadata: + labels: + app: mailu-webdav + role: mail + tier: backend + spec: + containers: + - name: radicale + image: mailu/radicale:master + imagePullPolicy: Always + envFrom: + - configMapRef: + name: mailu-config + volumeMounts: + - mountPath: /data + name: maildata + subPath: dav + ports: + - containerPort: 5232 + - containerPort: 80 + resources: + requests: + memory: 100Mi + cpu: 100m + limits: + memory: 100Mi + cpu: 100m + volumes: + - name: maildata + persistentVolumeClaim: + claimName: mail-storage +--- + +apiVersion: v1 +kind: Service +metadata: + name: webdav + namespace: mailu-mailserver + labels: + app: mailu-webdav + role: mail + tier: backend +spec: + selector: + app: mailu-webdav + role: mail + tier: backend + ports: + ports: + - name: http + port: 80 + protocol: TCP + - name: http-ui + port: 5232 + protocol: TCP \ No newline at end of file diff --git a/docs/kubernetes/1.6/mailu/webmail.yaml b/docs/kubernetes/1.6/mailu/webmail.yaml new file mode 100644 index 00000000..81798782 --- /dev/null +++ b/docs/kubernetes/1.6/mailu/webmail.yaml @@ -0,0 +1,59 @@ + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: mailu-roundcube + namespace: mailu-mailserver +spec: + replicas: 1 + template: + metadata: + labels: + app: mailu-roundcube + role: mail + tier: frontend + spec: + containers: + - name: roundcube + image: mailu/roundcube:1.5 + imagePullPolicy: Always + envFrom: + - configMapRef: + name: mailu-config + resources: + requests: + memory: 100Mi + cpu: 100m + limits: + memory: 200Mi + cpu: 200m + volumeMounts: + - mountPath: /data + name: maildata + subPath: webmail + ports: + - containerPort: 80 + volumes: + - name: maildata + persistentVolumeClaim: + claimName: mail-storage +--- +apiVersion: v1 +kind: Service +metadata: + name: webmail + namespace: mailu-mailserver + labels: + app: mailu-roundcube + role: mail + tier: frontend +spec: + selector: + app: mailu-roundcube + role: mail + tier: frontend + ports: + ports: + - name: http + port: 80 + protocol: TCP diff --git a/docs/kubernetes/1.6/nginx/default-http-backend.yaml b/docs/kubernetes/1.6/nginx/default-http-backend.yaml new file mode 100644 index 00000000..097fe7c5 --- /dev/null +++ b/docs/kubernetes/1.6/nginx/default-http-backend.yaml @@ -0,0 +1,55 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: default-http-backend + labels: + app: default-http-backend + namespace: kube-ingress +spec: + replicas: 1 + selector: + matchLabels: + app: default-http-backend + template: + metadata: + labels: + app: default-http-backend + spec: + terminationGracePeriodSeconds: 60 + containers: + - name: default-http-backend + # Any image is permissible as long as: + # 1. It serves a 404 page at / + # 2. It serves 200 on a /healthz endpoint + image: gcr.io/google_containers/defaultbackend:1.4 + livenessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + initialDelaySeconds: 30 + timeoutSeconds: 5 + ports: + - containerPort: 8080 + resources: + limits: + cpu: 10m + memory: 20Mi + requests: + cpu: 10m + memory: 20Mi +--- + +apiVersion: v1 +kind: Service +metadata: + name: default-http-backend + namespace: kube-ingress + labels: + app: default-http-backend +spec: + ports: + - port: 80 + targetPort: 8080 + selector: + app: default-http-backend \ No newline at end of file diff --git a/docs/kubernetes/1.6/nginx/nginx-ingress.yaml b/docs/kubernetes/1.6/nginx/nginx-ingress.yaml new file mode 100644 index 00000000..90b24f24 --- /dev/null +++ b/docs/kubernetes/1.6/nginx/nginx-ingress.yaml @@ -0,0 +1,139 @@ +apiVersion: v1 +kind: Service +metadata: + # keep it under 24 chars + name: appsynth-lb + namespace: kube-ingress + labels: + k8s-app: appsynth-lb + component: ingress-controller +spec: + type: ClusterIP + selector: + k8s-app: appsynth-lb + component: ingress-controller + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 + - name: https + protocol: TCP + port: 443 + targetPort: 443 +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: udp-services + namespace: kube-ingress + +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: tcp-services + namespace: kube-ingress +data: + 25: "mailu-mailserver/front:25" + 110: "mailu-mailserver/front:110" + 465: "mailu-mailserver/front:465" + 587: "mailu-mailserver/front:587" + 143: "mailu-mailserver/front:143" + 993: "mailu-mailserver/front:993" + 995: "mailu-mailserver/front:995" + +--- +apiVersion: v1 +data: + enable-vts-status: "true" +kind: ConfigMap +metadata: + name: nginx-ingress-lb-conf + namespace: kube-ingress +--- +apiVersion: apps/v1beta2 +kind: DaemonSet +metadata: + name: ingress-controller + namespace: kube-ingress + annotations: + prometheus.io/port: "10254" + prometheus.io/scrape: "true" + labels: + k8s-app: appsynth-lb + component: ingress-controller + type: nginx +spec: + updateStrategy: + rollingUpdate: + maxUnavailable: 1 + type: RollingUpdate + selector: + matchLabels: + k8s-app: appsynth-lb + component: ingress-controller + type: nginx + template: + metadata: + labels: + k8s-app: appsynth-lb + component: ingress-controller + type: nginx + spec: + serviceAccount: kube-nginx-ingress + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-role.kubernetes.io/master + operator: DoesNotExist + containers: + - name: nginx-ingress-lb + image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.16.2 + args: + - /nginx-ingress-controller + - --configmap=$(POD_NAMESPACE)/tectonic-custom-error + - --default-backend-service=$(POD_NAMESPACE)/default-http-backend + #- --default-ssl-certificate=tectonic-system/tectonic-ingress-tls-secret + - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services + - --udp-services-configmap=$(POD_NAMESPACE)/udp-services + - --annotations-prefix=ingress.kubernetes.io + - --enable-ssl-passthrough + - --ingress-class=tectonic + # use downward API + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + ports: + - name: http + containerPort: 80 + hostPort: 80 + - name: https + containerPort: 443 + hostPort: 443 + readinessProbe: + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + livenessProbe: + initialDelaySeconds: 10 + timeoutSeconds: 1 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + hostNetwork: true + nodeSelector: + node-role.kubernetes.io/node: "" + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 60 diff --git a/docs/kubernetes/1.6/nginx/rbac.yaml b/docs/kubernetes/1.6/nginx/rbac.yaml new file mode 100644 index 00000000..d3c01384 --- /dev/null +++ b/docs/kubernetes/1.6/nginx/rbac.yaml @@ -0,0 +1,129 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kube-ingress +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kube-nginx-ingress + namespace: kube-ingress +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: kube-nginx-ingress +rules: + - apiGroups: + - "" + resources: + - configmaps + - endpoints + - nodes + - pods + - secrets + verbs: + - list + - watch + - update + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - apiGroups: + - "extensions" + resources: + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - apiGroups: + - "extensions" + resources: + - ingresses/status + verbs: + - update +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: Role +metadata: + name: kube-nginx-ingress + namespace: kube-ingress +rules: + - apiGroups: + - "" + resources: + - configmaps + - pods + - secrets + - namespaces + verbs: + - get + - apiGroups: + - "" + resources: + - configmaps + resourceNames: + - "ingress-controller-leader-nginx" + verbs: + - get + - update + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - apiGroups: + - "" + resources: + - endpoints + verbs: + - get + - create + - update +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: kube-nginx-ingress + namespace: kube-ingress +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kube-nginx-ingress +subjects: + - kind: ServiceAccount + name: kube-nginx-ingress + namespace: kube-ingress +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: kube-nginx-ingress +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kube-nginx-ingress +subjects: + - kind: ServiceAccount + name: kube-nginx-ingress + namespace: kube-ingress \ No newline at end of file diff --git a/docs/kubernetes/index.rst b/docs/kubernetes/stable/index.rst similarity index 100% rename from docs/kubernetes/index.rst rename to docs/kubernetes/stable/index.rst diff --git a/docs/kubernetes/kubernetes-mailu.yaml b/docs/kubernetes/stable/kubernetes-mailu.yaml similarity index 100% rename from docs/kubernetes/kubernetes-mailu.yaml rename to docs/kubernetes/stable/kubernetes-mailu.yaml diff --git a/docs/kubernetes/kubernetes-nginx-ingress-controller.yaml b/docs/kubernetes/stable/kubernetes-nginx-ingress-controller.yaml similarity index 100% rename from docs/kubernetes/kubernetes-nginx-ingress-controller.yaml rename to docs/kubernetes/stable/kubernetes-nginx-ingress-controller.yaml From db3cb2aac15104843cd858227635face8c961a02 Mon Sep 17 00:00:00 2001 From: hacor Date: Tue, 31 Jul 2018 17:17:21 +0200 Subject: [PATCH 019/144] Updated docs Signed-off-by: hacor --- docs/kubernetes/1.6/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/kubernetes/1.6/README.md b/docs/kubernetes/1.6/README.md index 345609f9..dcbd2418 100644 --- a/docs/kubernetes/1.6/README.md +++ b/docs/kubernetes/1.6/README.md @@ -36,7 +36,11 @@ spec: server: https://acme-v02.api.letsencrypt.org/directory ``` -### Things to change +## Deploying Mailu + +All manifests can be found in the `mailu` subdirectory. All commands below need to be run from this subdirectory + +### Personalization - All services run in the same namespace, currently `mailu-mailserver`. So if you want to use a different one, change the `namespace` value in **every** file - Check the `storage-class` field in the `pvc.yaml` file, you can also change the sizes to your liking. Note that you need `RWX` (read-write-many) and `RWO` (read-write-once) storageclasses. - Check the `configmap.yaml` and adapt it to your needs. Be sure to check the kubernetes DNS values at the end (if you use a different namespace) From 699a25939f2ecd8da77eb2830169fdbc562d00d1 Mon Sep 17 00:00:00 2001 From: hacor Date: Tue, 31 Jul 2018 17:45:11 +0200 Subject: [PATCH 020/144] Updated docs for Travis Signed-off-by: hacor --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 0920bb96..5219145f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,7 +55,7 @@ the version of Mailu that you are running. configuration compose/requirements compose/setup - kubernetes/index + kubernetes/stable/index dns reverse From 151aeb9c067a3005750e773bc035b9443ff52131 Mon Sep 17 00:00:00 2001 From: hacor Date: Wed, 1 Aug 2018 15:45:49 +0200 Subject: [PATCH 021/144] Updated adaptations for dovecot on shared filesystem and indexing errors Signed-off-by: hacor --- docs/kubernetes/1.6/README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/kubernetes/1.6/README.md b/docs/kubernetes/1.6/README.md index dcbd2418..c0dd935b 100644 --- a/docs/kubernetes/1.6/README.md +++ b/docs/kubernetes/1.6/README.md @@ -89,6 +89,7 @@ Now you should be able to login on the mail account: `https://mail.example.com/a ## Adaptations +### Postfix I noticed you need an override for the `postfix` server in order to be able to send mail. I noticed Google wasn't able to deliver mail to my account and it had to do with the `smtpd_authorized_xclient_hosts` value in the config file. The config can be read [here](https://github.com/hacor/Mailu/blob/master/core/postfix/conf/main.cf#L35) and is pointing to a single IP of the service. But the requests come from the host IPs (the NGINX Ingress proxy) and they don't use the service specific IP. Enter the `postfix` pod: @@ -118,5 +119,39 @@ Save and close the file and exit. Now you need to delete the pod in order to rec kubectl -n mailu-mailserver delete po/mailu-smtp-.... ``` +### Dovecot +- If you are using Dovecot on a shared file system (Glusterfs, NFS,...), you need to create a special override otherwise a lot of indexing errors will occur on your Dovecot pod. +- I also higher the number of max connections per IP. Now it's limited to 10. +Enter the dovecot pod: + +```bash +kubectl -n mailu-mailserver get po +kubectl -n mailu-mailserver exec -it mailu-imap-.... /bin/sh +``` + +Create the file `/overrides/dovecot.conf` + +```bash +vi /overrides/dovecot.conf +``` + +And enter following contents: +```bash +mail_nfs_index = yes +mail_nfs_storage = yes +mail_fsync = always +mmap_disable = yes +mail_max_userip_connections=100 +``` + +Save and close the file and delete the imap pod to get it recreated. + +```bash +kubectl -n mailu-mailserver delete po/mailu-imap-.... +``` + +Wait for the pod to recreate and you're online! +Happy mailing! + Wait for the pod to recreate and you're online! Happy mailing! \ No newline at end of file From 5ad02ae2e55f4622873165d43c710b251e4e5f9f Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Wed, 1 Aug 2018 21:23:50 +0200 Subject: [PATCH 022/144] Use a more uniform 'Save' for most form submits, fixes #523 --- core/admin/mailu/ui/forms.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py index 82d98210..326d721b 100644 --- a/core/admin/mailu/ui/forms.py +++ b/core/admin/mailu/ui/forms.py @@ -50,7 +50,7 @@ class DomainForm(flask_wtf.FlaskForm): max_quota_bytes = fields_.IntegerSliderField(_('Maximum user quota'), default=0) signup_enabled = fields.BooleanField(_('Enable sign-up'), default=False) comment = fields.StringField(_('Comment')) - submit = fields.SubmitField(_('Create')) + submit = fields.SubmitField(_('Save')) class DomainSignupForm(flask_wtf.FlaskForm): @@ -64,14 +64,14 @@ class DomainSignupForm(flask_wtf.FlaskForm): class AlternativeForm(flask_wtf.FlaskForm): name = fields.StringField(_('Alternative name'), [validators.DataRequired()]) - submit = fields.SubmitField(_('Create')) + submit = fields.SubmitField(_('Save')) class RelayForm(flask_wtf.FlaskForm): name = fields.StringField(_('Relayed domain name'), [validators.DataRequired()]) smtp = fields.StringField(_('Remote host')) comment = fields.StringField(_('Comment')) - submit = fields.SubmitField(_('Create')) + submit = fields.SubmitField(_('Save')) class UserForm(flask_wtf.FlaskForm): @@ -130,7 +130,7 @@ class TokenForm(flask_wtf.FlaskForm): ip = fields.StringField( _('Authorized IP'), [validators.Optional(), validators.IPAddress()] ) - submit = fields.SubmitField(_('Create')) + submit = fields.SubmitField(_('Save')) class AliasForm(flask_wtf.FlaskForm): @@ -139,7 +139,7 @@ class AliasForm(flask_wtf.FlaskForm): _('Use SQL LIKE Syntax (e.g. for catch-all aliases)')) destination = DestinationField(_('Destination')) comment = fields.StringField(_('Comment')) - submit = fields.SubmitField(_('Create')) + submit = fields.SubmitField(_('Save')) class AdminForm(flask_wtf.FlaskForm): From 18fe8cd9f21f916cc64f188aaf45ab6a69f3d560 Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Wed, 1 Aug 2018 21:29:18 +0200 Subject: [PATCH 023/144] Pin alpine:3.7 for Dovecot since extdata was removed from repos, fixes #528 --- core/dovecot/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/dovecot/Dockerfile b/core/dovecot/Dockerfile index cacfe354..f27bbdba 100644 --- a/core/dovecot/Dockerfile +++ b/core/dovecot/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:edge +FROM alpine:3.7 RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ && apk add --no-cache \ From 3dca1a834cb17086c32e8889874528f43c8a29a9 Mon Sep 17 00:00:00 2001 From: Pierre Jaury Date: Wed, 1 Aug 2018 21:56:29 +0200 Subject: [PATCH 024/144] Pin alpine 3.7 until we fix the certbot issue, see #522 --- core/nginx/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/nginx/Dockerfile b/core/nginx/Dockerfile index 3be4b50f..8a6536eb 100644 --- a/core/nginx/Dockerfile +++ b/core/nginx/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:edge +FROM alpine:3.7 RUN apk add --no-cache nginx nginx-mod-mail python py-jinja2 certbot openssl From 9350bb9b9a0234afa8ed844a7e7777b426b71898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 3 Aug 2018 00:18:39 +0300 Subject: [PATCH 025/144] Use fixed alpine:3.7 tag to prevent postix upgrade --- core/postfix/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/postfix/Dockerfile b/core/postfix/Dockerfile index bb5831a2..168f3c60 100644 --- a/core/postfix/Dockerfile +++ b/core/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine +FROM alpine:3.7 RUN apk add --no-cache postfix postfix-sqlite postfix-pcre rsyslog python py-jinja2 From f506966abc5ec5452f7db599a11e16d7cc497e80 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Fri, 3 Aug 2018 08:24:06 +0200 Subject: [PATCH 026/144] Pin Alpine 3.7 to preserve the Postfix version --- core/postfix/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/postfix/Dockerfile b/core/postfix/Dockerfile index bb5831a2..168f3c60 100644 --- a/core/postfix/Dockerfile +++ b/core/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine +FROM alpine:3.7 RUN apk add --no-cache postfix postfix-sqlite postfix-pcre rsyslog python py-jinja2 From bd6026384aab0bc2e6530ea1c76f5342210750f5 Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 15:27:27 +0000 Subject: [PATCH 027/144] Documentation to deploy mailu on a doxker swarm --- docs/swarm/1.5/README.md | 67 +++++ .../swarm/1.5/docker-compose-stack-simple.yml | 275 ++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 docs/swarm/1.5/README.md create mode 100644 docs/swarm/1.5/docker-compose-stack-simple.yml diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md new file mode 100644 index 00000000..c9b10dc8 --- /dev/null +++ b/docs/swarm/1.5/README.md @@ -0,0 +1,67 @@ +# Install Mailu master on kubernetes + +## Prequisites + +### Swarm + +You need to have a swarm running + +```bash +In order to deploy mailu on a swarm, you will first need to initialize it: +The main command will be docker swarm init --advertise-addr +See https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/ +If you want to add other managers or workers, please use docker swarm join --token xxxxx +See https://docs.docker.com/engine/swarm/join-nodes/ + +You have now a working swarm, and you can check its status with +docker node ls +```bash +core@coreos-01 ~/git/Mailu/docs/swarm/1.5 $ docker node ls +ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION +ptpmtgih78v9q14mapt5hyxrb black-pearl Ready Active 18.06.0-ce +sczlqi2pigpw7117hbkh71nvb * coreos-01 Ready Active Leader 18.03.1-ce +mzrm98cc9i2y8obvi2fzo5i6n flying-dutchman Ready Active 18.06.0-ce +``` + +### Volume definition +For data persistance (the mailu services might be launched/relaunched on any of the swarm nodes), we need to have mailu data stored in a manner accessible by every manager or worker in the swarm. +Hereafer we will use a NFS share: +```bash +core@coreos-01 ~/git/Mailu/docs $ showmount -e 192.168.0.30 +Export list for 192.168.0.30: +/mnt/Pool1/pv 192.168.0.0 +``` + +on the nfs server, I am using the following /etc/exports +```bash +$more /etc/exports +/mnt/Pool1/pv -alldirs -mapall=root -network 192.168.0.0 -mask 255.255.255.0 +``` +on the nfs server, I created the mailu directory (in fact I copied a working mailu set-up) +```bash +$mkdir /mnt/Pool1/pv/mailu +``` + +On your manager node, mount the nfs share to check that the share is available: +```bash +core@coreos-01 ~ $ sudo mount -t nfs 192.168.0.30:/mnt/Pool1/pv/mailu /mnt/local/ +``` +If this is ok, you can umount it: +```bashcore@coreos-01 ~ $ sudo umount /mnt/local/ +``` + + +### Networking mode +On a swarm, the services are available (default mode) through a routing mesh managed by docker itself. With this mode, each service is given a virtual IP adress and docker manages the routing between this virtual IP and the container(s) provinding this service. +With this default networking mode, I cannot get login working properly... As found in https://github.com/Mailu/Mailu/issues/375 , a workaround is to use the dnsrr networking mode at least for the front services +The main consequence/limiation will be that the front services will *not* be available on every node, but only on the node where it will be deployed. In my case, I have only one manager and I choose to deploy the front service to the manager node, so I know on wich IP the front service will be available (aka the IP adress of my manager node). + +### Variable substitution +The docker stack deploy command doesn't support variable substitution in the .yml file itself (vut we still can use .env file to pass variables to the services). As a consequence we need to adjust the docker-compose file to : +- remove all variables : $VERSION , $BIND_ADDRESS4 , $BIND_ADDRESS6 , $ANTIVIRUS , $WEBMAIL , etc +- change the way we define the volumes (nfs share in our case) + +### Docker compose +A working docker-compose.yml file is avalable here: + + diff --git a/docs/swarm/1.5/docker-compose-stack-simple.yml b/docs/swarm/1.5/docker-compose-stack-simple.yml new file mode 100644 index 00000000..47ef7cb1 --- /dev/null +++ b/docs/swarm/1.5/docker-compose-stack-simple.yml @@ -0,0 +1,275 @@ +version: '3.2' + +services: + + front: + image: mailu/nginx:1.5 + env_file: .env + ports: + - target: 80 + published: 80 + mode: host + - target: 443 + published: 443 + mode: host + - target: 110 + published: 110 + mode: host + - target: 143 + published: 143 + mode: host + - target: 993 + published: 993 + mode: host + - target: 995 + published: 995 + mode: host + - target: 25 + published: 25 + mode: host + - target: 465 + published: 465 + mode: host + - target: 587 + published: 587 + mode: host + volumes: +# - "/mailu/certs:/certs" + - type: volume + source: mailu_certs + target: /certs + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + redis: + image: redis:alpine + restart: always + volumes: +# - "/mailu/redis:/data" + - type: volume + source: mailu_redis + target: /data + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + imap: +# image: mailu/dovecot:$VERSION + image: ofthesun9/dovecot:1.5 + restart: always + env_file: .env + volumes: +# - "$ROOT/data:/data" + - type: volume + source: mailu_data + target: /data +# - "$ROOT/mail:/mail" + - type: volume + source: mailu_mail + target: /mail +# - "$ROOT/overrides:/overrides" + - type: volume + source: mailu_overrides + target: /overrides + depends_on: + - front + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + smtp: + image: ofthesun9/postfix:1.5 + restart: always + env_file: .env + volumes: +# - "$ROOT/data:/data" + - type: volume + source: mailu_data + target: /data +# - "$ROOT/overrides:/overrides" + - type: volume + source: mailu_overrides + target: /overrides + depends_on: + - front + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + antispam: +# image: mailu/rspamd:$VERSION + image: ofthesun9/rspamd:fuzzydev + restart: always + env_file: .env + depends_on: + - front + volumes: +# - "$ROOT/filter:/var/lib/rspamd" + - type: volume + source: mailu_filter + target: /var/lib/rspamd +# - "$ROOT/dkim:/dkim" + - type: volume + source: mailu_dkim + target: /dkim +# - "$ROOT/overrides/rspamd:/etc/rspamd/override.d" + - type: volume + source: mailu_overrides_rspamd + target: /etc/rspamd/override.d + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + antivirus: + image: mailu/none:1.5 + restart: always + env_file: .env + volumes: +# - "/mailu/filter:/data" + - type: volume + source: mailu_filter + target: /data + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + webdav: + image: mailu/none:1.5 + restart: always + env_file: .env + volumes: +# - /mailu/dav:/data" + - type: volume + source: mailu_dav + target: /data + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + admin: + image: ofthesun9/admin:1.5-backports + restart: always + env_file: .env + volumes: +# - "/mailu/data:/data" + - type: volume + source: mailu_data + target: /data +# - "/mailu/dkim:/dkim" + - type: volume + source: mailu_dkim + target: /dkim + - /var/run/docker.sock:/var/run/docker.sock:ro + depends_on: + - redis + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + webmail: + image: "mailu/roundcube:1.5" + restart: always + env_file: .env + volumes: +# - "/mailu/webmail:/data" + - type: volume + source: mailu_data + target: /data + depends_on: + - imap + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + fetchmail: + image: mailu/fetchmail:1.5 + restart: always + env_file: .env + volumes: +# - "/mailu/data:/data" + - type: volume + source: mailu_data + target: /data + logging: + driver: none + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + +volumes: + mailu_filter: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/filter" + mailu_dkim: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/dkim" + mailu_overrides_rspamd: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/overrides/rspamd" + mailu_data: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/data" + mailu_mail: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/mail" + mailu_overrides: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/overrides" + mailu_dav: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/dav" + mailu_certs: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/certs" + mailu_nginx.conf: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/1.5/nginx.conf.wp" + mailu_tls.conf: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/1.5/tls.conf" + mailu_redis: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/redis" From 806dfc804a1d20a7392800c82248ee592fac7fbb Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 15:33:08 +0000 Subject: [PATCH 028/144] Typo --- docs/swarm/1.5/README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index c9b10dc8..e7c37ac9 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -6,21 +6,25 @@ You need to have a swarm running -```bash In order to deploy mailu on a swarm, you will first need to initialize it: -The main command will be docker swarm init --advertise-addr +The main command will be: +```bash +docker swarm init --advertise-addr +``` See https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/ -If you want to add other managers or workers, please use docker swarm join --token xxxxx +If you want to add other managers or workers, please use: +```bash +docker swarm join --token xxxxx +``` See https://docs.docker.com/engine/swarm/join-nodes/ -You have now a working swarm, and you can check its status with -docker node ls +You have now a working swarm, and you can check its status with: ```bash core@coreos-01 ~/git/Mailu/docs/swarm/1.5 $ docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION -ptpmtgih78v9q14mapt5hyxrb black-pearl Ready Active 18.06.0-ce -sczlqi2pigpw7117hbkh71nvb * coreos-01 Ready Active Leader 18.03.1-ce -mzrm98cc9i2y8obvi2fzo5i6n flying-dutchman Ready Active 18.06.0-ce +xhgeekkrlttpmtgmapt5hyxrb black-pearl Ready Active 18.06.0-ce +sczlqjgfhehsfdjhfhhph1nvb * coreos-01 Ready Active Leader 18.03.1-ce +mzrm9nbdggsfz4sgq6dhs5i6n flying-dutchman Ready Active 18.06.0-ce ``` ### Volume definition From a34090502d7cc9ae73e59c79d117783a5db761a3 Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 15:38:25 +0000 Subject: [PATCH 029/144] Documentation to deploy mailu on a docker swarm --- docs/swarm/1.5/README.md | 283 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 281 insertions(+), 2 deletions(-) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index e7c37ac9..6793a267 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -12,6 +12,7 @@ The main command will be: docker swarm init --advertise-addr ``` See https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/ + If you want to add other managers or workers, please use: ```bash docker swarm join --token xxxxx @@ -29,7 +30,7 @@ mzrm9nbdggsfz4sgq6dhs5i6n flying-dutchman Ready Active ### Volume definition For data persistance (the mailu services might be launched/relaunched on any of the swarm nodes), we need to have mailu data stored in a manner accessible by every manager or worker in the swarm. -Hereafer we will use a NFS share: +Hereafter we will use a NFS share: ```bash core@coreos-01 ~/git/Mailu/docs $ showmount -e 192.168.0.30 Export list for 192.168.0.30: @@ -51,7 +52,8 @@ On your manager node, mount the nfs share to check that the share is available: core@coreos-01 ~ $ sudo mount -t nfs 192.168.0.30:/mnt/Pool1/pv/mailu /mnt/local/ ``` If this is ok, you can umount it: -```bashcore@coreos-01 ~ $ sudo umount /mnt/local/ +```bash +core@coreos-01 ~ $ sudo umount /mnt/local/ ``` @@ -68,4 +70,281 @@ The docker stack deploy command doesn't support variable substitution in the .ym ### Docker compose A working docker-compose.yml file is avalable here: +```yaml +version: '3.2' + +services: + + front: + image: mailu/nginx:1.5 + env_file: .env + ports: + - target: 80 + published: 80 + mode: host + - target: 443 + published: 443 + mode: host + - target: 110 + published: 110 + mode: host + - target: 143 + published: 143 + mode: host + - target: 993 + published: 993 + mode: host + - target: 995 + published: 995 + mode: host + - target: 25 + published: 25 + mode: host + - target: 465 + published: 465 + mode: host + - target: 587 + published: 587 + mode: host + volumes: +# - "/mailu/certs:/certs" + - type: volume + source: mailu_certs + target: /certs + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + redis: + image: redis:alpine + restart: always + volumes: +# - "/mailu/redis:/data" + - type: volume + source: mailu_redis + target: /data + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + imap: +# image: mailu/dovecot:$VERSION + image: ofthesun9/dovecot:1.5 + restart: always + env_file: .env + volumes: +# - "$ROOT/data:/data" + - type: volume + source: mailu_data + target: /data +# - "$ROOT/mail:/mail" + - type: volume + source: mailu_mail + target: /mail +# - "$ROOT/overrides:/overrides" + - type: volume + source: mailu_overrides + target: /overrides + depends_on: + - front + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + smtp: + image: ofthesun9/postfix:1.5 + restart: always + env_file: .env + volumes: +# - "$ROOT/data:/data" + - type: volume + source: mailu_data + target: /data +# - "$ROOT/overrides:/overrides" + - type: volume + source: mailu_overrides + target: /overrides + depends_on: + - front + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + antispam: +# image: mailu/rspamd:$VERSION + image: ofthesun9/rspamd:fuzzydev + restart: always + env_file: .env + depends_on: + - front + volumes: +# - "$ROOT/filter:/var/lib/rspamd" + - type: volume + source: mailu_filter + target: /var/lib/rspamd +# - "$ROOT/dkim:/dkim" + - type: volume + source: mailu_dkim + target: /dkim +# - "$ROOT/overrides/rspamd:/etc/rspamd/override.d" + - type: volume + source: mailu_overrides_rspamd + target: /etc/rspamd/override.d + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + antivirus: + image: mailu/none:1.5 + restart: always + env_file: .env + volumes: +# - "/mailu/filter:/data" + - type: volume + source: mailu_filter + target: /data + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + webdav: + image: mailu/none:1.5 + restart: always + env_file: .env + volumes: +# - /mailu/dav:/data" + - type: volume + source: mailu_dav + target: /data + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + admin: + image: ofthesun9/admin:1.5-backports + restart: always + env_file: .env + volumes: +# - "/mailu/data:/data" + - type: volume + source: mailu_data + target: /data +# - "/mailu/dkim:/dkim" + - type: volume + source: mailu_dkim + target: /dkim + - /var/run/docker.sock:/var/run/docker.sock:ro + depends_on: + - redis + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + webmail: + image: "mailu/roundcube:1.5" + restart: always + env_file: .env + volumes: +# - "/mailu/webmail:/data" + - type: volume + source: mailu_data + target: /data + depends_on: + - imap + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + + fetchmail: + image: mailu/fetchmail:1.5 + restart: always + env_file: .env + volumes: +# - "/mailu/data:/data" + - type: volume + source: mailu_data + target: /data + logging: + driver: none + deploy: + endpoint_mode: dnsrr + replicas: 1 + placement: + constraints: [node.role == manager] + +volumes: + mailu_filter: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/filter" + mailu_dkim: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/dkim" + mailu_overrides_rspamd: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/overrides/rspamd" + mailu_data: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/data" + mailu_mail: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/mail" + mailu_overrides: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/overrides" + mailu_dav: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/dav" + mailu_certs: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/certs" + mailu_nginx.conf: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/1.5/nginx.conf.wp" + mailu_tls.conf: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/1.5/tls.conf" + mailu_redis: + driver_opts: + type: "nfs" + o: "addr=192.168.0.30,nolock,soft,rw" + device: ":/mnt/Pool1/pv/mailu/redis" +``` From 8a0ff1153e4eb28be86f60cadadd921eae5259db Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 15:44:43 +0000 Subject: [PATCH 030/144] Documentation to deploy mailu on a docker swarm --- docs/swarm/1.5/README.md | 13 +- .../swarm/1.5/docker-compose-stack-simple.yml | 275 ------------------ 2 files changed, 6 insertions(+), 282 deletions(-) delete mode 100644 docs/swarm/1.5/docker-compose-stack-simple.yml diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index 6793a267..ef8cbb6b 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -59,7 +59,8 @@ core@coreos-01 ~ $ sudo umount /mnt/local/ ### Networking mode On a swarm, the services are available (default mode) through a routing mesh managed by docker itself. With this mode, each service is given a virtual IP adress and docker manages the routing between this virtual IP and the container(s) provinding this service. -With this default networking mode, I cannot get login working properly... As found in https://github.com/Mailu/Mailu/issues/375 , a workaround is to use the dnsrr networking mode at least for the front services +With this default networking mode, I cannot get login working properly... As found in https://github.com/Mailu/Mailu/issues/375 , a workaround is to use the dnsrr networking mode at least for the front services. + The main consequence/limiation will be that the front services will *not* be available on every node, but only on the node where it will be deployed. In my case, I have only one manager and I choose to deploy the front service to the manager node, so I know on wich IP the front service will be available (aka the IP adress of my manager node). ### Variable substitution @@ -133,8 +134,7 @@ services: constraints: [node.role == manager] imap: -# image: mailu/dovecot:$VERSION - image: ofthesun9/dovecot:1.5 + image: mailu/dovecot:1.5 restart: always env_file: .env volumes: @@ -159,7 +159,7 @@ services: constraints: [node.role == manager] smtp: - image: ofthesun9/postfix:1.5 + image: mailu/postfix:1.5 restart: always env_file: .env volumes: @@ -180,8 +180,7 @@ services: constraints: [node.role == manager] antispam: -# image: mailu/rspamd:$VERSION - image: ofthesun9/rspamd:fuzzydev + image: mailu/rspamd:1.5 restart: always env_file: .env depends_on: @@ -236,7 +235,7 @@ services: constraints: [node.role == manager] admin: - image: ofthesun9/admin:1.5-backports + image: mailu/admin:1.5 restart: always env_file: .env volumes: diff --git a/docs/swarm/1.5/docker-compose-stack-simple.yml b/docs/swarm/1.5/docker-compose-stack-simple.yml deleted file mode 100644 index 47ef7cb1..00000000 --- a/docs/swarm/1.5/docker-compose-stack-simple.yml +++ /dev/null @@ -1,275 +0,0 @@ -version: '3.2' - -services: - - front: - image: mailu/nginx:1.5 - env_file: .env - ports: - - target: 80 - published: 80 - mode: host - - target: 443 - published: 443 - mode: host - - target: 110 - published: 110 - mode: host - - target: 143 - published: 143 - mode: host - - target: 993 - published: 993 - mode: host - - target: 995 - published: 995 - mode: host - - target: 25 - published: 25 - mode: host - - target: 465 - published: 465 - mode: host - - target: 587 - published: 587 - mode: host - volumes: -# - "/mailu/certs:/certs" - - type: volume - source: mailu_certs - target: /certs - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - - redis: - image: redis:alpine - restart: always - volumes: -# - "/mailu/redis:/data" - - type: volume - source: mailu_redis - target: /data - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - - imap: -# image: mailu/dovecot:$VERSION - image: ofthesun9/dovecot:1.5 - restart: always - env_file: .env - volumes: -# - "$ROOT/data:/data" - - type: volume - source: mailu_data - target: /data -# - "$ROOT/mail:/mail" - - type: volume - source: mailu_mail - target: /mail -# - "$ROOT/overrides:/overrides" - - type: volume - source: mailu_overrides - target: /overrides - depends_on: - - front - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - - smtp: - image: ofthesun9/postfix:1.5 - restart: always - env_file: .env - volumes: -# - "$ROOT/data:/data" - - type: volume - source: mailu_data - target: /data -# - "$ROOT/overrides:/overrides" - - type: volume - source: mailu_overrides - target: /overrides - depends_on: - - front - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - - antispam: -# image: mailu/rspamd:$VERSION - image: ofthesun9/rspamd:fuzzydev - restart: always - env_file: .env - depends_on: - - front - volumes: -# - "$ROOT/filter:/var/lib/rspamd" - - type: volume - source: mailu_filter - target: /var/lib/rspamd -# - "$ROOT/dkim:/dkim" - - type: volume - source: mailu_dkim - target: /dkim -# - "$ROOT/overrides/rspamd:/etc/rspamd/override.d" - - type: volume - source: mailu_overrides_rspamd - target: /etc/rspamd/override.d - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - - antivirus: - image: mailu/none:1.5 - restart: always - env_file: .env - volumes: -# - "/mailu/filter:/data" - - type: volume - source: mailu_filter - target: /data - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - - webdav: - image: mailu/none:1.5 - restart: always - env_file: .env - volumes: -# - /mailu/dav:/data" - - type: volume - source: mailu_dav - target: /data - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - - admin: - image: ofthesun9/admin:1.5-backports - restart: always - env_file: .env - volumes: -# - "/mailu/data:/data" - - type: volume - source: mailu_data - target: /data -# - "/mailu/dkim:/dkim" - - type: volume - source: mailu_dkim - target: /dkim - - /var/run/docker.sock:/var/run/docker.sock:ro - depends_on: - - redis - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - - webmail: - image: "mailu/roundcube:1.5" - restart: always - env_file: .env - volumes: -# - "/mailu/webmail:/data" - - type: volume - source: mailu_data - target: /data - depends_on: - - imap - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - - fetchmail: - image: mailu/fetchmail:1.5 - restart: always - env_file: .env - volumes: -# - "/mailu/data:/data" - - type: volume - source: mailu_data - target: /data - logging: - driver: none - deploy: - endpoint_mode: dnsrr - replicas: 1 - placement: - constraints: [node.role == manager] - -volumes: - mailu_filter: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/filter" - mailu_dkim: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/dkim" - mailu_overrides_rspamd: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/overrides/rspamd" - mailu_data: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/data" - mailu_mail: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/mail" - mailu_overrides: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/overrides" - mailu_dav: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/dav" - mailu_certs: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/certs" - mailu_nginx.conf: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/1.5/nginx.conf.wp" - mailu_tls.conf: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/1.5/tls.conf" - mailu_redis: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/redis" From 820e5c667bf9ddf226d6d601ab768ffa56ef35ce Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 17:47:10 +0200 Subject: [PATCH 031/144] Update README.md Typo --- docs/swarm/1.5/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index ef8cbb6b..8a890b74 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -61,7 +61,7 @@ core@coreos-01 ~ $ sudo umount /mnt/local/ On a swarm, the services are available (default mode) through a routing mesh managed by docker itself. With this mode, each service is given a virtual IP adress and docker manages the routing between this virtual IP and the container(s) provinding this service. With this default networking mode, I cannot get login working properly... As found in https://github.com/Mailu/Mailu/issues/375 , a workaround is to use the dnsrr networking mode at least for the front services. -The main consequence/limiation will be that the front services will *not* be available on every node, but only on the node where it will be deployed. In my case, I have only one manager and I choose to deploy the front service to the manager node, so I know on wich IP the front service will be available (aka the IP adress of my manager node). +The main consequence/limitation will be that the front services will *not* be available on every node, but only on the node where it will be deployed. In my case, I have only one manager and I choose to deploy the front service to the manager node, so I know on wich IP the front service will be available (aka the IP adress of my manager node). ### Variable substitution The docker stack deploy command doesn't support variable substitution in the .yml file itself (vut we still can use .env file to pass variables to the services). As a consequence we need to adjust the docker-compose file to : From 91300c1c5cf26906e92e933fe324f5174f55a791 Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 17:48:37 +0200 Subject: [PATCH 032/144] Update README.md Typo --- docs/swarm/1.5/README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index 8a890b74..42e3b174 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -331,16 +331,6 @@ volumes: type: "nfs" o: "addr=192.168.0.30,nolock,soft,rw" device: ":/mnt/Pool1/pv/mailu/certs" - mailu_nginx.conf: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/1.5/nginx.conf.wp" - mailu_tls.conf: - driver_opts: - type: "nfs" - o: "addr=192.168.0.30,nolock,soft,rw" - device: ":/mnt/Pool1/pv/mailu/1.5/tls.conf" mailu_redis: driver_opts: type: "nfs" From 27d43384c5227e5e13fa935662721ca720375a4b Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 15:58:53 +0000 Subject: [PATCH 033/144] Documentation to deploy mailu on a docker swarm --- docs/swarm/1.5/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index 42e3b174..026b5d11 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -337,3 +337,28 @@ volumes: o: "addr=192.168.0.30,nolock,soft,rw" device: ":/mnt/Pool1/pv/mailu/redis" ``` + +### Deploy mailu on the docker swarm +Run the following command: +```bash +docker stack deploy -c docker-compose-stack.yml mailu +``` +See how the services are being deployed: +```bash +core@coreos-01 ~ $ docker service ls +ID NAME MODE REPLICAS IMAGE PORTS +ywnsetmtkb1l mailu_antivirus replicated 1/1 mailu/none:1.5 +pqokiaz0q128 mailu_fetchmail replicated 1/1 mailu/fetchmail:1.5 +``` +check a specific service: +```bash +core@coreos-01 ~ $ docker service ps mailu_fetchmail +ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS +tbu8ppgsdffj mailu_fetchmail.1 mailu/fetchmail:1.5 coreos-01 Running Running 11 days ago +``` + +### Remove the stack +Run the follwoing command: +```bash +core@coreos-01 ~ $ docker stack rm mailu +``` From b3131496c6e427a9225ac7d86ca02ef91744a8fe Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 18:00:57 +0200 Subject: [PATCH 034/144] Update README.md --- docs/swarm/1.5/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index 026b5d11..15d79068 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -69,7 +69,7 @@ The docker stack deploy command doesn't support variable substitution in the .ym - change the way we define the volumes (nfs share in our case) ### Docker compose -A working docker-compose.yml file is avalable here: +A working docker-compose-stack.yml file is available here: ```yaml From a6412f3f23bd1d92dfedf2f84ae9145a7b9ff420 Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 18:03:50 +0200 Subject: [PATCH 035/144] Update README.md --- docs/swarm/1.5/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index 15d79068..f9e9dfb8 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -1,4 +1,4 @@ -# Install Mailu master on kubernetes +# Install Mailu on a docker swarm ## Prequisites @@ -6,7 +6,7 @@ You need to have a swarm running -In order to deploy mailu on a swarm, you will first need to initialize it: +In order to deploy Mailu on a swarm, you will first need to initialize it: The main command will be: ```bash docker swarm init --advertise-addr @@ -29,10 +29,10 @@ mzrm9nbdggsfz4sgq6dhs5i6n flying-dutchman Ready Active ``` ### Volume definition -For data persistance (the mailu services might be launched/relaunched on any of the swarm nodes), we need to have mailu data stored in a manner accessible by every manager or worker in the swarm. +For data persistance (the Mailu services might be launched/relaunched on any of the swarm nodes), we need to have Mailu data stored in a manner accessible by every manager or worker in the swarm. Hereafter we will use a NFS share: ```bash -core@coreos-01 ~/git/Mailu/docs $ showmount -e 192.168.0.30 +core@coreos-01 ~ $ showmount -e 192.168.0.30 Export list for 192.168.0.30: /mnt/Pool1/pv 192.168.0.0 ``` @@ -42,7 +42,7 @@ on the nfs server, I am using the following /etc/exports $more /etc/exports /mnt/Pool1/pv -alldirs -mapall=root -network 192.168.0.0 -mask 255.255.255.0 ``` -on the nfs server, I created the mailu directory (in fact I copied a working mailu set-up) +on the nfs server, I created the Mailu directory (in fact I copied a working Mailu set-up) ```bash $mkdir /mnt/Pool1/pv/mailu ``` @@ -338,7 +338,7 @@ volumes: device: ":/mnt/Pool1/pv/mailu/redis" ``` -### Deploy mailu on the docker swarm +### Deploy Mailu on the docker swarm Run the following command: ```bash docker stack deploy -c docker-compose-stack.yml mailu From dc8df569763e09fdaecd1da0c2bcc6fcf2cf4e35 Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 18:08:07 +0200 Subject: [PATCH 036/144] Update README.md Typo --- docs/swarm/1.5/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index f9e9dfb8..da0ecf0b 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -109,7 +109,7 @@ services: published: 587 mode: host volumes: -# - "/mailu/certs:/certs" +# - "$ROOT/certs:/certs" - type: volume source: mailu_certs target: /certs @@ -123,7 +123,7 @@ services: image: redis:alpine restart: always volumes: -# - "/mailu/redis:/data" +# - "$ROOT/redis:/data" - type: volume source: mailu_redis target: /data @@ -209,7 +209,7 @@ services: restart: always env_file: .env volumes: -# - "/mailu/filter:/data" +# - "$ROOT/filter:/data" - type: volume source: mailu_filter target: /data @@ -224,7 +224,7 @@ services: restart: always env_file: .env volumes: -# - /mailu/dav:/data" +# - "$ROOT/dav:/data" - type: volume source: mailu_dav target: /data @@ -239,11 +239,11 @@ services: restart: always env_file: .env volumes: -# - "/mailu/data:/data" +# - "$ROOT/data:/data" - type: volume source: mailu_data target: /data -# - "/mailu/dkim:/dkim" +# - "$ROOT/dkim:/dkim" - type: volume source: mailu_dkim target: /dkim @@ -261,7 +261,7 @@ services: restart: always env_file: .env volumes: -# - "/mailu/webmail:/data" +# - "$ROOT/webmail:/data" - type: volume source: mailu_data target: /data @@ -278,7 +278,7 @@ services: restart: always env_file: .env volumes: -# - "/mailu/data:/data" +# - "$ROOT/data:/data" - type: volume source: mailu_data target: /data From d13725ce337036cf152a893c647db258bb968c8f Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 18:09:18 +0200 Subject: [PATCH 037/144] Update README.md Typo --- docs/swarm/1.5/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index da0ecf0b..8e468f51 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -4,9 +4,7 @@ ### Swarm -You need to have a swarm running - -In order to deploy Mailu on a swarm, you will first need to initialize it: +In order to deploy Mailu on a swarm, you will first need to initialize the swarm: The main command will be: ```bash docker swarm init --advertise-addr From 480fc6c4374daf3359d3c2a240a5c0b8fd6ab0e4 Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 18:12:41 +0200 Subject: [PATCH 038/144] Update README.md Typo --- docs/swarm/1.5/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index 8e468f51..058de519 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -61,13 +61,14 @@ With this default networking mode, I cannot get login working properly... As fou The main consequence/limitation will be that the front services will *not* be available on every node, but only on the node where it will be deployed. In my case, I have only one manager and I choose to deploy the front service to the manager node, so I know on wich IP the front service will be available (aka the IP adress of my manager node). -### Variable substitution -The docker stack deploy command doesn't support variable substitution in the .yml file itself (vut we still can use .env file to pass variables to the services). As a consequence we need to adjust the docker-compose file to : +### Variable substitution and docker-compose.yml +The docker stack deploy command doesn't support variable substitution in the .yml file itself (but we still can use .env file to pass variables to the services). As a consequence we need to adjust the docker-compose file in order to : - remove all variables : $VERSION , $BIND_ADDRESS4 , $BIND_ADDRESS6 , $ANTIVIRUS , $WEBMAIL , etc - change the way we define the volumes (nfs share in our case) +- add a deploy section for every service ### Docker compose -A working docker-compose-stack.yml file is available here: +An example of docker-compose-stack.yml file is available here: ```yaml From 935cd7f706d58e383a11e41039350a2b80607a13 Mon Sep 17 00:00:00 2001 From: ofthesun9 Date: Sat, 4 Aug 2018 18:13:08 +0200 Subject: [PATCH 039/144] Update README.md --- docs/swarm/1.5/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/swarm/1.5/README.md b/docs/swarm/1.5/README.md index 058de519..6b56e642 100644 --- a/docs/swarm/1.5/README.md +++ b/docs/swarm/1.5/README.md @@ -5,6 +5,7 @@ ### Swarm In order to deploy Mailu on a swarm, you will first need to initialize the swarm: + The main command will be: ```bash docker swarm init --advertise-addr From fb62e6b5a206ac45b2ecaaaadd03e15d45bd0543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20S=C3=A4nger?= Date: Sun, 5 Aug 2018 18:59:57 +0200 Subject: [PATCH 040/144] add full-text search support --- core/dovecot/Dockerfile | 2 +- core/dovecot/conf/dovecot.conf | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/dovecot/Dockerfile b/core/dovecot/Dockerfile index f27bbdba..80e3539a 100644 --- a/core/dovecot/Dockerfile +++ b/core/dovecot/Dockerfile @@ -3,7 +3,7 @@ FROM alpine:3.7 RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ && apk add --no-cache \ dovecot dovecot-sqlite dovecot-pigeonhole-plugin dovecot-pigeonhole-plugin-extdata \ - rspamd-client@testing python py-jinja2 + dovecot-fts-lucene rspamd-client@testing python py-jinja2 COPY conf /conf COPY sieve /var/lib/dovecot diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index 94c43901..b7ce1834 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -18,6 +18,20 @@ dict { sieve = sqlite:/etc/dovecot/pigeonhole-sieve.dict } +############### +# Full-text search +############### +mail_plugins = $mail_plugins fts fts_lucene + +plugin { + fts = lucene + + fts_autoindex = yes + fts_autoindex_exclude = \Junk + + fts_lucene = whitespace_chars=@. +} + ############### # Mailboxes ############### From 0bdb2a16bcd1f6c89f9fe7731b819af83331e8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20S=C3=A4nger?= Date: Sun, 5 Aug 2018 19:48:24 +0200 Subject: [PATCH 041/144] add optional Maildir-Compression --- core/dovecot/conf/dovecot.conf | 10 +++++++++- docs/compose/.env | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/dovecot/conf/dovecot.conf b/core/dovecot/conf/dovecot.conf index 94c43901..d6b79410 100644 --- a/core/dovecot/conf/dovecot.conf +++ b/core/dovecot/conf/dovecot.conf @@ -32,7 +32,7 @@ mail_access_groups = mail maildir_stat_dirs = yes mailbox_list_index = yes mail_vsize_bg_after_count = 100 -mail_plugins = $mail_plugins quota quota_clone +mail_plugins = $mail_plugins quota quota_clone zlib namespace inbox { inbox = yes @@ -58,6 +58,14 @@ plugin { quota = count:User quota quota_vsizes = yes quota_clone_dict = redis:host={{ REDIS_ADDRESS }}:port=6379:db=1 + + {% if COMPRESSION in [ 'gz', 'bz2' ] %} + zlib_save = {{ COMPRESSION }} + {% endif %} + + {% if COMPRESSION_LEVEL %} + zlib_save_level = {{ COMPRESSION_LEVEL }} + {% endif %} } ############### diff --git a/docs/compose/.env b/docs/compose/.env index 06038bc8..9477448a 100644 --- a/docs/compose/.env +++ b/docs/compose/.env @@ -87,6 +87,12 @@ WELCOME=false WELCOME_SUBJECT=Welcome to your new email account WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly! +# Maildir Compression +# choose compression-method, default: none (value: bz2, gz) +COMPRESSION= +# change compression-level, default: 6 (value: 1-9) +COMPRESSION_LEVEL= + ################################### # Web settings ################################### From cc8e15748bebd090f8abcc6ca06147843236864d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Wed, 8 Aug 2018 17:54:15 +0300 Subject: [PATCH 042/144] Retry 10 times when resolving fails in start.py scripts --- core/dovecot/start.py | 20 ++++++++++++++++---- core/postfix/start.py | 16 ++++++++++++++-- services/rspamd/start.py | 14 +++++++++++++- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/core/dovecot/start.py b/core/dovecot/start.py index 83f91fab..2de114fd 100755 --- a/core/dovecot/start.py +++ b/core/dovecot/start.py @@ -4,14 +4,26 @@ import jinja2 import os import socket import glob +import time convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) # Actual startup script -os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front")) -os.environ["REDIS_ADDRESS"] = socket.gethostbyname(os.environ.get("REDIS_ADDRESS", "redis")) -if os.environ["WEBMAIL"] != "none": - os.environ["WEBMAIL_ADDRESS"] = socket.gethostbyname(os.environ.get("WEBMAIL_ADDRESS", "webmail")) +i = 0 +t = 10 +while True: + i += 1 + try: + os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front")) + os.environ["REDIS_ADDRESS"] = socket.gethostbyname(os.environ.get("REDIS_ADDRESS", "redis")) + if os.environ["WEBMAIL"] != "none": + os.environ["WEBMAIL_ADDRESS"] = socket.gethostbyname(os.environ.get("WEBMAIL_ADDRESS", "webmail")) + except socket.gaierror as err: + if i >= t: + raise + time.sleep(10) + continue + break for dovecot_file in glob.glob("/conf/*"): convert(dovecot_file, os.path.join("/etc/dovecot", os.path.basename(dovecot_file))) diff --git a/core/postfix/start.py b/core/postfix/start.py index 4dbf2206..f3c6aaca 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -5,11 +5,23 @@ import os import socket import glob import shutil - +import time + convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) # Actual startup script -os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front")) +i = 0 +t = 10 +while True: + i += 1 + try: + os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front")) + except socket.gaierror as err: + if i >= t: + raise + time.sleep(10) + continue + break os.environ["HOST_ANTISPAM"] = os.environ.get("HOST_ANTISPAM", "antispam:11332") os.environ["HOST_LMTP"] = os.environ.get("HOST_LMTP", "imap:2525") diff --git a/services/rspamd/start.py b/services/rspamd/start.py index 87309cee..08301a0d 100755 --- a/services/rspamd/start.py +++ b/services/rspamd/start.py @@ -4,11 +4,23 @@ import jinja2 import os import socket import glob +import time convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) # Actual startup script -os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front")) +i = 0 +t = 10 +while True: + i += 1 + try: + os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front")) + except socket.gaierror as err: + if i >= t: + raise + time.sleep(10) + continue + break if "HOST_REDIS" not in os.environ: os.environ["HOST_REDIS"] = "redis" for rspamd_file in glob.glob("/conf/*"): From 6fc51d879b341c1abf8fd9c18a76282895266480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Sun, 26 Aug 2018 21:08:20 +0300 Subject: [PATCH 043/144] Add docker-compose.yml file for Setup utility --- setup/docker-compose.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 setup/docker-compose.yml diff --git a/setup/docker-compose.yml b/setup/docker-compose.yml new file mode 100644 index 00000000..9288bb7e --- /dev/null +++ b/setup/docker-compose.yml @@ -0,0 +1,13 @@ +# This file is used to run the mailu/setup utility + +version: '2' + +services: + redis: + image: redis:alpine + + setup: + image: mailu/setup + ports: + - "80:80" + From fe7e32dc82eb745a6e79cc06c6a484e09dbb08f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Sun, 26 Aug 2018 21:11:48 +0300 Subject: [PATCH 044/144] Make gunicorn bind to port 80 of any available protocol --- setup/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/Dockerfile b/setup/Dockerfile index 9111ae44..1fc808f1 100644 --- a/setup/Dockerfile +++ b/setup/Dockerfile @@ -15,4 +15,4 @@ RUN python setup.py https://github.com/mailu/mailu /data EXPOSE 80/tcp -CMD gunicorn -w 4 -b 0.0.0.0:80 -b [::]:80 --access-logfile - --error-logfile - --preload main:app +CMD gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload main:app From 89c55ba8fe59b6f629659fa70385ca150b1cd0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20S=C3=A4nger?= Date: Wed, 19 Sep 2018 01:36:22 +0200 Subject: [PATCH 045/144] use safer cipher in roundcube "Default is set for backward compatibility to DES-EDE3-CBC, but you can choose e.g. AES-256-CBC which we consider a better choice." https://github.com/roundcube/roundcubemail/blob/master/config/defaults.inc.php#L512 --- webmails/roundcube/config.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/webmails/roundcube/config.inc.php b/webmails/roundcube/config.inc.php index 603fc95b..35088107 100644 --- a/webmails/roundcube/config.inc.php +++ b/webmails/roundcube/config.inc.php @@ -6,6 +6,7 @@ $config = array(); $config['db_dsnw'] = 'sqlite:////data/roundcube.db'; $config['temp_dir'] = '/tmp/'; $config['des_key'] = getenv('SECRET_KEY'); +$config['cipher_method'] = 'AES-256-CBC'; $config['identities_level'] = 3; $config['reply_all_mode'] = 1; From f5f8d1d84b5661bf984c2af3f0f340973ac23e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Mon, 24 Sep 2018 02:23:07 +0300 Subject: [PATCH 046/144] Test-building using travis-ci --- .travis.yml | 17 ++++++++++------- tests/build.yml | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 tests/build.yml diff --git a/.travis.yml b/.travis.yml index 2ee30837..d5114c0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ -language: python -python: - - "3.6" -install: - - pip install -r docs/requirements.txt +sudo: required +services: docker +addons: + apt: + packages: + - docker-ce +env: + - VERSION=$TRAVIS_BRANCH + script: - - sphinx-versioning build -b -B 1.5 -r 1.5 -w '^[0-9.]*$' -w master -W '^$' docs/ build/ - - python "docs/conf.py" "build" "$DEPLOY_HOST" "$DEPLOY_USERNAME" "$DEPLOY_PASSWORD" "$DEPLOY_REMOTEDIR" +- docker-compose -f tests/build.yml -p Mailu build diff --git a/tests/build.yml b/tests/build.yml new file mode 100644 index 00000000..674abf8c --- /dev/null +++ b/tests/build.yml @@ -0,0 +1,47 @@ +version: '3' + +services: + + front: + image: mailu/nginx:$VERSION + build: ../core/nginx + + imap: + image: mailu/dovecot:$VERSION + build: ../core/dovecot + + smtp: + image: mailu/postfix:$VERSION + build: ../core/postfix + + antispam: + image: mailu/rspamd:$VERSION + build: ../services/rspamd + + antivirus: + image: mailu/clamav:$VERSION + build: ../optional/clamav + + webdav: + image: mailu/radicale:$VERSION + build: ../optional/radicale + + admin: + image: mailu/admin:$VERSION + build: ../core/admin + + roundcube: + image: mailu/roundcube:$VERSION + build: ../webmails/roundcube + + rainloop: + image: mailu/rainloop:$VERSION + build: ../webmails/rainloop + + fetchmail: + image: mailu/fetchmail:$VERSION + build: ../services/fetchmail + + none: + image: mailu/none:$VERSION + build: ../core/none From 73ca5fb3d3f296c43ec2bf8556bcb732a0cfdb0d Mon Sep 17 00:00:00 2001 From: kaiyou Date: Mon, 24 Sep 2018 23:19:06 +0200 Subject: [PATCH 047/144] Provide a more generic skeletton for postfix virtual lookups --- core/admin/mailu/internal/views.py | 20 ++++++++++++++++++++ core/postfix/conf/main.cf | 6 +++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/core/admin/mailu/internal/views.py b/core/admin/mailu/internal/views.py index b5b1da77..2a71e786 100644 --- a/core/admin/mailu/internal/views.py +++ b/core/admin/mailu/internal/views.py @@ -71,6 +71,26 @@ def postfix_alias_map(alias): flask.abort(404) +@internal.route("/postfix/alias/domain/") +def postfix_alias_domain(domain): + pass + + +@internal.route("/postfix/alias/map/") +def postfix_alias_map(alias): + pass + + +@internal.route("/postfix/mailbox/domain/") +def postfix_mailbox_domain(domain): + pass + + +@internal.route("/postfix/mailbox/map/") +def postfix_mailbox_map(domain): + pass + + @internal.route("/dovecot/auth/passdb/") def dovecot_passdb_dict(user_email): user = models.User.query.get(user_email) or flask.abort(403) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 19c2d0c2..eee01f69 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -56,9 +56,9 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache # The alias map actually returns both aliases and local mailboxes, which is # required for reject_unlisted_sender to work properly -virtual_alias_maps = ${podop}alias -virtual_mailbox_domains = ${podop}domains -virtual_mailbox_maps = ${podop}mailbox +virtual_alias_maps = ${podop}alias/domain +virtual_mailbox_domains = ${podop}mailbox/domain +virtual_mailbox_maps = ${podop}mailbox/map # Mails are transported if required, then forwarded to Dovecot for delivery relay_domains = ${podop}transport From a684739b9c481d5e4021a810953f62968f32714f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20S=C3=A4nger?= Date: Tue, 25 Sep 2018 05:59:31 +0200 Subject: [PATCH 048/144] update to PHP 7.2 and remove mcrypt removed mcrypt because Rouncube uses openssl exclusively since version 1.2 and mcrypt was removed from PHP 7.2 --- webmails/roundcube/Dockerfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile index 3f7eee0d..5ff4dcb8 100644 --- a/webmails/roundcube/Dockerfile +++ b/webmails/roundcube/Dockerfile @@ -1,11 +1,10 @@ -FROM php:7.0-apache +FROM php:7.2-apache RUN apt-get update && apt-get install -y \ libfreetype6-dev \ libjpeg62-turbo-dev \ - libmcrypt-dev \ libpng-dev \ - && docker-php-ext-install pdo_mysql mcrypt zip + && docker-php-ext-install pdo_mysql zip ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.7/roundcubemail-1.3.7-complete.tar.gz From 0b885548ab03e135b558e4e23c716b024f193470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20S=C3=A4nger?= Date: Tue, 25 Sep 2018 06:29:53 +0200 Subject: [PATCH 049/144] bind to any protocol --- core/admin/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/start.sh b/core/admin/start.sh index 4f60e39d..8208e4a1 100755 --- a/core/admin/start.sh +++ b/core/admin/start.sh @@ -2,4 +2,4 @@ python manage.py advertise python manage.py db upgrade -gunicorn -w 4 -b 0.0.0.0:80 -b [::]:80 --access-logfile - --error-logfile - --preload mailu:app +gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload mailu:app From cbaac01790b412a89b1da3bc880272e74c762cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20S=C3=A4nger?= Date: Tue, 25 Sep 2018 07:38:49 +0200 Subject: [PATCH 050/144] remove unused dependencies --- webmails/roundcube/Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile index 5ff4dcb8..99567502 100644 --- a/webmails/roundcube/Dockerfile +++ b/webmails/roundcube/Dockerfile @@ -1,10 +1,8 @@ FROM php:7.2-apache RUN apt-get update && apt-get install -y \ - libfreetype6-dev \ - libjpeg62-turbo-dev \ - libpng-dev \ - && docker-php-ext-install pdo_mysql zip + zlib1g-dev \ + && docker-php-ext-install zip ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.7/roundcubemail-1.3.7-complete.tar.gz From 5341ee4472e0f9067b5391d918dda38f77e99c46 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Tue, 25 Sep 2018 21:04:30 +0200 Subject: [PATCH 051/144] Add a Dockerfile for buliding the docs --- docs/Dockerfile | 13 +++++++++++++ docs/requirements.txt | 1 - 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 docs/Dockerfile diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..a850a664 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3-alpine + +RUN apk add --no-cache git + +COPY docs/requirements.txt requirements.txt +RUN pip install -r /requirements.txt \ + && mkdir /src + +WORKDIR /src +COPY .git /src/.git + +RUN sphinx-versioning build -b -B 1.5 -r 1.5 -w '^[0-9.]*$' -w master -W '^$' /src /build + diff --git a/docs/requirements.txt b/docs/requirements.txt index 2572817f..98338e63 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,4 +3,3 @@ Sphinx sphinx-autobuild sphinx-rtd-theme sphinxcontrib-versioning -paramiko From 72cfadd5e8d722b0a4b7e039c6f095c10241fb69 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Tue, 25 Sep 2018 21:08:04 +0200 Subject: [PATCH 052/144] Build the docs during tests --- tests/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/build.yml b/tests/build.yml index 674abf8c..308b821a 100644 --- a/tests/build.yml +++ b/tests/build.yml @@ -45,3 +45,9 @@ services: none: image: mailu/none:$VERSION build: ../core/none + + docs: + image: mailu/docs:$VERSION + build: + context: ../ + dockerfile: ../docs/Dockerfile From 39cd0d5034af33108223db4be4788f860d835094 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Wed, 26 Sep 2018 00:14:30 +0200 Subject: [PATCH 053/144] Upgrade to alpine 3.8 for smtp and imap --- core/dovecot/Dockerfile | 4 ++-- core/postfix/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/dovecot/Dockerfile b/core/dovecot/Dockerfile index b15b83c3..670acc9b 100644 --- a/core/dovecot/Dockerfile +++ b/core/dovecot/Dockerfile @@ -1,7 +1,7 @@ -FROM alpine:3.7 +FROM alpine:3.8 RUN apk add --no-cache \ - dovecot dovecot-pop3d dovecot-lmtpd dovecot-pigeonhole-plugin dovecot-fts-lucene rspamd-client \ + dovecot dovecot-pigeonhole-plugin dovecot-fts-lucene rspamd-client \ python3 py3-pip \ && pip3 install jinja2 podop diff --git a/core/postfix/Dockerfile b/core/postfix/Dockerfile index e7501a54..30d1103e 100644 --- a/core/postfix/Dockerfile +++ b/core/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.7 +FROM alpine:3.8 RUN apk add --no-cache postfix postfix-pcre rsyslog \ python3 py3-pip \ From 7143fb8c47d5e70775e868de99f6852e571ec60f Mon Sep 17 00:00:00 2001 From: kaiyou Date: Wed, 26 Sep 2018 00:14:46 +0200 Subject: [PATCH 054/144] Implement some basic views for podop --- core/admin/mailu/internal/views.py | 42 ++++++++++++++++-------------- core/admin/mailu/models.py | 8 +++--- core/postfix/start.py | 2 +- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/core/admin/mailu/internal/views.py b/core/admin/mailu/internal/views.py index 2a71e786..fff9b481 100644 --- a/core/admin/mailu/internal/views.py +++ b/core/admin/mailu/internal/views.py @@ -52,6 +52,18 @@ def basic_authentication(): return response +@internal.route("/postfix/domain/") +def postfix_mailbox_domain(domain_name): + domain = models.Domain.query.get(domain_name) or flask.abort(404) + return flask.jsonify(domain.name) + + +@internal.route("/postfix/mailbox/") +def postfix_mailbox_map(email): + user = models.User.query.get(email) or flask.abort(404) + return flask.jsonify(user.email) + + @internal.route("/postfix/alias/") def postfix_alias_map(alias): localpart, domain = alias.split('@', 1) if '@' in alias else (None, alias) @@ -60,35 +72,25 @@ def postfix_alias_map(alias): domain = alternative.domain_name email = '{}@{}'.format(localpart, domain) if localpart is None: - return domain + return flask.jsonify(domain) else: alias_obj = models.Alias.resolve(localpart, domain) if alias_obj: - return alias_obj.destination + return flask.jsonify(alias_obj.destination) user_obj = models.User.query.get(email) if user_obj: - return user_obj.destination - flask.abort(404) + return flask.jsonify(user_obj.destination) + return flask.abort(404) -@internal.route("/postfix/alias/domain/") -def postfix_alias_domain(domain): - pass +@internal.route("/postfix/spoofed/") +def postfix_spoofed(email): + return flask.abort(404) -@internal.route("/postfix/alias/map/") -def postfix_alias_map(alias): - pass - - -@internal.route("/postfix/mailbox/domain/") -def postfix_mailbox_domain(domain): - pass - - -@internal.route("/postfix/mailbox/map/") -def postfix_mailbox_map(domain): - pass +@internal.route("/postfix/transport/") +def postfix_transport(email): + return flask.abort(404) @internal.route("/dovecot/auth/passdb/") diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 7e13682b..ec94f989 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -268,7 +268,7 @@ class User(Base, Email): @property def destination(self): - if self.foward_enabled: + if self.forward_enabled: result = self.self.forward_destination if self.forward_keep: result += ',' + self.email @@ -339,13 +339,13 @@ class Alias(Base, Email): @classmethod def resolve(cls, localpart, domain_name): return cls.query.filter( - sqlalchemy._and(cls.domain_name == domain_name, - sqlalchemy._or( + sqlalchemy.and_(cls.domain_name == domain_name, + sqlalchemy.or_( cls.localpart == localpart, cls.wildcard.like(localpart) ) ) - ) + ).first() class Token(Base): diff --git a/core/postfix/start.py b/core/postfix/start.py index 38905224..a4af9328 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -15,7 +15,7 @@ def start_podop(): run_server(40, "postfix", "/tmp/podop.socket", [ ("transport", "url", "http://admin/internal/postfix/transport/§"), ("alias", "url", "http://admin/internal/postfix/alias/§"), - ("domains", "url", "http://admin/internal/postfix/domains/§"), + ("domain", "url", "http://admin/internal/postfix/domain/§"), ("mailbox", "url", "http://admin/internal/postfix/mailbox/§"), ("spoofed", "url", "http://admin/internal/postfix/spoofed/§"), ]) From 697caaab81058cfa012117d18334000eed5b9051 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Wed, 26 Sep 2018 00:15:24 +0200 Subject: [PATCH 055/144] Update podop access and mail restrictions --- core/postfix/conf/main.cf | 20 +++++++++++++------- core/postfix/conf/master.cf | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index eee01f69..1306457a 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -56,13 +56,14 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache # The alias map actually returns both aliases and local mailboxes, which is # required for reject_unlisted_sender to work properly -virtual_alias_maps = ${podop}alias/domain -virtual_mailbox_domains = ${podop}mailbox/domain -virtual_mailbox_maps = ${podop}mailbox/map +virtual_alias_domains = +virtual_alias_maps = ${podop}alias +virtual_mailbox_domains = ${podop}domain +virtual_mailbox_maps = ${podop}mailbox # Mails are transported if required, then forwarded to Dovecot for delivery -relay_domains = ${podop}transport -transport_maps = ${podop}transport +# relay_domains = ${podop}transport +# transport_maps = ${podop}transport virtual_transport = lmtp:inet:{{ HOST_LMTP }} # In order to prevent Postfix from running DNS query, enforce the use of the @@ -82,15 +83,20 @@ smtpd_sender_login_maps = $virtual_alias_maps # Restrictions for incoming SMTP, other restrictions are applied in master.cf smtpd_helo_required = yes -smtpd_recipient_restrictions = +smtpd_client_restrictions = permit_mynetworks, - check_sender_access ${podop}spoofed + reject_unauth_destination, reject_non_fqdn_sender, reject_unknown_sender_domain, reject_unknown_recipient_domain, reject_unverified_recipient, permit +smtpd_relay_restrictions = + permit_mynetworks, + permit_sasl_authenticated, + reject + unverified_recipient_reject_reason = Address lookup failure ############### diff --git a/core/postfix/conf/master.cf b/core/postfix/conf/master.cf index cbcc5e56..661a02f1 100644 --- a/core/postfix/conf/master.cf +++ b/core/postfix/conf/master.cf @@ -7,7 +7,7 @@ smtp inet n - n - - smtpd # Internal SMTP service 10025 inet n - n - - smtpd -o smtpd_sasl_auth_enable=yes - -o smtpd_recipient_restrictions=reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit + -o smtpd_client_restrictions=reject_unlisted_sender,reject_unauth_destination,reject_authenticated_sender_login_mismatch,permit -o cleanup_service_name=outclean outclean unix n - n - 0 cleanup -o header_checks=pcre:/etc/postfix/outclean_header_filter.cf From 82140baa69ff88c329ad500224d6dfa605867536 Mon Sep 17 00:00:00 2001 From: kaiyou Date: Thu, 27 Sep 2018 07:52:43 +0200 Subject: [PATCH 056/144] Add a very simple test script for sending mails --- tests/smtp.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/smtp.py diff --git a/tests/smtp.py b/tests/smtp.py new file mode 100644 index 00000000..cfe09583 --- /dev/null +++ b/tests/smtp.py @@ -0,0 +1,17 @@ +import smtplib +import sys +from email import mime + +from email.mime.image import MIMEImage +from email.mime.multipart import MIMEMultipart + +msg = mime.multipart.MIMEMultipart() +msg['Subject'] = 'Test email' +msg['From'] = sys.argv[1] +msg['To'] = sys.argv[2] +msg.preamble = 'Test email' + +s = smtplib.SMTP('localhost') +s.set_debuglevel(1) +s.send_message(msg) +s.quit() From d8365bfbcf35b56920428cd8bc6ed830716d2cdb Mon Sep 17 00:00:00 2001 From: kaiyou Date: Thu, 27 Sep 2018 07:55:54 +0200 Subject: [PATCH 057/144] Use simpler routes for Dovecot --- core/admin/mailu/internal/views.py | 7 ++++--- core/dovecot/start.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/admin/mailu/internal/views.py b/core/admin/mailu/internal/views.py index fff9b481..78d1528a 100644 --- a/core/admin/mailu/internal/views.py +++ b/core/admin/mailu/internal/views.py @@ -93,7 +93,7 @@ def postfix_transport(email): return flask.abort(404) -@internal.route("/dovecot/auth/passdb/") +@internal.route("/dovecot/passdb/") def dovecot_passdb_dict(user_email): user = models.User.query.get(user_email) or flask.abort(403) return flask.jsonify({ @@ -101,7 +101,7 @@ def dovecot_passdb_dict(user_email): }) -@internal.route("/dovecot/auth/userdb/") +@internal.route("/dovecot/userdb/") def dovecot_userdb_dict(user_email): user = models.User.query.get(user_email) or flask.abort(403) return flask.jsonify({ @@ -109,10 +109,11 @@ def dovecot_userdb_dict(user_email): }) -@internal.route("/dovecot/quota/quota//", methods=["POST"]) +@internal.route("/dovecot/quota//", methods=["POST"]) def dovecot_quota(ns, user_email): user = models.User.query.get(user_email) or flask.abort(403) if ns == "storage": user.quota_bytes_used = flask.request.get_json() db.session.commit() return flask.jsonify(None) + diff --git a/core/dovecot/start.py b/core/dovecot/start.py index 48e9377c..d2163e56 100755 --- a/core/dovecot/start.py +++ b/core/dovecot/start.py @@ -12,9 +12,9 @@ from podop import run_server def start_podop(): os.setuid(8) run_server(40, "dovecot", "/tmp/podop.socket", [ - ("quota", "url", "http://admin/internal/dovecot/quota/§"), - ("auth", "url", "http://admin/internal/dovecot/auth/§"), - ("sieve", "url", "http://admin/internal/dovecot/sieve/§"), + ("quota", "url", "http://admin/internal/dovecot/§"), + ("auth", "url", "http://admin/internal/dovecot/§"), + ("sieve", "url", "http://admin/internal/dovecot/§"), ]) convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) From cfeaa189f96d357d82928658c86001cc3923236a Mon Sep 17 00:00:00 2001 From: kaiyou Date: Thu, 27 Sep 2018 08:00:31 +0200 Subject: [PATCH 058/144] Use proper 404 return codes for missing objects --- core/admin/mailu/internal/views.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/core/admin/mailu/internal/views.py b/core/admin/mailu/internal/views.py index 78d1528a..83cd4718 100644 --- a/core/admin/mailu/internal/views.py +++ b/core/admin/mailu/internal/views.py @@ -95,7 +95,7 @@ def postfix_transport(email): @internal.route("/dovecot/passdb/") def dovecot_passdb_dict(user_email): - user = models.User.query.get(user_email) or flask.abort(403) + user = models.User.query.get(user_email) or flask.abort(404) return flask.jsonify({ "password": user.password, }) @@ -103,7 +103,7 @@ def dovecot_passdb_dict(user_email): @internal.route("/dovecot/userdb/") def dovecot_userdb_dict(user_email): - user = models.User.query.get(user_email) or flask.abort(403) + user = models.User.query.get(user_email) or flask.abort(404) return flask.jsonify({ "quota_rule": "*:bytes={}".format(user.quota_bytes) }) @@ -111,9 +111,18 @@ def dovecot_userdb_dict(user_email): @internal.route("/dovecot/quota//", methods=["POST"]) def dovecot_quota(ns, user_email): - user = models.User.query.get(user_email) or flask.abort(403) + user = models.User.query.get(user_email) or flask.abort(404) if ns == "storage": user.quota_bytes_used = flask.request.get_json() db.session.commit() return flask.jsonify(None) + +@internal.route("/dovecot/sieve/name/