2276: Autoconfig of email clients r=mergify[bot] a=nextgens

## What type of PR?

Feature

## What does this PR do?

It provides auto-configuration templates for email clients and encourages them to use implicit TLS (see https://nostarttls.secvuln.info/)

There are numerous caveats:
- it will only work if suitable DNS records are created and certificates obtained (autoconfig, autodiscover, ...)
- the mobileconfig file isn't signed
- the credentials will be prompted... we could/should provision a token on each request instead
- it currently doesn't advertise caldav
- it's IMAP only

### Related issue(s)
- close #224 

## Prerequisites
Before we can consider review and merge, please make sure the following list is done and checked.
If an entry in not applicable, you can check it or remove it from the list.

- [x] In case of feature or enhancement: documentation updated accordingly
- [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file.


Co-authored-by: Florent Daigniere <nextgens@freenetproject.org>
This commit is contained in:
bors[bot]
2022-03-22 08:53:47 +00:00
committed by GitHub
14 changed files with 235 additions and 71 deletions

View File

@@ -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

View File

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

View 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)

View File

@@ -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):

View File

@@ -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>

View File

@@ -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 }}

View File

@@ -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

View File

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

View File

@@ -120,6 +120,30 @@ http {
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;

View File

@@ -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",

View File

@@ -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
----------------

View File

@@ -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

View File

@@ -59,14 +59,14 @@ 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_pass https://localhost:8443;

View File

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