mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge pull request #3729 from modoboa/feature/autoconfig
Autoconfiguration service
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2009-2023, Antoine Nguyen <tonio@ngyn.org>
|
||||
Copyright (c) 2009-2025, Antoine Nguyen <tonio@ngyn.org>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
|
||||
@@ -130,6 +130,27 @@ Rebuild Virtual Environment
|
||||
Specific instructions
|
||||
*********************
|
||||
|
||||
Version 2.6.0
|
||||
=============
|
||||
|
||||
New built-in auto-configuration service.
|
||||
|
||||
Required changes to :file:`settings.py`
|
||||
---------------------------------------
|
||||
|
||||
- Add the following line to ``MODOBOA_APPS`` variable:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
'modoboa.autoconfig'
|
||||
|
||||
- Add the following hostnames to ``ALLOWED_HOSTS`` variable:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
'autoconfig.<MY DOMAIN>',
|
||||
'autodiscover.<MY DOMAIN>',
|
||||
|
||||
Version 2.5.0
|
||||
=============
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AutoconfigConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "modoboa.autoconfig"
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="{{ domain }}">
|
||||
<domain>{{ domain }}</domain>
|
||||
<displayName>Modoboa</displayName>
|
||||
<displayShortName>Mail</displayShortName>
|
||||
|
||||
<incomingServer type="imap">
|
||||
<hostname>{{ connection_settings.imap.HOSTNAME }}</hostname>
|
||||
<port>{{ connection_settings.imap.PORT }}</port>
|
||||
<socketType>{{ connection_settings.imap.SOCKET_TYPE }}</socketType>
|
||||
<authentication>plain</authentication>
|
||||
<username>{{ emailaddress }}</username>
|
||||
</incomingServer>
|
||||
|
||||
<outgoingServer type="smtp">
|
||||
<hostname>{{ connection_settings.smtp.HOSTNAME }}</hostname>
|
||||
<port>{{ connection_settings.smtp.PORT }}</port>
|
||||
<socketType>{{ connection_settings.smtp.SOCKET_TYPE }}</socketType>
|
||||
<authentication>plain</authentication>
|
||||
<username>{{ emailaddress }}</username>
|
||||
</outgoingServer>
|
||||
</emailProvider>
|
||||
</clientConfig>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
||||
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
||||
<Account>
|
||||
<AccountType>email</AccountType>
|
||||
<Action>settings</Action>
|
||||
|
||||
<Protocol>
|
||||
<Type>IMAP</Type>
|
||||
<Server>{{ connection_settings.imap.HOSTNAME }}</Server>
|
||||
<Port>{{ connection_settings.imap.PORT }}</Port>
|
||||
<SSL>true</SSL>
|
||||
<LoginName>{{ emailaddress }}</LoginName>
|
||||
</Protocol>
|
||||
|
||||
<Protocol>
|
||||
<Type>SMTP</Type>
|
||||
<Server>{{ connection_settings.smtp.HOSTNAME }}</Server>
|
||||
<Port>{{ connection_settings.smtp.PORT }}</Port>
|
||||
<SSL>false</SSL>
|
||||
<TLS>true</TLS>
|
||||
<LoginName>{{ emailaddress }}</LoginName>
|
||||
</Protocol>
|
||||
|
||||
</Account>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
@@ -0,0 +1,38 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class ViewsTestCase(TestCase):
|
||||
|
||||
databases = "__all__"
|
||||
|
||||
def test_autoconfig(self):
|
||||
url = reverse("autoconfig:autoconfig")
|
||||
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
resp = self.client.get(f"{url}?emailaddress=test@test.com")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.assertIn(b'<emailProvider id="test.com">', resp.content)
|
||||
|
||||
def test_autodiscover(self):
|
||||
url = reverse("autoconfig:autodiscover")
|
||||
|
||||
resp = self.client.post(url)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
resp = self.client.post(url, {"EmailAddress": "test@test.com"})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.assertIn(b"<LoginName>test@test.com", resp.content)
|
||||
|
||||
def test_mobileconfig(self):
|
||||
url = reverse("autoconfig:mobileconfig")
|
||||
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
resp = self.client.get(f"{url}?emailaddress=test@test.com")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Autoconfig urls."""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from modoboa.autoconfig import views
|
||||
|
||||
app_name = "autoconfig"
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"mail/config-v1.1.xml",
|
||||
views.AutoConfigView.as_view(),
|
||||
name="autoconfig",
|
||||
),
|
||||
path(
|
||||
"autodiscover/autodiscover.xml",
|
||||
views.AutoDiscoverView.as_view(),
|
||||
name="autodiscover",
|
||||
),
|
||||
path("mobileconfig", views.MobileConfigView.as_view(), name="mobileconfig"),
|
||||
]
|
||||
@@ -0,0 +1,109 @@
|
||||
import plistlib
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from modoboa.lib.email_utils import split_address
|
||||
|
||||
|
||||
class ConfigBaseMixin:
|
||||
|
||||
content_type = "application/xml"
|
||||
|
||||
def get_common_context(self, emailaddress: str) -> dict:
|
||||
local_part, domain = split_address(emailaddress)
|
||||
return {
|
||||
"emailaddress": emailaddress,
|
||||
"domain": domain,
|
||||
"connection_settings": settings.EMAIL_CLIENT_CONNECTION_SETTINGS,
|
||||
}
|
||||
|
||||
|
||||
class AutoConfigView(ConfigBaseMixin, generic.TemplateView):
|
||||
|
||||
http_method_names = ["get"]
|
||||
template_name = "autoconfig/autoconfig.xml"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
emailaddress = self.request.GET.get("emailaddress")
|
||||
if not emailaddress:
|
||||
raise Http404
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(self.get_common_context(emailaddress))
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
class AutoDiscoverView(ConfigBaseMixin, generic.TemplateView):
|
||||
|
||||
http_method_names = ["post"]
|
||||
template_name = "autoconfig/autodiscover.xml"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
emailaddress = self.request.POST.get("EmailAddress")
|
||||
if not emailaddress:
|
||||
raise Http404
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(self.get_common_context(emailaddress))
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
context = self.get_context_data(**kwargs)
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
class MobileConfigView(generic.View):
|
||||
|
||||
http_method_names = ["get"]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
emailaddress = self.request.GET.get("emailaddress")
|
||||
if not emailaddress:
|
||||
raise Http404
|
||||
local_part, domain = split_address(emailaddress)
|
||||
parts = domain.split(".")
|
||||
parts.reverse()
|
||||
reverse_domain = ".".join(parts)
|
||||
imap_settings = settings.EMAIL_CLIENT_CONNECTION_SETTINGS["imap"]
|
||||
smtp_settings = settings.EMAIL_CLIENT_CONNECTION_SETTINGS["smtp"]
|
||||
profile = {
|
||||
"PayloadType": "Configuration",
|
||||
"PayloadVersion": 1,
|
||||
"PayloadIdentifier": f"{reverse_domain}.mailprofile",
|
||||
"PayloadUUID": str(uuid.uuid4()),
|
||||
"PayloadDisplayName": f"{domain} Mail Configuration",
|
||||
"PayloadOrganization": domain,
|
||||
"PayloadContent": [
|
||||
{
|
||||
"PayloadType": "com.apple.mail.managed",
|
||||
"PayloadVersion": 1,
|
||||
"PayloadIdentifier": f"{reverse_domain}.mailprofile.mail",
|
||||
"PayloadUUID": str(uuid.uuid4()),
|
||||
"PayloadDisplayName": "Mail",
|
||||
"EmailAccountDescription": f"{domain} Mail",
|
||||
"EmailAccountType": "EmailTypeIMAP",
|
||||
"EmailAddress": emailaddress,
|
||||
# incoming
|
||||
"IncomingMailServerHostName": imap_settings["HOSTNAME"],
|
||||
"IncomingMailServerPortNumber": imap_settings["PORT"],
|
||||
"IncomingMailServerUseSSL": True,
|
||||
"IncomingMailServerUsername": emailaddress,
|
||||
# outgoing
|
||||
"OutgoingMailServerHostName": smtp_settings["HOSTNAME"],
|
||||
"OutgoingMailServerPortNumber": smtp_settings["PORT"],
|
||||
"OutgoingMailServerUseSSL": True,
|
||||
"OutgoingMailServerUsername": emailaddress,
|
||||
"OutgoingPasswordSameAsIncomingPassword": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
plist_bytes = plistlib.dumps(profile, fmt=plistlib.FMT_XML)
|
||||
resp = HttpResponse(
|
||||
plist_bytes, content_type="application/x-apple-aspen-config"
|
||||
)
|
||||
resp["Content-Disposition"] = 'attachment; filename="mail.mobileconfig"'
|
||||
return resp
|
||||
@@ -38,6 +38,19 @@ SITE_ID = 1
|
||||
# The email address that error messages come from, such as those sent to ADMINS
|
||||
#SERVER_EMAIL = 'webmaster@example.net'
|
||||
|
||||
EMAIL_CLIENT_CONNECTION_SETTINGS = {
|
||||
'imap': {
|
||||
'HOSTNAME': '{{ allowed_host }}',
|
||||
'SOCKET_TYPE': 'SSL',
|
||||
'PORT': 993,
|
||||
},
|
||||
'smtp': {
|
||||
'HOSTNAME': '{{ allowed_host }}',
|
||||
'SOCKET_TYPE': 'STARTTLS',
|
||||
'PORT': 587
|
||||
}
|
||||
}
|
||||
|
||||
# Security settings
|
||||
|
||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||
@@ -75,6 +88,7 @@ MODOBOA_APPS = (
|
||||
'modoboa.core',
|
||||
'modoboa.lib',
|
||||
'modoboa.admin',
|
||||
'modoboa.autoconfig',
|
||||
'modoboa.transport',
|
||||
'modoboa.relaydomains',
|
||||
'modoboa.limits',
|
||||
|
||||
@@ -13,6 +13,7 @@ from modoboa.core.extensions import exts_pool
|
||||
|
||||
urlpatterns = [
|
||||
path("", include("modoboa.core.urls")),
|
||||
path("", include("modoboa.autoconfig.urls")),
|
||||
path(
|
||||
"accounts/password_reset/",
|
||||
core_views.PasswordResetView.as_view(),
|
||||
|
||||
@@ -50,6 +50,15 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
# SERVER_EMAIL = 'webmaster@example.net'
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
EMAIL_CLIENT_CONNECTION_SETTINGS = {
|
||||
"imap": {
|
||||
"HOSTNAME": "localhost",
|
||||
"SOCKET_TYPE": "SSL",
|
||||
"PORT": 993,
|
||||
},
|
||||
"smtp": {"HOSTNAME": "localhost", "SOCKET_TYPE": "STARTTLS", "PORT": 587},
|
||||
}
|
||||
|
||||
# Security settings
|
||||
|
||||
X_FRAME_OPTIONS = "SAMEORIGIN"
|
||||
@@ -87,6 +96,7 @@ MODOBOA_APPS = (
|
||||
"modoboa.core",
|
||||
"modoboa.lib",
|
||||
"modoboa.admin",
|
||||
"modoboa.autoconfig",
|
||||
"modoboa.transport",
|
||||
"modoboa.relaydomains",
|
||||
"modoboa.limits",
|
||||
@@ -397,6 +407,7 @@ TEST_RUNNER = "modoboa.lib.test_runners.UnManagedModelTestRunner"
|
||||
|
||||
# We force sqlite backend for tests because the generated database is
|
||||
# not the same as the one provided by amavis...
|
||||
|
||||
DATABASES["amavis"] = { # noqa
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": "amavis.db",
|
||||
|
||||
Reference in New Issue
Block a user