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

master
Florent Daigniere 2 years ago
commit 04b7ddfffd

@ -3,10 +3,7 @@ Changelog
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.
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 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.

@ -17,7 +17,7 @@ Features
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
- **Web access**, multiple Webmails and administration interface
- **User features**, aliases, auto-reply, auto-forward, fetched accounts

@ -1,5 +1,5 @@
# First stage to build assets
ARG DISTRO=alpine:3.14.3
ARG DISTRO=alpine:3.14.5
ARG ARCH=""
FROM ${ARCH}node:16 as assets
@ -13,7 +13,7 @@ COPY webpack.config.js ./
COPY assets ./assets
RUN set -eu \
&& 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; \
done \
&& node_modules/.bin/webpack-cli --color
@ -59,4 +59,4 @@ ENV FLASK_APP mailu
CMD /start.py
HEALTHCHECK CMD curl -f -L http://localhost/sso/login?next=ui.index || exit 1
RUN echo $VERSION >> /version
RUN echo $VERSION >> /version

@ -94,11 +94,11 @@ def handle_authentication(headers):
else:
try:
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:
exc = str(exc).split('\n', 1)[0]
app.logger.warn(f'Invalid user {user_email!r}: {exc}')
else:
is_valid_user = user is not None
ip = urllib.parse.unquote(headers["Client-Ip"])
if check_credentials(user, password, ip, protocol, headers["Auth-Port"]):
server, port = get_server(headers["Auth-Protocol"], True)

@ -1,3 +1,3 @@
__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_login
import base64
import sqlalchemy.exc
@internal.route("/auth/email")
def nginx_authentication():
@ -32,7 +33,7 @@ def nginx_authentication():
for key, value in headers.items():
response.headers[key] = str(value)
is_valid_user = False
if response.headers.get("Auth-User-Exists"):
if response.headers.get("Auth-User-Exists") == "True":
username = response.headers["Auth-User"]
if utils.limiter.should_rate_limit_user(username, client_ip):
# 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['Retry-After'] = '60'
return response
user = models.User.query.get(user_email)
if user 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
utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)
try:
user = models.User.query.get(user_email) if '@' in user_email else None
except sqlalchemy.exc.StatementError as exc:
exc = str(exc).split('\n', 1)[0]
app.logger.warn(f'Invalid user {user_email!r}: {exc}')
else:
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.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'
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) """
hostname = app.config['HOSTNAME']
protocols = [
('submission', 587),
('imap', 143),
('pop3', 110),
('imap', 143, 20),
('pop3', 110, 20),
('submission', 587, 20),
]
if app.config['TLS_FLAVOR'] != 'notls':
protocols.extend([
('imaps', 993),
('pop3s', 995),
('autodiscover', 443, 10),
('submissions', 465, 10),
('imaps', 993, 10),
('pop3s', 995, 10),
])
return list([
f'_{proto}._tcp.{self.name}. 600 IN SRV 1 1 {port} {hostname}.'
for proto, port
return [
f'_{proto}._tcp.{self.name}. 600 IN SRV {prio} 1 {port} {hostname}.'
for proto, port, prio
in protocols
])
]+[f'autoconfig.{self.name}. 600 IN CNAME {hostname}.']
@cached_property
def dns_tlsa(self):

@ -9,6 +9,7 @@
{%- endblock %}
{%- 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) %}
<tbody>
<tr>
@ -17,7 +18,7 @@
</tr>
<tr>
<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>
<th>{% trans %}Server name{% endtrans %}</th>
@ -42,7 +43,7 @@
</tr>
<tr>
<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>
<th>{% trans %}Server name{% endtrans %}</th>

@ -60,7 +60,7 @@
</tr>
{%- endif %}
<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">
{%- for line in domain.dns_autoconfig %}
{{ line }}

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

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

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

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

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

@ -17,7 +17,7 @@ http {
keepalive_timeout 65;
server_tokens off;
absolute_redirect off;
resolver {{ RESOLVER }} ipv6=off valid=30s;
resolver {{ RESOLVER }} valid=30s;
{% if REAL_IP_HEADER %}
real_ip_header {{ REAL_IP_HEADER }};
@ -117,9 +117,32 @@ http {
add_header X-Frame-Options 'SAMEORIGIN';
add_header X-Content-Type-Options 'nosniff';
add_header X-Permitted-Cross-Domain-Policies 'none';
add_header X-XSS-Protection '1; mode=block';
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' %}
location ^~ /.well-known/acme-challenge/ {
proxy_pass http://127.0.0.1:8008;
@ -254,7 +277,6 @@ mail {
server_name {{ HOSTNAMES.split(",")[0] }};
auth_http http://127.0.0.1:8000/auth/email;
proxy_pass_error_message on;
resolver {{ RESOLVER }} ipv6=off valid=30s;
error_log /dev/stderr info;
{% if TLS and not TLS_ERROR %}

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

@ -1,6 +1,6 @@
# 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
CMD sleep 1000000d

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

@ -80,7 +80,7 @@ virtual_mailbox_maps = ${podop}mailbox
# Mails are transported if required, then forwarded to Dovecot for delivery
relay_domains = ${podop}transport
transport_maps = ${podop}transport
transport_maps = lmdb:/etc/postfix/transport.map, ${podop}transport
virtual_transport = lmtp:inet:{{ LMTP_ADDRESS }}
# 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 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
pickup unix n - n 60 1 pickup
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():
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/"
# TODO: Remove verbosity setting from Podop?
run_server(0, "postfix", "/tmp/podop.socket", [
@ -74,9 +74,10 @@ if os.path.exists("/overrides/mta-sts-daemon.yml"):
else:
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"):
open("/etc/postfix/tls_policy.map", "a").close()
os.system("postmap /etc/postfix/tls_policy.map")
for policy in ['tls_policy', 'transport']:
if not os.path.exists(f'/etc/postfix/{policy}.map.lmdb'):
open(f'/etc/postfix/{policy}.map', 'a').close()
os.system(f'postmap /etc/postfix/{policy}.map')
if "RELAYUSER" in os.environ:
path = "/etc/postfix/sasl_passwd"

@ -1,6 +1,6 @@
{% if ANTIVIRUS == 'clamav' %}
clamav {
attachments_only = true;
scan_mime_parts = true;
symbol = "CLAM_VIRUS";
type = "clamav";
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
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.

@ -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
.. _`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
----------------
@ -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
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?
`````````````````````````````````````````````
@ -488,7 +452,7 @@ If delivery to a specific domain fails because their DANE records are invalid or
domain.example.com may
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
@ -511,7 +475,7 @@ These issues are typically caused by four scenarios:
#. Certificates expired;
#. When ``TLS_FLAVOR=letsencrypt``, it might be that the *certbot* script is not capable of
obtaining the certificates for your domain. See `letsencrypt issues`_
#. When ``TLS_FLAVOR=certs``, certificates are supposed to be copied to ``/mailu/certs``.
#. 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
``letsencrypt/live`` directory containing symlinks. Symlinks do not resolve inside the
container and therefore it breaks the TLS implementation.

@ -23,7 +23,7 @@ popular groupware.
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
- **Web access**, multiple Webmails and administration interface
- **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
docker-compose pull
docker-compose down
docker-compose up -d
Monitoring the mail server

@ -4,8 +4,8 @@ Release notes
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.
See the section `Upgrading` for important information in regard to upgrading to Mailu 1.9.
Mailu 1.9 is available now.
Please see the section `Upgrading` for important information in regard to upgrading to Mailu 1.9.
Highlights
````````````````````````````````
@ -119,7 +119,7 @@ A short summary of the new features:
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`.
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 / {
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;
}
}
@ -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
#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
server {
# [...] 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 X-Real-IP $remote_addr
proxy_set_header X-Real-IP $remote_addr;
proxy_pass https://localhost:8443;
}
@ -111,7 +111,7 @@ Here is an example configuration :
location /webmail {
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;
}
}
@ -123,7 +123,7 @@ Here is an example configuration :
location /admin {
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_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
#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
#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:
- ``ALPINE_VER``: version of ALPINE to use
- ``DISTRO``: is the main distro used. Dockerfiles are set on Alpine 3.10, and
build script overrides for ``balenalib/rpi-alpine:3.10``
- ``DISTRO``: is the main distro used. Dockerfiles are set on Alpine 3.14, and
build script overrides for ``balenalib/rpi-alpine:3.14``
- ``QEMU``: Used by webmails dockerfiles. It will add ``qemu-arm-static`` only
if ``QEMU`` is set to ``arm``
- ``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
ARG VERSION

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

@ -1,4 +1,4 @@
ARG DISTRO=alpine:3.14.3
ARG DISTRO=alpine:3.14.5
FROM $DISTRO
ARG VERSION
@ -13,7 +13,7 @@ RUN apk add --no-cache \
# Image specific layers under this line
RUN apk add --no-cache curl \
&& pip3 install radicale~=3.0
&& pip3 install pytz radicale~=3.0
COPY radicale.conf /radicale.conf
@ -24,4 +24,4 @@ VOLUME ["/data"]
CMD radicale -S -C /radicale.conf
HEALTHCHECK CMD curl -f -L http://localhost:5232/ || exit 1
RUN echo $VERSION >> /version
RUN echo $VERSION >> /version

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

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

@ -4,7 +4,7 @@
<p>Docker Stack expects a project file, named <code>docker-compose.yml</code>
in a project directory. First create your project directory.</p>
<pre><code>mkdir -p {{ root }}/{redis,certs,data,dkim,mail,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>
<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 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>
<!-- Validates IPv6 address -->
<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
allow_sync = On
[plugins]
[defaults]
contacts_autosave = On

Loading…
Cancel
Save