mirror of
https://github.com/Mailu/Mailu.git
synced 2025-03-05 14:55:20 +02:00
Merge remote-tracking branch 'upstream/master' into Riscue-master
This commit is contained in:
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
|
||||
|
183
core/admin/mailu/internal/views/autoconfig.py
Normal file
183
core/admin/mailu/internal/views/autoconfig.py
Normal file
@ -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.
|
||||
|
72
docs/faq.rst
72
docs/faq.rst
@ -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"
|
||||
|
1
towncrier/newsfragments/2210.bugfix
Normal file
1
towncrier/newsfragments/2210.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Add input validation for domain creation
|
1
towncrier/newsfragments/2213.feature
Normal file
1
towncrier/newsfragments/2213.feature
Normal file
@ -0,0 +1 @@
|
||||
Create a polite and turtle delivery queue to accommodate destinations that expect emails to be sent slowly
|
1
towncrier/newsfragments/224.feature
Normal file
1
towncrier/newsfragments/224.feature
Normal file
@ -0,0 +1 @@
|
||||
Provide auto-configuration files (autodiscover, autoconfig & mobileconfig); Please update your DNS records
|
1
towncrier/newsfragments/2260.bugfix
Normal file
1
towncrier/newsfragments/2260.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fix a bug where rspamd may trigger HFILTER_HOSTNAME_UNKNOWN if part of the delivery chain was using ipv6
|
1
towncrier/newsfragments/2281.bugfix
Normal file
1
towncrier/newsfragments/2281.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Update to Alpine Linux 3.14.4 which contains a security fix for openssl.
|
1
towncrier/newsfragments/2284.bugfix
Normal file
1
towncrier/newsfragments/2284.bugfix
Normal file
@ -0,0 +1 @@
|
||||
Fixed AUTH_RATELIMIT_IP not working on imap/pop3/smtp.
|
1
towncrier/newsfragments/2302.bugfix
Normal file
1
towncrier/newsfragments/2302.bugfix
Normal file
@ -0,0 +1 @@
|
||||
update alpine linux docker image to version 3.14.5 which includes a security fix for zlib’s CVE-2018-25032.
|
1
towncrier/newsfragments/2338.misc
Normal file
1
towncrier/newsfragments/2338.misc
Normal file
@ -0,0 +1 @@
|
||||
Don't send the `X-XSS-Protection` http header anymore.
|
1
towncrier/newsfragments/2346.bugfix
Normal file
1
towncrier/newsfragments/2346.bugfix
Normal file
@ -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…
x
Reference in New Issue
Block a user