Merge remote-tracking branch 'upstream/master' into fix-doc-commands-1

master
Roman Hoellen 5 years ago
commit ab33ba2172

@ -4,6 +4,8 @@ pull_request_rules:
conditions: conditions:
- -title~=(WIP|wip) - -title~=(WIP|wip)
- -label~=^(status/wip|status/blocked)$ - -label~=^(status/wip|status/blocked)$
- -closed
- -merged
actions: actions:
comment: comment:
message: | message: |

@ -9,6 +9,9 @@ v1.6.1 - unreleased
------------------- -------------------
- Enhancement: Make Unbound drop privileges after binding to port - Enhancement: Make Unbound drop privileges after binding to port
- Enhancement: Create an Authentication Token with IPv6 address restriction ([#829](https://github.com/Mailu/Mailu/issues/829)) - Enhancement: Create an Authentication Token with IPv6 address restriction ([#829](https://github.com/Mailu/Mailu/issues/829))
- Bug: Fix creating new fetched accounts
- Enhancement: Missing wildcard option in alias flask command ([#869](https://github.com/Mailu/Mailu/issues/869))
- Bug: Fix poor performance if ANTIVIRUS is configured to none.
v1.6.0 - 2019-01-18 v1.6.0 - 2019-01-18
------------------- -------------------
@ -34,7 +37,6 @@ v1.6.0 - 2019-01-18
- Feature: Automated Releases ([#487](https://github.com/Mailu/Mailu/issues/487)) - Feature: Automated Releases ([#487](https://github.com/Mailu/Mailu/issues/487))
- Feature: Support for ARC ([#495](https://github.com/Mailu/Mailu/issues/495)) - Feature: Support for ARC ([#495](https://github.com/Mailu/Mailu/issues/495))
- Feature: Add posibilty to run webmail on root ([#501](https://github.com/Mailu/Mailu/issues/501)) - Feature: Add posibilty to run webmail on root ([#501](https://github.com/Mailu/Mailu/issues/501))
- Feature: Upgrade docker-compose.yml to version 3 ([#539](https://github.com/Mailu/Mailu/issues/539))
- Feature: Documentation to deploy mailu on a docker swarm ([#551](https://github.com/Mailu/Mailu/issues/551)) - Feature: Documentation to deploy mailu on a docker swarm ([#551](https://github.com/Mailu/Mailu/issues/551))
- Feature: Add optional Maildir-Compression ([#553](https://github.com/Mailu/Mailu/issues/553)) - Feature: Add optional Maildir-Compression ([#553](https://github.com/Mailu/Mailu/issues/553))
- Feature: Preserve rspamd history on container restart ([#561](https://github.com/Mailu/Mailu/issues/561)) - Feature: Preserve rspamd history on container restart ([#561](https://github.com/Mailu/Mailu/issues/561))
@ -85,6 +87,7 @@ v1.6.0 - 2019-01-18
- Enhancement: Include favicon package ([#801](https://github.com/Mailu/Mailu/issues/801), ([#802](https://github.com/Mailu/Mailu/issues/802)) - Enhancement: Include favicon package ([#801](https://github.com/Mailu/Mailu/issues/801), ([#802](https://github.com/Mailu/Mailu/issues/802))
- Enhancement: Add logging at critical places in python start.py scripts. Implement LOG_LEVEL to control verbosity ([#588](https://github.com/Mailu/Mailu/issues/588)) - Enhancement: Add logging at critical places in python start.py scripts. Implement LOG_LEVEL to control verbosity ([#588](https://github.com/Mailu/Mailu/issues/588))
- Enhancement: Mark message as seen when reporting as spam - Enhancement: Mark message as seen when reporting as spam
- Enhancement: Better support and document IPv6 ([#827](https://github.com/Mailu/Mailu/issues/827))
- Upstream: Update Roundcube - Upstream: Update Roundcube
- Upstream: Update Rainloop - Upstream: Update Rainloop
- Bug: Rainloop fails with "domain not allowed" ([#93](https://github.com/Mailu/Mailu/issues/93)) - Bug: Rainloop fails with "domain not allowed" ([#93](https://github.com/Mailu/Mailu/issues/93))

@ -1,8 +1,9 @@
FROM alpine:3.8 FROM alpine:3.8
# python3 shared with most images # python3 shared with most images
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 py3-pip \ python3 py3-pip git \
&& pip3 install --upgrade pip && pip3 install --upgrade pip
RUN pip3 install git+https://github.com/usrpro/MailuStart.git#egg=mailustart
# Image specific layers under this line # Image specific layers under this line
RUN mkdir -p /app RUN mkdir -p /app
WORKDIR /app WORKDIR /app

@ -1,5 +1,5 @@
import os import os
from mailustart import resolve
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
# Specific to the admin UI # Specific to the admin UI
@ -61,7 +61,6 @@ DEFAULT_CONFIG = {
'POD_ADDRESS_RANGE': None 'POD_ADDRESS_RANGE': None
} }
class ConfigManager(dict): class ConfigManager(dict):
""" Naive configuration manager that uses environment only """ Naive configuration manager that uses environment only
""" """
@ -75,6 +74,12 @@ class ConfigManager(dict):
def __init__(self): def __init__(self):
self.config = dict() self.config = dict()
def resolve_host(self):
self.config['HOST_IMAP'] = resolve(self.config['HOST_IMAP'])
self.config['HOST_POP3'] = resolve(self.config['HOST_POP3'])
self.config['HOST_AUTHSMTP'] = resolve(self.config['HOST_AUTHSMTP'])
self.config['HOST_SMTP'] = resolve(self.config['HOST_SMTP'])
def __coerce_value(self, value): def __coerce_value(self, value):
if isinstance(value, str) and value.lower() in ('true','yes'): if isinstance(value, str) and value.lower() in ('true','yes'):
return True return True
@ -89,6 +94,7 @@ class ConfigManager(dict):
key: self.__coerce_value(os.environ.get(key, value)) key: self.__coerce_value(os.environ.get(key, value))
for key, value in DEFAULT_CONFIG.items() for key, value in DEFAULT_CONFIG.items()
}) })
self.resolve_host()
# automatically set the sqlalchemy string # automatically set the sqlalchemy string
if self.config['DB_FLAVOR']: if self.config['DB_FLAVOR']:

@ -2,7 +2,6 @@ from mailu import models
from flask import current_app as app from flask import current_app as app
import re import re
import socket
import urllib import urllib
@ -89,5 +88,4 @@ def get_server(protocol, authenticated=False):
hostname, port = extract_host_port(app.config['HOST_AUTHSMTP'], 10025) hostname, port = extract_host_port(app.config['HOST_AUTHSMTP'], 10025)
else: else:
hostname, port = extract_host_port(app.config['HOST_SMTP'], 25) hostname, port = extract_host_port(app.config['HOST_SMTP'], 25)
address = socket.gethostbyname(hostname) return hostname, port
return address, port

@ -292,8 +292,9 @@ def alias_delete(email):
@click.argument('localpart') @click.argument('localpart')
@click.argument('domain_name') @click.argument('domain_name')
@click.argument('destination') @click.argument('destination')
@click.option('-w', '--wildcard', is_flag=True)
@flask_cli.with_appcontext @flask_cli.with_appcontext
def alias(localpart, domain_name, destination): def alias(localpart, domain_name, destination, wildcard=False):
""" Create an alias """ Create an alias
""" """
domain = models.Domain.query.get(domain_name) domain = models.Domain.query.get(domain_name)
@ -303,6 +304,7 @@ def alias(localpart, domain_name, destination):
alias = models.Alias( alias = models.Alias(
localpart=localpart, localpart=localpart,
domain=domain, domain=domain,
wildcard=wildcard,
destination=destination.split(','), destination=destination.split(','),
email="%s@%s" % (localpart, domain_name) email="%s@%s" % (localpart, domain_name)
) )

@ -22,7 +22,7 @@ def fetch_create(user_email):
user_email = user_email or flask_login.current_user.email user_email = user_email or flask_login.current_user.email
user = models.User.query.get(user_email) or flask.abort(404) user = models.User.query.get(user_email) or flask.abort(404)
form = forms.FetchForm() form = forms.FetchForm()
form.pw.validators = [wtforms.validators.DataRequired()] form.password.validators = [wtforms.validators.DataRequired()]
if form.validate_on_submit(): if form.validate_on_submit():
fetch = models.Fetch(user=user) fetch = models.Fetch(user=user)
form.populate_obj(fetch) form.populate_obj(fetch)

@ -1,12 +1,10 @@
FROM alpine:3.8 FROM alpine:3.8
# python3 shared with most images # python3 shared with most images
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 py3-pip \ python3 py3-pip git \
&& pip3 install --upgrade pip && pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx # Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2 RUN pip3 install git+https://github.com/usrpro/MailuStart.git#egg=mailustart
# Shared layer between rspamd, postfix, dovecot
RUN pip3 install tenacity
# Image specific layers under this line # Image specific layers under this line
RUN apk add --no-cache \ RUN apk add --no-cache \
dovecot dovecot-pigeonhole-plugin rspamd-client bash \ dovecot dovecot-pigeonhole-plugin rspamd-client bash \

@ -1,47 +1,29 @@
#!/usr/bin/python3 #!/usr/bin/python3
import jinja2
import os import os
import socket
import glob import glob
import multiprocessing import multiprocessing
import tenacity
import logging as log import logging as log
import sys import sys
from mailustart import resolve, convert
from tenacity import retry
from podop import run_server from podop import run_server
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING")) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def start_podop(): def start_podop():
os.setuid(8) os.setuid(8)
url = "http://" + os.environ["ADMIN_ADDRESS"] + "/internal/dovecot/§"
run_server(0, "dovecot", "/tmp/podop.socket", [ run_server(0, "dovecot", "/tmp/podop.socket", [
("quota", "url", "http://admin/internal/dovecot/§"), ("quota", "url", url ),
("auth", "url", "http://admin/internal/dovecot/§"), ("auth", "url", url),
("sieve", "url", "http://admin/internal/dovecot/§"), ("sieve", "url", url),
]) ])
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", 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),
before=tenacity.before_log(log.getLogger("tenacity.retry"), log.DEBUG),
before_sleep=tenacity.before_sleep_log(log.getLogger("tenacity.retry"), log.INFO),
after=tenacity.after_log(log.getLogger("tenacity.retry"), log.DEBUG)
)
def resolve(hostname):
logger = log.getLogger("resolve()")
logger.info(hostname)
return socket.gethostbyname(hostname)
# Actual startup script # Actual startup script
os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front")) os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
os.environ["REDIS_ADDRESS"] = resolve(os.environ.get("REDIS_ADDRESS", "redis")) os.environ["REDIS_ADDRESS"] = resolve(os.environ.get("REDIS_ADDRESS", "redis"))
os.environ["ADMIN_ADDRESS"] = resolve(os.environ.get("ADMIN_ADDRESS", "admin"))
if os.environ["WEBMAIL"] != "none": if os.environ["WEBMAIL"] != "none":
os.environ["WEBMAIL_ADDRESS"] = resolve(os.environ.get("WEBMAIL_ADDRESS", "webmail")) os.environ["WEBMAIL_ADDRESS"] = resolve(os.environ.get("WEBMAIL_ADDRESS", "webmail"))

@ -1,10 +1,10 @@
FROM alpine:3.8 FROM alpine:3.8
# python3 shared with most images # python3 shared with most images
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 py3-pip \ python3 py3-pip git \
&& pip3 install --upgrade pip && pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx # Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2 RUN pip3 install git+https://github.com/usrpro/MailuStart.git#egg=mailustart
# Image specific layers under this line # Image specific layers under this line
RUN apk add --no-cache certbot nginx nginx-mod-mail openssl curl \ RUN apk add --no-cache certbot nginx nginx-mod-mail openssl curl \
&& pip3 install idna requests watchdog && pip3 install idna requests watchdog

@ -1,32 +1,27 @@
#!/usr/bin/python3 #!/usr/bin/python3
import jinja2
import os import os
import logging as log import logging as log
import sys import sys
from mailustart import resolve, convert
args = os.environ.copy() args = os.environ.copy()
log.basicConfig(stream=sys.stderr, level=args.get("LOG_LEVEL", "WARNING")) log.basicConfig(stream=sys.stderr, level=args.get("LOG_LEVEL", "WARNING"))
def convert(src, dst, args):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", src, dst)
open(dst, "w").write(jinja2.Template(open(src).read()).render(**args))
# Get the first DNS server # Get the first DNS server
with open("/etc/resolv.conf") as handle: with open("/etc/resolv.conf") as handle:
content = handle.read().split() content = handle.read().split()
args["RESOLVER"] = content[content.index("nameserver") + 1] args["RESOLVER"] = content[content.index("nameserver") + 1]
if "HOST_WEBMAIL" not in args: args["HOST_ADMIN"] = resolve(args.get("HOST_ADMIN", "admin"))
args["HOST_WEBMAIL"] = "webmail" args["HOST_ANTISPAM"] = resolve(args.get("HOST_ANTISPAM", "antispam:11334"))
if "HOST_ADMIN" not in args: args["HOST_WEBMAIL"] = args.get("HOST_WEBMAIL", "webmail")
args["HOST_ADMIN"] = "admin" if args["WEBMAIL"] != "none":
if "HOST_WEBDAV" not in args: args["HOST_WEBMAIL"] = resolve(args.get("HOST_WEBMAIL"))
args["HOST_WEBDAV"] = "webdav:5232" args["HOST_WEBDAV"] = args.get("HOST_WEBDAV", "webdav:5232")
if "HOST_ANTISPAM" not in args: if args["WEBDAV"] != "none":
args["HOST_ANTISPAM"] = "antispam:11334" args["HOST_WEBDAV"] = resolve(args.get("HOST_WEBDAV"))
# TLS configuration # TLS configuration
cert_name = os.getenv("TLS_CERT_FILENAME", default="cert.pem") cert_name = os.getenv("TLS_CERT_FILENAME", default="cert.pem")

@ -1,12 +1,10 @@
FROM alpine:3.8 FROM alpine:3.8
# python3 shared with most images # python3 shared with most images
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 py3-pip \ python3 py3-pip git \
&& pip3 install --upgrade pip && pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx # Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2 RUN pip3 install git+https://github.com/usrpro/MailuStart.git#egg=mailustart
# Shared layer between rspamd, postfix, dovecot
RUN pip3 install tenacity
# Image specific layers under this line # Image specific layers under this line
RUN apk add --no-cache postfix postfix-pcre rsyslog \ RUN apk add --no-cache postfix postfix-pcre rsyslog \

@ -1,53 +1,35 @@
#!/usr/bin/python3 #!/usr/bin/python3
import jinja2
import os import os
import socket
import glob import glob
import shutil import shutil
import tenacity
import multiprocessing import multiprocessing
import logging as log import logging as log
import sys import sys
from mailustart import resolve, convert
from tenacity import retry
from podop import run_server from podop import run_server
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING")) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def start_podop(): def start_podop():
os.setuid(100) os.setuid(100)
url = "http://" + os.environ["ADMIN_ADDRESS"] + "/internal/postfix/"
# TODO: Remove verbosity setting from Podop? # TODO: Remove verbosity setting from Podop?
run_server(0, "postfix", "/tmp/podop.socket", [ run_server(0, "postfix", "/tmp/podop.socket", [
("transport", "url", "http://admin/internal/postfix/transport/§"), ("transport", "url", url + "transport/§"),
("alias", "url", "http://admin/internal/postfix/alias/§"), ("alias", "url", url + "alias/§"),
("domain", "url", "http://admin/internal/postfix/domain/§"), ("domain", "url", url + "domain/§"),
("mailbox", "url", "http://admin/internal/postfix/mailbox/§"), ("mailbox", "url", url + "mailbox/§"),
("senderaccess", "url", "http://admin/internal/postfix/sender/access/§"), ("senderaccess", "url", url + "sender/access/§"),
("senderlogin", "url", "http://admin/internal/postfix/sender/login/§") ("senderlogin", "url", url + "sender/login/§")
]) ])
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", 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),
before=tenacity.before_log(log.getLogger("tenacity.retry"), log.DEBUG),
before_sleep=tenacity.before_sleep_log(log.getLogger("tenacity.retry"), log.INFO),
after=tenacity.after_log(log.getLogger("tenacity.retry"), log.DEBUG)
)
def resolve(hostname):
logger = log.getLogger("resolve()")
logger.info(hostname)
return socket.gethostbyname(hostname)
# Actual startup script # Actual startup script
os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front")) os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
os.environ["HOST_ANTISPAM"] = os.environ.get("HOST_ANTISPAM", "antispam:11332") os.environ["ADMIN_ADDRESS"] = resolve(os.environ.get("ADMIN_ADDRESS", "admin"))
os.environ["HOST_LMTP"] = os.environ.get("HOST_LMTP", "imap:2525") os.environ["HOST_ANTISPAM"] = resolve(os.environ.get("HOST_ANTISPAM", "antispam:11332"))
os.environ["HOST_LMTP"] = resolve(os.environ.get("HOST_LMTP", "imap:2525"))
for postfix_file in glob.glob("/conf/*.cf"): for postfix_file in glob.glob("/conf/*.cf"):
convert(postfix_file, os.path.join("/etc/postfix", os.path.basename(postfix_file))) convert(postfix_file, os.path.join("/etc/postfix", os.path.basename(postfix_file)))

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

@ -227,12 +227,9 @@ trying to fix. When happy, you can approve the PR. When running into failures, m
Additional commits Additional commits
`````````````````` ``````````````````
Sometimes users add new commits after ``bors try`` was run automatically. On every new commit ``bors try`` is run automatically. Past approvals get dismissed automatically.
In such cases, a reviewer will have to re-issue a ``bors try`` manually in order When doing a subsequent review on the same PR, be sure to pull the latest image from docker hub
to get the latest changes in the test image. The reviewer will have to be sure the after Bors confirms a successful build.
build finished successful before pulling the new images.
Any previous reviews get dismissed automatically, whenever a new commit is done afterwards.
When bors try fails When bors try fails
``````````````````` ```````````````````

@ -136,6 +136,49 @@ You're mail service will be reachable for IMAP, POP3, SMTP and Webmail at the ad
*Issue reference:* `742`_, `747`_. *Issue reference:* `742`_, `747`_.
How to make IPv6 work?
``````````````````````
Docker currently does not expose the IPv6 ports properly, as it does not interface with ``ip6tables``.
Lets start with quoting everything that's wrong:
Unfortunately, initially Docker was not created with IPv6 in mind.
It was added later and, while it has come a long way, is still not as usable as one would want.
Much discussion is still going on as to how IPv6 should be used in a containerized world;
See the various GitHub issues linked below:
- Giving each container a publicly routable address means all ports (even unexposed / unpublished ports) are suddenly
reachable by everyone, if no additional filtering is done
(`docker/docker#21614 <https://github.com/docker/docker/issues/21614>`_)
- By default, each container gets a random IPv6, making it impossible to do properly do DNS;
the alternative is to assign a specific IPv6 address to each container,
still an administrative hassle (`docker/docker#13481 <https://github.com/docker/docker/issues/13481>`_)
- Published ports won't work on IPv6, unless you have the userland proxy enabled
(which, for now, is enabled by default in Docker)
- The userland proxy, however, seems to be on its way out
(`docker/docker#14856 <https://github.com/docker/docker/issues/14856>`_) and has various issues, like:
- It can use a lot of RAM (`docker/docker#11185 <https://github.com/docker/docker/issues/11185>`_)
- Source IP addresses are rewritten, making it completely unusable for many purposes, e.g. mail servers
(`docker/docker#17666 <https://github.com/docker/docker/issues/17666>`_),
(`docker/libnetwork#1099 <https://github.com/docker/libnetwork/issues/1099>`_).
-- `Robbert Klarenbeek <https://github.com/robbertkl>`_ (docker-ipv6nat author)
So, how to make it work? Well, by using `docker-ipv6nat`_! This nifty container will set up ``ip6tables``,
just as Docker would do for IPv4. We know that nat-ing is not advised in IPv6,
however exposing all containers to public network neither. The choice is ultimately yous.
Mailu `setup utility`_ generates a safe IPv6 ULA subnet by default. So when you run the following command,
Mailu will start to function on IPv6:
.. code-block:: bash
docker run -d --restart=always -v /var/run/docker.sock:/var/run/docker.sock:ro --privileged --net=host robbertkl/ipv6nat
.. _`docker-ipv6nat`: https://github.com/robbertkl/docker-ipv6nat
.. _`setup utility`: https://setup.mailu.io
How does Mailu scale up? How does Mailu scale up?
```````````````````````` ````````````````````````

@ -4,20 +4,16 @@ function dump() {
echo "$(date) Dumping certificates" echo "$(date) Dumping certificates"
bash dumpcerts.sh /traefik/acme.json /tmp/work/ || return bash dumpcerts.sh /traefik/acme.json /tmp/work/ || return
for crt_file in $(ls /tmp/work/certs/*); do # private-keys are rsa, we need pem though
pem_file=$(echo $crt_file | sed 's/certs/pem/g' | sed 's/.crt/-public.pem/g')
echo "openssl x509 -inform PEM -in $crt_file > $pem_file"
openssl x509 -inform PEM -in $crt_file > $pem_file
done
for key_file in $(ls /tmp/work/private/*); do for key_file in $(ls /tmp/work/private/*); do
pem_file=$(echo $key_file | sed 's/private/pem/g' | sed 's/.key/-private.pem/g') pem_file=$(echo $key_file | sed 's/private/pem/g' | sed 's/.key/-private.pem/g')
echo "openssl rsa -in $key_file -text > $pem_file"
openssl rsa -in $key_file -text > $pem_file openssl rsa -in $key_file -text > $pem_file
done done
echo "$(date) Copying certificates" echo "$(date) Copying certificates"
cp -v /tmp/work/pem/${DOMAIN}-private.pem /output/key.pem cp -v /tmp/work/pem/${DOMAIN}-private.pem /output/key.pem
cp -v /tmp/work/pem/${DOMAIN}-public.pem /output/cert.pem # the .crt is a chained-pem, as common for letsencrypt
cp -v /tmp/work/certs/${DOMAIN}.crt /output/cert.pem
} }
mkdir -p /tmp/work/pem /tmp/work/certs mkdir -p /tmp/work/pem /tmp/work/certs

@ -1,12 +1,10 @@
FROM alpine:3.8 FROM alpine:3.8
# python3 shared with most images # python3 shared with most images
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 py3-pip \ python3 py3-pip git \
&& pip3 install --upgrade pip && pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx # Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2 RUN pip3 install git+https://github.com/usrpro/MailuStart.git#egg=mailustart
# Shared layer between rspamd, postfix, dovecot
RUN pip3 install tenacity
# Image specific layers under this line # Image specific layers under this line
RUN apk add --no-cache rspamd rspamd-controller rspamd-proxy rspamd-fuzzy ca-certificates curl RUN apk add --no-cache rspamd rspamd-controller rspamd-proxy rspamd-fuzzy ca-certificates curl

@ -1,6 +1,8 @@
{% if ANTIVIRUS == 'clamav' %}
clamav { clamav {
attachments_only = true; attachments_only = true;
symbol = "CLAM_VIRUS"; symbol = "CLAM_VIRUS";
type = "clamav"; type = "clamav";
servers = "antivirus:3310"; servers = "antivirus:3310";
} }
{% endif %}

@ -1,34 +1,13 @@
#!/usr/bin/python3 #!/usr/bin/python3
import jinja2
import os import os
import socket
import glob import glob
import tenacity
import logging as log import logging as log
import sys import sys
from mailustart import resolve, convert
from tenacity import retry
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING")) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", 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),
before=tenacity.before_log(log.getLogger("tenacity.retry"), log.DEBUG),
before_sleep=tenacity.before_sleep_log(log.getLogger("tenacity.retry"), log.INFO),
after=tenacity.after_log(log.getLogger("tenacity.retry"), log.DEBUG)
)
def resolve(hostname):
logger = log.getLogger("resolve()")
logger.info(hostname)
return socket.gethostbyname(hostname)
# Actual startup script # Actual startup script
os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front")) os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))

@ -1,10 +1,10 @@
FROM alpine:3.8 FROM alpine:3.8
# python3 shared with most images # python3 shared with most images
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 py3-pip \ python3 py3-pip git \
&& pip3 install --upgrade pip && pip3 install --upgrade pip
# Shared layer between rspamd, postfix, dovecot, unbound and nginx # Shared layer between rspamd, postfix, dovecot, unbound and nginx
RUN pip3 install jinja2 RUN pip3 install git+https://github.com/usrpro/MailuStart.git#egg=mailustart
# Image specific layers under this line # Image specific layers under this line
RUN apk add --no-cache unbound curl bind-tools \ RUN apk add --no-cache unbound curl bind-tools \
&& curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \ && curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \

@ -1,17 +1,12 @@
#!/usr/bin/python3 #!/usr/bin/python3
import jinja2
import os import os
import logging as log import logging as log
import sys import sys
from mailustart import convert
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING")) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", src, dst)
open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
convert("/unbound.conf", "/etc/unbound/unbound.conf") convert("/unbound.conf", "/etc/unbound/unbound.conf")
os.execv("/usr/sbin/unbound", ["-c /etc/unbound/unbound.conf"]) os.execv("/usr/sbin/unbound", ["-c /etc/unbound/unbound.conf"])

@ -3,7 +3,7 @@
# Please read the documentation before attempting any change. # Please read the documentation before attempting any change.
# Generated for {{ flavor }} flavor # Generated for {{ flavor }} flavor
version: '3.6' version: '2.2'
services: services:
@ -160,8 +160,14 @@ services:
networks: networks:
default: default:
{% if ipv6_enabled %}
enable_ipv6: true
{% endif %}
driver: bridge driver: bridge
ipam: ipam:
driver: default driver: default
config: config:
- subnet: {{ subnet }} - subnet: {{ subnet }}
{% if ipv6_enabled %}
- subnet: {{ subnet6 }}
{% endif %}

@ -27,6 +27,9 @@ SECRET_KEY={{ secret(16) }}
# Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!) # Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)
SUBNET={{ subnet }} SUBNET={{ subnet }}
{% if ipv6_enabled %}
SUBNET6={{ subnet6 }}
{% endif %}
# Main mail domain # Main mail domain
DOMAIN={{ domain }} DOMAIN={{ domain }}
@ -116,7 +119,11 @@ WEBROOT_REDIRECT=/webmail
WEB_ADMIN={{ admin_path }} WEB_ADMIN={{ admin_path }}
# Path to the webmail if enabled # Path to the webmail if enabled
{% if webmail_type != 'none' and webmail_path == '' %}
WEB_WEBMAIL=/
{% else %}
WEB_WEBMAIL={{ webmail_path }} WEB_WEBMAIL={{ webmail_path }}
{% endif %}
# Website name # Website name
SITENAME={{ site_name }} SITENAME={{ site_name }}

@ -44,8 +44,13 @@ Before you can use Mailu, you must create the primary administrator user account
one of the hostnames one of the hostnames
<a href="https://{{ hostnames.split(',')[0] }}{{ admin_path }}">{{ hostnames.split(',')[0] }}{{ admin_path }}</a>. <a href="https://{{ hostnames.split(',')[0] }}{{ admin_path }}">{{ hostnames.split(',')[0] }}{{ admin_path }}</a>.
{% else %} {% else %}
<a href="http://127.0.0.1:8080">http://127.0.0.1:8080</a> (only directly from the host running docker). <a href="http://127.0.0.1:8080/ui">http://127.0.0.1:8080/ui</a> (only directly from the host running docker).
If you run mailu on a remote server, and wish to access the admin interface via a SSH tunnel, you can create a port-forward from your local machine to your server like
<pre><code>ssh -L 127.0.0.1:8080:127.0.0.1:8080 &lt;user&gt;@&lt;server&gt;
</code></pre>
And access the above URL from your local machine.
<br />
{% endif %} {% endif %}
And choose the "Update password" option in the left menu. Also, choose the "Update password" option in the left menu.
</p> </p>
{% endcall %} {% endcall %}

@ -50,11 +50,16 @@ Before you can use Mailu, you must create the primary administrator user account
<p>Login to the admin interface to change the password for a safe one, at <p>Login to the admin interface to change the password for a safe one, at
{% if admin_enabled %} {% if admin_enabled %}
one of the hostnames one of the hostnames
<a href="https://{{ hostnames.split(',')[0] }}{{ admin_path }}">{{ hostnames.split(',')[0] }}{{ admin_path }}</a>. <a href="https://{{ hostnames.split(',')[0] }}{{ admin_path }}">{{ hostnames.split(',')[0] }}{{ admin_path }}</a>.
{% else %} {% else %}
<a href="http://127.0.0.1:8080">http://127.0.0.1:8080</a> (only directly from the host running docker). <a href="http://127.0.0.1:8080/ui">http://127.0.0.1:8080/ui</a> (only directly from the host running docker).
If you run mailu on a remote server, and wish to access the admin interface via a SSH tunnel, you can create a port-forward from your local machine to your server like
<pre><code>ssh -L 127.0.0.1:8080:127.0.0.1:8080 &lt;user&gt;@&lt;server&gt;
</code></pre>
And access the above URL from your local machine.
<br />
{% endif %} {% endif %}
And choose the "Update password" option in the left menu. Also, choose the "Update password" option in the left menu.
</p> </p>
{% endcall %} {% endcall %}

@ -9,6 +9,7 @@ import string
import random import random
import ipaddress import ipaddress
import hashlib import hashlib
import time
version = os.getenv("this_version", "master") version = os.getenv("this_version", "master")
@ -33,6 +34,17 @@ def secret(length=16):
for _ in range(length) for _ in range(length)
) )
#Original copied from https://github.com/andrewlkho/ulagen
def random_ipv6_subnet():
eui64 = uuid.getnode() >> 24 << 48 | 0xfffe000000 | uuid.getnode() & 0xffffff
eui64_canon = "-".join([format(eui64, "02X")[i:i+2] for i in range(0, 18, 2)])
h = hashlib.sha1()
h.update((eui64_canon + str(time.time() - time.mktime((1900, 1, 1, 0, 0, 0, 0, 1, -1)))).encode('utf-8'))
globalid = h.hexdigest()[0:10]
prefix = ":".join(("fd" + globalid[0:2], globalid[2:6], globalid[6:10]))
return prefix
def build_app(path): def build_app(path):
@ -69,8 +81,9 @@ def build_app(path):
@root_bp.route("/submit_flavor", methods=["POST"]) @root_bp.route("/submit_flavor", methods=["POST"])
def submit_flavor(): def submit_flavor():
data = flask.request.form.copy() data = flask.request.form.copy()
subnet6 = random_ipv6_subnet()
steps = sorted(os.listdir(os.path.join(path, "templates", "steps", data["flavor"]))) steps = sorted(os.listdir(os.path.join(path, "templates", "steps", data["flavor"])))
return flask.render_template('wizard.html', flavor=data["flavor"], steps=steps) return flask.render_template('wizard.html', flavor=data["flavor"], steps=steps, subnet6=subnet6)
@prefix_bp.route("/submit", methods=["POST"]) @prefix_bp.route("/submit", methods=["POST"])
@root_bp.route("/submit", methods=["POST"]) @root_bp.route("/submit", methods=["POST"])

@ -86,3 +86,16 @@ $(document).ready(function() {
} }
}); });
}); });
$(document).ready(function() {
if ($('#enable_ipv6').prop('checked')) {
$("#ipv6").show();
}
$("#enable_ipv6").change(function() {
if ($(this).is(":checked")) {
$("#ipv6").show();
} else {
$("#ipv6").hide();
}
});
});

@ -18,13 +18,26 @@ avoid generic all-interfaces addresses like <code>0.0.0.0</code> or <code>::</co
<!-- Validates IPv4 address --> <!-- Validates IPv4 address -->
<input class="form-control" type="text" name="bind4" value="127.0.0.1" <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]?)$"> 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]?)$">
<label>Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)</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.203.0/24">
</div> </div>
<div class="form-group"> <div class="form-check form-check-inline">
<label class="form-check-label">
<input class="form-check-input" type="checkbox" name="ipv6_enabled" value="true" id="enable_ipv6">
Enable IPv6
</label>
</div>
<div class="form-group" id="ipv6" style="display: none">
<p><span class="label label-danger">Read this:</span> Docker currently does not expose the IPv6 ports properly, as it does not interface with <code>ip6tables</code>. Be sure to read our <a href="https://mailu.io/{{ version }}/faq.html#how-to-make-ipv6-work">FAQ section</a>!</p>
<label>IPv6 listen address</label> <label>IPv6 listen address</label>
<!-- Validates IPv6 address --> <!-- Validates IPv6 address -->
<input class="form-control" type="text" name="bind6" value="::1" <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]))?$"> 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]))?$">
<label>Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)</label>
<input class="form-control" type="text" name="subnet6" required value="{{ subnet6 }}:beef::/64">
</div> </div>
<p>The unbound resolver enables Mailu to do DNSsec verification, DNS root lookups and caching. This also helps the antispam service not to get blocked by the public or ISP DNS servers.</p> <p>The unbound resolver enables Mailu to do DNSsec verification, DNS root lookups and caching. This also helps the antispam service not to get blocked by the public or ISP DNS servers.</p>
@ -34,12 +47,6 @@ avoid generic all-interfaces addresses like <code>0.0.0.0</code> or <code>::</co
Enable unbound resolver Enable unbound resolver
</label> </label>
</div> </div>
<br><br>
<div class="form-group">
<label>Subnet of the docker network. This should not conflict with any networks to which your system is connected. (Internal and external!)</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.203.0/24">
</div>
<p>You server will be available under a main hostname but may expose multiple public <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. Every e-mail domain that points to this server must have one of the

@ -60,7 +60,7 @@ WEBMAIL=none
WEBDAV=none WEBDAV=none
# Antivirus solution (value: clamav, none) # Antivirus solution (value: clamav, none)
#ANTIVIRUS=clamav ANTIVIRUS=clamav
#Antispam solution #Antispam solution
ANTISPAM=none ANTISPAM=none

@ -1,7 +1,7 @@
FROM php:7.2-apache FROM php:7.2-apache
#Shared layer between rainloop and roundcube #Shared layer between rainloop and roundcube
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
python3 curl \ python3 curl python3-pip git \
&& rm -rf /var/lib/apt/lists \ && rm -rf /var/lib/apt/lists \
&& echo "ServerSignature Off" >> /etc/apache2/apache2.conf && echo "ServerSignature Off" >> /etc/apache2/apache2.conf
@ -21,6 +21,8 @@ RUN apt-get update && apt-get install -y \
&& chown -R www-data: * \ && chown -R www-data: * \
&& apt-get purge -y unzip \ && apt-get purge -y unzip \
&& rm -rf /var/lib/apt/lists && rm -rf /var/lib/apt/lists
RUN pip3 install git+https://github.com/usrpro/MailuStart.git#egg=mailustart
COPY include.php /var/www/html/include.php COPY include.php /var/www/html/include.php
COPY php.ini /php.ini COPY php.ini /php.ini

@ -1,21 +1,16 @@
#!/usr/bin/python3 #!/usr/bin/python3
import jinja2
import os import os
import shutil import shutil
import logging as log import logging as log
import sys import sys
from mailustart import resolve, convert
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING")) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", src, dst)
open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
# Actual startup script # Actual startup script
os.environ["FRONT_ADDRESS"] = os.environ.get("FRONT_ADDRESS", "front") os.environ["FRONT_ADDRESS"] = resolve(os.environ.get("FRONT_ADDRESS", "front"))
os.environ["IMAP_ADDRESS"] = os.environ.get("IMAP_ADDRESS", "imap") os.environ["IMAP_ADDRESS"] = resolve(os.environ.get("IMAP_ADDRESS", "imap"))
os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576)) os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576))

@ -1,7 +1,7 @@
FROM php:7.2-apache FROM php:7.2-apache
#Shared layer between rainloop and roundcube #Shared layer between rainloop and roundcube
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
python3 curl \ python3 curl python3-pip git \
&& rm -rf /var/lib/apt/lists \ && rm -rf /var/lib/apt/lists \
&& echo "ServerSignature Off" >> /etc/apache2/apache2.conf && echo "ServerSignature Off" >> /etc/apache2/apache2.conf
@ -23,6 +23,8 @@ RUN apt-get update && apt-get install -y \
&& chown -R www-data: logs temp \ && chown -R www-data: logs temp \
&& rm -rf /var/lib/apt/lists && rm -rf /var/lib/apt/lists
RUN pip3 install git+https://github.com/usrpro/MailuStart.git#egg=mailustart
COPY php.ini /php.ini COPY php.ini /php.ini
COPY config.inc.php /var/www/html/config/ COPY config.inc.php /var/www/html/config/
COPY start.py /start.py COPY start.py /start.py

@ -1,17 +1,12 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
import jinja2
import logging as log import logging as log
import sys import sys
from mailustart import convert
log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING")) log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def convert(src, dst):
logger = log.getLogger("convert()")
logger.debug("Source: %s, Destination: %s", src, dst)
open(dst, "w").write(jinja2.Template(open(src).read()).render(**os.environ))
os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576)) os.environ["MAX_FILESIZE"] = str(int(int(os.environ.get("MESSAGE_SIZE_LIMIT"))*0.66/1048576))
convert("/php.ini", "/usr/local/etc/php/conf.d/roundcube.ini") convert("/php.ini", "/usr/local/etc/php/conf.d/roundcube.ini")

Loading…
Cancel
Save