Merge branch 'master' into feat-fuzzyhashes

master
ofthesun9 6 years ago
commit 0eb963eb21

@ -0,0 +1,10 @@
rules:
default: null
branches:
master:
protection:
required_status_checks:
contexts:
- continuous-integration/travis-ci
required_pull_request_reviews:
required_approving_review_count: 2

@ -1,8 +1,11 @@
language: python sudo: required
python: services: docker
- "3.6" addons:
install: apt:
- pip install -r docs/requirements.txt packages:
- docker-ce
env:
- VERSION=$TRAVIS_BRANCH
script: script:
- sphinx-versioning build -b -B 1.5 -r 1.5 -w '^[0-9.]*$' -w master -W '^$' docs/ build/ - docker-compose -f tests/build.yml -p Mailu build
- python "docs/conf.py" "build" "$DEPLOY_HOST" "$DEPLOY_USERNAME" "$DEPLOY_PASSWORD" "$DEPLOY_REMOTEDIR"

@ -1,4 +1,5 @@
![Logo](docs/assets/logo.png) <p align="leftr"><img src="docs/assets/logomark.png" alt="Mailu" height="200px"></p>
Mailu is a simple yet full-featured mail server as a set of Docker images. Mailu is a simple yet full-featured mail server as a set of Docker images.
It is free software (both as in free beer and as in free speech), open to It is free software (both as in free beer and as in free speech), open to

@ -17,5 +17,6 @@ COPY start.sh /start.sh
RUN pybabel compile -d mailu/translations RUN pybabel compile -d mailu/translations
EXPOSE 80/tcp EXPOSE 80/tcp
VOLUME ["/data"]
CMD ["/start.sh"] CMD ["/start.sh"]

@ -1,3 +1,5 @@
from flask_limiter import RateLimitExceeded
from mailu import limiter from mailu import limiter
import socket import socket
@ -6,6 +8,14 @@ import flask
internal = flask.Blueprint('internal', __name__) internal = flask.Blueprint('internal', __name__)
@internal.app_errorhandler(RateLimitExceeded)
def rate_limit_handler(e):
response = flask.Response()
response.headers['Auth-Status'] = 'Authentication rate limit from one source exceeded'
response.headers['Auth-Error-Code'] = '451 4.3.2'
if int(flask.request.headers['Auth-Login-Attempt']) < 10:
response.headers['Auth-Wait'] = '3'
return response
@limiter.request_filter @limiter.request_filter
def whitelist_webmail(): def whitelist_webmail():

@ -6,6 +6,7 @@ import flask_login
import flask_wtf import flask_wtf
import re import re
LOCALPART_REGEX = "^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+$"
class DestinationField(fields.SelectMultipleField): class DestinationField(fields.SelectMultipleField):
""" Allow for multiple emails selection from current user choices and """ Allow for multiple emails selection from current user choices and
@ -49,7 +50,7 @@ class DomainForm(flask_wtf.FlaskForm):
max_quota_bytes = fields_.IntegerSliderField(_('Maximum user quota'), default=0) max_quota_bytes = fields_.IntegerSliderField(_('Maximum user quota'), default=0)
signup_enabled = fields.BooleanField(_('Enable sign-up'), default=False) signup_enabled = fields.BooleanField(_('Enable sign-up'), default=False)
comment = fields.StringField(_('Comment')) comment = fields.StringField(_('Comment'))
submit = fields.SubmitField(_('Create')) submit = fields.SubmitField(_('Save'))
class DomainSignupForm(flask_wtf.FlaskForm): class DomainSignupForm(flask_wtf.FlaskForm):
@ -63,18 +64,18 @@ class DomainSignupForm(flask_wtf.FlaskForm):
class AlternativeForm(flask_wtf.FlaskForm): class AlternativeForm(flask_wtf.FlaskForm):
name = fields.StringField(_('Alternative name'), [validators.DataRequired()]) name = fields.StringField(_('Alternative name'), [validators.DataRequired()])
submit = fields.SubmitField(_('Create')) submit = fields.SubmitField(_('Save'))
class RelayForm(flask_wtf.FlaskForm): class RelayForm(flask_wtf.FlaskForm):
name = fields.StringField(_('Relayed domain name'), [validators.DataRequired()]) name = fields.StringField(_('Relayed domain name'), [validators.DataRequired()])
smtp = fields.StringField(_('Remote host')) smtp = fields.StringField(_('Remote host'))
comment = fields.StringField(_('Comment')) comment = fields.StringField(_('Comment'))
submit = fields.SubmitField(_('Create')) submit = fields.SubmitField(_('Save'))
class UserForm(flask_wtf.FlaskForm): class UserForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('E-mail'), [validators.DataRequired()]) localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000) quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000)
@ -86,7 +87,7 @@ class UserForm(flask_wtf.FlaskForm):
class UserSignupForm(flask_wtf.FlaskForm): class UserSignupForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('Email address'), [validators.DataRequired(), validators.Regexp("^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+$")]) localpart = fields.StringField(_('Email address'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
captcha = flask_wtf.RecaptchaField() captcha = flask_wtf.RecaptchaField()
@ -129,7 +130,7 @@ class TokenForm(flask_wtf.FlaskForm):
ip = fields.StringField( ip = fields.StringField(
_('Authorized IP'), [validators.Optional(), validators.IPAddress()] _('Authorized IP'), [validators.Optional(), validators.IPAddress()]
) )
submit = fields.SubmitField(_('Create')) submit = fields.SubmitField(_('Save'))
class AliasForm(flask_wtf.FlaskForm): class AliasForm(flask_wtf.FlaskForm):
@ -138,7 +139,7 @@ class AliasForm(flask_wtf.FlaskForm):
_('Use SQL LIKE Syntax (e.g. for catch-all aliases)')) _('Use SQL LIKE Syntax (e.g. for catch-all aliases)'))
destination = DestinationField(_('Destination')) destination = DestinationField(_('Destination'))
comment = fields.StringField(_('Comment')) comment = fields.StringField(_('Comment'))
submit = fields.SubmitField(_('Create')) submit = fields.SubmitField(_('Save'))
class AdminForm(flask_wtf.FlaskForm): class AdminForm(flask_wtf.FlaskForm):

@ -32,7 +32,9 @@
</a> </a>
</li> </li>
{% if current_user.manager_of or current_user.global_admin %}
<li class="header">{% trans %}Administration{% endtrans %}</li> <li class="header">{% trans %}Administration{% endtrans %}</li>
{% endif %}
{% if current_user.global_admin %} {% if current_user.global_admin %}
<li> <li>
<a href="{{ url_for('.announcement') }}"> <a href="{{ url_for('.announcement') }}">

@ -2,4 +2,4 @@
python manage.py advertise python manage.py advertise
python manage.py db upgrade python manage.py db upgrade
gunicorn -w 4 -b 0.0.0.0:80 -b [::]:80 --access-logfile - --error-logfile - --preload mailu:app gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload mailu:app

@ -1,14 +1,17 @@
FROM alpine:edge FROM alpine:3.7
RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \
&& apk add --no-cache \ && apk add --no-cache \
dovecot dovecot-sqlite dovecot-pigeonhole-plugin dovecot-pigeonhole-plugin-extdata \ dovecot dovecot-sqlite dovecot-pigeonhole-plugin dovecot-pigeonhole-plugin-extdata \
rspamd-client@testing python py-jinja2 dovecot-fts-lucene rspamd-client@testing python py-jinja2 py-pip \
&& pip install --upgrade pip \
&& pip install tenacity
COPY conf /conf COPY conf /conf
COPY sieve /var/lib/dovecot COPY sieve /var/lib/dovecot
COPY start.py /start.py COPY start.py /start.py
EXPOSE 110/tcp 143/tcp 993/tcp 4190/tcp 2525/tcp EXPOSE 110/tcp 143/tcp 993/tcp 4190/tcp 2525/tcp
VOLUME ["/data", "/mail"]
CMD /start.py CMD /start.py

@ -18,6 +18,20 @@ dict {
sieve = sqlite:/etc/dovecot/pigeonhole-sieve.dict sieve = sqlite:/etc/dovecot/pigeonhole-sieve.dict
} }
###############
# Full-text search
###############
mail_plugins = $mail_plugins fts fts_lucene
plugin {
fts = lucene
fts_autoindex = yes
fts_autoindex_exclude = \Junk
fts_lucene = whitespace_chars=@.
}
############### ###############
# Mailboxes # Mailboxes
############### ###############
@ -32,7 +46,7 @@ mail_access_groups = mail
maildir_stat_dirs = yes maildir_stat_dirs = yes
mailbox_list_index = yes mailbox_list_index = yes
mail_vsize_bg_after_count = 100 mail_vsize_bg_after_count = 100
mail_plugins = $mail_plugins quota quota_clone mail_plugins = $mail_plugins quota quota_clone zlib
namespace inbox { namespace inbox {
inbox = yes inbox = yes
@ -58,6 +72,14 @@ plugin {
quota = count:User quota quota = count:User quota
quota_vsizes = yes quota_vsizes = yes
quota_clone_dict = redis:host={{ REDIS_ADDRESS }}:port=6379:db=1 quota_clone_dict = redis:host={{ REDIS_ADDRESS }}:port=6379:db=1
{% if COMPRESSION in [ 'gz', 'bz2' ] %}
zlib_save = {{ COMPRESSION }}
{% endif %}
{% if COMPRESSION_LEVEL %}
zlib_save_level = {{ COMPRESSION_LEVEL }}
{% endif %}
} }
############### ###############

@ -1,3 +1,11 @@
require "vnd.dovecot.execute"; require ["vnd.dovecot.execute", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.mailbox" "*" {
set "mailbox" "${1}";
}
if string "${mailbox}" "Trash" {
stop;
}
execute :pipe "mailtrain" "ham"; execute :pipe "mailtrain" "ham";

@ -4,15 +4,20 @@ import jinja2
import os import os
import socket import socket
import glob import glob
import tenacity
from tenacity import retry
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
# Actual startup script @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["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
os.environ["REDIS_ADDRESS"] = socket.gethostbyname(os.environ.get("REDIS_ADDRESS", "redis")) os.environ["REDIS_ADDRESS"] = socket.gethostbyname(os.environ.get("REDIS_ADDRESS", "redis"))
if os.environ["WEBMAIL"] != "none": if os.environ["WEBMAIL"] != "none":
os.environ["WEBMAIL_ADDRESS"] = socket.gethostbyname(os.environ.get("WEBMAIL_ADDRESS", "webmail")) os.environ["WEBMAIL_ADDRESS"] = socket.gethostbyname(os.environ.get("WEBMAIL_ADDRESS", "webmail"))
# Actual startup script
resolve()
for dovecot_file in glob.glob("/conf/*"): for dovecot_file in glob.glob("/conf/*"):
convert(dovecot_file, os.path.join("/etc/dovecot", os.path.basename(dovecot_file))) convert(dovecot_file, os.path.join("/etc/dovecot", os.path.basename(dovecot_file)))

@ -1,4 +1,4 @@
FROM alpine:edge FROM alpine:3.7
RUN apk add --no-cache nginx nginx-mod-mail python py-jinja2 certbot openssl RUN apk add --no-cache nginx nginx-mod-mail python py-jinja2 certbot openssl
@ -6,5 +6,6 @@ COPY conf /conf
COPY *.py / COPY *.py /
EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 10025/tcp 10143/tcp EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 10025/tcp 10143/tcp
VOLUME ["/certs"]
CMD /start.py CMD /start.py

@ -84,15 +84,17 @@ http {
# Actual logic # Actual logic
{% if WEBMAIL != 'none' %} {% if WEBMAIL != 'none' %}
{% if WEB_WEBMAIL != '/' %}
location / { location / {
return 301 {{ WEB_WEBMAIL }}; return 301 {{ WEB_WEBMAIL }};
} }
{% endif %}
location {{ WEB_WEBMAIL }} { location {{ WEB_WEBMAIL }} {
rewrite ^({{ WEB_WEBMAIL }})$ $1/ permanent; rewrite ^({{ WEB_WEBMAIL }})$ $1/ permanent;
rewrite ^{{ WEB_WEBMAIL }}/(.*) /$1 break; rewrite ^{{ WEB_WEBMAIL }}/(.*) /$1 break;
include /etc/nginx/proxy.conf; include /etc/nginx/proxy.conf;
client_max_body_size 30M; client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
proxy_pass http://$webmail; proxy_pass http://$webmail;
} }
{% endif %} {% endif %}

@ -1,10 +1,13 @@
FROM alpine FROM alpine:3.7
RUN apk add --no-cache postfix postfix-sqlite postfix-pcre rsyslog python py-jinja2 RUN apk add --no-cache postfix postfix-sqlite postfix-pcre rsyslog python py-jinja2 py-pip \
&& pip install --upgrade pip \
&& pip install tenacity
COPY conf /conf COPY conf /conf
COPY start.py /start.py COPY start.py /start.py
EXPOSE 25/tcp 10025/tcp EXPOSE 25/tcp 10025/tcp
VOLUME ["/data"]
CMD /start.py CMD /start.py

@ -8,6 +8,7 @@ smtp inet n - n - - smtpd
10025 inet n - n - - smtpd 10025 inet n - n - - smtpd
-o smtpd_sasl_auth_enable=yes -o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit -o smtpd_recipient_restrictions=reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit
-o smtpd_reject_unlisted_recipient={% if REJECT_UNLISTED_RECIPIENT %}{{ REJECT_UNLISTED_RECIPIENT }}{% else %}no{% endif %}
-o cleanup_service_name=outclean -o cleanup_service_name=outclean
outclean unix n - n - 0 cleanup outclean unix n - n - 0 cleanup
-o header_checks=pcre:/etc/postfix/outclean_header_filter.cf -o header_checks=pcre:/etc/postfix/outclean_header_filter.cf

@ -5,11 +5,17 @@ import os
import socket import socket
import glob import glob
import shutil import shutil
import tenacity
from tenacity import retry
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
# Actual startup script @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["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
# Actual startup script
resolve()
os.environ["HOST_ANTISPAM"] = os.environ.get("HOST_ANTISPAM", "antispam:11332") os.environ["HOST_ANTISPAM"] = os.environ.get("HOST_ANTISPAM", "antispam:11332")
os.environ["HOST_LMTP"] = os.environ.get("HOST_LMTP", "imap:2525") os.environ["HOST_LMTP"] = os.environ.get("HOST_LMTP", "imap:2525")

@ -0,0 +1,14 @@
FROM python:3-alpine
COPY requirements.txt /requirements.txt
RUN pip install -r /requirements.txt \
&& apk add --no-cache nginx \
&& mkdir /run/nginx
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY . /docs
RUN sphinx-build /docs /build
CMD nginx -g "daemon off;"

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

@ -87,6 +87,12 @@ WELCOME=false
WELCOME_SUBJECT=Welcome to your new email account WELCOME_SUBJECT=Welcome to your new email account
WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly! WELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!
# Maildir Compression
# choose compression-method, default: none (value: bz2, gz)
COMPRESSION=
# change compression-level, default: 6 (value: 1-9)
COMPRESSION_LEVEL=
################################### ###################################
# Web settings # Web settings
################################### ###################################
@ -126,3 +132,6 @@ REAL_IP_HEADER=
# IPs for nginx set_real_ip_from (CIDR list separated by commas) # 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=

@ -92,7 +92,7 @@ setting. The configuration option must be one of the following:
- ``none`` disables antivirus checks; - ``none`` disables antivirus checks;
- ``clamav`` is the default values, the popular ClamAV antivirus is enabled. - ``clamav`` is the default values, the popular ClamAV antivirus is enabled.
Make sure that you have at least 1GB or memory for ClamAV to load its signature Make sure that you have at least 1GB of memory for ClamAV to load its signature
database. database.
If you run Mailu behind a reverse proxy you can use ``REAL_IP_HEADER`` and If you run Mailu behind a reverse proxy you can use ``REAL_IP_HEADER`` and

@ -7,7 +7,7 @@ templates_path = ['_templates']
source_suffix = '.rst' source_suffix = '.rst'
master_doc = 'index' master_doc = 'index'
project = 'Mailu' project = 'Mailu'
copyright = '2017, Mailu authors' copyright = '2018, Mailu authors'
author = 'Mailu authors' author = 'Mailu authors'
version = release = 'latest' version = release = 'latest'
language = None language = None
@ -23,7 +23,7 @@ htmlhelp_basename = 'Mailudoc'
# to template names. # to template names.
html_sidebars = { html_sidebars = {
'**': [ '**': [
'relations.html', # needs 'show_related': True theme option to display 'relations.html',
'searchbox.html', 'searchbox.html',
] ]
} }
@ -36,24 +36,3 @@ html_context = {
'github_version': 'master', 'github_version': 'master',
'conf_py_path': '/docs/' 'conf_py_path': '/docs/'
} }
# Upload function when the script is called directly
if __name__ == "__main__":
import os, sys, paramiko
build_dir, hostname, username, password, dest_dir = sys.argv[1:]
transport = paramiko.Transport((hostname, 22))
transport.connect(username=username, password=password)
sftp = paramiko.SFTPClient.from_transport(transport)
os.chdir(build_dir)
for dirpath, dirnames, filenames in os.walk("."):
remote_path = os.path.join(dest_dir, dirpath)
try:
sftp.mkdir(remote_path)
except:
pass
for filename in filenames:
sftp.put(
os.path.join(dirpath, filename),
os.path.join(remote_path, filename)
)

@ -89,3 +89,20 @@ Any change to the files will automatically restart the Web server and reload the
When using the development environment, a debugging toolbar is displayed on the right side When using the development environment, a debugging toolbar is displayed on the right side
of the screen, that you can open to access query details, internal variables, etc. of the screen, that you can open to access query details, internal variables, etc.
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:
.. code-block:: bash
cd <Mailu repo>
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.
.. 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

@ -55,7 +55,7 @@ the version of Mailu that you are running.
configuration configuration
compose/requirements compose/requirements
compose/setup compose/setup
kubernetes/index kubernetes/stable/index
dns dns
reverse reverse

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

@ -0,0 +1,64 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-admin
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-admin
role: mail
tier: backend
spec:
containers:
- name: admin
image: mailu/admin:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- name: maildata
mountPath: /data
subPath: maildata
- name: maildata
mountPath: /dkim
subPath: dkim
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
requests:
memory: 500Mi
cpu: 500m
limits:
memory: 500Mi
cpu: 500m
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: admin
namespace: mailu-mailserver
labels:
app: mailu-admin
role: mail
tier: backend
spec:
selector:
app: mailu-admin
role: mail
tier: backend
ports:
- name: http
port: 80
protocol: TCP

@ -0,0 +1,153 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: mailu-config
namespace: mailu-mailserver
data:
# Mailu main configuration file
#
# Most configuration variables can be modified through the Web interface,
# these few settings must however be configured before starting the mail
# server and require a restart upon change.
###################################
# Common configuration variables
###################################
# Set this to the path where Mailu data and configuration is stored
ROOT: "/mailu"
# Mailu version to run (1.0, 1.1, etc. or master)
VERSION: "master"
# Set to a randomly generated 16 bytes string
SECRET_KEY: "YourKeyHere"
# Address where listening ports should bind
BIND_ADDRESS4: "127.0.0.1"
#BIND_ADDRESS6: "::1"
# Main mail domain
DOMAIN: "example.com"
# Hostnames for this server, separated with comas
HOSTNAMES: "mail.example.com"
# Postmaster local part (will append the main mail domain)
POSTMASTER: "admin"
# Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt)
TLS_FLAVOR: "cert"
# Authentication rate limit (per source IP address)
AUTH_RATELIMIT: "10/minute;1000/hour"
# Opt-out of statistics, replace with "True" to opt out
DISABLE_STATISTICS: "False"
###################################
# Optional features
###################################
# Expose the admin interface (value: true, false)
ADMIN: "true"
# Run the admin interface in debug mode
#DEBUG: "True"
# Choose which webmail to run if any (values: roundcube, rainloop, none)
WEBMAIL: "roundcube"
# Dav server implementation (value: radicale, none)
WEBDAV: "radicale"
# Antivirus solution (value: clamav, none)
ANTIVIRUS: "clamav"
###################################
# Mail settings
###################################
# Message size limit in bytes
# Default: accept messages up to 50MB
MESSAGE_SIZE_LIMIT: "50000000"
# Networks granted relay permissions, make sure that you include your Docker
# internal network (default to 172.17.0.0/16)
# For kubernetes this is the CIDR of the pod network
RELAYNETS: "10.2.0.0/16"
POD_ADDRESS_RANGE: "10.2.0.0/16"
# Will relay all outgoing mails if configured
#RELAYHOST=
# This part is needed for the XCLIENT login for postfix. This should be the POD ADDRESS range
FRONT_ADDRESS: "front.mailu-mailserver.svc.cluster.local"
# Fetchmail delay
FETCHMAIL_DELAY: "600"
# Recipient delimiter, character used to delimiter localpart from custom address part
# e.g. localpart+custom@domain;tld
RECIPIENT_DELIMITER: "+"
# DMARC rua and ruf email
DMARC_RUA: "root"
DMARC_RUF: "root"
# Welcome email, enable and set a topic and body if you wish to send welcome
# emails to all users.
WELCOME: "false"
WELCOME_SUBJECT: "Welcome to your new email account"
WELCOME_BODY: "Welcome to your new email account, if you can read this, then it is configured properly!"
###################################
# Web settings
###################################
# Path to the admin interface if enabled
WEB_ADMIN: "/admin"
# Path to the webmail if enabled
WEB_WEBMAIL: "/webmail"
# Website name
SITENAME: "AppSynth"
# Linked Website URL
WEBSITE: "https://example.com"
# Registration reCaptcha settings (warning, this has some privacy impact)
# RECAPTCHA_PUBLIC_KEY=
# RECAPTCHA_PRIVATE_KEY=
# Domain registration, uncomment to enable
# DOMAIN_REGISTRATION=true
###################################
# Advanced settings
###################################
# Docker-compose project name, this will prepended to containers names.
COMPOSE_PROJECT_NAME: "mailu"
# Default password scheme used for newly created accounts and changed passwords
# (value: SHA512-CRYPT, SHA256-CRYPT, MD5-CRYPT, CRYPT)
PASSWORD_SCHEME: "SHA512-CRYPT"
# Header to take the real ip from
#REAL_IP_HEADER:
# IPs for nginx set_real_ip_from (CIDR list separated by commas)
#REAL_IP_FROM:
# Host settings
HOST_IMAP: "imap.mailu-mailserver.svc.cluster.local"
HOST_POP3: "imap.mailu-mailserver.svc.cluster.local"
HOST_SMTP: "smtp.mailu-mailserver.svc.cluster.local"
HOST_AUTHSMTP: "smtp.mailu-mailserver.svc.cluster.local"
HOST_WEBMAIL: "webmail.mailu-mailserver.svc.cluster.local"
HOST_ADMIN: "admin.mailu-mailserver.svc.cluster.local"
HOST_WEBDAV: "webdav.mailu-mailserver.svc.cluster.local:5232"
HOST_ANTISPAM: "antispam.mailu-mailserver.svc.cluster.local:11332"
HOST_REDIS: "redis.mailu-mailserver.svc.cluster.local"

@ -0,0 +1,39 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-fetchmail
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-fetchmail
role: mail
tier: backend
spec:
containers:
- name: fetchmail
image: mailu/fetchmail:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- name: maildata
mountPath: /data
subPath: maildata
ports:
- containerPort: 5232
- containerPort: 80
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 100Mi
cpu: 100m
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage

@ -0,0 +1,129 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-front
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-front
role: mail
tier: backend
spec:
restartPolicy: Always
terminationGracePeriodSeconds: 60
containers:
- name: front
image: mailu/nginx:latest
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- name: certs
mountPath: /certs
ports:
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
- name: pop3
containerPort: 110
protocol: TCP
- name: pop3s
containerPort: 995
protocol: TCP
- name: imap
containerPort: 143
protocol: TCP
- name: imaps
containerPort: 993
protocol: TCP
- name: smtp
containerPort: 25
protocol: TCP
- name: smtp-auth
containerPort: 10025
protocol: TCP
- name: imap-auth
containerPort: 10143
protocol: TCP
- name: smtps
containerPort: 465
protocol: TCP
- name: smtpd
containerPort: 587
protocol: TCP
- name: auth
containerPort: 8000
protocol: TCP
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 200Mi
cpu: 200m
volumes:
- name: certs
secret:
items:
- key: tls.crt
path: cert.pem
- key: tls.key
path: key.pem
secretName: letsencrypt-certs-all
---
apiVersion: v1
kind: Service
metadata:
name: front
namespace: mailu-mailserver
labels:
app: mailu-admin
role: mail
tier: backend
spec:
selector:
app: mailu-front
role: mail
tier: backend
ports:
- name: http
port: 80
protocol: TCP
- name: https
port: 443
protocol: TCP
- name: pop3
port: 110
protocol: TCP
- name: pop3s
port: 995
protocol: TCP
- name: imap
port: 143
protocol: TCP
- name: imaps
port: 993
protocol: TCP
- name: smtp
port: 25
protocol: TCP
- name: smtps
port: 465
protocol: TCP
- name: smtpd
port: 587
protocol: TCP
- name: smtp-auth
port: 10025
protocol: TCP
- name: imap-auth
port: 10143
protocol: TCP

@ -0,0 +1,80 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-imap
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-imap
role: mail
tier: backend
spec:
containers:
- name: imap
image: mailu/dovecot:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- mountPath: /data
name: maildata
subPath: maildata
- mountPath: /mail
name: maildata
subPath: mailstate
- mountPath: /overrides
name: maildata
subPath: overrides
ports:
- containerPort: 2102
- containerPort: 2525
- containerPort: 143
- containerPort: 993
- containerPort: 4190
resources:
requests:
memory: 500Mi
cpu: 500m
limits:
memory: 1Gi
cpu: 1000m
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: imap
namespace: mailu-mailserver
labels:
app: mailu
role: mail
tier: backend
spec:
selector:
app: mailu-imap
role: mail
tier: backend
ports:
ports:
- name: imap-auth
port: 2102
protocol: TCP
- name: imap-transport
port: 2525
protocol: TCP
- name: imap-default
port: 143
protocol: TCP
- name: imap-ssl
port: 993
protocol: TCP
- name: sieve
port: 4190
protocol: TCP

@ -0,0 +1,32 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mailu-ssl-ingress
namespace: mailu-mailserver
annotations:
kubernetes.io/ingress.class: tectonic
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
ingress.kubernetes.io/ssl-redirect: "true"
# Replace letsencrypt-prod with the name of the certificate issuer
certmanager.k8s.io/cluster-issuer: letsencrypt-prod
#ingress.kubernetes.io/rewrite-target: "/"
#ingress.kubernetes.io/app-root: "/ui"
#ingress.kubernetes.io/follow-redirects: "true"
labels:
app: mailu
role: mail
tier: backend
spec:
tls:
- hosts:
- "mail.example.com"
secretName: letsencrypt-certs-all # If unsure how to generate these, check out https://github.com/ployst/docker-letsencrypt
rules:
- host: "mail.example.com"
http:
paths:
- path: "/"
backend:
serviceName: front
servicePort: 80

@ -0,0 +1,27 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: redis-hdd
namespace: mailu-mailserver
annotations:
volume.beta.kubernetes.io/storage-class: "glusterblock-hdd"
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: mail-storage
namespace: mailu-mailserver
annotations:
volume.beta.kubernetes.io/storage-class: "gluster-heketi-hdd"
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: mailu-mailserver

@ -0,0 +1,56 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-redis
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-redis
role: mail
tier: backend
spec:
containers:
- name: redis
image: redis:4.0-alpine
imagePullPolicy: Always
volumeMounts:
- mountPath: /data
name: redisdata
ports:
- containerPort: 6379
name: redis
protocol: TCP
resources:
requests:
memory: 200Mi
cpu: 100m
limits:
memory: 300Mi
cpu: 200m
volumes:
- name: redisdata
persistentVolumeClaim:
claimName: redis-hdd
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: mailu-mailserver
labels:
app: mailu-redis
role: mail
tier: backend
spec:
selector:
app: mailu-redis
role: mail
tier: backend
ports:
- name: redis
port: 6379
protocol: TCP

@ -0,0 +1,110 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-security
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-security
role: mail
tier: backend
spec:
containers:
- name: antispam
image: mailu/rspamd:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 200Mi
cpu: 200m
ports:
- name: antispam
containerPort: 11332
protocol: TCP
volumeMounts:
- name: filter
subPath: filter
mountPath: /var/lib/rspamd
- name: filter
mountPath: /dkim
subPath: dkim
- name: filter
mountPath: /etc/rspamd/override.d
subPath: rspamd-overrides
- name: antivirus
image: mailu/clamav:master
imagePullPolicy: Always
resources:
requests:
memory: 1Gi
cpu: 1000m
limits:
memory: 2Gi
cpu: 1000m
envFrom:
- configMapRef:
name: mailu-config
ports:
- name: antivirus
containerPort: 3310
protocol: TCP
volumeMounts:
- name: filter
subPath: filter
mountPath: /data
volumes:
- name: filter
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: antispam
namespace: mailu-mailserver
labels:
app: mailu-antispam
role: mail
tier: backend
spec:
selector:
app: mailu-security
role: mail
tier: backend
ports:
- name: antispam
port: 11332
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: antivirus
namespace: mailu-mailserver
labels:
app: mailu-antivirus
role: mail
tier: backend
spec:
selector:
app: mailu-security
role: mail
tier: backend
ports:
- name: antivirus
port: 3310
protocol: TCP

@ -0,0 +1,80 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-smtp
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-smtp
role: mail
tier: backend
spec:
containers:
- name: smtp
image: mailu/postfix:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
resources:
requests:
memory: 500Mi
cpu: 200m
limits:
memory: 1Gi
cpu: 500m
volumeMounts:
- mountPath: /data
name: maildata
subPath: maildata
- mountPath: /overrides
name: maildata
subPath: overrides
ports:
- name: smtp
containerPort: 25
protocol: TCP
- name: smtp-ssl
containerPort: 465
protocol: TCP
- name: smtp-starttls
containerPort: 587
protocol: TCP
- name: smtp-auth
containerPort: 10025
protocol: TCP
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: smtp
namespace: mailu-mailserver
labels:
app: mailu
role: mail
tier: backend
spec:
selector:
app: mailu-smtp
role: mail
tier: backend
ports:
- name: smtp
port: 25
protocol: TCP
- name: smtp-ssl
port: 465
protocol: TCP
- name: smtp-starttls
port: 587
protocol: TCP
- name: smtp-auth
port: 10025
protocol: TCP

@ -0,0 +1,63 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-webdav
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-webdav
role: mail
tier: backend
spec:
containers:
- name: radicale
image: mailu/radicale:master
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
volumeMounts:
- mountPath: /data
name: maildata
subPath: dav
ports:
- containerPort: 5232
- containerPort: 80
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 100Mi
cpu: 100m
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: webdav
namespace: mailu-mailserver
labels:
app: mailu-webdav
role: mail
tier: backend
spec:
selector:
app: mailu-webdav
role: mail
tier: backend
ports:
ports:
- name: http
port: 80
protocol: TCP
- name: http-ui
port: 5232
protocol: TCP

@ -0,0 +1,59 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: mailu-roundcube
namespace: mailu-mailserver
spec:
replicas: 1
template:
metadata:
labels:
app: mailu-roundcube
role: mail
tier: frontend
spec:
containers:
- name: roundcube
image: mailu/roundcube:1.5
imagePullPolicy: Always
envFrom:
- configMapRef:
name: mailu-config
resources:
requests:
memory: 100Mi
cpu: 100m
limits:
memory: 200Mi
cpu: 200m
volumeMounts:
- mountPath: /data
name: maildata
subPath: webmail
ports:
- containerPort: 80
volumes:
- name: maildata
persistentVolumeClaim:
claimName: mail-storage
---
apiVersion: v1
kind: Service
metadata:
name: webmail
namespace: mailu-mailserver
labels:
app: mailu-roundcube
role: mail
tier: frontend
spec:
selector:
app: mailu-roundcube
role: mail
tier: frontend
ports:
ports:
- name: http
port: 80
protocol: TCP

@ -0,0 +1,55 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: default-http-backend
labels:
app: default-http-backend
namespace: kube-ingress
spec:
replicas: 1
selector:
matchLabels:
app: default-http-backend
template:
metadata:
labels:
app: default-http-backend
spec:
terminationGracePeriodSeconds: 60
containers:
- name: default-http-backend
# Any image is permissible as long as:
# 1. It serves a 404 page at /
# 2. It serves 200 on a /healthz endpoint
image: gcr.io/google_containers/defaultbackend:1.4
livenessProbe:
httpGet:
path: /healthz
port: 8080
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 5
ports:
- containerPort: 8080
resources:
limits:
cpu: 10m
memory: 20Mi
requests:
cpu: 10m
memory: 20Mi
---
apiVersion: v1
kind: Service
metadata:
name: default-http-backend
namespace: kube-ingress
labels:
app: default-http-backend
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: default-http-backend

@ -0,0 +1,139 @@
apiVersion: v1
kind: Service
metadata:
# keep it under 24 chars
name: appsynth-lb
namespace: kube-ingress
labels:
k8s-app: appsynth-lb
component: ingress-controller
spec:
type: ClusterIP
selector:
k8s-app: appsynth-lb
component: ingress-controller
ports:
- name: http
protocol: TCP
port: 80
targetPort: 80
- name: https
protocol: TCP
port: 443
targetPort: 443
---
kind: ConfigMap
apiVersion: v1
metadata:
name: udp-services
namespace: kube-ingress
---
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: kube-ingress
data:
25: "mailu-mailserver/front:25"
110: "mailu-mailserver/front:110"
465: "mailu-mailserver/front:465"
587: "mailu-mailserver/front:587"
143: "mailu-mailserver/front:143"
993: "mailu-mailserver/front:993"
995: "mailu-mailserver/front:995"
---
apiVersion: v1
data:
enable-vts-status: "true"
kind: ConfigMap
metadata:
name: nginx-ingress-lb-conf
namespace: kube-ingress
---
apiVersion: apps/v1beta2
kind: DaemonSet
metadata:
name: ingress-controller
namespace: kube-ingress
annotations:
prometheus.io/port: "10254"
prometheus.io/scrape: "true"
labels:
k8s-app: appsynth-lb
component: ingress-controller
type: nginx
spec:
updateStrategy:
rollingUpdate:
maxUnavailable: 1
type: RollingUpdate
selector:
matchLabels:
k8s-app: appsynth-lb
component: ingress-controller
type: nginx
template:
metadata:
labels:
k8s-app: appsynth-lb
component: ingress-controller
type: nginx
spec:
serviceAccount: kube-nginx-ingress
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/master
operator: DoesNotExist
containers:
- name: nginx-ingress-lb
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.16.2
args:
- /nginx-ingress-controller
- --configmap=$(POD_NAMESPACE)/tectonic-custom-error
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
#- --default-ssl-certificate=tectonic-system/tectonic-ingress-tls-secret
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
- --annotations-prefix=ingress.kubernetes.io
- --enable-ssl-passthrough
- --ingress-class=tectonic
# use downward API
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
hostPort: 80
- name: https
containerPort: 443
hostPort: 443
readinessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
livenessProbe:
initialDelaySeconds: 10
timeoutSeconds: 1
httpGet:
path: /healthz
port: 10254
scheme: HTTP
hostNetwork: true
nodeSelector:
node-role.kubernetes.io/node: ""
dnsPolicy: ClusterFirst
restartPolicy: Always
terminationGracePeriodSeconds: 60

@ -0,0 +1,129 @@
apiVersion: v1
kind: Namespace
metadata:
name: kube-ingress
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-nginx-ingress
namespace: kube-ingress
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: kube-nginx-ingress
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- nodes
- pods
- secrets
verbs:
- list
- watch
- update
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- "extensions"
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- "extensions"
resources:
- ingresses/status
verbs:
- update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: kube-nginx-ingress
namespace: kube-ingress
rules:
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
- namespaces
verbs:
- get
- apiGroups:
- ""
resources:
- configmaps
resourceNames:
- "ingress-controller-leader-nginx"
verbs:
- get
- update
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
- create
- update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: kube-nginx-ingress
namespace: kube-ingress
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kube-nginx-ingress
subjects:
- kind: ServiceAccount
name: kube-nginx-ingress
namespace: kube-ingress
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: kube-nginx-ingress
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kube-nginx-ingress
subjects:
- kind: ServiceAccount
name: kube-nginx-ingress
namespace: kube-ingress

@ -0,0 +1,5 @@
server {
listen 80;
listen [::]:80;
root /build;
}

@ -2,5 +2,3 @@ recommonmark
Sphinx Sphinx
sphinx-autobuild sphinx-autobuild
sphinx-rtd-theme sphinx-rtd-theme
sphinxcontrib-versioning
paramiko

@ -0,0 +1,364 @@
# 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 a swarm, the services are available (default mode) through a routing mesh managed by docker itself. With this mode, each service is given a virtual IP adress and docker manages the routing between this virtual IP and the container(s) provinding this service.
With this default networking mode, I cannot get login working properly... As found in https://github.com/Mailu/Mailu/issues/375 , a workaround is to use the dnsrr networking mode at least for the front services.
The main consequence/limitation will be that the front services will *not* be available on every node, but only on the node where it will be deployed. In my case, I have only one manager and I choose to deploy the front service to the manager node, so I know on wich IP the front service will be available (aka the IP adress of my manager node).
### Variable substitution and docker-compose.yml
The docker stack deploy command doesn't support variable substitution in the .yml file itself (but we still can use .env file to pass variables to the services). As a consequence we need to adjust the docker-compose file in order to :
- remove all variables : $VERSION , $BIND_ADDRESS4 , $BIND_ADDRESS6 , $ANTIVIRUS , $WEBMAIL , etc
- change the way we define the volumes (nfs share in our case)
- add a deploy section for every service
### Docker compose
An example of docker-compose-stack.yml file is available here:
```yaml
version: '3.2'
services:
front:
image: mailu/nginx:1.5
env_file: .env
ports:
- target: 80
published: 80
mode: host
- target: 443
published: 443
mode: host
- target: 110
published: 110
mode: host
- target: 143
published: 143
mode: host
- target: 993
published: 993
mode: host
- target: 995
published: 995
mode: host
- target: 25
published: 25
mode: host
- target: 465
published: 465
mode: host
- target: 587
published: 587
mode: host
volumes:
# - "$ROOT/certs:/certs"
- type: volume
source: mailu_certs
target: /certs
deploy:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
redis:
image: redis:alpine
restart: always
volumes:
# - "$ROOT/redis:/data"
- type: volume
source: mailu_redis
target: /data
deploy:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
imap:
image: mailu/dovecot:1.5
restart: always
env_file: .env
volumes:
# - "$ROOT/data:/data"
- type: volume
source: mailu_data
target: /data
# - "$ROOT/mail:/mail"
- type: volume
source: mailu_mail
target: /mail
# - "$ROOT/overrides:/overrides"
- type: volume
source: mailu_overrides
target: /overrides
depends_on:
- front
deploy:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
smtp:
image: mailu/postfix:1.5
restart: always
env_file: .env
volumes:
# - "$ROOT/data:/data"
- type: volume
source: mailu_data
target: /data
# - "$ROOT/overrides:/overrides"
- type: volume
source: mailu_overrides
target: /overrides
depends_on:
- front
deploy:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
antispam:
image: mailu/rspamd:1.5
restart: always
env_file: .env
depends_on:
- front
volumes:
# - "$ROOT/filter:/var/lib/rspamd"
- type: volume
source: mailu_filter
target: /var/lib/rspamd
# - "$ROOT/dkim:/dkim"
- type: volume
source: mailu_dkim
target: /dkim
# - "$ROOT/overrides/rspamd:/etc/rspamd/override.d"
- type: volume
source: mailu_overrides_rspamd
target: /etc/rspamd/override.d
deploy:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
antivirus:
image: mailu/none:1.5
restart: always
env_file: .env
volumes:
# - "$ROOT/filter:/data"
- type: volume
source: mailu_filter
target: /data
deploy:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
webdav:
image: mailu/none:1.5
restart: always
env_file: .env
volumes:
# - "$ROOT/dav:/data"
- type: volume
source: mailu_dav
target: /data
deploy:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
admin:
image: mailu/admin:1.5
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:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
webmail:
image: "mailu/roundcube:1.5"
restart: always
env_file: .env
volumes:
# - "$ROOT/webmail:/data"
- type: volume
source: mailu_data
target: /data
depends_on:
- imap
deploy:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
fetchmail:
image: mailu/fetchmail:1.5
restart: always
env_file: .env
volumes:
# - "$ROOT/data:/data"
- type: volume
source: mailu_data
target: /data
logging:
driver: none
deploy:
endpoint_mode: dnsrr
replicas: 1
placement:
constraints: [node.role == manager]
volumes:
mailu_filter:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,nolock,soft,rw"
device: ":/mnt/Pool1/pv/mailu/filter"
mailu_dkim:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,nolock,soft,rw"
device: ":/mnt/Pool1/pv/mailu/dkim"
mailu_overrides_rspamd:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,nolock,soft,rw"
device: ":/mnt/Pool1/pv/mailu/overrides/rspamd"
mailu_data:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,nolock,soft,rw"
device: ":/mnt/Pool1/pv/mailu/data"
mailu_mail:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,nolock,soft,rw"
device: ":/mnt/Pool1/pv/mailu/mail"
mailu_overrides:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,nolock,soft,rw"
device: ":/mnt/Pool1/pv/mailu/overrides"
mailu_dav:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,nolock,soft,rw"
device: ":/mnt/Pool1/pv/mailu/dav"
mailu_certs:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,nolock,soft,rw"
device: ":/mnt/Pool1/pv/mailu/certs"
mailu_redis:
driver_opts:
type: "nfs"
o: "addr=192.168.0.30,nolock,soft,rw"
device: ":/mnt/Pool1/pv/mailu/redis"
```
### Deploy Mailu on the docker swarm
Run the following command:
```bash
docker stack deploy -c docker-compose-stack.yml mailu
```
See how the services are being deployed:
```bash
core@coreos-01 ~ $ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
ywnsetmtkb1l mailu_antivirus replicated 1/1 mailu/none:1.5
pqokiaz0q128 mailu_fetchmail replicated 1/1 mailu/fetchmail:1.5
```
check a specific service:
```bash
core@coreos-01 ~ $ docker service ps mailu_fetchmail
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
tbu8ppgsdffj mailu_fetchmail.1 mailu/fetchmail:1.5 coreos-01 Running Running 11 days ago
```
### Remove the stack
Run the follwoing command:
```bash
core@coreos-01 ~ $ docker stack rm mailu
```

@ -6,5 +6,6 @@ COPY conf /etc/clamav
COPY start.sh /start.sh COPY start.sh /start.sh
EXPOSE 3310/tcp EXPOSE 3310/tcp
VOLUME ["/data"]
CMD ["/start.sh"] CMD ["/start.sh"]

@ -6,5 +6,6 @@ RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/re
COPY radicale.conf /radicale.conf COPY radicale.conf /radicale.conf
EXPOSE 5232/tcp EXPOSE 5232/tcp
VOLUME ["/data"]
CMD radicale -f -S -C /radicale.conf CMD radicale -f -S -C /radicale.conf

@ -1,6 +1,8 @@
FROM alpine:edge FROM alpine:edge
RUN apk add --no-cache python py-jinja2 rspamd rspamd-controller rspamd-proxy rspamd-fuzzy ca-certificates RUN apk add --no-cache python py-jinja2 rspamd rspamd-controller rspamd-proxy rspamd-fuzzy ca-certificates py-pip \
&& pip install --upgrade pip \
&& pip install tenacity
RUN mkdir /run/rspamd RUN mkdir /run/rspamd
@ -9,4 +11,6 @@ COPY start.py /start.py
EXPOSE 11332/tcp 11334/tcp EXPOSE 11332/tcp 11334/tcp
VOLUME ["/var/lib/rspamd"]
CMD /start.py CMD /start.py

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

@ -4,11 +4,17 @@ import jinja2
import os import os
import socket import socket
import glob import glob
import tenacity
from tenacity import retry
convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ)) convert = lambda src, dst: open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
# Actual startup script @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["FRONT_ADDRESS"] = socket.gethostbyname(os.environ.get("FRONT_ADDRESS", "front"))
# Actual startup script
resolve()
if "HOST_REDIS" not in os.environ: os.environ["HOST_REDIS"] = "redis" if "HOST_REDIS" not in os.environ: os.environ["HOST_REDIS"] = "redis"
for rspamd_file in glob.glob("/conf/*"): for rspamd_file in glob.glob("/conf/*"):

@ -15,4 +15,4 @@ RUN python setup.py https://github.com/mailu/mailu /data
EXPOSE 80/tcp EXPOSE 80/tcp
CMD gunicorn -w 4 -b 0.0.0.0:80 -b [::]:80 --access-logfile - --error-logfile - --preload main:app CMD gunicorn -w 4 -b :80 --access-logfile - --error-logfile - --preload main:app

@ -0,0 +1,13 @@
# This file is used to run the mailu/setup utility
version: '2'
services:
redis:
image: redis:alpine
setup:
image: mailu/setup
ports:
- "80:80"

@ -0,0 +1,51 @@
version: '3'
services:
front:
image: mailu/nginx:$VERSION
build: ../core/nginx
imap:
image: mailu/dovecot:$VERSION
build: ../core/dovecot
smtp:
image: mailu/postfix:$VERSION
build: ../core/postfix
antispam:
image: mailu/rspamd:$VERSION
build: ../services/rspamd
antivirus:
image: mailu/clamav:$VERSION
build: ../optional/clamav
webdav:
image: mailu/radicale:$VERSION
build: ../optional/radicale
admin:
image: mailu/admin:$VERSION
build: ../core/admin
roundcube:
image: mailu/roundcube:$VERSION
build: ../webmails/roundcube
rainloop:
image: mailu/rainloop:$VERSION
build: ../webmails/rainloop
fetchmail:
image: mailu/fetchmail:$VERSION
build: ../services/fetchmail
none:
image: mailu/none:$VERSION
build: ../core/none
docs:
image: mailu/docs:$VERSION
build: ../docs

@ -3,7 +3,7 @@ FROM php:5-apache
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
unzip python3 python3-jinja2 unzip python3 python3-jinja2
ENV RAINLOOP_URL https://github.com/RainLoop/rainloop-webmail/releases/download/v1.12.0/rainloop-community-1.12.0.zip ENV RAINLOOP_URL https://github.com/RainLoop/rainloop-webmail/releases/download/v1.12.1/rainloop-community-1.12.1.zip
RUN rm -rf /var/www/html/ \ RUN rm -rf /var/www/html/ \
&& mkdir /var/www/html \ && mkdir /var/www/html \
@ -24,4 +24,7 @@ COPY default.ini /default.ini
COPY start.py /start.py COPY start.py /start.py
EXPOSE 80/tcp
VOLUME ["/data"]
CMD /start.py CMD /start.py

@ -1,13 +1,10 @@
FROM php:7.0-apache FROM php:7.2-apache
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
libfreetype6-dev \ zlib1g-dev \
libjpeg62-turbo-dev \ && docker-php-ext-install zip
libmcrypt-dev \
libpng12-dev \
&& docker-php-ext-install pdo_mysql mcrypt zip
ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.6/roundcubemail-1.3.6-complete.tar.gz ENV ROUNDCUBE_URL https://github.com/roundcube/roundcubemail/releases/download/1.3.7/roundcubemail-1.3.7-complete.tar.gz
RUN echo date.timezone=UTC > /usr/local/etc/php/conf.d/timezone.ini RUN echo date.timezone=UTC > /usr/local/etc/php/conf.d/timezone.ini
@ -28,4 +25,7 @@ COPY config.inc.php /var/www/html/config/
COPY start.sh /start.sh COPY start.sh /start.sh
EXPOSE 80/tcp
VOLUME ["/data"]
CMD ["/start.sh"] CMD ["/start.sh"]

@ -6,6 +6,7 @@ $config = array();
$config['db_dsnw'] = 'sqlite:////data/roundcube.db'; $config['db_dsnw'] = 'sqlite:////data/roundcube.db';
$config['temp_dir'] = '/tmp/'; $config['temp_dir'] = '/tmp/';
$config['des_key'] = getenv('SECRET_KEY'); $config['des_key'] = getenv('SECRET_KEY');
$config['cipher_method'] = 'AES-256-CBC';
$config['identities_level'] = 3; $config['identities_level'] = 3;
$config['reply_all_mode'] = 1; $config['reply_all_mode'] = 1;
@ -18,16 +19,19 @@ $config['plugins'] = array(
'enigma' 'enigma'
); );
$front = getenv('FRONT_ADDRESS') ? getenv('FRONT_ADDRESS') : 'front';
$imap = getenv('IMAP_ADDRESS') ? getenv('IMAP_ADDRESS') : 'imap';
// Mail servers // Mail servers
$config['default_host'] = getenv('FRONT_ADDRESS') || 'front'; $config['default_host'] = $front;
$config['default_port'] = 10143; $config['default_port'] = 10143;
$config['smtp_server'] = getenv('FRONT_ADDRESS') || 'front'; $config['smtp_server'] = $front;
$config['smtp_port'] = 10025; $config['smtp_port'] = 10025;
$config['smtp_user'] = '%u'; $config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p'; $config['smtp_pass'] = '%p';
// Sieve script management // Sieve script management
$config['managesieve_host'] = getenv('IMAP_ADDRESS') || 'imap'; $config['managesieve_host'] = $imap;
$config['managesieve_usetls'] = false; $config['managesieve_usetls'] = false;
// We access the IMAP and SMTP servers locally with internal names, SSL // We access the IMAP and SMTP servers locally with internal names, SSL

Loading…
Cancel
Save