mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
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
|
||||
|
||||
@@ -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>password-cleartext</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>password-cleartext</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,37 @@
|
||||
<?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>
|
||||
<payloadContent>
|
||||
<dict>
|
||||
<PayloadType>com.apple.mail.managed</PayloadType>
|
||||
<PayloadVersion>1</PayloadVersion>
|
||||
<PayloadIdentifier>{{ reverse_domain }}.email</PayloadIdentifier>
|
||||
<PayloadUUID>{{ content_uuid }}</PayloadUUID>
|
||||
<PayloadDisplayName>Mail</PayloadDisplayName>
|
||||
|
||||
<EmailAccountDescription>Modoboa</EmailAccountDescription>
|
||||
<EmailAccountType>EmailTypeIMAP</EmailAccountType>
|
||||
<EmailAddress>{{ emailaddress }}</EmailAddress>
|
||||
<IncomingMailServerHostName>{{ connection_settings.imap.HOSTNAME }}</IncomingMailServerHostName>
|
||||
<IncomingMailServerPortNumber>{{ connection_settings.imap.PORT }}</IncomingMailServerPortNumber>
|
||||
<IncomingMailServerUseSSL>true</IncomingMailServerUseSSL>
|
||||
<IncomingMailServerUsername>{{ emailaddress }}</IncomingMailServerUsername>
|
||||
|
||||
<OutgoingMailServerHostName>{{ connection_settings.smtp.HOSTNAME }}</OutgoingMailServerHostName>
|
||||
<OutgoingMailServerPortNumber>{{ connection_settings.smtp.PORT }}</OutgoingMailServerPortNumber>
|
||||
<OutgoingMailServerUseSSL>true</OutgoingMailServerUseSSL>
|
||||
<OutgoingMailServerUsername>{{ emailaddress }}</OutgoingMailServerUsername>
|
||||
<OutgoingPasswordSameAsIncomingPassword>true</OutgoingPasswordSameAsIncomingPassword>
|
||||
|
||||
</dict>
|
||||
</payloadContent>
|
||||
<PayloadOrganization>Modoboa</PayloadOrganization>
|
||||
<PayloadDisplayName>Mail Configuration</PayloadDisplayName>
|
||||
<PayloadIdentifier>{{ reverse_domain }}.mailconfig</PayloadIdentifier>
|
||||
<PayloadUUID>{{ global_uuid }}</PayloadUUID>
|
||||
<PayloadVersion>1</PayloadVersion>
|
||||
<PayloadType>Configuration</PayloadType>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,38 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class ViewsTestCase(TestCase):
|
||||
|
||||
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}?email=test@test.com")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.assertIn(b"<EmailAddress>test@test.com", resp.content)
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Autoconfig urls."""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from modoboa.autoconfig import views
|
||||
|
||||
app_name = "autoconfig"
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
".well-known/autoconfig/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,86 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.template.loader import render_to_string
|
||||
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(ConfigBaseMixin, generic.TemplateView):
|
||||
|
||||
content_type = "application/x-apple-aspen-config"
|
||||
http_method_names = ["get"]
|
||||
template_name = "autoconfig/mobileconfig.xml"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
emailaddress = self.request.GET.get("email")
|
||||
if not emailaddress:
|
||||
raise Http404
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(self.get_common_context(emailaddress))
|
||||
parts = context["domain"].split(".")
|
||||
parts.reverse()
|
||||
context.update(
|
||||
{
|
||||
"reverse_domain": ".".join(parts),
|
||||
"content_uuid": uuid.uuid4(),
|
||||
"global_uuid": uuid.uuid4(),
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
def render_to_response(self, context, **responsse_kwargs):
|
||||
content = render_to_string(self.template_name, context)
|
||||
response = HttpResponse(content, content_type=self.content_type)
|
||||
response["Content-Disposition"] = 'attachment; filename="mail.mobileconfig"'
|
||||
return response
|
||||
@@ -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