Merge branch 'master' into refactor-config

master
kaiyou 6 years ago
commit 5b769e23da

@ -1,17 +1,21 @@
FROM python:3-alpine
FROM alpine:3.8
# python3 shared with most images
RUN apk add --no-cache \
python3 py3-pip \
&& pip3 install --upgrade pip
# Image specific layers under this line
RUN mkdir -p /app
WORKDIR /app
COPY requirements-prod.txt requirements.txt
RUN apk add --no-cache openssl \
&& apk add --no-cache --virtual build-dep openssl-dev libffi-dev python-dev build-base \
&& pip install -r requirements.txt \
RUN apk add --no-cache openssl curl \
&& apk add --no-cache --virtual build-dep openssl-dev libffi-dev python3-dev build-base \
&& pip3 install -r requirements.txt \
&& apk del --no-cache build-dep
COPY mailu ./mailu
COPY migrations ./migrations
COPY start.sh /start.sh
COPY start.py /start.py
RUN pybabel compile -d mailu/translations
@ -19,4 +23,6 @@ EXPOSE 80/tcp
VOLUME ["/data"]
ENV FLASK_APP mailu
CMD ["/start.sh"]
CMD /start.py
HEALTHCHECK CMD curl -f -L http://localhost/ui || exit 1

@ -54,3 +54,4 @@ def create_app():
"""
config = configuration.ConfigManager()
return create_app_from_config(config)

@ -32,9 +32,6 @@ if exists "X-Virus" {
stop;
}
{% if user.reply_enabled %}
if currentdate :value "le" "date" "{{ user.reply_enddate }}"
{
vacation :days 1 :subject "{{ user.reply_subject }}" "{{ user.reply_body }}";
}
{% if user.reply_active %}
vacation :days 1 :subject "{{ user.reply_subject }}" "{{ user.reply_body }}";
{% endif %}

@ -3,13 +3,24 @@ from mailu.internal import internal
from flask import current_app as app
import flask
import socket
import os
@internal.route("/dovecot/passdb/<user_email>")
def dovecot_passdb_dict(user_email):
user = models.User.query.get(user_email) or flask.abort(404)
allow_nets = []
allow_nets.append(
app.config.get("POD_ADDRESS_RANGE") or
socket.gethostbyname(app.config["HOST_FRONT"])
)
if os.environ["WEBMAIL"] != "none":
allow_nets.append(socket.gethostbyname(app.config["HOST_WEBMAIL"]))
print(allow_nets)
return flask.jsonify({
"password": user.password,
"password": None,
"nopassword": "Y",
"allow_nets": ",".join(allow_nets)
})

@ -40,11 +40,14 @@ class IdnaEmail(db.TypeDecorator):
impl = db.String(255, collation="NOCASE")
def process_bind_param(self, value, dialect):
localpart, domain_name = value.split('@')
return "{0}@{1}".format(
localpart,
idna.encode(domain_name).decode('ascii'),
)
try:
localpart, domain_name = value.split('@')
return "{0}@{1}".format(
localpart,
idna.encode(domain_name).decode('ascii'),
)
except ValueError:
pass
def process_result_value(self, value, dialect):
localpart, domain_name = value.split('@')
@ -276,6 +279,8 @@ class User(Base, Email):
reply_enabled = db.Column(db.Boolean(), nullable=False, default=False)
reply_subject = db.Column(db.String(255), nullable=True, default=None)
reply_body = db.Column(db.Text(), nullable=True, default=None)
reply_startdate = db.Column(db.Date, nullable=False,
default=date(1900, 1, 1))
reply_enddate = db.Column(db.Date, nullable=False,
default=date(2999, 12, 31))
@ -295,14 +300,24 @@ class User(Base, Email):
@property
def destination(self):
if self.forward_enabled:
result = self.self.forward_destination
result = self.forward_destination
if self.forward_keep:
result += ',' + self.email
return result
else:
return self.email
scheme_dict = {'BLF-CRYPT': "bcrypt",
@property
def reply_active(self):
now = date.today()
return (
self.reply_enabled and
self.reply_startdate < now and
self.reply_enddate > now
)
scheme_dict = {'PBKDF2': "pbkdf2_sha512",
'BLF-CRYPT': "bcrypt",
'SHA512-CRYPT': "sha512_crypt",
'SHA256-CRYPT': "sha256_crypt",
'MD5-CRYPT': "md5_crypt",
@ -315,8 +330,14 @@ class User(Base, Email):
)
def check_password(self, password):
context = User.pw_context
reference = re.match('({[^}]+})?(.*)', self.password).group(2)
return self.get_password_context().verify(password, reference)
result = context.verify(password, reference)
if result and context.identify(reference) != context.default_scheme():
self.set_password(password)
db.session.add(self)
db.session.commit()
return result
def set_password(self, password, hash_scheme=None, raw=False):
"""Set password for user with specified encryption scheme

@ -117,6 +117,7 @@ class UserReplyForm(flask_wtf.FlaskForm):
reply_subject = fields.StringField(_('Reply subject'))
reply_body = fields.StringField(_('Reply body'),
widget=widgets.TextArea())
reply_startdate = fields.html5.DateField(_('Start of vacation'))
reply_enddate = fields.html5.DateField(_('End of vacation'))
submit = fields.SubmitField(_('Update'))

@ -13,14 +13,17 @@
<form class="form" method="post" role="form">
{{ form.hidden_tag() }}
{{ macros.form_field(form.reply_enabled,
onchange="if(this.checked){$('#reply_subject,#reply_body,#reply_enddate').removeAttr('readonly')}
onchange="if(this.checked){$('#reply_subject,#reply_body,#reply_enddate,#reply_startdate').removeAttr('readonly')}
else{$('#reply_subject,#reply_body,#reply_enddate').attr('readonly', '')}") }}
{{ macros.form_field(form.reply_subject,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.reply_body, rows=10,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.reply_enddate,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.reply_startdate,
**{("rw" if user.reply_enabled else "readonly"): ""}) }}
{{ macros.form_field(form.submit) }}
</form>
{% endcall %}

@ -0,0 +1,24 @@
""" Add a start day for vacations
Revision ID: 3b281286c7bd
Revises: 25fd6c7bcb4a
Create Date: 2018-09-27 22:20:08.158553
"""
revision = '3b281286c7bd'
down_revision = '25fd6c7bcb4a'
from alembic import op
import sqlalchemy as sa
def upgrade():
with op.batch_alter_table('user') as batch:
batch.add_column(sa.Column('reply_startdate', sa.Date(), nullable=False,
server_default="1900-01-01"))
def downgrade():
with op.batch_alter_table('user') as batch:
batch.drop_column('reply_startdate')

@ -0,0 +1,7 @@
#!/usr/bin/python3
import os
os.system("flask mailu advertise")
os.system("flask db upgrade")
os.system("gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload 'mailu:create_app()'")

@ -1,6 +0,0 @@
#!/bin/sh
flask mailu advertise
flask db upgrade
gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload "$FLASK_APP:create_app()"

@ -1,10 +1,16 @@
FROM alpine:3.8
# python3 shared with most images
RUN apk add --no-cache \
dovecot dovecot-pigeonhole-plugin dovecot-fts-lucene rspamd-client \
python3 py3-pip \
&& pip3 install --upgrade pip \
&& pip3 install jinja2 podop tenacity
python3 py3-pip \
&& pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2
# Shared layer between rspamd, postfix, dovecot
RUN pip3 install tenacity
# Image specific layers under this line
RUN apk add --no-cache \
dovecot dovecot-pigeonhole-plugin dovecot-fts-lucene rspamd-client bash \
&& pip3 install podop
COPY conf /conf
COPY start.py /start.py
@ -13,3 +19,5 @@ EXPOSE 110/tcp 143/tcp 993/tcp 4190/tcp 2525/tcp
VOLUME ["/data", "/mail"]
CMD /start.py
HEALTHCHECK --start-period=350s CMD echo QUIT|nc localhost 110|grep "Dovecot ready."

@ -0,0 +1,4 @@
#!/bin/bash
tee >(rspamc -h antispam:11334 -P mailu learn_ham /dev/stdin) \
| rspamc -h antispam:11334 -P mailu -f 13 fuzzy_add /dev/stdin

@ -1,3 +0,0 @@
#!/bin/sh
rspamc -h antispam:11334 -P mailu "learn_$1" /dev/stdin <&0

@ -0,0 +1,4 @@
#!/bin/bash
tee >(rspamc -h antispam:11334 -P mailu learn_spam /dev/stdin) \
>(rspamc -h antispam:11334 -P mailu -f 11 fuzzy_add /dev/stdin)

@ -136,7 +136,8 @@ service managesieve {
}
plugin {
sieve = dict:proxy:/tmp/podop.socket:sieve
sieve = file:~/sieve;active=~/.dovecot.sieve
sieve_before = dict:proxy:/tmp/podop.socket:sieve
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve_extensions = +spamtest +spamtestplus +editheader
sieve_global_extensions = +vnd.dovecot.execute

@ -8,4 +8,4 @@ if string "${mailbox}" "Trash" {
stop;
}
execute :pipe "mailtrain" "ham";
execute :pipe "ham";

@ -1,3 +1,3 @@
require "vnd.dovecot.execute";
execute :pipe "mailtrain" "spam";
execute :pipe "spam";

@ -21,20 +21,17 @@ def start_podop():
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
@retry(stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
def resolve():
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"))
# Actual startup script
resolve()
resolve = retry(socket.gethostbyname, stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
os.environ["REDIS_ADDRESS"] = resolve(os.environ.get("REDIS_ADDRESS", "redis"))
if os.environ["WEBMAIL"] != "none":
os.environ["WEBMAIL_ADDRESS"] = resolve(os.environ.get("WEBMAIL_ADDRESS", "webmail"))
for dovecot_file in glob.glob("/conf/*.conf"):
convert(dovecot_file, os.path.join("/etc/dovecot", os.path.basename(dovecot_file)))
# Run Podop, then postfix
multiprocessing.Process(target=start_podop).start()
os.system("chown -R mail:mail /mail /var/lib/dovecot")
os.system("chown -R mail:mail /mail /var/lib/dovecot /conf")
os.execv("/usr/sbin/dovecot", ["dovecot", "-c", "/etc/dovecot/dovecot.conf", "-F"])

@ -1,9 +1,13 @@
FROM alpine:3.8
RUN apk add --no-cache certbot nginx nginx-mod-mail openssl \
python py-jinja2 py-requests-toolbelt py-pip \
&& pip install --upgrade pip \
&& pip install idna
# python3 shared with most images
RUN apk add --no-cache \
python3 py3-pip \
&& pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2
# Image specific layers under this line
RUN apk add --no-cache certbot nginx nginx-mod-mail openssl curl \
&& pip3 install idna requests
COPY conf /conf
COPY *.py /
@ -12,3 +16,5 @@ EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 100
VOLUME ["/certs"]
CMD /start.py
HEALTHCHECK CMD curl -k -f -L http://localhost/health || exit 1

@ -34,6 +34,8 @@ http {
'' $scheme;
}
# Disable the main http server when on kubernetes (port 80 and 443)
{% if KUBERNETES_INGRESS != 'true' %}
# Main HTTP server
server {
# Variables for proxifying
@ -48,8 +50,8 @@ http {
# Only enable HTTPS if TLS is enabled with no error
{% if TLS and not TLS_ERROR %}
listen 443 ssl;
listen [::]:443 ssl;
listen 443 ssl http2;
listen [::]:443 ssl http2;
include /etc/nginx/tls.conf;
ssl_session_cache shared:SSLHTTP:50m;
@ -91,8 +93,10 @@ http {
{% endif %}
location {{ WEB_WEBMAIL }} {
{% if WEB_WEBMAIL != '/' %}
rewrite ^({{ WEB_WEBMAIL }})$ $1/ permanent;
rewrite ^{{ WEB_WEBMAIL }}/(.*) /$1 break;
{% endif %}
include /etc/nginx/proxy.conf;
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
proxy_pass http://$webmail;
@ -146,7 +150,12 @@ http {
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
location /health {
return 204;
}
}
{% endif %}
# Forwarding authentication server
server {

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
import jinja2
import os

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
import os
import time

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
import os
import subprocess

@ -1,9 +1,16 @@
FROM alpine:3.8
# python3 shared with most images
RUN apk add --no-cache \
python3 py3-pip \
&& pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2
# Shared layer between rspamd, postfix, dovecot
RUN pip3 install tenacity
# Image specific layers under this line
RUN apk add --no-cache postfix postfix-pcre rsyslog \
python3 py3-pip \
&& pip3 install --upgrade pip \
&& pip3 install jinja2 podop tenacity
&& pip3 install podop
COPY conf /conf
COPY start.py /start.py
@ -12,3 +19,5 @@ EXPOSE 25/tcp 10025/tcp
VOLUME ["/data"]
CMD /start.py
HEALTHCHECK --start-period=350s CMD echo QUIT|nc localhost 25|grep "220 .* ESMTP Postfix"

@ -32,7 +32,7 @@ relayhost = {{ RELAYHOST }}
recipient_delimiter = {{ RECIPIENT_DELIMITER }}
# Only the front server is allowed to perform xclient
smtpd_authorized_xclient_hosts={{ FRONT_ADDRESS }}
smtpd_authorized_xclient_hosts={{ FRONT_ADDRESS }} {{ POD_ADDRESS_RANGE }}
###############
# TLS

@ -24,12 +24,10 @@ def start_podop():
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
@retry(stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
def resolve():
os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
# Actual startup script
resolve()
resolve = retry(socket.gethostbyname, stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
os.environ["HOST_ANTISPAM"] = os.environ.get("HOST_ANTISPAM", "antispam:11332")
os.environ["HOST_LMTP"] = os.environ.get("HOST_LMTP", "imap:2525")

@ -2,13 +2,21 @@ FROM python:3-alpine
COPY requirements.txt /requirements.txt
ARG version=master
ENV VERSION=$version
RUN pip install -r /requirements.txt \
&& apk add --no-cache nginx \
&& apk add --no-cache nginx curl \
&& mkdir /run/nginx
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY . /docs
RUN sphinx-build /docs /build
RUN mkdir -p /build/$VERSION \
&& sphinx-build /docs /build/$VERSION
CMD nginx -g "daemon off;"
EXPOSE 80/tcp
CMD nginx -g "daemon off;"
HEALTHCHECK CMD curl -f -L http://localhost/ || exit 1

@ -1,2 +1,9 @@
{% set version=github_version %}
{% extends "!layout.html" %}
{% block document %}
{% if version != stable_version %}
<div class="wy-alert info">
<p>You are currently browsing documentation for the <b>{{ version }}</b> branch. Documentation for the stable <b>{{ stable_version }}</b> branch can be found <a href="/{{ stable_version }}/">here</a>.</p>
</div>
{% endif %}
{{ super() }}
{% endblock %}

@ -1,4 +0,0 @@
{%- extends "layout.html" %}
{% block body %}
{{ body|replace("VERSION_TAG", version) }}
{% endblock %}

@ -0,0 +1,16 @@
<div class="rst-versions" data-toggle="rst-versions" role="note" aria-label="versions">
<span class="rst-current-version" data-toggle="rst-current-version">
<span class="fa fa-book"> Versions</span>
v: {{ version }}
<span class="fa fa-caret-down"></span>
</span>
<div class="rst-other-versions">
<dl>
<dt>{{ _('Versions') }}</dt>
{% for slug, url in versions %}
<dd><a href="{{ url }}">{{ slug }}</a></dd>
{% endfor %}
</dl>
</div>
</div>

@ -39,7 +39,7 @@ primary difference with simple `user` command is that password is being imported
.. code-block:: bash
docker-compose exec admin flask mailu user --hash_scheme='SHA512-CRYPT' myuser example.net '$6$51ebe0cb9f1dab48effa2a0ad8660cb489b445936b9ffd812a0b8f46bca66dd549fea530ce'
docker-compose run --rm admin python manage.py user --hash_scheme='SHA512-CRYPT' myuser example.net '$6$51ebe0cb9f1dab48effa2a0ad8660cb489b445936b9ffd812a0b8f46bca66dd549fea530ce'
user_delete
------------

@ -130,8 +130,8 @@ LOG_DRIVER=json-file
COMPOSE_PROJECT_NAME=mailu
# Default password scheme used for newly created accounts and changed passwords
# (value: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME=BLF-CRYPT
# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT)
PASSWORD_SCHEME=PBKDF2
# Header to take the real ip from
REAL_IP_HEADER=

@ -2,6 +2,8 @@
# -*- coding: utf-8 -*-
#
import os
extensions = ['sphinx.ext.imgmath', 'sphinx.ext.viewcode']
templates_path = ['_templates']
source_suffix = '.rst'
@ -9,9 +11,9 @@ master_doc = 'index'
project = 'Mailu'
copyright = '2018, Mailu authors'
author = 'Mailu authors'
version = release = 'latest'
version = release = os.environ.get('VERSION', 'master')
language = None
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'Dockerfile', 'docker-compose.yml']
pygments_style = 'sphinx'
todo_include_todos = False
html_theme = 'sphinx_rtd_theme'
@ -33,6 +35,11 @@ html_context = {
'display_github': True,
'github_user': 'mailu',
'github_repo': 'mailu',
'github_version': 'master',
'github_version': version,
'stable_version': '1.5',
'versions': [
('1.5', '/1.5/'),
('master', '/master/')
],
'conf_py_path': '/docs/'
}

@ -1,20 +1,117 @@
Development environment
=======================
Git
---
Before any partaking in development, you will need to fork the Mailu repository on GitHub.
For this you will need a `GitHub`_ account. GitHub has excellent documentation on:
#. How to `fork a repo`_ and set upstream (Mailu);
#. Keeping your fork `synced`_;
#. Sending a `pull request`_.
Working on Mailu usually requires you to clone (download) your fork to your work station and
create a branch. From here you can work on Mailu. When done, create a commit and push the
branch to your GitHub repository. Then, on GitHub you can create a "pull request".
Please make sure you have read the :ref:`git_workflow` section of the *Development guidelines*
before submitting any pull requests.
.. note:: It is strongly advised to **never** modify the ``master`` branch of your fork.
This will make it impossible to sync your fork with upstream and creating new (and clean)
branches! This includes never merging other branches from yourself or other users into your
``master``. If you want to do that, create a separate branch for it.
Short work flow example
```````````````````````
.. code-block:: bash
git clone https://github.com/<YOUR_USERNAME>/Mailu.git
cd Mailu
git add remote upstream https://github.com/Mailu/Mailu.git
git checkout -b fix-something master
Work on the code as desired. Before doing a commit, you should at least build
and run the containers. Keep reading this guide for more information. After this,
continue to commit and send a PR.
.. code-block:: bash
git commit -a
#Enter commit message in editor, save and close.
git push --set-upstream origin fix-something
Now you can go to your GitHub page, select the new branch and "send pull request".
Updating your fork
``````````````````
The Mailu ``master`` branch is an ever evolving target. It is important that newly
created branches originate from the latest ``upstream/master``. In order to do so, you will
need to `sync your fork`__:
.. code-block:: bash
git fetch --all
git checkout master
git merge upstream/master
If you kept your master branch clean, this should fast-forward it to the latest upstream version.
Likewise, if you worked on your branch for a longer amount of time, it is advised to merge the
latest ``upstream/master`` into the branch.
.. code-block:: bash
git checkout my-old-branch
git merge upstream/master
Now, git won't fast forward but write a merge commit. Typically you can accept the commit message
presented. Read the output if there are any merge conflicts. In ``git status`` you can find the files
that need editing to have the desired contents. Also, it will tell you how to mark them as resolved.
Optionally, you can ``git push`` after any of above merges to propagate them to GitHub.
__ `synced`_
Bad habits
```````````
Some bad habits from users that we are sometimes confronted with. Please refrain yourself from:
- ``git reset REF`` and ``git push --force`` after submitting a PR.
- Merge a branch (other then master) into yours and submitting a PR before that other branch got
merged into master. It will cause you to submit commits someone else wrote and are probably outside
the subject of your PR. (There are valid cases however, but take care!)
- ``git reset REF`` after merging ``upstream/master`` into your branch. It will unstage **all**
changed files that where updated in the merge. Your will have to clean up all of them
(don't delete!) using ``git checkout -- <file>``. And take care not to do that to the files you
have modified. However, it can be that the merge modified some other lines then yours. You'll have
to make sure there will be no conflicts when you are submitting this messed up branch to Mailu! You
get the point, I hope.
- ``git rebase`` on a branch that is pull-requested. Others will not be able to see you modified the
branch and it messes with the order of commits, compared to a merge. It might break things after we
have conducted tests.
.. _`GitHub`: https://github.com/
.. _`fork a repo`: https://help.github.com/articles/fork-a-repo/
.. _`synced`: https://help.github.com/articles/syncing-a-fork/
.. _`pull request`: https://help.github.com/articles/about-pull-requests/
Docker containers
-----------------
The development environment is quite similar to the production one. You should always use
the ``master`` version when developing.
The development environment is quite similar to the production one.
Building images
```````````````
We supply a separate ``test/build.yml`` file for
convenience. To build all Mailu containers:
We supply a separate ``test/build.yml`` file for convenience.
After cloning the git repository to your workstation, you can build the images:
.. code-block:: bash
cd Mailu
docker-compose -f tests/build.yml build
The ``build.yml`` file has two variables:
@ -73,10 +170,96 @@ Finally, if you need to install packages inside the containers for debugging:
docker-compose exec admin apk add --no-cache package-name
Reviewing
---------
System requirements
```````````````````
Reviewing pull requests requires some additional git setup. First, for 90% of the review jobs,
you will need a PC or server that can expose all Mailu ports to the outside world. Also, a valid
domain name would be required. This can be a simple free DynDNS account. Do not use a production
server, as there are cases where data corruption occurs and you need to delete the ``/mailu``
directory structure.
If you do no posses the resources, but want to become an involved tester/reviewer.
Please contact `muhlemmer on Matrix`_.
He can provide access to a testing server, if a thrust relation can be established.
.. _`muhlemmer on Matrix`: https://matrix.to/#/@muhlemmer:matrix.org
Preparations
````````````
#. Setup `Git`_ the same way as on a development PC. It is advised to keep ``origin`` as your
own repository and ``upstream`` as the one from Mailu. This will avoid confusion;
#. You will need a ``docker-compose.yml`` and ``.env``, set up for the test server;
#. Make sure that the build ``$VERSION`` corresponds with those files.
Add the sender
``````````````
Replace ``<SENDER>`` with the repository name the PR is sent from.
.. code-block:: bash
git remote add <SENDER> https://github.com/<SENDER>/Mailu.git
Merge conflicts
```````````````
Before proceeding, check the PR page in the bottom. It should not indicate a merge conflict.
If there are merge conflicts, you have 2 options:
#. Do a review "request changes" and ask the author to resolve the merge conflict.
#. Solve the merge conflict yourself on Github, using the web editor.
If it can't be done in the web editor, go for option 1. Unless you want to go through the trouble of
importing the branch into your fork, do the merge and send a PR to the repository of the *sender*.
Merge the PR locally
```````````````````````
When someone sends a PR, you need merge his PR into master locally. This example will put you in a
"detached head" state and do the merge in that state. Any commits done in this state will be lost
forever when you checkout a "normal" branch. This is exactly what we want, as we do not want to mess
with our repositories. This is just a test run.
The following must be done on every PR or after every new commit to an existing PR:
1. Fetch the latest status of all the remotes.
2. List all local and remote available branches (this is not needed, but very helpful at times)
3. Checkout ``upstream/master``
4. Merge ``upstream/master`` with ``SENDER/branch``
.. code-block:: bash
git fetch --all
git checkout upstream/master
# ...You are in 'detached HEAD' state.... (bla bla bla)
git branch -a
# Hit `q` to exit the viewer, if it was opened. Uses arrows up/down for scrolling.
git merge kaiyou/fix-sender-checks
If git opens a editor for a commit message just save and exit as-is. If you have a merge conflict,
see above and do the complete procedure from ``git fetch`` onward again.
Test
````
You can now build and run the containers for testing. See the "`Docker containers`_" section for
instructions. Play around. See if (external) mails work. Check for whatever functionality the PR is
trying to fix. When happy, you can approve the PR. When running into failures, mark the review as
"request changes" and try to provide as much as possible details on the failure.
(Logs, error codes form clients etc).
.. note:: Github marks positive reviews as obsolete when a new commit is added to a PR.
This requires a new review from your side.
Web administration
------------------
The administration Web interface requires a proper dev environment that can easily be setup using ``virtualenv`` (make sure you are using Python 3) :
The administration Web interface requires a proper dev environment that can easily be setup using
``virtualenv`` (make sure you are using Python 3) :
.. code-block:: bash
@ -105,7 +288,8 @@ of the screen, that you can open to access query details, internal variables, et
Documentation
-------------
Documentation is maintained in the ``docs`` directory and are maintained as `reStructuredText`_ files. It is possible to run a local documentation server for reviewing purposes, using Docker:
Documentation is maintained in the ``docs`` directory and are maintained as `reStructuredText`_
files. It is possible to run a local documentation server for reviewing purposes, using Docker:
.. code-block:: bash
@ -113,8 +297,10 @@ Documentation is maintained in the ``docs`` directory and are maintained as `reS
docker build -t docs docs
docker run -p 127.0.0.1:8080:80 docs
You can now read the local documentation by navigating to http://localhost:8080.
In a local build Docker always assumes the version to be master.
You can read the local documentation by navigating to http://localhost:8080/master.
.. note:: After modifying the documentation, the image needs to be rebuild and the container restarted for the changes to become visible.
.. note:: After modifying the documentation, the image needs to be rebuild and the container
restarted for the changes to become visible.
.. _`reStructuredText`: http://docutils.sourceforge.net/rst.html

@ -13,6 +13,8 @@ Docker best practices and be as generic as possible :
- interesting settings should be available as environment variables
- base images should be well-trusted (officiel Alpine or Debian for instance).
.. _git_workflow:
Git workflow
------------

@ -0,0 +1,21 @@
version: '3'
services:
docs_master:
image: mailu/docs:master
labels:
- traefik.enable=true
- traefik.port=80
- traefik.main.frontend.rule=Host:${hostname};PathPrefix:/master/
docs_15:
image: mailu/docs:1.5
labels:
- traefik.enable=true
- traefik.port=80
- traefik.root.frontend.redirect.regex=.*
- traefik.root.frontend.redirect.replacement=/1.5/
- traefik.root.frontend.rule=Host:${hostname};PathPrefix:/
- traefik.main.frontend.rule=Host:${hostname};PathPrefix:/1.5/

@ -0,0 +1,276 @@
Frequently asked questions
==========================
Informational
-------------
Where to ask questions?
```````````````````````
First, please read this FAQ to check if your question is listed here.
Simple questions best fit in our `Matrix`_ room.
For more complex questions, you can always open a `new issue`_ on GitHub.
We actively monitor the issues list.
My installation is broken!
``````````````````````````
We're sorry to hear that. Please check for common mistakes and troubleshooting
advice in the `Technical issues`_ section of this page.
I think I found a bug!
``````````````````````
If you did not manage to solve the issue using this FAQ and there is not any
`open issues`_ describing the same problem, you can continue to open a
`new issue`_ on GitHub.
I want a new feature or enhancement!
````````````````````````````````````
Great! We are always open for suggestions. We currently maintain two tags:
- `Enhancement issues`_: Typically used for optimization of features in the project.
- `Feature request issues`_: For implementing new functionality,
plugins and applications.
Please check if your idea (or something similar) is already mentioned there.
If there is one open, you can choose to vote with a thumbs up, so we can
estimate the popular demand. Please refrain from writing comments like
*"me too"* as it clobbers the actual discussion.
If you can't find anything similar, you can open a `new issue`_.
Please also share (where applicable):
- Use case: how does this improve the project?
- Any research done on the subject. Perhaps some links to upstream website,
reference implementations etc.
Why does my feature/bug take so long to solve?
``````````````````````````````````````````````
You should be aware that creating, maintaining and expanding a mail server
distribution requires a lot of effort. Mail servers are highly exposed to hacking attempts,
open relay scanners, spam and malware distributors etc. We need to work in a safe way and
have to prevent pushing out something quickly.
**TODO: Move the next section into the contributors part of docs**
We currently maintain a strict work flow:
#. Someone writes a solution and sends a pull request;
#. We use Travis-CI for some very basic building and testing;
#. The pull request needs to be code-reviewed and tested by at least two members
from the contributors team.
Please consider that this project is mostly developed in people their free time.
We thank you for your understanding and patience.
I would like to donate (for a feature)
``````````````````````````````````````
Donations are welcome at the `patreon`_ account of the project lead. It will be used to pay
for infra structure and project related costs. If there are leftovers, it will be distributed
among the developers.
It is not yet possible to pay for a specific feature. We don't have
any bounty system implemented. Feel free to come with suggestions in
our ongoing `project management`_ discussion issue.
.. _`Matrix`: https://matrix.to/#/#mailu:tedomum.net
.. _`open issues`: https://github.com/Mailu/Mailu/issues
.. _`new issue`: https://github.com/Mailu/Mailu/issues/new
.. _`Enhancement issues`: https://github.com/Mailu/Mailu/issues?q=is%3Aissue+is%3Aopen+label%3Atype%2Fenhancement
.. _`Feature request issues`: https://github.com/Mailu/Mailu/issues?q=is%3Aopen+is%3Aissue+label%3Atype%2Ffeature
.. _`patreon`: https://patreon.com/kaiyou
.. _`project management`: https://github.com/Mailu/Mailu/issues/508
Deployment related
------------------
How does Mailu scale up?
````````````````````````
Recent works allow Mailu to be deployed in Docker Swarm and Kubernetes.
This means it can be scaled horizontally. For more information, refer to :ref:`kubernetes`
or the `Docker swarm howto`_.
*Issue reference:* `165`_, `520`_.
How to achieve HA / failover?
`````````````````````````````
The mailboxes and databases for Mailu are kept on the host filesystem under ``$ROOT/``.
For making the **storage** highly available, all sorts of techniques can be used:
- Local raid-1
- btrfs in raid configuration
- Distributed network filesystems such as GlusterFS or CEPH
Note that no storage HA solution can protect against incidental deletes or file corruptions.
Therefore it is advised to create backups on a regular base!
A backup MX can be configured as **failover**. For this you need a separate server running
Mailu. On that server, your domains will need to be setup as "Relayed domains", pointing
to you main server. MX records for the mail domains with a higher priority number will have
to point to this server. Please be aware that a backup MX can act as a `spam magnet`_.
For **service** HA, please see: `How does Mailu scale up?`_
*Issue reference:* `177`_, `591`_.
.. _`spam magnet`: https://blog.zensoftware.co.uk/2012/07/02/why-we-tend-to-recommend-not-having-a-secondary-mx-these-days/
Can I run Mailu without host iptables?
``````````````````````````````````````
When disabling iptables in docker, its forwarding proxy process takes over.
This creates the situation that every incoming connection on port 25 seems to come from the
local network (docker's 172.17.x.x) and is accepted. This causes an open relay!
For that reason we do **not** support deployment on Docker hosts without iptables.
*Issue reference:* `332`_.
How can I override settings?
````````````````````````````
Postfix, dovecot and Rspamd support overriding configuration files. Override files belong in
``$ROOT/overrides``. Please refer to the official documentation of those programs for the
correct syntax. The following file names will be taken as override configuration:
- `Postfix`_ - ``postfix.cf``;
- `Dovecot`_ - ``dovecot.conf``;
- `Rspamd`_ - All files in the ``rspamd`` sub-directory.
.. _`Postfix`: http://www.postfix.org/postconf.5.html
.. _`Dovecot`: https://wiki.dovecot.org/ConfigFile
.. _`Rspamd`: https://www.rspamd.com/doc/configuration/index.html
.. _`Docker swarm howto`: https://github.com/Mailu/Mailu/tree/master/docs/swarm/master
.. _`165`: https://github.com/Mailu/Mailu/issues/165
.. _`177`: https://github.com/Mailu/Mailu/issues/177
.. _`332`: https://github.com/Mailu/Mailu/issues/332
.. _`520`: https://github.com/Mailu/Mailu/issues/520
.. _`591`: https://github.com/Mailu/Mailu/issues/591
Technical issues
----------------
In this section we are trying to cover the most common problems our users are having.
If your issue is not listed here, please consult issues with the `troubleshooting tag`_.
Changes in .env don't propagate
```````````````````````````````
Variables are sent to the containers at creation time. This means you need to take the project
down and up again. A container restart is not sufficient.
.. code-block:: bash
docker-compose down && \
docker-compose up -d
*Issue reference:* `615`_.
TLS certificate issues
``````````````````````
When there are issues with the TLS/SSL certificates, Mailu denies service on secure ports.
This is a security precaution. Symptoms are:
- 403 browser errors;
These issues are typically caused by four scenarios:
#. ``TLS_FLAVOR=notls`` in ``.env``;
#. Certificates expired;
#. When ``TLS_FLAVOR=letsencrypt``, it might be that the *certbot* script is not capable of
obtaining the certificates for your domain. See `letsencrypt issues`_
#. When ``TLS_FLAVOR=certs``, certificates are supposed to be copied to ``/mailu/certs``.
Using an external ``letsencrypt`` program, it tends to happen people copy the whole
``letsencrypt/live`` directory containing symlinks. Symlinks do not resolve inside the
container and therefore it breaks the TLS implementation.
letsencrypt issues
..................
In order to determine the exact problem on TLS / Let's encrypt issues, it might be helpful
to check the logs.
.. code-block:: bash
docker-compose logs front | less -R
docker-compose exec front less /var/log/letsencrypt/letsencrypt.log
Common problems:
- Port 80 not reachable from outside.
- Faulty DNS records: make sure that all ``HOSTNAMES`` have **A** (IPv4) and **AAAA** (IPv6)
records, pointing the the ``BIND_ADDRESS4`` and ``BIND_ADDRESS6``.
- DNS cache not yet expired. It might be that old / faulty DNS records are stuck in a cache
en-route to letsencrypt's server. The time this takes is set by the ``TTL`` field in the
records. You'll have to wait at least this time after changing the DNS entries.
Don't keep trying, as you might hit `rate-limits`_.
.. _`rate-limits`: https://letsencrypt.org/docs/rate-limits/
Copying certificates
....................
As mentioned above, care must be taken not to copy symlinks to the ``/mailu/certs`` location.
**The wrong way!:**
.. code-block:: bash
cp -r /etc/letsencrypt/live/domain.com /mailu/certs
**The right way!:**
.. code-block:: bash
mkdir -p /mailu/certs
cp /etc/letsencrypt/live/domain.com/privkey.pem /mailu/certs/key.pem
cp /etc/letsencrypt/live/domain.com/fullchain.pem /mailu/certs/cert.pem
See also :ref:`external_certs`.
*Issue reference:* `426`_, `615`_.
Do you support Fail2Ban?
````````````````````````
Fail2Ban is not included in Mailu. Fail2Ban needs to modify the host's IP tables in order to
ban the addresses. We consider such a program should be run on the host system and not
inside a container. The ``front`` container does use authentication rate limiting to slow
down brute force attacks.
We *do* provide a possibility to export the logs from the ``front`` service to the host.
For this you need to set ``LOG_DRIVER=journald`` or ``syslog``, depending on the log
manager of the host. You will need to setup the proper Regex in the Fail2Ban configuration.
Be aware that webmail authentication appears to come from the Docker network,
so don't ban those addresses!
*Issue reference:* `85`_, `116`_, `171`_, `584`_, `592`_.
Users can't change their password from webmail
``````````````````````````````````````````````
All users have the abilty to login to the admin interface. Non-admin users
have only restricted funtionality such as changing their password and the
spam filter weight settings.
*Issue reference:* `503`_.
.. _`troubleshooting tag`: https://github.com/Mailu/Mailu/issues?utf8=%E2%9C%93&q=label%3Afaq%2Ftroubleshooting
.. _`85`: https://github.com/Mailu/Mailu/issues/85
.. _`116`: https://github.com/Mailu/Mailu/issues/116
.. _`171`: https://github.com/Mailu/Mailu/issues/171
.. _`426`: https://github.com/Mailu/Mailu/issues/426
.. _`503`: https://github.com/Mailu/Mailu/issues/503
.. _`584`: https://github.com/Mailu/Mailu/issues/584
.. _`592`: https://github.com/Mailu/Mailu/issues/592
.. _`615`: https://github.com/Mailu/Mailu/issues/615

@ -44,6 +44,7 @@ the version of Mailu that you are running.
general
features
faq
releases
demo
@ -55,7 +56,7 @@ the version of Mailu that you are running.
configuration
compose/requirements
compose/setup
kubernetes/stable/index
kubernetes/mailu/index
dns
reverse

@ -1,157 +0,0 @@
# 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
```
## 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)
- 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
flask mailu admin root example.com password
```
Now you should be able to login on the mail account: `https://mail.example.com/admin`
## 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:
```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-....
```
### 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!

@ -1,32 +0,0 @@
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

@ -0,0 +1,86 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mailu-admin-ingress
namespace: mailu-mailserver
annotations:
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
certmanager.k8s.io/cluster-issuer: letsencrypt-stage
ingress.kubernetes.io/permanent-redirect: "https://mail.example.com/admin/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: "/admin"
backend:
serviceName: admin
servicePort: 80
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mailu-admin-ui-ingress
namespace: mailu-mailserver
annotations:
kubernetes.io/tls-acme: "true"
certmanager.k8s.io/cluster-issuer: letsencrypt-stage
ingress.kubernetes.io/rewrite-target: "/ui"
ingress.kubernetes.io/configuration-snippet: |
proxy_set_header X-Forwarded-Prefix /admin;
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: "/admin/ui"
backend:
serviceName: admin
servicePort: 80
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mailu-admin-static-ingress
namespace: mailu-mailserver
annotations:
kubernetes.io/tls-acme: "true"
certmanager.k8s.io/cluster-issuer: letsencrypt-stage
ingress.kubernetes.io/rewrite-target: "/static"
ingress.kubernetes.io/configuration-snippet: |
proxy_set_header X-Forwarded-Prefix /admin;
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: "/admin/static"
backend:
serviceName: admin
servicePort: 80

@ -1,4 +1,3 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:

@ -21,7 +21,7 @@
VERSION: "master"
# Set to a randomly generated 16 bytes string
SECRET_KEY: "YourKeyHere"
SECRET_KEY: "MySup3rS3cr3tPas"
# Address where listening ports should bind
BIND_ADDRESS4: "127.0.0.1"
@ -45,6 +45,14 @@
# Opt-out of statistics, replace with "True" to opt out
DISABLE_STATISTICS: "False"
###################################
# Kubernetes configuration
###################################
# Use Kubernetes Ingress Controller to handle all actions on port 80 and 443
# This way we can make use of the advantages of the cert-manager deployment
KUBERNETES_INGRESS: "true"
###################################
# Optional features
###################################
@ -71,19 +79,18 @@
# 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"
# This value is needed by the webmail to find the correct imap backend
IMAP_ADDRESS: "imap.mailu-mailserver.svc.cluster.local"
# This value is used by Dovecot to find the Redis server in the cluster
REDIS_ADDRESS: "redis.mailu-mailserver.svc.cluster.local"
# Fetchmail delay
FETCHMAIL_DELAY: "600"
@ -106,13 +113,16 @@
###################################
# Path to the admin interface if enabled
# Kubernetes addition: You need to change ALL the ingresses, when you want this URL to be different!!!
WEB_ADMIN: "/admin"
# Path to the webmail if enabled
# Currently, this is not used, because we intended to use a different subdomain: webmail.example.com
# This option can be added in a feature release
WEB_WEBMAIL: "/webmail"
# Website name
SITENAME: "AppSynth"
SITENAME: "Mailu"
# Linked Website URL
WEBSITE: "https://example.com"

@ -1,23 +1,41 @@
apiVersion: extensions/v1beta1
kind: Deployment
apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
name: mailu-front
namespace: mailu-mailserver
labels:
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
spec:
replicas: 1
selector:
matchLabels:
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
template:
metadata:
labels:
app: mailu-front
role: mail
tier: backend
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/node
operator: Exists
hostNetwork: true
nodeSelector:
node-role.kubernetes.io/node: ""
dnsPolicy: ClusterFirstWithHostNet
restartPolicy: Always
terminationGracePeriodSeconds: 60
containers:
- name: front
image: mailu/nginx:latest
image: mailu/nginx:master
imagePullPolicy: Always
envFrom:
- configMapRef:
@ -26,12 +44,6 @@ spec:
- name: certs
mountPath: /certs
ports:
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
- name: pop3
containerPort: 110
protocol: TCP
@ -85,21 +97,15 @@ metadata:
name: front
namespace: mailu-mailserver
labels:
app: mailu-admin
role: mail
tier: backend
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
spec:
selector:
app: mailu-front
role: mail
tier: backend
k8s-app: mail-loadbalancer
component: ingress-controller
type: nginx
ports:
- name: http
port: 80
protocol: TCP
- name: https
port: 443
protocol: TCP
- name: pop3
port: 110
protocol: TCP

@ -37,8 +37,8 @@ spec:
- containerPort: 4190
resources:
requests:
memory: 500Mi
cpu: 500m
memory: 1Gi
cpu: 1000m
limits:
memory: 1Gi
cpu: 1000m

@ -0,0 +1,195 @@
.. _kubernetes:
Kubernetes setup
================
Prequisites
-----------
Structure
~~~~~~~~~
Theres 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. We have chosen to have a double NGINX stack for Mailu.
- ``Cert manager``: Creates automatic Lets Encrypt certificates based on an ``Ingress``-objects domain name.
- ``Mailu NGINX Front daemonset``: This daemonset runs in parallel with the Nginx Ingress Controller and only listens on all E-mail specific ports (25, 110, 143, 587,...)
- ``Mailu components``: All Mailu components (imap, smtp, security, webmail,...) are split into separate files to make them more handy to use, you can find the ``YAML`` files in this directory
What you need
~~~~~~~~~~~~~
- A working Kubernetes cluster (tested with 1.10.5)
- A working `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`_. After booting the ``Cert-manager`` youll need a
``ClusterIssuer`` which takes care of all required certificates through
``Ingress`` items. We chose to provide a ``clusterIssuer`` so you can provide SSL certificates
for other namespaces (different websites/services), if you don't need this option, you can easily change this by
changing ``clusterIssuer`` to ``Issuer`` and adding the ``namespace: mailu-mailserver`` to the metadata.
An example of a production and a staging ``clusterIssuer``:
.. code:: yaml
# This clusterIssuer example uses the staging environment for testing first
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
name: letsencrypt-stage
spec:
acme:
email: something@example.com
http01: {}
privateKeySecretRef:
name: letsencrypt-stage
server: https://acme-staging-v02.api.letsencrypt.org/directory
.. code:: yaml
# This clusterIssuer example uses the production environment
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: something@example.com
http01: {}
privateKeySecretRef:
name: letsencrypt-prod
server: https://acme-v02.api.letsencrypt.org/directory
**IMPORTANT**: All ``*-ingress.yaml`` files use the ``letsencrypt-stage`` ``clusterIssuer``. If you are ready for production,
change this field in all ``*-ingress.yaml`` files to ``letsencrypt-prod`` or whatever name you chose for the production.
If you choose for ``Issuer`` instead of ``clusterIssuer`` you also need to change the annotation to ``certmanager.k8s.io/issuer`` instead of ``certmanager.k8s.io/cluster-issuer``
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)
- Check the ``*-ingress.yaml`` files and change it to the domain you want (this is for the kubernetes ingress controller to handle the admin, webmail, webdav and auth connections)
Installation
------------
Boot the Mailu components
~~~~~~~~~~~~~~~~~~~~~~~~~
To start Mailu, run the following commands from the ``docs/kubernetes/mailu`` directory
.. code-block:: bash
kubectl create -f rbac.yaml
kubectl create -f configmap.yaml
kubectl create -f pvc.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
kubectl create -f admin-ingress.yaml
kubectl create -f webdav-ingress.yaml
kubectl create -f security-ingress.yaml
kubectl create -f webmail-ingress.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:
.. code-block:: 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:
.. code-block:: bash
python manage.py admin root example.com password
- ``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
Now you should be able to login on the mail account: https://mail.example.com/admin
Adaptations
-----------
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:
.. code:: bash
kubectl -n mailu-mailserver get po
kubectl -n mailu-mailserver exec -it mailu-imap-.... /bin/sh
Create the file ``overrides/dovecot.conf``
.. code:: bash
vi /overrides/dovecot.conf
And enter following contents:
.. code:: 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.
.. code:: bash
kubectl -n mailu-mailserver delete po/mailu-imap-....
Wait for the pod to recreate and you're online!
Happy mailing!
.. _here: https://github.com/hacor/Mailu/blob/master/core/postfix/conf/main.cf#L35
.. _cert-manager: https://github.com/jetstack/cert-manager
.. _docs: https://cert-manager.readthedocs.io/en/latest/getting-started/2-installing.html
Imap login fix
~~~~~~~~~~~~~~
If it seems you're not able to login using IMAP on your Mailu accounts, check the logs of the imap container to see whether it's a permissions problem on the database.
This problem can be easily fixed by running following commands:
.. code:: bash
kubectl -n mailu-mailserver exec -it mailu-imap-... /bin/sh
chmod 777 /data/main.db

@ -0,0 +1,30 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mailu-antispam-ingress
namespace: mailu-mailserver
annotations:
kubernetes.io/tls-acme: "true"
certmanager.k8s.io/cluster-issuer: letsencrypt-stage
ingress.kubernetes.io/configuration-snippet: |
rewrite ^/admin/antispam/(.*) /$1 break;
auth_request /internal/auth/admin;
proxy_set_header X-Real-IP "";
proxy_set_header X-Forwarded-For "";
labels:
app: mailu
role: mail
tier: frontend
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: "/admin/antispam"
backend:
serviceName: antispam
servicePort: 11334

@ -31,6 +31,9 @@ spec:
- name: antispam
containerPort: 11332
protocol: TCP
- name: antispam-http
containerPort: 11334
protocol: TCP
volumeMounts:
- name: filter
subPath: filter
@ -87,6 +90,9 @@ spec:
- name: antispam
port: 11332
protocol: TCP
- name: antispam-http
protocol: TCP
port: 11334
---

@ -21,10 +21,10 @@ spec:
name: mailu-config
resources:
requests:
memory: 500Mi
cpu: 200m
memory: 2Gi
cpu: 500m
limits:
memory: 1Gi
memory: 2Gi
cpu: 500m
volumeMounts:
- mountPath: /data

@ -0,0 +1,46 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mailu-webdav-ingress
namespace: mailu-mailserver
annotations:
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
certmanager.k8s.io/cluster-issuer: letsencrypt-stage
#ingress.kubernetes.io/auth-url: http://admin.mailu-mailserver.svc.cluster.local/internal/auth/basic
ingress.kubernetes.io/configuration-snippet: |
rewrite ^/webdav/(.*) /$1 break;
auth_request /internal/auth/basic;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
auth_request_set $user $upstream_http_x_user;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Script-Name /webdav;
ingress.kubernetes.io/server-snippet: |
location /internal {
internal;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_pass http://admin.mailu-mailserver.svc.cluster.local;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
labels:
app: mailu
role: mail
tier: frontend
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: "/webdav"
backend:
serviceName: webdav
servicePort: 5232

@ -0,0 +1,31 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mailu-webmail-ingress
namespace: mailu-mailserver
annotations:
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
certmanager.k8s.io/cluster-issuer: letsencrypt-stage
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
labels:
app: mailu
role: mail
tier: backend
spec:
tls:
- hosts:
- "webmail.example.com"
secretName: letsencrypt-webmail # If unsure how to generate these, check out https://github.com/ployst/docker-letsencrypt
rules:
- host: "webmail.example.com"
http:
paths:
- path: "/"
backend:
serviceName: webmail
servicePort: 80

@ -15,7 +15,7 @@ spec:
spec:
containers:
- name: roundcube
image: mailu/roundcube:1.5
image: mailu/roundcube:master
imagePullPolicy: Always
envFrom:
- configMapRef:

@ -2,15 +2,15 @@ apiVersion: v1
kind: Service
metadata:
# keep it under 24 chars
name: appsynth-lb
name: ingress-lb
namespace: kube-ingress
labels:
k8s-app: appsynth-lb
k8s-app: ingress-lb
component: ingress-controller
spec:
type: ClusterIP
selector:
k8s-app: appsynth-lb
k8s-app: ingress-lb
component: ingress-controller
ports:
- name: http
@ -35,13 +35,6 @@ 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
@ -61,7 +54,7 @@ metadata:
prometheus.io/port: "10254"
prometheus.io/scrape: "true"
labels:
k8s-app: appsynth-lb
k8s-app: ingress-lb
component: ingress-controller
type: nginx
spec:
@ -71,13 +64,13 @@ spec:
type: RollingUpdate
selector:
matchLabels:
k8s-app: appsynth-lb
k8s-app: ingress-lb
component: ingress-controller
type: nginx
template:
metadata:
labels:
k8s-app: appsynth-lb
k8s-app: ingress-lb
component: ingress-controller
type: nginx
spec:
@ -94,14 +87,11 @@ spec:
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
@ -115,10 +105,8 @@ spec:
ports:
- name: http
containerPort: 80
hostPort: 80
- name: https
containerPort: 443
hostPort: 443
readinessProbe:
httpGet:
path: /healthz
@ -134,6 +122,6 @@ spec:
hostNetwork: true
nodeSelector:
node-role.kubernetes.io/node: ""
dnsPolicy: ClusterFirst
dnsPolicy: ClusterFirstWithHostNet
restartPolicy: Always
terminationGracePeriodSeconds: 60

@ -1,26 +0,0 @@
Kubernetes setup
================
Please note that Kubernetes setup is not yet well supported or documented, all
tests currently run on Docker Compose. The configuration has not yet been updated
to work properly with ngin authentication proxy.
Prepare the environment
-----------------------
The resource configurations in this folder assume that you have `Kubernetes Ingress`_
set up for your cluster. If you are not using the `NGINX Ingress Controller for Kubernetes`_,
please ensure that the configuration specified in the file matches your set up.
.. _`Kubernetes Ingress`: https://kubernetes.io/docs/concepts/services-networking/ingress/
.. _`NGINX Ingress Controller for Kubernetes`: https://github.com/kubernetes/ingress/tree/master/controllers/nginx
Setup the Kubernetes service
----------------------------
Using the resource configurations is simple:
1. ``kubectl apply -f kubernetes-nginx-ingress-controller.yaml`` to configure an ingress controller with the proper settings. (If you have one set up already you may need to port the configuration to your own ingress).
2. ``kubectl apply -f kubernetes-mailu.yaml`` to create the resources required to run Mailu.
Based on the configuration, your Mailu instance should be available at ``mail.<hostname>.tld/admin`` (note that visiting just ``mail.<hostname>.tld`` will likely result in a 404 error).

@ -1,419 +0,0 @@
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mailu-admin-ing
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: "/admin"
backend:
serviceName: mailu-admin
servicePort: 80
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-redis
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
volumes:
- name: redisdata
hostPath:
path: /var/data/mailu/redisdata
---
apiVersion: v1
kind: Service
metadata:
name: redis
labels:
app: mailu-redis
role: mail
tier: backend
spec:
selector:
app: mailu
role: mail
tier: backend
ports:
- name: redis
port: 6379
protocol: TCP
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-imap
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-imap
role: mail
tier: backend
spec:
containers:
- name: imap
image: mailu/dovecot:stable
imagePullPolicy: Always
env:
- name : DOMAIN
value : example.com
- name : HOSTNAME
value : mail.example.com
- name : POSTMASTER
value : admin
volumeMounts:
- mountPath: /data
name: maildata
- mountPath: /mail
name: mailstate
- mountPath: /overrides
name: overrides
- mountPath: /certs
name: certs
readOnly: true
ports:
- containerPort: 2102
- containerPort: 2525
- containerPort: 143
- containerPort: 993
- containerPort: 4190
volumes:
- name: maildata
hostPath:
path: /var/data/mailu/maildata
- name: mailstate
hostPath:
path: /var/data/mailu/mailstate
- name: overrides
hostPath:
path: /var/data/mailu/overrides
- 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: imap
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
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-smtp
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-smtp
role: mail
tier: backend
spec:
containers:
- name: smtp
image: mailu/postfix:stable
imagePullPolicy: Always
env:
- name : DOMAIN
value : example.com
- name : HOSTNAME
value : mail.example.com
- name : MESSAGE_SIZE_LIMIT
value : "50000000"
- name : RELAYHOST
value : ""
volumeMounts:
- mountPath: /data
name: maildata
- mountPath: /overrides
name: overrides
- mountPath: /certs
name: certs
readOnly: true
ports:
- name: smtp
containerPort: 25
protocol: TCP
- name: smtp-ssl
containerPort: 465
protocol: TCP
- name: smtp-starttls
containerPort: 587
protocol: TCP
volumes:
- name: maildata
hostPath:
path: /var/data/mailu/maildata
- name: overrides
hostPath:
path: /var/data/mailu/overrides
- 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: smtp
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
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-security
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-security
role: mail
tier: backend
spec:
containers:
- name: antispam
image: mailu/rspamd:stable
imagePullPolicy: Always
ports:
- name: antispam
containerPort: 11333
protocol: TCP
volumeMounts:
- name: filter
mountPath: /var/lib/rspamd
- name: antivirus
image: mailu/clamav:stable
imagePullPolicy: Always
ports:
- name: antivirus
containerPort: 3310
protocol: TCP
volumeMounts:
- name: filter
mountPath: /data
volumes:
- name: filter
hostPath:
path: /var/data/mailu/filter
---
apiVersion: v1
kind: Service
metadata:
name: antispam
labels:
app: mailu-antispam
role: mail
tier: backend
spec:
selector:
app: mailu-security
role: mail
tier: backend
ports:
- name: antispam
port: 11333
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: antivirus
labels:
app: mailu-antivirus
role: mail
tier: backend
spec:
selector:
app: mailu-security
role: mail
tier: backend
ports:
- name: antivirus
port: 3310
protocol: TCP
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-admin
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-admin
role: mail
tier: backend
spec:
containers:
- name: admin
image: mailu/admin:stable
imagePullPolicy: Always
env:
- name : DOMAIN
value : example.com
- name : HOSTNAME
value : mail.example.com
- name : POSTMASTER
value : core
- name : SECRET_KEY
value : pleasereplacethiswithabetterkey
- name : DEBUG
value : "True"
volumeMounts:
- name: maildata
mountPath: /data
- name: dkim
mountPath: /dkim
- name: certs
mountPath: /certs
readOnly: true
# - name: docker
# mountPath: /var/run/docker.sock
# readOnly: true
ports:
- name: http
containerPort: 80
protocol: TCP
volumes:
- name: maildata
hostPath:
path: /var/data/mailu/maildata
- name: dkim
hostPath:
path: /var/data/mailu/dkim
- name: certs
secret:
items:
- key: tls.crt
path: cert.pem
- key: tls.key
path: key.pem
secretName: letsencrypt-certs-all
# - name: docker
# hostPath:
# path: /var/run/docker.sock
---
apiVersion: v1
kind: Service
metadata:
name: mailu-admin
labels:
app: mailu-admin
role: mail
tier: backend
spec:
selector:
app: mailu-admin
role: mail
tier: backend
ports:
- name: http
port: 80
protocol: TCP

@ -1,84 +0,0 @@
---
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-configuration
namespace: ingress-nginx
labels:
app: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: udp-services
namespace: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: ingress-nginx
data:
25: "mailu/smtp:25"
465: "mailu/smtp:465"
587: "mailu/smtp:587"
143: "mailu/imap:143"
993: "mailu/imap:993"
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-ingress-controller
namespace: kube-system
labels:
k8s-app: nginx-ingress-controller
spec:
replicas: 1
template:
metadata:
labels:
k8s-app: nginx-ingress-controller
annotations:
prometheus.io/port: '10254'
prometheus.io/scrape: 'true'
spec:
# hostNetwork makes it possible to use ipv6 and to preserve the source IP correctly regardless of docker configuration
# however, it is not a hard dependency of the nginx-ingress-controller itself and it may cause issues if port 10254 already is taken on the host
# that said, since hostPort is broken on CNI (https://github.com/kubernetes/kubernetes/issues/31307) we have to use hostNetwork where CNI is used
# like with kubeadm
# hostNetwork: true
terminationGracePeriodSeconds: 60
containers:
- image: gcr.io/google_containers/nginx-ingress-controller:0.11.0
name: nginx-ingress-controller
args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
- --configmap=$(POD_NAMESPACE)/nginx-configuration
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
- --annotations-prefix=nginx.ingress.kubernetes.io
readinessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
livenessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 1
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace

@ -28,6 +28,33 @@ Logs are managed by Docker directly. You can easily read your logs using:
Docker is able to forward logs to multiple log engines. Read the following documentation for details: https://docs.docker.com/engine/admin/logging/overview/.
.. _external_certs:
Managing of external Let's encrypt certificates
-----------------------------------------------
When you are not using the embedded ``letsencrypt`` option from Mailu,
you cannot make use of it's symlink functionality in the ``letsencrypt/live`` directory.
You should take care that after every renewal new certificates are copied to ``/mailu/certs`` and
the *nginx* process in the ``front`` container is reloaded.
In the case of *certbot* you could write a script to be executed as `deploy hook`_. Example:
.. code-block:: bash
#!/bin/sh
cp /etc/letsencrypt/live/domain.com/privkey.pem /mailu/certs/key.pem || exit 1
cp /etc/letsencrypt/live/domain.com/fullchain.pem /mailu/certs/cert.pem || exit 1
docker exec mailu_front_1 nginx -s reload
And the certbot command you will use in crontab would look something like:
.. code-block:: bash
52 0,12 * * * root /usr/bin/certbot renew --deploy-hook /path/to/script.sh
.. _`deploy hook`: https://certbot.eff.org/docs/using.html#renewing-certificates
Migrating an instance
---------------------

@ -0,0 +1,252 @@
# Install Mailu on a docker swarm
## Prequisites
### 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 <IP_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
```
See https://docs.docker.com/engine/swarm/join-nodes/
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
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
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 assume that "Mailu Data" is available on every node at "$ROOT/certs:/certs" (GlusterFS and nfs shares have been successfully used).
On this example, we are using:
- the mesh routing mode (default mode). With this mode, each service is given a virtual IP adress and docker manages the routing between this virtual IP and the container(s) providing this service.
- the default ingress mode.
### Allow authentification with the mesh routing
In order to allow every (front & webmail) container to access the other services, we will use the variable POD_ADDRESS_RANGE.
Let's create the mailu_default network:
```bash
core@coreos-01 ~ $ docker network create -d overlay --attachable mailu_default
core@coreos-01 ~ $ docker network inspect mailu_default | grep Subnet
"Subnet": "10.0.1.0/24",
```
In the docker-compose.yml file, we will then use POD_ADDRESS_RANGE = 10.0.1.0/24
In fact, imap & smtp logs doesn't show the IPs from the front(s) container(s), but the IP of "mailu_default-endpoint". So it is sufficient to set POD_ADDRESS_RANGE to this specific ip (which can be found by inspecting mailu_default network). The issue is that this endpoint is created while the stack is created, I did'nt figure a way to determine this IP before the stack creation...
### Limitation with the ingress mode
With the default ingress mode, the front(s) container(s) will see origin IP(s) all being 10.255.0.x (which is the ingress-endpoint, can be found by inspecting the ingress network)
This issue is known and discussed here:
https://github.com/moby/moby/issues/25526
A workaround (using network host mode and global deployment) is discussed here:
https://github.com/moby/moby/issues/25526#issuecomment-336363408
### Don't create an open relay !
As a side effect of this ingress mode "feature", make sure that the ingress subnet is not in your RELAYHOST, otherwise you would create an smtp open relay :-(
## Scalability
- smtp and imap are scalable
- front and webmail are scalable (pending POD_ADDRESS_RANGE is used), although the let's encrypt magic might not like it (race condidtion ? or risk to be banned by let's encrypt server if too many front containers attemps to renew the certs at the same time)
- redis, antispam, antivirus, fetchmail, admin, webdav have not been tested (hence replicas=1 in the following docker-compose.yml file)
## Variable substitution and docker-compose.yml
The docker stack deploy command doesn't support variable substitution in the .yml file itself.
As a consequence, we cannot simply use ``` docker stack deploy -c docker.compose.yml mailu ```
Instead, we will use the following work-around:
``` echo "$(docker-compose -f /mnt/docker/apps/mailu/docker-compose.yml config 2>/dev/null)" | docker stack deploy -c- mailu ```
We need also to:
- add a deploy section for every service
- modify the way the ports are defined for the front service
- add the POD_ADDRESS_RANGE definition for imap, smtp and antispam services
## Docker compose
An example of docker-compose-stack.yml file is available here:
```yaml
version: '3.2'
services:
front:
image: mailu/nginx:$VERSION
restart: always
env_file: .env
ports:
- target: 80
published: 80
- target: 443
published: 443
- target: 110
published: 110
- target: 143
published: 143
- target: 993
published: 993
- target: 995
published: 995
- target: 25
published: 25
- target: 465
published: 465
- target: 587
published: 587
volumes:
- "$ROOT/certs:/certs"
deploy:
replicas: 2
redis:
image: redis:alpine
restart: always
volumes:
- "$ROOT/redis:/data"
deploy:
replicas: 1
imap:
image: mailu/dovecot:$VERSION
restart: always
env_file: .env
environment:
- POD_ADDRESS_RANGE=10.0.1.0/24
volumes:
- "$ROOT/mail:/mail"
- "$ROOT/overrides:/overrides"
depends_on:
- front
deploy:
replicas: 2
smtp:
image: mailu/postfix:$VERSION
restart: always
env_file: .env
environment:
- POD_ADDRESS_RANGE=10.0.1.0/24
volumes:
- "$ROOT/overrides:/overrides"
depends_on:
- front
deploy:
replicas: 2
antispam:
image: mailu/rspamd:$VERSION
restart: always
env_file: .env
environment:
- POD_ADDRESS_RANGE=10.0.1.0/24
volumes:
- "$ROOT/filter:/var/lib/rspamd"
- "$ROOT/dkim:/dkim"
- "$ROOT/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
deploy:
replicas: 1
antivirus:
image: mailu/none:$VERSION
restart: always
env_file: .env
volumes:
- "$ROOT/filter:/data"
deploy:
replicas: 1
webdav:
image: mailu/none:$VERSION
restart: always
env_file: .env
volumes:
- "$ROOT/dav:/data"
deploy:
replicas: 1
admin:
image: mailu/admin:$VERSION
restart: always
env_file: .env
volumes:
- "$ROOT/data:/data"
- "$ROOT/dkim:/dkim"
- /var/run/docker.sock:/var/run/docker.sock:ro
depends_on:
- redis
deploy:
replicas: 1
webmail:
image: mailu/roundcube:$VERSION
restart: always
env_file: .env
volumes:
- "$ROOT/webmail:/data"
depends_on:
- imap
deploy:
replicas: 2
fetchmail:
image: mailu/fetchmail:$VERSION
restart: always
env_file: .env
volumes:
deploy:
replicas: 1
networks:
default:
external:
name: mailu_default
```
## Deploy Mailu on the docker swarm
Run the following command:
```bash
echo "$(docker-compose -f /mnt/docker/apps/mailu/docker-compose.yml config 2>/dev/null)" | docker stack deploy -c- 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:master
pqokiaz0q128 mailu_fetchmail replicated 1/1 mailu/fetchmail:master
```
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:master coreos-01 Running Running 11 days ago
```
You might also have a look on the logs:
```bash
core@coreos-01 ~ $ docker service logs -f mailu_fetchmail
```
## Remove the stack
Run the follwoing command:
```bash
core@coreos-01 ~ $ docker stack rm mailu
```

@ -0,0 +1,357 @@
# Install Mailu on a docker swarm
## Prequisites
### 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 <IP_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
```
See https://docs.docker.com/engine/swarm/join-nodes/
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
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
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 ~ $ 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:
```bash
core@coreos-01 ~ $ sudo umount /mnt/local/
```
## Networking mode
On this example, we are using:
- the mesh routing mode (default mode). With this mode, each service is given a virtual IP adress and docker manages the routing between this virtual IP and the container(s) providing this service.
- the default ingress mode.
### Allow authentification with the mesh routing
In order to allow every (front & webmail) container to access the other services, we will use the variable POD_ADDRESS_RANGE.
Let's create the mailu_default network:
```bash
core@coreos-01 ~ $ docker network create -d overlay --attachable mailu_default
core@coreos-01 ~ $ docker network inspect mailu_default | grep Subnet
"Subnet": "10.0.1.0/24",
```
In the docker-compose.yml file, we will then use POD_ADDRESS_RANGE = 10.0.1.0/24
In fact, imap & smtp logs doesn't show the IPs from the front(s) container(s), but the IP of "mailu_default-endpoint". So it is sufficient to set POD_ADDRESS_RANGE to this specific ip (which can be found by inspecting mailu_default network). The issue is that this endpoint is created while the stack is created, I did'nt figure a way to determine this IP before the stack creation...
### Limitation with the ingress mode
With the default ingress mode, the front(s) container(s) will see origin IP(s) all being 10.255.0.x (which is the ingress-endpoint, can be found by inspecting the ingress network)
This issue is known and discussed here:
https://github.com/moby/moby/issues/25526
A workaround (using network host mode and global deployment) is discussed here:
https://github.com/moby/moby/issues/25526#issuecomment-336363408
### Don't create an open relay !
As a side effect of this ingress mode "feature", make sure that the ingress subnet is not in your RELAYHOST, otherwise you would create an smtp open relay :-(
## Scalability
- smtp and imap are scalable
- front and webmail are scalable (pending POD_ADDRESS_RANGE is used), although the let's encrypt magic might not like it (race condidtion ? or risk to be banned by let's encrypt server if too many front containers attemps to renew the certs at the same time)
- redis, antispam, antivirus, fetchmail, admin, webdav have not been tested (hence replicas=1 in the following docker-compose.yml file)
## Variable substitution and docker-compose.yml
The docker stack deploy command doesn't support variable substitution in the .yml file itself. As a consequence, we need to use the following work-around:
``` echo "$(docker-compose -f /mnt/docker/apps/mailu/docker-compose.yml config 2>/dev/null)" | docker stack deploy -c- mailu ```
We need also to:
- change the way we define the volumes (nfs share in our case)
- add a deploy section for every service
- the way the ports are defined for the front service
## Docker compose
An example of docker-compose-stack.yml file is available here:
```yaml
version: '3.2'
services:
front:
image: mailu/nginx:$VERSION
restart: always
env_file: .env
ports:
- target: 80
published: 80
- target: 443
published: 443
- target: 110
published: 110
- target: 143
published: 143
- target: 993
published: 993
- target: 995
published: 995
- target: 25
published: 25
- target: 465
published: 465
- target: 587
published: 587
volumes:
# - "$ROOT/certs:/certs"
- type: volume
source: mailu_certs
target: /certs
deploy:
replicas: 2
redis:
image: redis:alpine
restart: always
volumes:
# - "$ROOT/redis:/data"
- type: volume
source: mailu_redis
target: /data
deploy:
replicas: 1
imap:
image: mailu/dovecot:$VERSION
restart: always
env_file: .env
environment:
- POD_ADDRESS_RANGE=10.0.1.0/24
volumes:
# - "$ROOT/mail:/mail"
- type: volume
source: mailu_mail
target: /mail
# - "$ROOT/overrides:/overrides"
- type: volume
source: mailu_overrides
target: /overrides
depends_on:
- front
deploy:
replicas: 2
smtp:
image: mailu/postfix:$VERSION
restart: always
env_file: .env
environment:
- POD_ADDRESS_RANGE=10.0.1.0/24
volumes:
# - "$ROOT/overrides:/overrides"
- type: volume
source: mailu_overrides
target: /overrides
depends_on:
- front
deploy:
replicas: 2
antispam:
image: mailu/rspamd:$VERSION
restart: always
env_file: .env
environment:
- POD_ADDRESS_RANGE=10.0.1.0/24
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:
replicas: 1
antivirus:
image: mailu/none:$VERSION
restart: always
env_file: .env
volumes:
# - "$ROOT/filter:/data"
- type: volume
source: mailu_filter
target: /data
deploy:
replicas: 1
webdav:
image: mailu/none:$VERSION
restart: always
env_file: .env
volumes:
# - "$ROOT/dav:/data"
- type: volume
source: mailu_dav
target: /data
deploy:
replicas: 1
admin:
image: mailu/admin:$VERSION
restart: always
env_file: .env
volumes:
# - "$ROOT/data:/data"
- type: volume
source: mailu_data
target: /data
# - "$ROOT/dkim:/dkim"
- type: volume
source: mailu_dkim
target: /dkim
- /var/run/docker.sock:/var/run/docker.sock:ro
depends_on:
- redis
deploy:
replicas: 1
webmail:
image: mailu/roundcube:$VERSION
restart: always
env_file: .env
volumes:
# - "$ROOT/webmail:/data"
- type: volume
source: mailu_data
target: /data
depends_on:
- imap
deploy:
replicas: 2
fetchmail:
image: mailu/fetchmail:$VERSION
restart: always
env_file: .env
volumes:
deploy:
replicas: 1
networks:
default:
external:
name: mailu_default
volumes:
mailu_filter:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,soft,rw"
device: ":/mnt/Pool1/pv/mailu/filter"
mailu_dkim:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,soft,rw"
device: ":/mnt/Pool1/pv/mailu/dkim"
mailu_overrides_rspamd:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,soft,rw"
device: ":/mnt/Pool1/pv/mailu/overrides/rspamd"
mailu_data:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,soft,rw"
device: ":/mnt/Pool1/pv/mailu/data"
mailu_mail:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,soft,rw"
device: ":/mnt/Pool1/pv/mailu/mail"
mailu_overrides:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,soft,rw"
device: ":/mnt/Pool1/pv/mailu/overrides"
mailu_dav:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,soft,rw"
device: ":/mnt/Pool1/pv/mailu/dav"
mailu_certs:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,soft,rw"
device: ":/mnt/Pool1/pv/mailu/certs"
mailu_redis:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,soft,rw"
device: ":/mnt/Pool1/pv/mailu/redis"
```
## Deploy Mailu on the docker swarm
Run the following command:
```bash
echo "$(docker-compose -f /mnt/docker/apps/mailu/docker-compose.yml config 2>/dev/null)" | docker stack deploy -c- 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:master
pqokiaz0q128 mailu_fetchmail replicated 1/1 mailu/fetchmail:master
```
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:master coreos-01 Running Running 11 days ago
```
## Remove the stack
Run the follwoing command:
```bash
core@coreos-01 ~ $ docker stack rm mailu
```

@ -1,11 +1,18 @@
FROM alpine:3.8
# python3 shared with most images
RUN apk add --no-cache \
python3 py3-pip \
&& pip3 install --upgrade pip
# Image specific layers under this line
RUN apk add --no-cache clamav rsyslog wget clamav-libunrar
COPY conf /etc/clamav
COPY start.sh /start.sh
COPY start.py /start.py
COPY health.sh /health.sh
EXPOSE 3310/tcp
VOLUME ["/data"]
CMD ["/start.sh"]
CMD /start.py
HEALTHCHECK CMD /health.sh

@ -0,0 +1,8 @@
#!/bin/sh
if [ "$(echo PING | nc localhost 3310)" = "PONG" ]; then
echo "ping successful"
else
echo "ping failed"
exit 1
fi

@ -0,0 +1,12 @@
#!/usr/bin/python3
import os
# Bootstrap the database if clamav is running for the first time
os.system("[ -f /data/main.cvd ] || freshclam")
# Run the update daemon
os.system("freshclam -d -c 6")
# Run clamav
os.system("clamd")

@ -1,10 +0,0 @@
#!/bin/sh
# Bootstrap the database if clamav is running for the first time
[ -f /data/main.cvd ] || freshclam
# Run the update daemon
freshclam -d -c 6
# Run clamav
clamd

@ -1,7 +1,7 @@
FROM alpine:edge
RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \
&& apk add --no-cache radicale@testing py-dulwich@testing
&& apk add --no-cache radicale@testing py-dulwich@testing curl
COPY radicale.conf /radicale.conf
@ -9,3 +9,5 @@ EXPOSE 5232/tcp
VOLUME ["/data"]
CMD radicale -f -S -C /radicale.conf
HEALTHCHECK CMD curl -f -L http://localhost:5232/ || exit 1

@ -1,7 +1,11 @@
FROM python:3-alpine
FROM alpine:3.8
# python3 shared with most images
RUN apk add --no-cache \
python3 py3-pip \
&& pip3 install --upgrade pip
# Image specific layers under this line
RUN apk add --no-cache fetchmail ca-certificates \
&& pip install requests
&& pip3 install requests
COPY fetchmail.py /fetchmail.py
USER fetchmail

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python3
import time
import os

@ -1,19 +1,24 @@
FROM alpine:3.8
RUN apk add --no-cache python py-jinja2 rspamd rspamd-controller rspamd-proxy ca-certificates py-pip \
&& pip install --upgrade pip \
&& pip install tenacity
# python3 shared with most images
RUN apk add --no-cache \
python3 py3-pip \
&& pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2
# Shared layer between rspamd, postfix, dovecot
RUN pip3 install tenacity
# Image specific layers under this line
RUN apk add --no-cache rspamd rspamd-controller rspamd-proxy rspamd-fuzzy ca-certificates curl
RUN mkdir /run/rspamd
COPY conf/ /conf
COPY start.py /start.py
# Temporary fix to remove references to rspamd-fuzzy for now
RUN sed -i '/fuzzy/,$d' /etc/rspamd/rspamd.conf
EXPOSE 11332/tcp 11334/tcp
EXPOSE 11332/tcp 11334/tcp 11335/tcp
VOLUME ["/var/lib/rspamd"]
CMD /start.py
HEALTHCHECK --start-period=350s CMD curl -f -L http://localhost:11334/ || exit 1

@ -1,3 +1,4 @@
try_fallback = true;
path = "/dkim/$domain.$selector.key";
use_esld = false;
allow_username_mismatch = true;

@ -0,0 +1,34 @@
rule "local" {
# Fuzzy storage server list
servers = "localhost:11335";
# Default symbol for unknown flags
symbol = "LOCAL_FUZZY_UNKNOWN";
# Additional mime types to store/check
mime_types = ["application/*"];
# Hash weight threshold for all maps
max_score = 20.0;
# Whether we can learn this storage
read_only = no;
# Ignore unknown flags
skip_unknown = yes;
# Hash generation algorithm
algorithm = "mumhash";
# Map flags to symbols
fuzzy_map = {
LOCAL_FUZZY_DENIED {
# Local threshold
max_score = 20.0;
# Flag to match
flag = 11;
}
LOCAL_FUZZY_PROB {
max_score = 10.0;
flag = 12;
}
LOCAL_FUZZY_WHITE {
max_score = 2.0;
flag = 13;
}
}
}

@ -0,0 +1 @@
servers = "{{ HOST_REDIS }}";

@ -0,0 +1,19 @@
group "fuzzy" {
max_score = 12.0;
symbol "LOCAL_FUZZY_UNKNOWN" {
weight = 5.0;
description = "Generic fuzzy hash match";
}
symbol "LOCAL_FUZZY_DENIED" {
weight = 12.0;
description = "Denied fuzzy hash";
}
symbol "LOCAL_FUZZY_PROB" {
weight = 5.0;
description = "Probable fuzzy hash";
}
symbol "LOCAL_FUZZY_WHITE" {
weight = -2.1;
description = "Whitelisted fuzzy hash";
}
}

@ -1,3 +1,4 @@
type = "controller";
bind_socket = "*:11334";
password = "mailu";
secure_ip = "{{ FRONT_ADDRESS }}";
secure_ip = "{% if POD_ADDRESS_RANGE %}{{ POD_ADDRESS_RANGE }}{% else %}{{ FRONT_ADDRESS }}{% endif %}";

@ -0,0 +1,6 @@
type = "fuzzy";
bind_socket = "*:11335";
count = 1;
backend = "redis";
expire = 90d;
allow_update = ["127.0.0.1"];

@ -1 +1,2 @@
type = "normal";
enabled = false;

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
import jinja2
import os
@ -9,12 +9,11 @@ from tenacity import retry
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
@retry(stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
def resolve():
os.environ["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
# Actual startup script
resolve()
resolve = retry(socket.gethostbyname, stop=tenacity.stop_after_attempt(100), wait=tenacity.wait_random(min=2, max=5))
os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
if "HOST_REDIS" not in os.environ: os.environ["HOST_REDIS"] = "redis"
for rspamd_file in glob.glob("/conf/*"):

@ -0,0 +1,23 @@
FROM alpine:3.8
# python3 shared with most images
RUN apk add --no-cache \
python3 py3-pip \
&& pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2
# Image specific layers under this line
RUN apk add --no-cache unbound curl bind-tools \
&& curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \
&& chown root:unbound /etc/unbound \
&& chmod 775 /etc/unbound \
&& apk del --no-cache curl \
&& /usr/sbin/unbound-anchor -a /etc/unbound/trusted-key.key | true
COPY start.py /start.py
COPY unbound.conf /unbound.conf
EXPOSE 53/udp 53/tcp
CMD /start.py
HEALTHCHECK CMD dig @127.0.0.1 || exit 1

@ -0,0 +1,9 @@
#!/usr/bin/python3
import jinja2
import os
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
convert("/unbound.conf", "/etc/unbound/unbound.conf")
os.execv("/usr/sbin/unbound", ["-c /etc/unbound/unbound.conf"])

@ -0,0 +1,19 @@
server:
verbosity: 1
interface: 0.0.0.0
interface: ::0
logfile: /dev/stdout
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
do-daemonize: no
access-control: {{ SUBNET }} allow
directory: "/etc/unbound"
username: root
auto-trust-anchor-file: trusted-key.key
root-hints: "/etc/unbound/root.hints"
hide-identity: yes
hide-version: yes
max-udp-size: 4096
msg-buffer-size: 65552

@ -4,15 +4,19 @@ RUN mkdir -p /app
WORKDIR /app
COPY requirements.txt requirements.txt
RUN apk add --no-cache git \
RUN apk add --no-cache git curl \
&& pip install -r requirements.txt
COPY server.py ./server.py
COPY setup.py ./setup.py
COPY main.py ./main.py
COPY flavors /data/master/flavors
COPY templates /data/master/templates
RUN python setup.py https://github.com/mailu/mailu /data
#RUN python setup.py https://github.com/mailu/mailu /data
EXPOSE 80/tcp
CMD gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload main:app
HEALTHCHECK CMD curl -f -L http://localhost/ || exit 1

@ -9,5 +9,6 @@ services:
setup:
image: mailu/setup
ports:
- "80:80"
- "8000:80"
build: .

@ -1,124 +1,148 @@
{% set env='mailu.env' %}
# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for {{ flavor }} flavor
version: '2'
version: '3.6'
services:
# External dependencies
redis:
image: redis:alpine
restart: always
volumes:
- "$ROOT/redis:/data"
- "{{ root }}/redis:/data"
# Core services
front:
image: mailu/nginx:{{ version }}
restart: always
env_file: {{ env }}
env:
- TLS_FLAVOR={{ tls_flavor or 'letsencrypt' }}
- ADMIN={{ expose_admin or 'no' }}
ports:
{% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %}
{% if bind4 %}
- "$PUBLIC_IPV4:{{ port }}:{{ port }}"
- "{{ bind4 }}:{{ port }}:{{ port }}"
{% endif %}
{% if bind6 %}
- "$PUBLIC_IPV6:{{ port }}:{{ port }}"
- "{{ bind6 }}:{{ port }}:{{ port }}"
{% endif %}
{% endfor %}
{% if flavor in ('cert', 'mail') %}
volumes:
- "$ROOT/certs:/certs"
{% endif %}
- "{{ root }}/certs:/certs"
{% if resolver_enabled %}
resolver:
image: mailu/unbound:{{ version }}
env_file: {{ env }}
restart: always
networks:
default:
ipv4_address: {{ dns }}
{% endif %}
admin:
image: mailu/admin:{{ version }}
restart: always
env_file: {{ env }}
{% if not expose_admin %}
{% if not admin_enabled %}
ports:
- 127.0.0.1:8080:80
{% endif %}
volumes:
- "$ROOT/data:/data"
- "$ROOT/dkim:/dkim"
- "{{ root }}/data:/data"
- "{{ root }}/dkim:/dkim"
depends_on:
- redis
imap:
image: mailu/dovecot:{{ version }}
restart: always
env_file: {{ env }}
volumes:
- "$ROOT/data:/data"
- "$ROOT/mail:/mail"
- "$ROOT/overrides:/overrides"
- "{{ root }}/mail:/mail"
- "{{ root }}/overrides:/overrides"
depends_on:
- front
smtp:
image: mailu/postfix:{{ version }}
restart: always
env_file: {{ env }}
volumes:
- "$ROOT/data:/data"
- "$ROOT/overrides:/overrides"
- "{{ root }}/overrides:/overrides"
depends_on:
- front
{% if resolver_enabled %}
- resolver
dns:
- {{ dns }}
{% endif %}
# Optional services
{% if enable_antispam %}
{% if antispam_enabled %}
antispam:
image: mailu/rspamd:{{ version }}
restart: always
env_file: {{ env }}
volumes:
- "$ROOT/filter:/var/lib/rspamd"
- "$ROOT/dkim:/dkim"
- "$ROOT/overrides/rspamd:/etc/rspamd/override.d"
- "{{ root }}/filter:/var/lib/rspamd"
- "{{ root }}/dkim:/dkim"
- "{{ root }}/overrides/rspamd:/etc/rspamd/override.d"
depends_on:
- front
{% if resolver_enabled %}
- resolver
dns:
- {{ dns }}
{% endif %}
{% endif %}
{% if enable_antivirus %}
{% if antivirus_enabled %}
antivirus:
image: mailu/clamav:{{ version }}
restart: always
env_file: {{ env }}
volumes:
- "$ROOT/filter:/data"
- "{{ root }}/filter:/data"
{% if resolver_enabled %}
depends_on:
- resolver
dns:
- {{ dns }}
{% endif %}
{% endif %}
{% if enable_webdav %}
{% if webdav_enabled %}
webdav:
image: mailu/radivale:{{ version }}
restart: always
image: mailu/radicale:{{ version }}
env_file: {{ env }}
volumes:
- "$ROOT/dav:/data"
- "{{ root }}/dav:/data"
{% endif %}
{% if enable_fetchmail %}
{% if fetchmail_enabled %}
fetchmail:
image: mailu/fetchmail:{{ version }}
restart: always
env_file: {{ env }}
volumes:
- "$ROOT/data:/data"
{% if resolver_enabled %}
depends_on:
- resolver
dns:
- {{ dns }}
{% endif %}
{% endif %}
# Webmail
{% if enable_webmail %}
{% if webmail_type != 'none' %}
webmail:
image: mailu/{{ webmail }}:{{ version }}
restart: always
image: mailu/{{ webmail_type }}:{{ version }}
env_file: {{ env }}
volumes:
- "$ROOT/webmail:/data"
- "{{ root }}/webmail:/data"
depends_on:
- imap
{% endif %}
{% if resolver_enabled %}
networks:
default:
driver: bridge
ipam:
driver: default
config:
- subnet: {{ subnet }}
{% endif %}

@ -1,5 +1,7 @@
# Mailu main configuration file
#
# Generated for {{ flavor }} flavor
#
# This file is autogenerated by the configuration management wizard.
# For a detailed list of configuration variables, see the documentation at
# https://mailu.io
@ -9,60 +11,121 @@
###################################
# Set this to the path where Mailu data and configuration is stored
ROOT=/mailu
# This variable is now set directly in `docker-compose.yml by the setup utility
# ROOT={{ root }}
# Mailu version to run (1.0, 1.1, etc. or master)
#VERSION={{ version }}
# Set to a randomly generated 16 bytes string
SECRET_KEY={{ secret(16) }}
# Address where listening ports should bind
{% if bind4 %}PUBLIC_IPV4={{ bind4 }}{% endif %}
{% if bind6 %}PUBLIC_IPV6={{ bind6 }}{% endif %}
# This variables are now set directly in `docker-compose.yml by the setup utility
# PUBLIC_IPV4= {{ bind4 }} (default: 127.0.0.1)
# PUBLIC_IPV6= {{ bind6 }} (default: ::1)
# Mail address of the postmaster
POSTMASTER={{ postmaster }}
# Subnet
SUBNET={{ subnet }}
# Main mail domain
DOMAIN={{ domain }}
# Hostnames for this server, separated with comas
HOSTNAMES={{ hostnames }}
# Postmaster local part (will append the main mail domain)
POSTMASTER={{ postmaster }}
# Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt)
TLS_FLAVOR={{ tls_flavor }}
# Authentication rate limit (per source IP address)
AUTH_RATELIMIT={{ auth_ratelimit }}
{% if auth_ratelimit_pm > '0' and auth_ratelimit_ph > '0' %}
AUTH_RATELIMIT={{ auth_ratelimit_pm }}/minute;{{ auth_ratelimit_ph }}/hour
{% endif %}
# Opt-out of statistics, replace with "True" to opt out
DISABLE_STATISTICS={{ disable_statistics }}
DISABLE_STATISTICS={{ disable_statistics or 'False' }}
###################################
# Server behavior
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN={{ admin_enabled or 'false' }}
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL={{ webmail_type }}
# Dav server implementation (value: radicale, none)
WEBDAV={{ webdav_enabled or 'none' }}
# Antivirus solution (value: clamav, none)
#ANTIVIRUS={{ antivirus_enabled or 'none' }}
#Antispam solution
ANTISPAM={{ antispam_enabled or 'none'}}
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
MESSAGE_SIZE_LIMIT={{ message_size_limit }}
MESSAGE_SIZE_LIMIT={{ message_size_limit or '50000000' }}
# Networks granted relay permissions, make sure that you include your Docker
# internal network (default to 172.17.0.0/16)
RELAYNETS={{ relaynets }}
RELAYNETS={{ relaynets or '172.17.0.0/16' }}
# Will relay all outgoing mails if configured
RELAYHOST={{ relayhost }}
# Fetchmail delay
FETCHMAIL_DELAY={{ fetchmail_delay }}
FETCHMAIL_DELAY={{ fetchmail_delay or '600' }}
# Recipient delimiter, character used to delimiter localpart from custom address part
RECIPIENT_DELIMITER={{ recipient_delimiter }}
RECIPIENT_DELIMITER={{ recipient_delimiter or '+' }}
{% if dmarc_rua or dmarc_ruf %}
# DMARC rua and ruf email
{% if dmarc_rua %}DMARC_RUA={{ dmarc_rua }}{% endif %}
{% if dmarc_ruf %}DMARC_RUF={{ dmarc_ruf }}{% endif %}
{% endif %}
DMARC_RUA={{ dmarc_rua or 'admin' }}
DMARC_RUF={{ dmarc_ruf or 'admin' }}
{% if welcome_enabled %}
# Welcome email, enable and set a topic and body if you wish to send welcome
# emails to all users.
WELCOME={{ welcome_enable }}
WELCOME_SUBJECT={{ welcome_subject }}
WELCOME_BODY={{ welcome_body }}
WELCOME={{ welcome_enable or 'false' }}
WELCOME_SUBJECT={{ welcome_subject or 'Welcome to your new email account' }}
WELCOME_BODY={{ welcome_body or 'Welcome to your new email account, if you can read this, then it is configured properly!' }}
{% endif %}
# Maildir Compression
# choose compression-method, default: none (value: bz2, gz)
COMPRESSION={{ compression }}
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL={{ compression_level }}
###################################
# Web settings
###################################
# Path to the admin interface if enabled
WEB_ADMIN={{ admin_path }}
# Path to the webmail if enabled
WEB_WEBMAIL={{ webmail_path }}
# Website name
SITENAME={{ site_name }}
# Linked Website URL
WEBSITE={{ website }}
{% if recaptcha_public_key and recaptcha_private_key %}
# Registration reCaptcha settings (warning, this has some privacy impact)
# RECAPTCHA_PUBLIC_KEY={{ recaptcha_public_key }}
# RECAPTCHA_PRIVATE_KEY={{ recaptcha_private_key }}
{% endif %}
{% if domain_registration %}
@ -70,39 +133,28 @@ WELCOME_BODY={{ welcome_body }}
DOMAIN_REGISTRATION=true
{% endif %}
###################################
# Web settings
###################################
# Path to the admin interface if enabled
WEB_ADMIN=/admin
# Path to the webmail if enabled
WEB_WEBMAIL=/webmail
# Website name
SITENAME=Mailu
# Linked Website URL
WEBSITE=https://mailu.io
{% if recaptcha_public_key and recaptcha_private_key %}
# Registration reCaptcha settings (warning, this has some privacy impact)
# RECAPTCHA_PUBLIC_KEY={{ recaptcha_public_key }}
# RECAPTCHA_PRIVATE_KEY={{ recaptcha_private_key }}
{% endif %}
###################################
# Advanced settings
###################################
{% if password_scheme %}
# Specific password storage scheme
PASSWORD_SCHEME={{ password_scheme }}
{% endif %}
# Log driver for front service. Possible values:
# json-file (default)
# journald (On systemd platforms, useful for Fail2Ban integration)
# syslog (Non systemd platforms, Fail2Ban integration. Disables `docker-compose log` for front!)
LOG_DRIVER={{ log_driver or 'json-file' }}
# Docker-compose project name, this will prepended to containers names.
COMPOSE_PROJECT_NAME={{ compose_project_name or 'mailu' }}
# Default password scheme used for newly created accounts and changed passwords
# (value: BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME={{ password_scheme or 'BLF-CRYPT' }}
# Header to take the real ip from
REAL_IP_HEADER={{ real_ip_header }}
# IPs for nginx set_real_ip_from (CIDR list separated by commas)
REAL_IP_FROM={{ real_ip_from }}
# choose wether mailu bounces (no) or rejects (yes) mail when recipient is unknown (value: yes, no)
REJECT_UNLISTED_RECIPIENT={{ reject_unlisted_recipient }}

@ -4,15 +4,15 @@
<p>Docker Compose expects a project file, named <code>docker-compose.yml</code>
in a project directory. First create your project directory.</p>
<pre><code>mkdir /mailu
<pre><code>mkdir {{ root }}
</pre></code>
<p>Then download the project file. A side configuration file makes it easier
to read and check the configuration variables generated by the wizard.</p>
<pre><code>cd /mailu
wget {{ url_for('.file', uid=uid, filepath='docker-compose.yml', _external=True) }}
wget {{ url_for('.file', uid=uid, filepath='mailu.env', _external=True) }}
<pre><code>cd {{ root }}
curl {{ url_for('.file', uid=uid, filepath='docker-compose.yml', _external=True) }} > docker-compose.yml
curl {{ url_for('.file', uid=uid, filepath='mailu.env', _external=True) }} > mailu.env
</pre></code>
{% endcall %}
@ -30,7 +30,22 @@ files before going any further.</p>
<p>To start your compose project, simply run the Docker Compose <code>up</code>
command.</p>
<pre><code>cd /mailu
<pre><code>cd {{ root }}
docker-compose up -d
</pre></code>
Before you can use Mailu, you must create the primary administrator user account. This should be {{ postmaster }}@{{ domain }}. Use the following command, changing PASSWORD to your liking:
<pre><code>docker-compose exec admin python manage.py admin {{ postmaster }} {{ domain }} PASSWORD
</pre></code>
<p>Login to the admin interface to change the password for a safe one, at
{% if admin_enabled %}
one of the hostnames
<a href="https://{{ hostnames.split(',')[0] }}{{ admin_path }}">{{ hostnames.split(',')[0] }}{{ admin_path }}</a>.
{% else %}
<a href="http://127.0.0.1:8080">http://127.0.0.1:8080</a> (only directly from the host running docker).
{% endif %}
And choose the "Update password" option in the left menu.
</p>
{% endcall %}

@ -0,0 +1,153 @@
{% set env='mailu.env' %}
# This file is auto-generated by the Mailu configuration wizard.
# Please read the documentation before attempting any change.
# Generated for {{ flavor }} flavor
version: '3.6'
services:
# External dependencies
redis:
image: redis:alpine
restart: always
volumes:
- "{{ root }}/redis:/data"
# Core services
front:
image: mailu/nginx:{{ version }}
env_file: {{ env }}
ports:
{% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %}
- target: {{ port }}
published: {{ port }}
mode: overlay
{% endfor %}
volumes:
- "{{ root }}/certs:/certs"
deploy:
replicas: 1
{% if resolver_enabled %}
resolver:
image: mailu/unbound:{{ version }}
env_file: {{ env }}
networks:
default:
ipv4_address: {{ dns }}
{% endif %}
admin:
image: mailu/admin:{{ version }}
env_file: {{ env }}
{% if not admin_enabled %}
ports:
- 127.0.0.1:8080:80
{% endif %}
volumes:
- "{{ root }}/data:/data"
- "{{ root }}/dkim:/dkim"
deploy:
replicas: 1
imap:
image: mailu/dovecot:{{ version }}
env_file: {{ env }}
environment:
# Default to 10.0.1.0/24
- POD_ADDRESS_RANGE={{ subnet }}
volumes:
- "{{ root }}/mail:/mail"
- "{{ root }}/overrides:/overrides"
deploy:
replicas: 1
smtp:
image: mailu/postfix:{{ version }}
env_file: {{ env }}
environment:
- POD_ADDRESS_RANGE={{ subnet }}
volumes:
- "{{ root }}/overrides:/overrides"
deploy:
replicas: 1
{% if resolver_enabled %}
dns:
- {{ dns }}
{% endif %}
# Optional services
{% if antispam_enabled %}
antispam:
image: mailu/rspamd:{{ version }}
env_file: {{ env }}
environment:
- POD_ADDRESS_RANGE={{ subnet }}
volumes:
- "{{ root }}/filter:/var/lib/rspamd"
- "{{ root }}/dkim:/dkim"
- "{{ root }}/overrides/rspamd:/etc/rspamd/override.d"
deploy:
replicas: 1
{% if resolver_enabled %}
dns:
- {{ dns }}
{% endif %}
{% endif %}
{% if antivirus_enabled %}
antivirus:
image: mailu/clamav:{{ version }}
env_file: {{ env }}
volumes:
- "{{ root }}/filter:/data"
deploy:
replicas: 1
{% if resolver_enabled %}
dns:
- {{ dns }}
{% endif %}
{% endif %}
{% if webdav_enabled %}
webdav:
image: mailu/none:{{ version }}
env_file: {{ env }}
volumes:
- "{{ root }}/dav:/data"
deploy:
replicas: 1
{% endif %}
{% if fetchmail_enabled %}
fetchmail:
image: mailu/fetchmail:{{ version }}
env_file: {{ env }}
volumes:
- "{{ root }}/data:/data"
deploy:
replicas: 1
{% if resolver_enabled %}
dns:
- {{ dns }}
{% endif %}
{% endif %}
{% if webmail_type != 'none' %}
webmail:
image: mailu/roundcube:{{ version }}
env_file: {{ env }}
volumes:
- "{{ root }}/webmail:/data"
deploy:
replicas: 1
{% endif %}
networks:
default:
driver: overlay
ipam:
driver: default
config:
- subnet: {{ subnet }}

@ -0,0 +1 @@
../compose/mailu.env

@ -0,0 +1,60 @@
{% import "macros.html" as macros %}
{% call macros.panel("info", "Step 1 - Download your configuration files") %}
<p>Docker Stack expects a project file, named <code>docker-compose.yml</code>
in a project directory. First create your project directory.</p>
<pre><code>mkdir -p /{{ root }}/{redis,certs,data,dkim,mail,overrides/rspamd,filter,dav,webmail}
</pre></code>
<p>Then download the project file. A side configuration file makes it easier
to read and check the configuration variables generated by the wizard.</p>
<pre><code>cd {{ root }}
curl {{ url_for('.file', uid=uid, filepath='docker-compose.yml', _external=True) }} > docker-compose.yml
curl {{ url_for('.file', uid=uid, filepath='mailu.env', _external=True) }} > mailu.env
</pre></code>
{% endcall %}
{% call macros.panel("info", "Step 2 - Review the configuration") %}
<p>We did not insert any malicious code on purpose in the configurations we
distribute, but your download could have been intercepted, or our wizard
website could have been compromised, so make sure you check the configuration
files before going any further.</p>
<p>When you are done checking them, check them one last time.</p>
{% endcall %}
{% call macros.panel("info", "Step 3 - Deploy docker stack") %}
<p>To deploy the docker stack use the following commands. For more information about setting up docker swarm nodes read the
<a href="https://docs.docker.com/get-started">docker documentation</a></p>
<pre><code>cd {{ root }}
docker swarm init
docker stack deploy -c docker-compose.yml mailu
</pre></code>
In the docker stack deploy command, mailu is the app name. Feel free to change it.<br/>
In order to display the running container you can use<br/>
<pre><code>docker ps</code></pre>
or
<pre><code>docker stack ps --no-trunc mailu</code></pre>
Command for removing docker stack is
<pre><code>docker stack rm mailu</code></pre>
Before you can use Mailu, you must create the primary administrator user account. This should be {{ postmaster }}@{{ domain }}. Use the following command, changing PASSWORD to your liking:
<pre><code>docker exec $(docker ps | grep admin | cut -d ' ' -f1) python manage.py admin {{ postmaster }} {{ domain }} PASSWORD
</pre></code>
<p>Login to the admin interface to change the password for a safe one, at
{% if admin_enabled %}
one of the hostnames
<a href="https://{{ hostnames.split(',')[0] }}{{ admin_path }}">{{ hostnames.split(',')[0] }}{{ admin_path }}</a>.
{% else %}
<a href="http://127.0.0.1:8080">http://127.0.0.1:8080</a> (only directly from the host running docker).
{% endif %}
And choose the "Update password" option in the left menu.
</p>
{% endcall %}

@ -7,6 +7,7 @@ import jinja2
import uuid
import string
import random
import ipaddress
app = flask.Flask(__name__)
@ -32,9 +33,11 @@ def secret(length=16):
def build_app(path):
#Hardcoded master as the only version for test purposes
versions = [
version for version in os.listdir(path)
if os.path.isdir(os.path.join(path, version))
# version for version in os.listdir(path)
# if os.path.isdir(os.path.join(path, version))
"master"
]
app.jinja_env.trim_blocks = True
@ -63,10 +66,17 @@ def build_app(path):
def wizard():
return flask.render_template('wizard.html')
@bp.route("/submit_flavor", methods=["POST"])
def submit_flavor():
data = flask.request.form.copy()
steps = sorted(os.listdir(path + "/" + version + "/templates/steps/" + data["flavor"]))
return flask.render_template('wizard.html', flavor=data["flavor"], steps=steps)
@bp.route("/submit", methods=["POST"])
def submit():
data = flask.request.form.copy()
data['uid'] = str(uuid.uuid4())
data['dns'] = str(ipaddress.IPv4Network(data['subnet'])[-2])
db.set(data['uid'], json.dumps(data))
return flask.redirect(flask.url_for('.setup', uid=data['uid']))

@ -8,7 +8,7 @@
<h1>Mailu configuration</h1>
<p>
Version
<select onchange="window.location.href=this.value;">
<select onchange="window.location.href=this.value;" class="btn btn-primary dropdown-toggle">
{% for available in versions %}
<option value="{{ url_for('{}.wizard'.format(available)) }}" {% if available == version %}selected{% endif %}>{{ available }}</option>
{% endfor %}

@ -9,10 +9,10 @@
</div>
{% endmacro %}
{% macro radio(name, value, emph, text) %}
{% macro radio(name, value, emph, text, current) %}
<div class="radio">
<label>
<input type="radio" name="{{ name }}" value="{{ value }}">
<input type="radio" name="{{ name }}" value="{{ value }}"{% if current == value %} checked="checked"{% endif %}>
{% if emph %}
<strong>{{ emph }}</strong>,
{% endif %}

@ -3,31 +3,27 @@
interface, Web email clients (webmails), antispam, antivirus, etc. If you
wish to disable some of these features, you are free to do so.</p>
<p>The admin interface is the main Mailu-specific bit, it provides tools to
manage your email domains, users, etc.</p>
<div class="form-group">
<label>Enable the admin UI (and path to the admin UI)</label>
<div class="input-group">
<div class="input-group-addon"><input type="checkbox" name="admin_enabled" checked></div>
<input class="form-control" type="text" name="admin_path" value="/admin">
</div>
</div>
<p>Emails will be available through IMAP and POP3. You may also enable a Web
email client. These do add some complexity but provide an easier way of
accessing messages for beginner users.</p>
<!-- Switched from radio buttons to dropdown menu in order to remove the checkbox -->
<div class="form-group">
<label>Enable Web email client (and path to the Web email client)</label>
<div class="input-group">
<div class="input-group-addon"><input type="checkbox" name="webmail_enabled" checked></div>
<input class="form-control" type="text" name="webmail_path" value="/webmail">
</div>
<!-- <div class="radio"> -->
<!-- {{ macros.radio("webmail_type", "roundcube", "RoundCube", "popular Webmail running on top of PHP") }} -->
<!-- {{ macros.radio("webmail_type", "rainloop", "Rainloop", "lightweight Webmail based on PHP, no database") }} -->
<!-- </div> -->
<br/>
<select class="btn btn-primary dropdown-toggle" name="webmail_type">
{% for webmailtype in ["none", "roundcube", "rainloop"] %}
<option value="{{ webmailtype }}" >{{ webmailtype }}</option>
{% endfor %}
</select>
<p></p>
<div class="radio">
{{ macros.radio("webmail_type", "roundcube", "RoundCube", "popular Webmail running on top of PHP") }}
{{ macros.radio("webmail_type", "rainloop", "Rainloop", "lightweight Webmail based on PHP, no database") }}
<div class="input-group">
<!-- <div class="input-group-addon"><input type="checkbox" name="webmail_enabled" value="true"></div> -->
<input class="form-control" type="text" name="webmail_path" value="/webmail">
</div>
</div>
@ -38,15 +34,29 @@ also disable the antivirus if required (it does use aroung 1GB of ram).</p>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="antispam_enabled" checked>
Enable the filtering service
<input class="form-check-input" type="checkbox" name="antispam_enabled" value="rspamd" checked>
Enable the spam filtering service
</label>
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="antivirus_enabled" checked>
<input class="form-check-input" type="checkbox" name="antivirus_enabled" value="clamav">
Enable the antivirus service
</label>
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="webdav_enabled" value="radicale">
Enable the webdav service
</label>
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="fetchmail_enabled" value="true">
Enable fetchmail
</label>
</div>
{% endcall %}

@ -0,0 +1,52 @@
{% call macros.panel("info", "Step 4 - expose Mailu to the world") %}
<p>A mail server must be exposed to the world to receive emails, send emails,
and let users access their mailboxes. Mailu has some flexibility in the way
you expose it to the world.</p>
<p>Among Mailu services, the <em>front</em> server is the one accepting connections,
be it directly from the outside world, through a reverse proxy or in any
complex configuration that you might want to setup. It needs to listen on some
IP addresses in order to expose its public services. You must at least setup
an IPv4 or an IPv6 address if you wish to access Mailu.</p>
<p><span class="label label-warning">Warning</span> You must use specific addresses, please
avoid generic all-interfaces addresses like <code>0.0.0.0</code> or <code>::</code>.</p>
<div class="form-group">
<label>IPv4 listen address</label>
<!-- Validates IPv4 address -->
<input class="form-control" type="text" name="bind4" value="127.0.0.1"
pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
</div>
<div class="form-group">
<label>IPv6 listen address</label>
<!-- Validates IPv6 address -->
<input class="form-control" type="text" name="bind6" value="::1"
pattern="^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$">
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="resolver_enabled" value="true">
Enable unbound resolver
</label>
</div>
<div class="form-group">
<label>Subnet</label>
<input class="form-control" type="text" name="subnet" required pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))$"
value="192.168.0.0/24">
</div>
<p>You server will be available under a main hostname but may expose multiple public
hostnames. Every e-mail domain that points to this server must have one of the
hostnames in its <code>MX</code> record. Hostnames must be coma-separated.</p>
<div class="form-group">
<label>Public hostnames</label>
<!-- Validates hostname or list of hostnames -->
<input class="form-control" type="text" name="hostnames" placeholder="my.host.name,other.host.name" multiple required
pattern="^(?:(?:\w+(?:-+\w+)*\.)+[a-z]+)*(?:,(?:(?:\w+(?:-+\w+)*\.)+[a-z]+)\s*)*$">
</div>
{% endcall %}

@ -0,0 +1,78 @@
{% call macros.panel("info", "Step 2 - Initial configuration") %}
<p>Before starting some variables must be set</p>
<div class="form-group">
<label>Root path: </label>
<!-- Validates path -->
<input class="form-control" type="text" name="root" value="/mailu" required pattern="^/[-_A-Za-z0-9]+(/[-_A-Za-z0-9]*)*">
</div>
<p>In the next sections we need to set the postmaster address. This is a combination from the <i>postmaster</i> local part and the <i>main mail domain</i>.
The <i>main mail domain</i> is also used as </i>"server display name"</i>. This is the way the SMTP server identifies himself when connecting to others.
The Postmaster will get an e-mail address &lt;postmaster&gt;@&lt;main_domain&gt;. This address will receive the DMARC "rua" and "ruf" reports.
Or in plain english: if receivers start to classify your mail as spam, this postmaster will be informed.</p>
<div class="form-group">
<label>
Main mail domain and server display name.
</label>
<!-- Validates domain name -->
<input class="form-control" type="text" name="domain" placeholder="e.g. mailu.io"
required pattern="^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$">
</div>
<div class="form-group">
<label>Postmaster local part</label>
<input class="form-control" type="text" name="postmaster" value="admin" required>
</div>
<div class="form-group">
<label>Choose how you wish to handle security (TLS) certificates</label>
<br/>
<select class="btn btn-primary dropdown-toggle" name="tls_flavor">
{% for tlsflavor in ["letsencrypt", "cert", "notls", "mail", "mail-letsencrypt"] %}
<option value="{{ tlsflavor }}" >{{ tlsflavor }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label>Authentication rate limit (per source IP address)</label>
<!-- Validates number input only -->
<p><input class="form-control" style="width: 7%; display: inline;" type="number" name="auth_ratelimit_pm"
value="10" required >/minute;
<input class="form-control" style="width: 7%; display: inline;;" type="number" name="auth_ratelimit_ph"
value="1000" required >/hour</p>
</div>
<div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="disable_statistics" value="True">
Opt-out of statistics
</label>
</div>
<div class="form-group">
<label>Website name</label>
<input class="form-control" type="text" name="site_name" value="Mailu" required>
</div>
<div class="form-group">
<label>Linked Website URL</label>
<!-- Validates url with or without https:// -->
<input class="form-control" type="url" name="website" value="https://mailu.io" required
pattern="^(https?://)?([a-zA-Z0-9]([a-zA-ZäöüÄÖÜ0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$">
</div>
<p>The admin interface is the main Mailu-specific bit, it provides tools to
manage your email domains, users, etc.</p>
<div class="form-group">
<label>Enable the admin UI (and path to the admin UI)</label>
<div class="input-group">
<div class="input-group-addon"><input type="checkbox" name="admin_enabled" value="true"></div>
<input class="form-control" type="text" name="admin_path" value="/admin">
</div>
</div>
{% endcall %}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save