Merge remote-tracking branch 'upstream/master' into Riscue-master

master
Florent Daigniere 3 years ago
commit 04b7ddfffd

@ -3,10 +3,7 @@ Changelog
For full details see the [releases page](https://mailu.io/1.9/releases.html) For full details see the [releases page](https://mailu.io/1.9/releases.html)
Warning, the helm-chart repo is not in sync yet with the new Mailu 1.9 release. If you use helm-chart (kubernetes), we advise to stick to version 1.8. Upgrade should run fine as long as you generate a new compose or stack configuration and upgrade your mailu.env. Please note that once you have upgraded to 1.9 you won't be able to roll-back to earlier versions without resetting user passwords.
Upgrade should run fine as long as you generate a new compose or stack
configuration and upgrade your mailu.env.
If you use a reverse proxy in front of Mailu, it is vital to configure the newly introduced env variables REAL_IP_HEADER and REAL_IP_FROM. If you use a reverse proxy in front of Mailu, it is vital to configure the newly introduced env variables REAL_IP_HEADER and REAL_IP_FROM.
These settings tell Mailu that the HTTP header with the remote client IP address from the reverse proxy can be trusted. These settings tell Mailu that the HTTP header with the remote client IP address from the reverse proxy can be trusted.

@ -17,7 +17,7 @@ Features
Main features include: Main features include:
- **Standard email server**, IMAP and IMAP+, SMTP and Submission - **Standard email server**, IMAP and IMAP+, SMTP and Submission with autoconfiguration profiles for clients
- **Advanced email features**, aliases, domain aliases, custom routing - **Advanced email features**, aliases, domain aliases, custom routing
- **Web access**, multiple Webmails and administration interface - **Web access**, multiple Webmails and administration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts - **User features**, aliases, auto-reply, auto-forward, fetched accounts

@ -1,5 +1,5 @@
# First stage to build assets # First stage to build assets
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
ARG ARCH="" ARG ARCH=""
FROM ${ARCH}node:16 as assets FROM ${ARCH}node:16 as assets
@ -13,7 +13,7 @@ COPY webpack.config.js ./
COPY assets ./assets COPY assets ./assets
RUN set -eu \ RUN set -eu \
&& sed -i 's/#007bff/#55a5d9/' node_modules/admin-lte/build/scss/_bootstrap-variables.scss \ && sed -i 's/#007bff/#55a5d9/' node_modules/admin-lte/build/scss/_bootstrap-variables.scss \
&& for l in ca da de:de_de en:en-gb es:es_es eu fr:fr_fr he hu is it:it_it ja nb_NO:no_nb nl:nl_nl pl pt:pt_pt ru sv:sv_se zh; do \ && for l in ca da de:de-DE en:en-GB es:es-ES eu fr:fr-FR he hu is it:it-IT ja nb_NO:no-NB nl:nl-NL pl pt:pt-PT ru sv:sv-SE zh; do \
cp node_modules/datatables.net-plugins/i18n/${l#*:}.json assets/${l%:*}.json; \ cp node_modules/datatables.net-plugins/i18n/${l#*:}.json assets/${l%:*}.json; \
done \ done \
&& node_modules/.bin/webpack-cli --color && node_modules/.bin/webpack-cli --color

@ -94,11 +94,11 @@ def handle_authentication(headers):
else: else:
try: try:
user = models.User.query.get(user_email) if '@' in user_email else None user = models.User.query.get(user_email) if '@' in user_email else None
is_valid_user = user is not None
except sqlalchemy.exc.StatementError as exc: except sqlalchemy.exc.StatementError as exc:
exc = str(exc).split('\n', 1)[0] exc = str(exc).split('\n', 1)[0]
app.logger.warn(f'Invalid user {user_email!r}: {exc}') app.logger.warn(f'Invalid user {user_email!r}: {exc}')
else: else:
is_valid_user = user is not None
ip = urllib.parse.unquote(headers["Client-Ip"]) ip = urllib.parse.unquote(headers["Client-Ip"])
if check_credentials(user, password, ip, protocol, headers["Auth-Port"]): if check_credentials(user, password, ip, protocol, headers["Auth-Port"]):
server, port = get_server(headers["Auth-Protocol"], True) server, port = get_server(headers["Auth-Protocol"], True)

@ -1,3 +1,3 @@
__all__ = [ __all__ = [
'auth', 'postfix', 'dovecot', 'fetch', 'rspamd' 'auth', 'autoconfig', 'postfix', 'dovecot', 'fetch', 'rspamd'
] ]

@ -5,6 +5,7 @@ from flask import current_app as app
import flask import flask
import flask_login import flask_login
import base64 import base64
import sqlalchemy.exc
@internal.route("/auth/email") @internal.route("/auth/email")
def nginx_authentication(): def nginx_authentication():
@ -32,7 +33,7 @@ def nginx_authentication():
for key, value in headers.items(): for key, value in headers.items():
response.headers[key] = str(value) response.headers[key] = str(value)
is_valid_user = False is_valid_user = False
if response.headers.get("Auth-User-Exists"): if response.headers.get("Auth-User-Exists") == "True":
username = response.headers["Auth-User"] username = response.headers["Auth-User"]
if utils.limiter.should_rate_limit_user(username, client_ip): if utils.limiter.should_rate_limit_user(username, client_ip):
# FIXME could be done before handle_authentication() # FIXME could be done before handle_authentication()
@ -96,13 +97,19 @@ def basic_authentication():
response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit for this username exceeded"' response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit for this username exceeded"'
response.headers['Retry-After'] = '60' response.headers['Retry-After'] = '60'
return response return response
user = models.User.query.get(user_email) try:
if user and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"): user = models.User.query.get(user_email) if '@' in user_email else None
response = flask.Response() except sqlalchemy.exc.StatementError as exc:
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "") exc = str(exc).split('\n', 1)[0]
utils.limiter.exempt_ip_from_ratelimits(client_ip) app.logger.warn(f'Invalid user {user_email!r}: {exc}')
return response else:
utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip) if user is not None and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"):
response = flask.Response()
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "")
utils.limiter.exempt_ip_from_ratelimits(client_ip)
return response
# We failed check_credentials
utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)
response = flask.Response(status=401) response = flask.Response(status=401)
response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"' response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'
return response return response

@ -0,0 +1,183 @@
from mailu.internal import internal
from flask import current_app as app
import flask
import xmltodict
@internal.route("/autoconfig/mozilla")
def autoconfig_mozilla():
# https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
hostname = app.config['HOSTNAME']
xml = f'''<?xml version="1.0"?>
<clientConfig version="1.1">
<emailProvider id="%EMAILDOMAIN%">
<domain>%EMAILDOMAIN%</domain>
<displayName>Email</displayName>
<displayShortName>Email</displayShortName>
<incomingServer type="imap">
<hostname>{hostname}</hostname>
<port>993</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
</incomingServer>
<outgoingServer type="smtp">
<hostname>{hostname}</hostname>
<port>465</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
<addThisServer>true</addThisServer>
<useGlobalPreferredServer>true</useGlobalPreferredServer>
</outgoingServer>
<documentation url="https://{hostname}/admin/client">
<descr lang="en">Configure your email client</descr>
</documentation>
</emailProvider>
</clientConfig>\r\n'''
return flask.Response(xml, mimetype='text/xml', status=200)
@internal.route("/autoconfig/microsoft.json")
def autoconfig_microsoft_json():
proto = flask.request.args.get('Protocol', 'Autodiscoverv1')
if proto == 'Autodiscoverv1':
hostname = app.config['HOSTNAME']
json = f'"Protocol":"Autodiscoverv1","Url":"https://{hostname}/autodiscover/autodiscover.xml"'
return flask.Response('{'+json+'}', mimetype='application/json', status=200)
else:
return flask.abort(404)
@internal.route("/autoconfig/microsoft", methods=['POST'])
def autoconfig_microsoft():
# https://docs.microsoft.com/en-us/previous-versions/office/office-2010/cc511507(v=office.14)?redirectedfrom=MSDN#Anchor_3
hostname = app.config['HOSTNAME']
try:
xmlRequest = (flask.request.data).decode("utf-8")
xml = xmltodict.parse(xmlRequest[xmlRequest.find('<'):xmlRequest.rfind('>')+1])
schema = xml['Autodiscover']['Request']['AcceptableResponseSchema']
if schema != 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a':
return flask.abort(404)
email = xml['Autodiscover']['Request']['EMailAddress']
xml = f'''<?xml version="1.0" encoding="utf-8" ?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<Response xmlns="{schema}">
<Account>
<AccountType>email</AccountType>
<Action>settings</Action>
<Protocol>
<Type>IMAP</Type>
<Server>{hostname}</Server>
<Port>993</Port>
<LoginName>{email}</LoginName>
<DomainRequired>on</DomainRequired>
<SPA>off</SPA>
<SSL>on</SSL>
</Protocol>
<Protocol>
<Type>SMTP</Type>
<Server>{hostname}</Server>
<Port>465</Port>
<LoginName>{email}</LoginName>
<DomainRequired>on</DomainRequired>
<SPA>off</SPA>
<SSL>on</SSL>
</Protocol>
</Account>
</Response>
</Autodiscover>'''
return flask.Response(xml, mimetype='text/xml', status=200)
except:
return flask.abort(400)
@internal.route("/autoconfig/apple")
def autoconfig_apple():
# https://developer.apple.com/business/documentation/Configuration-Profile-Reference.pdf
hostname = app.config['HOSTNAME']
sitename = app.config['SITENAME']
xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>EmailAccountDescription</key>
<string>{sitename}</string>
<key>EmailAccountName</key>
<string>{hostname}</string>
<key>EmailAccountType</key>
<string>EmailTypeIMAP</string>
<key>EmailAddress</key>
<string></string>
<key>IncomingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>IncomingMailServerHostName</key>
<string>{hostname}</string>
<key>IncomingMailServerPortNumber</key>
<integer>993</integer>
<key>IncomingMailServerUseSSL</key>
<true/>
<key>IncomingMailServerUsername</key>
<string></string>
<key>IncomingPassword</key>
<string></string>
<key>OutgoingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>OutgoingMailServerHostName</key>
<string>{hostname}</string>
<key>OutgoingMailServerPortNumber</key>
<integer>465</integer>
<key>OutgoingMailServerUseSSL</key>
<true/>
<key>OutgoingMailServerUsername</key>
<string></string>
<key>OutgoingPasswordSameAsIncomingPassword</key>
<true/>
<key>PayloadDescription</key>
<string>{sitename}</string>
<key>PayloadDisplayName</key>
<string>{hostname}</string>
<key>PayloadIdentifier</key>
<string>{hostname}.email</string>
<key>PayloadOrganization</key>
<string></string>
<key>PayloadType</key>
<string>com.apple.mail.managed</string>
<key>PayloadUUID</key>
<string>72e152e2-d285-4588-9741-25bdd50c4d11</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PreventAppSheet</key>
<true/>
<key>PreventMove</key>
<false/>
<key>SMIMEEnabled</key>
<false/>
<key>disableMailRecentsSyncing</key>
<false/>
</dict>
</array>
<key>PayloadDescription</key>
<string>{hostname} - E-Mail Account Configuration</string>
<key>PayloadDisplayName</key>
<string>E-Mail Account {hostname}</string>
<key>PayloadIdentifier</key>
<string>E-Mail Account {hostname}</string>
<key>PayloadOrganization</key>
<string>{hostname}</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>56db43a5-d29e-4609-a908-dce94d0be48e</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>\r\n'''
return flask.Response(xml, mimetype='text/xml', status=200)

@ -255,20 +255,23 @@ class Domain(Base):
""" return list of auto configuration records (RFC6186) """ """ return list of auto configuration records (RFC6186) """
hostname = app.config['HOSTNAME'] hostname = app.config['HOSTNAME']
protocols = [ protocols = [
('submission', 587), ('imap', 143, 20),
('imap', 143), ('pop3', 110, 20),
('pop3', 110), ('submission', 587, 20),
] ]
if app.config['TLS_FLAVOR'] != 'notls': if app.config['TLS_FLAVOR'] != 'notls':
protocols.extend([ protocols.extend([
('imaps', 993), ('autodiscover', 443, 10),
('pop3s', 995), ('submissions', 465, 10),
('imaps', 993, 10),
('pop3s', 995, 10),
]) ])
return list([
f'_{proto}._tcp.{self.name}. 600 IN SRV 1 1 {port} {hostname}.' return [
for proto, port f'_{proto}._tcp.{self.name}. 600 IN SRV {prio} 1 {port} {hostname}.'
for proto, port, prio
in protocols in protocols
]) ]+[f'autoconfig.{self.name}. 600 IN CNAME {hostname}.']
@cached_property @cached_property
def dns_tlsa(self): def dns_tlsa(self):

@ -9,6 +9,7 @@
{%- endblock %} {%- endblock %}
{%- block content %} {%- block content %}
<div>If you use an Apple device, <a href="/apple.mobileconfig">click here to autoconfigure it.</a></div>
{%- call macros.table(title=_("Incoming mail"), datatable=False) %} {%- call macros.table(title=_("Incoming mail"), datatable=False) %}
<tbody> <tbody>
<tr> <tr>
@ -17,7 +18,7 @@
</tr> </tr>
<tr> <tr>
<th>{% trans %}TCP port{% endtrans %}</th> <th>{% trans %}TCP port{% endtrans %}</th>
<td>{{ "143" if config["TLS_FLAVOR"] == "notls" else "993 (TLS) or 143 (STARTTLS)" }}</td> <td>{{ "143" if config["TLS_FLAVOR"] == "notls" else "993 (TLS)" }}</td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Server name{% endtrans %}</th> <th>{% trans %}Server name{% endtrans %}</th>
@ -42,7 +43,7 @@
</tr> </tr>
<tr> <tr>
<th>{% trans %}TCP port{% endtrans %}</th> <th>{% trans %}TCP port{% endtrans %}</th>
<td>{{ "25" if config["TLS_FLAVOR"] == "notls" else "465 (TLS) or 587 (STARTTLS)" }}</td> <td>{{ "25" if config["TLS_FLAVOR"] == "notls" else "465 (TLS)" }}</td>
</tr> </tr>
<tr> <tr>
<th>{% trans %}Server name{% endtrans %}</th> <th>{% trans %}Server name{% endtrans %}</th>

@ -60,7 +60,7 @@
</tr> </tr>
{%- endif %} {%- endif %}
<tr> <tr>
<th>{% trans %}DNS client auto-configuration (RFC6186) entries{% endtrans %}</th> <th>{% trans %}DNS client auto-configuration entries{% endtrans %}</th>
<td>{{ macros.clip("dns_autoconfig") }}<pre id="dns_autoconfig" class="pre-config border bg-light"> <td>{{ macros.clip("dns_autoconfig") }}<pre id="dns_autoconfig" class="pre-config border bg-light">
{%- for line in domain.dns_autoconfig %} {%- for line in domain.dns_autoconfig %}
{{ line }} {{ line }}

@ -2,6 +2,7 @@ from mailu import models
from mailu.ui import ui, forms, access from mailu.ui import ui, forms, access
from flask import current_app as app from flask import current_app as app
import validators
import flask import flask
import flask_login import flask_login
import wtforms_components import wtforms_components
@ -18,18 +19,21 @@ def domain_list():
def domain_create(): def domain_create():
form = forms.DomainForm() form = forms.DomainForm()
if form.validate_on_submit(): if form.validate_on_submit():
conflicting_domain = models.Domain.query.get(form.name.data) if validators.domain(form.name.data):
conflicting_alternative = models.Alternative.query.get(form.name.data) conflicting_domain = models.Domain.query.get(form.name.data)
conflicting_relay = models.Relay.query.get(form.name.data) conflicting_alternative = models.Alternative.query.get(form.name.data)
if conflicting_domain or conflicting_alternative or conflicting_relay: conflicting_relay = models.Relay.query.get(form.name.data)
flask.flash('Domain %s is already used' % form.name.data, 'error') if conflicting_domain or conflicting_alternative or conflicting_relay:
flask.flash('Domain %s is already used' % form.name.data, 'error')
else:
domain = models.Domain()
form.populate_obj(domain)
models.db.session.add(domain)
models.db.session.commit()
flask.flash('Domain %s created' % domain)
return flask.redirect(flask.url_for('.domain_list'))
else: else:
domain = models.Domain() flask.flash('Domain %s is invalid' % form.name.data, 'error')
form.populate_obj(domain)
models.db.session.add(domain)
models.db.session.commit()
flask.flash('Domain %s created' % domain)
return flask.redirect(flask.url_for('.domain_list'))
return flask.render_template('domain/create.html', form=form) return flask.render_template('domain/create.html', form=form)

@ -73,3 +73,4 @@ webencodings==0.5.1
Werkzeug==2.0.2 Werkzeug==2.0.2
WTForms==2.3.3 WTForms==2.3.3
WTForms-Components==0.10.5 WTForms-Components==0.10.5
xmltodict==0.12.0

@ -25,3 +25,4 @@ srslib
marshmallow marshmallow
flask-marshmallow flask-marshmallow
marshmallow-sqlalchemy marshmallow-sqlalchemy
xmltodict

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
FROM $DISTRO FROM $DISTRO
ARG VERSION ARG VERSION

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
FROM $DISTRO FROM $DISTRO
ARG VERSION ARG VERSION

@ -17,7 +17,7 @@ http {
keepalive_timeout 65; keepalive_timeout 65;
server_tokens off; server_tokens off;
absolute_redirect off; absolute_redirect off;
resolver {{ RESOLVER }} ipv6=off valid=30s; resolver {{ RESOLVER }} valid=30s;
{% if REAL_IP_HEADER %} {% if REAL_IP_HEADER %}
real_ip_header {{ REAL_IP_HEADER }}; real_ip_header {{ REAL_IP_HEADER }};
@ -117,9 +117,32 @@ http {
add_header X-Frame-Options 'SAMEORIGIN'; add_header X-Frame-Options 'SAMEORIGIN';
add_header X-Content-Type-Options 'nosniff'; add_header X-Content-Type-Options 'nosniff';
add_header X-Permitted-Cross-Domain-Policies 'none'; add_header X-Permitted-Cross-Domain-Policies 'none';
add_header X-XSS-Protection '1; mode=block';
add_header Referrer-Policy 'same-origin'; add_header Referrer-Policy 'same-origin';
# mozilla autoconfiguration
location ~ ^/(\.well\-known/autoconfig/)?mail/config\-v1\.1\.xml {
rewrite ^ /internal/autoconfig/mozilla break;
include /etc/nginx/proxy.conf;
proxy_pass http://$admin;
}
# microsoft autoconfiguration
location ~* ^/Autodiscover/Autodiscover.json {
rewrite ^ /internal/autoconfig/microsoft.json break;
include /etc/nginx/proxy.conf;
proxy_pass http://$admin;
}
location ~* ^/Autodiscover/Autodiscover.xml {
rewrite ^ /internal/autoconfig/microsoft break;
include /etc/nginx/proxy.conf;
proxy_pass http://$admin;
}
# apple mobileconfig
location ~ ^/(apple\.)?mobileconfig {
rewrite ^ /internal/autoconfig/apple break;
include /etc/nginx/proxy.conf;
proxy_pass http://$admin;
}
{% if TLS_FLAVOR == 'mail-letsencrypt' %} {% if TLS_FLAVOR == 'mail-letsencrypt' %}
location ^~ /.well-known/acme-challenge/ { location ^~ /.well-known/acme-challenge/ {
proxy_pass http://127.0.0.1:8008; proxy_pass http://127.0.0.1:8008;
@ -254,7 +277,6 @@ mail {
server_name {{ HOSTNAMES.split(",")[0] }}; server_name {{ HOSTNAMES.split(",")[0] }};
auth_http http://127.0.0.1:8000/auth/email; auth_http http://127.0.0.1:8000/auth/email;
proxy_pass_error_message on; proxy_pass_error_message on;
resolver {{ RESOLVER }} ipv6=off valid=30s;
error_log /dev/stderr info; error_log /dev/stderr info;
{% if TLS and not TLS_ERROR %} {% if TLS and not TLS_ERROR %}

@ -4,10 +4,12 @@ import os
import time import time
import subprocess import subprocess
hostnames = ','.join(set(host.strip() for host in os.environ['HOSTNAMES'].split(',')))
command = [ command = [
"certbot", "certbot",
"-n", "--agree-tos", # non-interactive "-n", "--agree-tos", # non-interactive
"-d", os.environ["HOSTNAMES"], "-d", hostnames, "--expand", "--allow-subset-of-names",
"-m", "{}@{}".format(os.environ["POSTMASTER"], os.environ["DOMAIN"]), "-m", "{}@{}".format(os.environ["POSTMASTER"], os.environ["DOMAIN"]),
"certonly", "--standalone", "certonly", "--standalone",
"--cert-name", "mailu", "--cert-name", "mailu",
@ -20,7 +22,7 @@ command = [
command2 = [ command2 = [
"certbot", "certbot",
"-n", "--agree-tos", # non-interactive "-n", "--agree-tos", # non-interactive
"-d", os.environ["HOSTNAMES"], "-d", hostnames, "--expand", "--allow-subset-of-names",
"-m", "{}@{}".format(os.environ["POSTMASTER"], os.environ["DOMAIN"]), "-m", "{}@{}".format(os.environ["POSTMASTER"], os.environ["DOMAIN"]),
"certonly", "--standalone", "certonly", "--standalone",
"--cert-name", "mailu-ecdsa", "--cert-name", "mailu-ecdsa",

@ -1,6 +1,6 @@
# This is an idle image to dynamically replace any component if disabled. # This is an idle image to dynamically replace any component if disabled.
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
FROM $DISTRO FROM $DISTRO
CMD sleep 1000000d CMD sleep 1000000d

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
FROM $DISTRO FROM $DISTRO
ARG VERSION ARG VERSION

@ -80,7 +80,7 @@ virtual_mailbox_maps = ${podop}mailbox
# Mails are transported if required, then forwarded to Dovecot for delivery # Mails are transported if required, then forwarded to Dovecot for delivery
relay_domains = ${podop}transport relay_domains = ${podop}transport
transport_maps = ${podop}transport transport_maps = lmdb:/etc/postfix/transport.map, ${podop}transport
virtual_transport = lmtp:inet:{{ LMTP_ADDRESS }} virtual_transport = lmtp:inet:{{ LMTP_ADDRESS }}
# Sender and recipient canonical maps, mostly for SRS # Sender and recipient canonical maps, mostly for SRS

@ -15,6 +15,22 @@ 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
-o nested_header_checks= -o nested_header_checks=
# Polite policy
polite unix - - n - - smtp
-o syslog_name=postfix-polite
-o polite_destination_concurrency_limit=3
-o polite_destination_rate_delay=0
-o polite_destination_recipient_limit=20
-o polite_destination_concurrency_failed_cohort_limit=10
# Turtle policy
turtle unix - - n - - smtp
-o syslog_name=postfix-turtle
-o turtle_destination_concurrency_limit=1
-o turtle_destination_rate_delay=1
-o turtle_destination_recipient_limit=5
-o turtle_destination_concurrency_failed_cohort_limit=10
# Internal postfix services # Internal postfix services
pickup unix n - n 60 1 pickup pickup unix n - n 60 1 pickup
cleanup unix n - n - 0 cleanup cleanup unix n - n - 0 cleanup

@ -15,7 +15,7 @@ log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def start_podop(): def start_podop():
os.setuid(getpwnam('postfix').pw_uid) os.setuid(getpwnam('postfix').pw_uid)
os.mkdir('/dev/shm/postfix',mode=0o700) os.makedirs('/dev/shm/postfix',mode=0o700, exist_ok=True)
url = "http://" + os.environ["ADMIN_ADDRESS"] + "/internal/postfix/" 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", [
@ -74,9 +74,10 @@ if os.path.exists("/overrides/mta-sts-daemon.yml"):
else: else:
conf.jinja("/conf/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml") conf.jinja("/conf/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml")
if not os.path.exists("/etc/postfix/tls_policy.map.lmdb"): for policy in ['tls_policy', 'transport']:
open("/etc/postfix/tls_policy.map", "a").close() if not os.path.exists(f'/etc/postfix/{policy}.map.lmdb'):
os.system("postmap /etc/postfix/tls_policy.map") open(f'/etc/postfix/{policy}.map', 'a').close()
os.system(f'postmap /etc/postfix/{policy}.map')
if "RELAYUSER" in os.environ: if "RELAYUSER" in os.environ:
path = "/etc/postfix/sasl_passwd" path = "/etc/postfix/sasl_passwd"

@ -1,6 +1,6 @@
{% if ANTIVIRUS == 'clamav' %} {% if ANTIVIRUS == 'clamav' %}
clamav { clamav {
attachments_only = true; scan_mime_parts = true;
symbol = "CLAM_VIRUS"; symbol = "CLAM_VIRUS";
type = "clamav"; type = "clamav";
servers = "{{ ANTIVIRUS_ADDRESS }}"; servers = "{{ ANTIVIRUS_ADDRESS }}";

@ -29,5 +29,5 @@ Update information files
If you added a feature or fixed a bug or committed anything that is worth mentionning If you added a feature or fixed a bug or committed anything that is worth mentionning
for the next upgrade, add it in the ``CHANGELOG.md`` file. for the next upgrade, add it in the ``CHANGELOG.md`` file.
Also, if you would like to be mentionned by name or add a comment in ``AUTHORS.md``, Also, if you would like to be mentioned by name or add a comment in ``AUTHORS.md``,
feel free to do so. feel free to do so.

@ -396,58 +396,6 @@ Mailu can serve an `MTA-STS policy`_; To configure it you will need to:
.. _`1798`: https://github.com/Mailu/Mailu/issues/1798 .. _`1798`: https://github.com/Mailu/Mailu/issues/1798
.. _`MTA-STS policy`: https://datatracker.ietf.org/doc/html/rfc8461 .. _`MTA-STS policy`: https://datatracker.ietf.org/doc/html/rfc8461
How do I setup client autoconfiguration?
````````````````````````````````````````
Mailu can serve an `XML file for autoconfiguration`_; To configure it you will need to:
1. add ``autoconfig.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it; this may mean restarting your smtp container)
2. configure an override with the policy itself; for example, your ``overrides/nginx/autoconfiguration.conf`` could read:
.. code-block:: bash
location ^~ /mail/config-v1.1.xml {
return 200 "<?xml version=\"1.0\"?>
<clientConfig version=\"1.1\">
<emailProvider id=\"%EMAILDOMAIN%\">
<domain>%EMAILDOMAIN%</domain>
<displayName>Email</displayName>
<displayShortName>Email</displayShortName>
<incomingServer type=\"imap\">
<hostname>mailu.example.com</hostname>
<port>993</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
</incomingServer>
<outgoingServer type=\"smtp\">
<hostname>mailu.example.com</hostname>
<port>465</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
<addThisServer>true</addThisServer>
<useGlobalPreferredServer>true</useGlobalPreferredServer>
</outgoingServer>
<documentation url=\"https://mailu.example.com/admin/client\">
<descr lang=\"en\">Configure your email client</descr>
</documentation>
</emailProvider>
</clientConfig>\r\n";
}
3. setup the appropriate DNS/CNAME record (``autoconfig.example.com`` -> ``mailu.example.com``).
*issue reference:* `224`_.
.. _`224`: https://github.com/Mailu/Mailu/issues/224
.. _`XML file for autoconfiguration`: https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
Technical issues Technical issues
---------------- ----------------
@ -476,6 +424,22 @@ Any mail related connection is proxied by nginx. Therefore the SMTP Banner is al
.. _`1368`: https://github.com/Mailu/Mailu/issues/1368 .. _`1368`: https://github.com/Mailu/Mailu/issues/1368
My emails are getting rejected, I am being told to slow down, what can I do?
````````````````````````````````````````````````````````````````````````````
Some email operators insist that emails are delivered slowly. Mailu maintains two separate queues for such destinations: ``polite`` and ``turtle``. To enable them for some destination you can creating an override at ``overrides/postfix/transport.map`` as follow:
.. code-block:: bash
yahoo.com polite:
orange.fr turtle:
Re-starting the smtp container will be required for changes to take effect.
*Issue reference:* `2213`_.
.. _`2213`: https://github.com/Mailu/Mailu/issues/2213
My emails are getting defered, what can I do? My emails are getting defered, what can I do?
````````````````````````````````````````````` `````````````````````````````````````````````
@ -488,7 +452,7 @@ If delivery to a specific domain fails because their DANE records are invalid or
domain.example.com may domain.example.com may
domain.example.org encrypt domain.example.org encrypt
The syntax and options are as described in `postfix's documentation`_. Re-creating the smtp container will be required for changes to take effect. The syntax and options are as described in `postfix's documentation`_. Re-starting the smtp container will be required for changes to take effect.
.. _`postfix's documentation`: http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps .. _`postfix's documentation`: http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps
@ -511,7 +475,7 @@ These issues are typically caused by four scenarios:
#. Certificates expired; #. Certificates expired;
#. When ``TLS_FLAVOR=letsencrypt``, it might be that the *certbot* script is not capable of #. When ``TLS_FLAVOR=letsencrypt``, it might be that the *certbot* script is not capable of
obtaining the certificates for your domain. See `letsencrypt issues`_ obtaining the certificates for your domain. See `letsencrypt issues`_
#. When ``TLS_FLAVOR=certs``, certificates are supposed to be copied to ``/mailu/certs``. #. When ``TLS_FLAVOR=cert``, certificates are supposed to be copied to ``/mailu/certs``.
Using an external ``letsencrypt`` program, it tends to happen people copy the whole Using an external ``letsencrypt`` program, it tends to happen people copy the whole
``letsencrypt/live`` directory containing symlinks. Symlinks do not resolve inside the ``letsencrypt/live`` directory containing symlinks. Symlinks do not resolve inside the
container and therefore it breaks the TLS implementation. container and therefore it breaks the TLS implementation.

@ -23,7 +23,7 @@ popular groupware.
Main features include: Main features include:
- **Standard email server**, IMAP and IMAP+, SMTP and Submission - **Standard email server**, IMAP and IMAP+, SMTP and Submission with autoconfiguration profiles for clients
- **Advanced email features**, aliases, domain aliases, custom routing - **Advanced email features**, aliases, domain aliases, custom routing
- **Web access**, multiple Webmails and administration interface - **Web access**, multiple Webmails and administration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts - **User features**, aliases, auto-reply, auto-forward, fetched accounts

@ -15,6 +15,7 @@ simply pull the latest images and recreate the containers :
.. code-block:: bash .. code-block:: bash
docker-compose pull docker-compose pull
docker-compose down
docker-compose up -d docker-compose up -d
Monitoring the mail server Monitoring the mail server

@ -4,8 +4,8 @@ Release notes
Mailu 1.9 - 2021-12-29 Mailu 1.9 - 2021-12-29
---------------------- ----------------------
Mailu 1.9 is available now. The helm-chart repo is not in sync yet with the new Mailu 1.9 release. If you use helm-chart (kubernetes), we advise to stick to version 1.8 for now. Mailu 1.9 is available now.
See the section `Upgrading` for important information in regard to upgrading to Mailu 1.9. Please see the section `Upgrading` for important information in regard to upgrading to Mailu 1.9.
Highlights Highlights
```````````````````````````````` ````````````````````````````````
@ -119,7 +119,7 @@ A short summary of the new features:
Upgrading Upgrading
````````` `````````
Upgrade should run fine as long as you generate a new compose or stack configuration and upgrade your mailu.env. Upgrade should run fine as long as you generate a new compose or stack configuration and upgrade your mailu.env. Please note that once you have upgraded to 1.9 you won't be able to roll-back to earlier versions without resetting user passwords.
If you use a reverse proxy in front of Mailu, it is vital to configure the newly introduced environment variables `REAL_IP_HEADER`` and `REAL_IP_FROM`. If you use a reverse proxy in front of Mailu, it is vital to configure the newly introduced environment variables `REAL_IP_HEADER`` and `REAL_IP_FROM`.
These settings tell Mailu that the HTTP header with the remote client IP address from the reverse proxy can be trusted. These settings tell Mailu that the HTTP header with the remote client IP address from the reverse proxy can be trusted.

@ -47,7 +47,7 @@ Then on your own frontend, point to these local ports. In practice, you only nee
location / { location / {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr proxy_set_header X-Real-IP $remote_addr;
proxy_pass https://localhost:8443; proxy_pass https://localhost:8443;
} }
} }
@ -59,16 +59,16 @@ Then on your own frontend, point to these local ports. In practice, you only nee
REAL_IP_FROM=x.x.x.x,y.y.y.y.y REAL_IP_FROM=x.x.x.x,y.y.y.y.y
#x.x.x.x,y.y.y.y.y is the static IP address your reverse proxy uses for connecting to Mailu. #x.x.x.x,y.y.y.y.y is the static IP address your reverse proxy uses for connecting to Mailu.
Because the admin interface is served as ``/admin``, the Webmail as ``/webmail``, the single sign on page as ``/sso``, webdav as ``/webdav`` and the static files endpoint as ``/static``, you may also want to use a single virtual host and serve other applications (still Nginx): Because the admin interface is served as ``/admin``, the Webmail as ``/webmail``, the single sign on page as ``/sso``, webdav as ``/webdav``, the client-autoconfiguration and the static files endpoint as ``/static``, you may also want to use a single virtual host and serve other applications (still Nginx):
.. code-block:: nginx .. code-block:: nginx
server { server {
# [...] here goes your standard configuration # [...] here goes your standard configuration
location ~ ^/(admin|sso|static|webdav|webmail) { location ~* ^/(admin|sso|static|webdav|webmail|(apple\.)?mobileconfig|(\.well\-known/autoconfig/)?mail/|Autodiscover/Autodiscover) {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr proxy_set_header X-Real-IP $remote_addr;
proxy_pass https://localhost:8443; proxy_pass https://localhost:8443;
} }
@ -111,7 +111,7 @@ Here is an example configuration :
location /webmail { location /webmail {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr proxy_set_header X-Real-IP $remote_addr;
proxy_pass https://localhost:8443/webmail; proxy_pass https://localhost:8443/webmail;
} }
} }
@ -123,7 +123,7 @@ Here is an example configuration :
location /admin { location /admin {
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr proxy_set_header X-Real-IP $remote_addr;
proxy_pass https://localhost:8443/admin; proxy_pass https://localhost:8443/admin;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
} }
@ -189,7 +189,7 @@ Mailu must also be configured with the information what header is used by the re
.. code-block:: docker .. code-block:: docker
#mailu.env file #mailu.env file
REAL_IP_HEADER=X-Real-IP REAL_IP_HEADER=X-Real-Ip
REAL_IP_FROM=x.x.x.x,y.y.y.y.y REAL_IP_FROM=x.x.x.x,y.y.y.y.y
#x.x.x.x,y.y.y.y.y is the static IP address your reverse proxy uses for connecting to Mailu. #x.x.x.x,y.y.y.y.y is the static IP address your reverse proxy uses for connecting to Mailu.

@ -17,8 +17,8 @@ Adjustments
``build_arm.sh`` uses some variables passed as ``build-arg`` to docker-compose: ``build_arm.sh`` uses some variables passed as ``build-arg`` to docker-compose:
- ``ALPINE_VER``: version of ALPINE to use - ``ALPINE_VER``: version of ALPINE to use
- ``DISTRO``: is the main distro used. Dockerfiles are set on Alpine 3.10, and - ``DISTRO``: is the main distro used. Dockerfiles are set on Alpine 3.14, and
build script overrides for ``balenalib/rpi-alpine:3.10`` build script overrides for ``balenalib/rpi-alpine:3.14``
- ``QEMU``: Used by webmails dockerfiles. It will add ``qemu-arm-static`` only - ``QEMU``: Used by webmails dockerfiles. It will add ``qemu-arm-static`` only
if ``QEMU`` is set to ``arm`` if ``QEMU`` is set to ``arm``
- ``ARCH``: Architecture to use for ``admin``, and ``webmails`` as their images - ``ARCH``: Architecture to use for ``admin``, and ``webmails`` as their images

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
FROM $DISTRO FROM $DISTRO
ARG VERSION ARG VERSION

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
FROM $DISTRO FROM $DISTRO
ARG VERSION ARG VERSION

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
FROM $DISTRO FROM $DISTRO
ARG VERSION ARG VERSION
@ -13,7 +13,7 @@ RUN apk add --no-cache \
# Image specific layers under this line # Image specific layers under this line
RUN apk add --no-cache curl \ RUN apk add --no-cache curl \
&& pip3 install radicale~=3.0 && pip3 install pytz radicale~=3.0
COPY radicale.conf /radicale.conf COPY radicale.conf /radicale.conf

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
FROM $DISTRO FROM $DISTRO
ARG VERSION ARG VERSION

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.14.3 ARG DISTRO=alpine:3.14.5
FROM $DISTRO FROM $DISTRO
ARG VERSION ARG VERSION
ENV TZ Etc/UTC ENV TZ Etc/UTC

@ -4,7 +4,7 @@
<p>Docker Stack expects a project file, named <code>docker-compose.yml</code> <p>Docker Stack expects a project file, named <code>docker-compose.yml</code>
in a project directory. First create your project directory.</p> in a project directory. First create your project directory.</p>
<pre><code>mkdir -p {{ root }}/{redis,certs,data,dkim,mail,mailqueue,overrides/rspamd,overrides/postfix,overrides/dovecot,overrides/nginx,filter,dav,webmail} <pre><code>mkdir -p {{ root }}/{redis,certs,data,data/fetchmail,dkim,mail,mailqueue,overrides/rspamd,overrides/postfix,overrides/dovecot,overrides/nginx,filter,dav,webmail}
</pre></code> </pre></code>
<p>Then download the project file. A side configuration file makes it easier <p>Then download the project file. A side configuration file makes it easier

@ -31,7 +31,7 @@ avoid generic all-interfaces addresses like <code>0.0.0.0</code> or <code>::</co
</div> </div>
<div class="form-group" id="ipv6" style="display: none"> <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> and be <b>very careful</b> if you still wish to enable this!</p> <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>. Read <a href="https://mailu.io/{{ version }}/faq.html#how-to-make-ipv6-work">FAQ section</a> and be <b>very careful</b>. We do <b>NOT</b> recommend that you enable this!</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"

@ -0,0 +1 @@
Add input validation for domain creation

@ -0,0 +1 @@
Create a polite and turtle delivery queue to accommodate destinations that expect emails to be sent slowly

@ -0,0 +1 @@
Provide auto-configuration files (autodiscover, autoconfig & mobileconfig); Please update your DNS records

@ -0,0 +1 @@
Fix a bug where rspamd may trigger HFILTER_HOSTNAME_UNKNOWN if part of the delivery chain was using ipv6

@ -0,0 +1 @@
Update to Alpine Linux 3.14.4 which contains a security fix for openssl.

@ -0,0 +1 @@
Fixed AUTH_RATELIMIT_IP not working on imap/pop3/smtp.

@ -0,0 +1 @@
update alpine linux docker image to version 3.14.5 which includes a security fix for zlibs CVE-2018-25032.

@ -0,0 +1 @@
Don't send the `X-XSS-Protection` http header anymore.

@ -0,0 +1 @@
Disable the built-in nginx resolver for traffic going through the mail plugin. This will silence errors about DNS resolution when the connecting host has no rDNS.

@ -15,5 +15,5 @@ custom_logout_link='/sso/logout'
enable = On enable = On
allow_sync = On allow_sync = On
[plugins] [defaults]
contacts_autosave = On contacts_autosave = On

Loading…
Cancel
Save