Page MenuHomePhorge

D5256.1775328281.diff
No OneTemporary

Authored By
Unknown
Size
41 KB
Referenced Files
None
Subscribers
None

D5256.1775328281.diff

diff --git a/docker/proxy/rootfs/init.sh b/docker/proxy/rootfs/init.sh
--- a/docker/proxy/rootfs/init.sh
+++ b/docker/proxy/rootfs/init.sh
@@ -179,20 +179,6 @@
fastcgi_read_timeout 910s;
}
- location ~* ^/\\.well-known/autoconfig {
- proxy_pass $ROUNDCUBE_BACKEND;
- proxy_set_header Host \$host;
- proxy_set_header X-Real-IP \$remote_addr;
- proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
- }
-
- location ~* ^/autodiscover/autodiscover\.xml {
- proxy_pass $ROUNDCUBE_BACKEND;
- proxy_set_header Host \$host;
- proxy_set_header X-Real-IP \$remote_addr;
- proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
- }
-
location ~* ^/\.well-known/(caldav|carddav) {
proxy_pass $DAV_BACKEND;
proxy_redirect http:// \$scheme://;
@@ -282,7 +268,6 @@
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
-
}
}
diff --git a/src/app/Discovery/Engine.php b/src/app/Discovery/Engine.php
new file mode 100644
--- /dev/null
+++ b/src/app/Discovery/Engine.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace App\Discovery;
+
+use App\Tenant;
+use App\User;
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+
+abstract class Engine
+{
+ protected $config = [];
+ protected $email;
+ protected $host;
+ protected $user;
+
+ /**
+ * Get services configuration
+ */
+ protected function configure()
+ {
+ $this->config = [
+ 'email' => $this->user->email,
+ 'domain' => $this->user->domainNamespace(),
+ 'displayName' => (string) Tenant::getConfig($this->user->tenant_id, 'services.discovery.name'),
+ 'displayShortName' => (string) Tenant::getConfig($this->user->tenant_id, 'services.discovery.short_name'),
+ ];
+
+ $proto_map = ['tls' => 'STARTTLS', 'ssl' => 'SSL'];
+
+ foreach (['imap', 'pop3', 'smtp'] as $type) {
+ $value = (string) Tenant::getConfig($this->user->tenant_id, 'services.discovery.' . $type);
+
+ if ($value) {
+ $params = explode(';', $value);
+ $pass_secure = in_array($params[1] ?? null, ['CRAM-MD5', 'DIGEST-MD5']);
+ $host = $params[0];
+ $host = str_replace('%d', $this->config['domain'], $host);
+ $host = str_replace('%h', $this->host, $host);
+
+ $url = parse_url($host);
+
+ $this->config[$type] = [
+ 'hostname' => $url['host'],
+ 'port' => $url['port'],
+ 'socketType' => ($proto_map[$url['scheme']] ?? false) ?: 'plain',
+ 'username' => $this->config['email'],
+ 'authentication' => 'password-' . ($pass_secure ? 'encrypted' : 'cleartext'),
+ ];
+ }
+ }
+
+ if ($host = Tenant::getConfig($this->user->tenant_id, 'services.discovery.activesync')) {
+ $host = str_replace('%d', $this->config['domain'], $host);
+ $host = str_replace('%h', $this->host, $host);
+
+ $this->config['activesync'] = $host;
+ }
+ }
+
+ /**
+ * Send error to the client and exit
+ */
+ protected function error($msg): Response
+ {
+ $response = new Response();
+ $response->setStatusCode(500, $msg);
+
+ return $response;
+ }
+
+ /**
+ * Handle request
+ */
+ public function handle(Request $request): Response
+ {
+ $this->host = $request->host();
+
+ // read request parameters
+ $response = $this->handleRequest($request);
+
+ if ($response) {
+ return $response;
+ }
+
+ // validate requested email address
+ if (empty($this->email)) {
+ return $this->error("Email address not provided");
+ }
+
+ if (!str_contains($this->email, '@')) {
+ return $this->error("Invalid email address");
+ }
+
+ $this->user = User::where('email', $this->email)->first();
+
+ if (!$this->user) {
+ return $this->error("Invalid email address");
+ }
+
+ // find/set services parameters
+ $this->configure();
+
+ // create a response
+ return $this->getResponse();
+ }
+
+ /**
+ * Process incoming request
+ */
+ abstract protected function handleRequest(Request $request): ?Response;
+
+ /**
+ * Generates JSON response
+ */
+ abstract protected function getResponse(): Response;
+}
diff --git a/src/app/Discovery/MicrosoftJson.php b/src/app/Discovery/MicrosoftJson.php
new file mode 100644
--- /dev/null
+++ b/src/app/Discovery/MicrosoftJson.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace App\Discovery;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+
+/**
+ * Autodiscover Service class for Microsoft Autodiscover V2
+ */
+class MicrosoftJson extends Engine
+{
+ protected $protocol;
+
+ /**
+ * Process incoming request
+ */
+ protected function handleRequest(Request $request): ?Response
+ {
+ // Check protocol (at this state we don't know if autodiscover is configured)
+ $allowedProtocols = ['activesync', 'autodiscoverv1'];
+ $this->protocol = $request->Protocol;
+
+ if (empty($this->protocol)) {
+ return $this->error(
+ "A valid value must be provided for the query parameter 'Protocol'",
+ 'MandatoryParameterMissing'
+ );
+ }
+
+ if (!in_array(strtolower($request->Protocol), $allowedProtocols)) {
+ return $this->error(
+ sprintf(
+ "The given protocol value '%s' is invalid. Supported values are '%s'",
+ $this->protocol,
+ implode(",", $allowedProtocols)
+ ),
+ 'InvalidProtocol'
+ );
+ }
+
+ // Check email
+ if (preg_match('|autodiscover.json/v1.0/([^\?]+)|', $request->url(), $regs)) {
+ $this->email = $regs[1];
+ } elseif (!empty($request->Email)) {
+ $this->email = $request->Email;
+ } elseif (!empty($request->email)) {
+ $this->email = $request->email;
+ }
+
+ if (empty($this->email) || !str_contains($this->email, '@')) {
+ return $this->error('A valid email address must be provided', 'MandatoryParameterMissing');
+ }
+
+ return null;
+ }
+
+ /**
+ * Generates JSON response
+ */
+ protected function getResponse(): Response
+ {
+ switch (strtolower($this->protocol)) {
+ case 'activesync':
+ // throw error if activesync is disabled
+ if (empty($this->config['activesync'])) {
+ return $this->error(
+ sprintf(
+ "The given protocol value '%s' is invalid. Supported values are '%s'",
+ $this->protocol,
+ 'autodiscoverv1'
+ ),
+ 'InvalidProtocol'
+ );
+ }
+
+ if (!preg_match('/^https?:/i', $this->config['activesync'])) {
+ $this->config['activesync'] = "https://{$this->config['activesync']}/Microsoft-Server-ActiveSync";
+ }
+
+ $json = [
+ 'Protocol' => 'ActiveSync',
+ 'Url' => $this->config['activesync'],
+ ];
+
+ break;
+ case 'autodiscoverv1':
+ default:
+ $json = [
+ 'Protocol' => 'AutodiscoverV1',
+ 'Url' => "https://{$this->host}/Autodiscover/Autodiscover.xml",
+ ];
+ }
+
+ return response(json_encode($json, \JSON_PRETTY_PRINT), 200, ['Content-Type' => 'application/json']);
+ }
+
+ /**
+ * Send error to the client and exit
+ */
+ protected function error($msg, $code = 'InternalServerError'): Response
+ {
+ $json = [
+ 'ErrorCode' => $code,
+ 'ErrorMessage' => $msg,
+ ];
+
+ return response(json_encode($json, \JSON_PRETTY_PRINT), 400, ['Content-Type' => 'application/json']);
+ }
+}
diff --git a/src/app/Discovery/MicrosoftXml.php b/src/app/Discovery/MicrosoftXml.php
new file mode 100644
--- /dev/null
+++ b/src/app/Discovery/MicrosoftXml.php
@@ -0,0 +1,291 @@
+<?php
+
+namespace App\Discovery;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+
+/**
+ * Autodiscover Service class for Microsoft Outlook and Activesync devices
+ */
+class MicrosoftXml extends Engine
+{
+ public const NS = "http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006";
+ public const OUTLOOK_NS = "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a";
+ public const MOBILESYNC_NS = "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006";
+
+ private $type = 'outlook';
+ // private $password;
+
+ /**
+ * Handle request parameters (find email address)
+ */
+ protected function handleRequest(Request $request): ?Response
+ {
+ try {
+ $post = $request->getContent();
+
+ // Parse XML input
+ $doc = new \DOMDocument();
+ $doc->loadXML($post);
+
+ $ns = $doc->documentElement->getAttributeNode('xmlns')->nodeValue;
+ if (empty($ns)) {
+ return $this->error("Invalid input. Missing XML request schema");
+ }
+
+ if ($email = $doc->getElementsByTagName('EMailAddress')->item(0)) {
+ $this->email = $email->nodeValue;
+ }
+
+ if ($schema = $doc->getElementsByTagName('AcceptableResponseSchema')->item(0)) {
+ $schema = $schema->nodeValue;
+
+ if (str_contains($schema, 'mobilesync')) {
+ $this->type = 'mobilesync';
+ }
+ }
+ } catch (\Throwable $e) {
+ return $this->error("Invalid input");
+ }
+
+ if (\config('services.autodiscover.mobilesync_only') && $this->type != 'mobilesync') {
+ return $this->error("Only mobilesync schema supported");
+ }
+
+ // Check for basic authentication
+ // FIXME: Is the authentication required or not? Looking at the old kolab-autoconf
+ // code it seems like we do not authenticate user if there's no LDAP.
+ // From what I see it would be needed if we wanted to support alias authentication.
+ // or return real user displayName. We don't do this right now.
+ /*
+ $user = $request->getUser();
+ $this->password = $request->getPassword();
+
+ // basic auth username must match with given email address
+ if (empty($user) || empty($this->password) || strcasecmp($user, $this->email) != 0) {
+ return $this->unauthorized();
+ }
+ */
+
+ return null;
+ }
+
+ /**
+ * Handle response
+ */
+ protected function getResponse(): Response
+ {
+ // FIXME: here we would authenticate the user (but see above)
+
+ if ($this->type == 'mobilesync') {
+ $xml = $this->mobilesyncResponse();
+ } else {
+ $xml = $this->outlookResponse();
+ }
+
+ if ($xml === null) {
+ return $this->error("Schema '{$this->type}' not supported");
+ }
+
+ $xml->formatOutput = true;
+
+ $body = $xml->saveXML();
+
+ return response($body, 200)->header('Content-Type', 'text/xml; charset=utf-8');
+ }
+
+ /**
+ * Send error to the client and exit
+ */
+ protected function error($msg): Response
+ {
+ $xml = new \DOMDocument('1.0', 'utf-8');
+ $doc = $xml->createElementNS(self::NS, 'Autodiscover');
+ $doc = $xml->appendChild($doc);
+
+ $response = $xml->createElement('Response');
+ $response = $doc->appendChild($response);
+
+ [$usec, $sec] = explode(' ', microtime());
+
+ $error = $xml->createElement('Error');
+ $error->setAttribute('Time', date('H:i:s', (int) $sec) . '.' . substr($usec, 2, 6));
+ $error->setAttribute('Id', sprintf("%u", crc32($this->host)));
+ $response->appendChild($error);
+
+ $code = $xml->createElement('ErrorCode');
+ $code->appendChild($xml->createTextNode('600'));
+ $error->appendChild($code);
+
+ $message = $xml->createElement('Message');
+ $message->appendChild($xml->createTextNode($msg));
+ $error->appendChild($message);
+
+ $response->appendChild($xml->createElement('DebugData'));
+
+ $xml->formatOutput = true;
+
+ $body = $xml->saveXML();
+
+ return response($body, 200, ['Content-Type' => 'text/xml; charset=utf-8']);
+ }
+
+ /**
+ * Generates XML response for Activesync
+ */
+ protected function mobilesyncResponse(): ?\DOMDocument
+ {
+ if (empty($this->config['activesync'])) {
+ return null;
+ }
+
+ if (!preg_match('/^https?:/i', $this->config['activesync'])) {
+ $this->config['activesync'] = "https://{$this->config['activesync']}/Microsoft-Server-ActiveSync";
+ }
+
+ $xml = new \DOMDocument('1.0', 'utf-8');
+
+ // create main elements (tree)
+ $doc = $xml->createElementNS(self::NS, 'Autodiscover');
+ $doc = $xml->appendChild($doc);
+
+ $response = $xml->createElementNS(self::MOBILESYNC_NS, 'Response');
+ $response = $doc->appendChild($response);
+
+ $user = $xml->createElement('User');
+ $user = $response->appendChild($user);
+
+ $action = $xml->createElement('Action');
+ $action = $response->appendChild($action);
+
+ $settings = $xml->createElement('Settings');
+ $settings = $action->appendChild($settings);
+
+ $server = $xml->createElement('Server');
+ $server = $settings->appendChild($server);
+
+ // configuration
+ $dispname = $xml->createElement('DisplayName');
+ $dispname = $user->appendChild($dispname);
+ $dispname->appendChild($xml->createTextNode($this->config['username'] ?? ''));
+
+ $email = $xml->createElement('EMailAddress');
+ $email = $user->appendChild($email);
+ $email->appendChild($xml->createTextNode($this->config['email']));
+
+ $element = $xml->createElement('Type');
+ $element = $server->appendChild($element);
+ $element->appendChild($xml->createTextNode('MobileSync'));
+
+ $element = $xml->createElement('Url');
+ $element = $server->appendChild($element);
+ $element->appendChild($xml->createTextNode($this->config['activesync']));
+
+ return $xml;
+ }
+
+ /**
+ * Generates XML response for Outlook
+ */
+ protected function outlookResponse(): \DOMDocument
+ {
+ $xml = new \DOMDocument('1.0', 'utf-8');
+
+ // create main elements (tree)
+ $doc = $xml->createElementNS(self::NS, 'Autodiscover');
+ $doc = $xml->appendChild($doc);
+
+ $response = $xml->createElementNS(self::OUTLOOK_NS, 'Response');
+ $response = $doc->appendChild($response);
+
+ $user = $xml->createElement('User');
+ $user = $response->appendChild($user);
+
+ $account = $xml->createElement('Account');
+ $account = $response->appendChild($account);
+
+ $accountType = $xml->createElement('AccountType');
+ $accountType = $account->appendChild($accountType);
+ $accountType->appendChild($xml->createTextNode('email'));
+
+ $action = $xml->createElement('Action');
+ $action = $account->appendChild($action);
+ $action->appendChild($xml->createTextNode('settings'));
+
+ // configuration
+ $dispname = $xml->createElement('DisplayName');
+ $dispname = $user->appendChild($dispname);
+ $dispname->appendChild($xml->createTextNode($this->config['username'] ?? ''));
+
+ $email = $xml->createElement('AutoDiscoverSMTPAddress');
+ $email = $user->appendChild($email);
+ $email->appendChild($xml->createTextNode($this->config['email']));
+
+ // @TODO: Microsoft supports also DAV protocol here
+ foreach (['imap', 'pop3', 'smtp'] as $type) {
+ if (!empty($this->config[$type])) {
+ $protocol = $this->addProtocolElement($xml, $type);
+ $account->appendChild($protocol);
+ }
+ }
+
+ return $xml;
+ }
+
+ /**
+ * Creates Protocol element for XML response
+ */
+ private function addProtocolElement(\DOMDocument $xml, string $type): \DOMElement
+ {
+ $protocol = $xml->createElement('Protocol');
+
+ $element = $xml->createElement('Type');
+ $element = $protocol->appendChild($element);
+ $element->appendChild($xml->createTextNode(strtoupper($type)));
+
+ // @TODO: TTL/ExpirationDate tags
+
+ // server attributes map
+ $server_attributes = [
+ 'Server' => 'hostname',
+ 'Port' => 'port',
+ 'LoginName' => 'username',
+ ];
+
+ foreach ($server_attributes as $tag_name => $conf_name) {
+ $value = $this->config[$type][$conf_name];
+ if (!empty($value)) {
+ $element = $xml->createElement($tag_name);
+ $element->appendChild($xml->createTextNode($value));
+ $protocol->appendChild($element);
+ }
+ }
+
+ $spa = $this->config[$type]['authentication'] == 'password-encrypted' ? 'on' : 'off';
+ $element = $xml->createElement('SPA');
+ $element->appendChild($xml->createTextNode($spa));
+ $protocol->appendChild($element);
+
+ $map = ['STARTTLS' => 'TLS', 'SSL' => 'SSL', 'plain' => 'None'];
+ $element = $xml->createElement('Encryption');
+ $element->appendChild($xml->createTextNode($map[$this->config[$type]['socketType']] ?? 'Auto'));
+ $protocol->appendChild($element);
+
+ return $protocol;
+ }
+
+ /**
+ * Send 401 Unauthorized to the client
+ */
+ protected function unauthorized($basicauth = true): Response
+ {
+ $response = response('', 401);
+
+ if ($basicauth) {
+ $response->headers->set('WWW-Authenticate', "Basic realm=\"{$this->host}\"");
+ }
+
+ return $response;
+ }
+}
diff --git a/src/app/Discovery/Mozilla.php b/src/app/Discovery/Mozilla.php
new file mode 100644
--- /dev/null
+++ b/src/app/Discovery/Mozilla.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace App\Discovery;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+
+/**
+ * Autodiscover Service class for Mozilla Thunderbird
+ */
+class Mozilla extends Engine
+{
+ /**
+ * Handles the request
+ */
+ protected function handleRequest(Request $request): ?Response
+ {
+ $this->email = $request->emailaddress;
+
+ return null;
+ }
+
+ /**
+ * Generates (XML) response
+ */
+ protected function getResponse(): Response
+ {
+ $xml = new \DOMDocument('1.0', 'utf-8');
+
+ // create main elements
+ $doc = $xml->createElement('clientConfig');
+ $doc->setAttribute('version', '1.1');
+ $doc = $xml->appendChild($doc);
+
+ $provider = $xml->createElement('emailProvider');
+ $provider->setAttribute('id', $this->config['domain']);
+ $provider = $doc->appendChild($provider);
+
+ // provider description tags
+ foreach (['domain', 'displayName', 'displayShortName'] as $tag_name) {
+ if (!empty($this->config[$tag_name])) {
+ $element = $xml->createElement($tag_name);
+ $element->appendChild($xml->createTextNode($this->config[$tag_name]));
+ $provider->appendChild($element);
+ }
+ }
+
+ foreach (['imap', 'pop3', 'smtp'] as $type) {
+ if (!empty($this->config[$type])) {
+ $server = $this->addServerElement($xml, $type);
+ $provider->appendChild($server);
+ }
+ }
+
+ $xml->formatOutput = true;
+
+ $body = $xml->saveXML();
+
+ return response($body, 200, ['Content-Type' => 'application/xml; charset=utf-8']);
+ }
+
+ /**
+ * Creates server element for XML response
+ */
+ private function addServerElement(\DOMDocument $xml, string $type): \DOMElement
+ {
+ $server = $xml->createElement($type == 'smtp' ? 'outgoingServer' : 'incomingServer');
+ $server->setAttribute('type', $type);
+
+ // server attributes
+ $server_attributes = [
+ 'hostname',
+ 'port',
+ 'socketType', // SSL or STARTTLS or plain
+ 'username',
+ 'authentication', // 'password-cleartext', 'password-encrypted'
+ ];
+
+ foreach ($server_attributes as $tag_name) {
+ $value = $this->config[$type][$tag_name] ?? null;
+ if ($value) {
+ $element = $xml->createElement($tag_name);
+ $element->appendChild($xml->createTextNode($value));
+ $server->appendChild($element);
+ }
+ }
+
+ return $server;
+ }
+}
diff --git a/src/app/Http/Controllers/DiscoveryController.php b/src/app/Http/Controllers/DiscoveryController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/DiscoveryController.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\Response;
+use Illuminate\Support\Facades\Route;
+
+class DiscoveryController extends Controller
+{
+ /**
+ * Handle the Mozilla client autoconfig request
+ */
+ public function mozilla(Request $request): Response
+ {
+ $engine = new \App\Discovery\Mozilla();
+ return $engine->handle($request);
+ }
+
+ /**
+ * Handle the Microsoft Autodiscovery v2 request
+ */
+ public function microsoftJson(Request $request): Response
+ {
+ $engine = new \App\Discovery\MicrosoftJson();
+ return $engine->handle($request);
+ }
+
+ /**
+ * Handle the Microsoft Outlook/Activesync request
+ */
+ public function microsoftXml(Request $request): Response
+ {
+ $engine = new \App\Discovery\MicrosoftXml();
+ return $engine->handle($request);
+ }
+
+ /**
+ * Register all controller routes
+ */
+ public static function registerRoutes(): void
+ {
+ Route::post('/autodiscover/autodiscover.xml', [self::class, 'microsoftXml']);
+ Route::post('/Autodiscover/Autodiscover.xml', [self::class, 'microsoftXml']);
+ Route::post('/AutoDiscover/AutoDiscover.xml', [self::class, 'microsoftXml']);
+ Route::get('/autodiscover/autodiscover.json', [self::class, 'microsoftJson']);
+ Route::get('/autodiscover/autodiscover.json/v1.0/{email}', [self::class, 'microsoftJson']);
+ Route::get('/mail/config-v1.1.xml', [self::class, 'mozilla']);
+ Route::get('/.well-known/autoconfig/mail/config-v1.1.xml', [self::class, 'mozilla']);
+ }
+}
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -21,7 +21,7 @@
|
*/
- 'name' => env('APP_NAME', 'Laravel'),
+ 'name' => env('APP_NAME', 'Kolab'),
/*
|--------------------------------------------------------------------------
diff --git a/src/config/services.php b/src/config/services.php
--- a/src/config/services.php
+++ b/src/config/services.php
@@ -108,8 +108,13 @@
'domain_filter' => env('LDAP_DOMAIN_FILTER', '(associateddomain=%s)'),
],
- 'autodiscover' => [
- 'uri' => env('AUTODISCOVER_URI', env('APP_URL', 'http://localhost')),
+ 'discovery' => [
+ 'activesync' => env('APP_DISCOVERY_ACTIVESYNC', 'activesync.%h'),
+ 'imap' => env('APP_DISCOVERY_IMAP', 'ssl://imap.%h:993'),
+ 'pop3' => env('APP_DISCOVERY_POP3', 'ssl://pop3.%h:995'),
+ 'smtp' => env('APP_DISCOVERY_SMTP', 'ssl://smtp.%h:465'),
+ 'name' => env('APP_NAME', 'Kolab'),
+ 'name_short' => env('APP_NAME_SHORT', ''),
],
'activesync' => [
diff --git a/src/routes/web.php b/src/routes/web.php
--- a/src/routes/web.php
+++ b/src/routes/web.php
@@ -65,3 +65,5 @@
Route::get('/mta-sts.txt', [Controllers\WellKnownController::class, 'mtaSts']);
}
);
+
+Controllers\DiscoveryController::registerRoutes();
diff --git a/src/tests/Feature/Controller/DiscoveryTest.php b/src/tests/Feature/Controller/DiscoveryTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/DiscoveryTest.php
@@ -0,0 +1,265 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Discovery;
+use Tests\TestCase;
+
+class DiscoveryTest extends TestCase
+{
+ /**
+ * Test Microsoft Autodiscover
+ */
+ public function testMicrosoftJson()
+ {
+ $host = parse_url(\config('app.url'), \PHP_URL_HOST);
+
+ $response = $this->get('/autodiscover/autodiscover.json?Email=john@kolab.org&Protocol=AutodiscoverV1');
+ $response->assertStatus(200)
+ ->assertHeader('Content-Type', 'application/json')
+ ->assertExactJson([
+ 'Protocol' => 'AutodiscoverV1',
+ 'Url' => "https://{$host}/Autodiscover/Autodiscover.xml",
+ ]);
+
+ $response = $this->get('/autodiscover/autodiscover.json?email=john@kolab.org&Protocol=AutodiscoverV1');
+ $response->assertStatus(200)
+ ->assertHeader('Content-Type', 'application/json')
+ ->assertExactJson([
+ 'Protocol' => 'AutodiscoverV1',
+ 'Url' => "https://{$host}/Autodiscover/Autodiscover.xml",
+ ]);
+
+ $response = $this->get('/autodiscover/autodiscover.json/v1.0/john@kolab.org?Protocol=ActiveSync');
+ $response->assertStatus(200)
+ ->assertHeader('Content-Type', 'application/json')
+ ->assertExactJson([
+ 'Protocol' => 'ActiveSync',
+ 'Url' => "https://activesync.{$host}/Microsoft-Server-ActiveSync",
+ ]);
+
+ // Test error handling (missing Protocol)
+ $response = $this->get('/autodiscover/autodiscover.json?Email=john@kolab.org');
+ $response->assertStatus(400)
+ ->assertHeader('Content-Type', 'application/json')
+ ->assertExactJson([
+ 'ErrorCode' => 'MandatoryParameterMissing',
+ 'ErrorMessage' => "A valid value must be provided for the query parameter 'Protocol'",
+ ]);
+
+ // Test error handling (invalid Protocol)
+ $response = $this->get('/autodiscover/autodiscover.json?Email=john@kolab.org&Protocol=unknown');
+ $response->assertStatus(400)
+ ->assertHeader('Content-Type', 'application/json')
+ ->assertExactJson([
+ 'ErrorCode' => 'InvalidProtocol',
+ 'ErrorMessage' => "The given protocol value 'unknown' is invalid."
+ . " Supported values are 'activesync,autodiscoverv1'",
+ ]);
+
+ // Test error handling (missing email)
+ $response = $this->get('/autodiscover/autodiscover.json?Protocol=ActiveSync');
+ $response->assertStatus(400)
+ ->assertHeader('Content-Type', 'application/json')
+ ->assertExactJson([
+ 'ErrorCode' => 'MandatoryParameterMissing',
+ 'ErrorMessage' => "A valid email address must be provided",
+ ]);
+
+ // Test error handling (unknown user email)
+ $response = $this->get('/autodiscover/autodiscover.json?Protocol=ActiveSync&email=unknown@kolab.org');
+ $response->assertStatus(400)
+ ->assertHeader('Content-Type', 'application/json')
+ ->assertExactJson([
+ 'ErrorCode' => 'InternalServerError',
+ 'ErrorMessage' => "Invalid email address",
+ ]);
+ }
+
+ /**
+ * Test Microsoft Outlook/Mobilesync discovery
+ */
+ public function testMicrosoftXml()
+ {
+ $host = parse_url(\config('app.url'), \PHP_URL_HOST);
+
+ // Test outlook schema
+ $body = <<<'EOF'
+ <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
+ <Request>
+ <EMailAddress>john@kolab.org</EMailAddress>
+ <AcceptableResponseSchema>
+ http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a
+ </AcceptableResponseSchema>
+ </Request>
+ </Autodiscover>
+ EOF;
+
+ $response = $this->call('POST', '/autodiscover/autodiscover.xml', [], [], [], [], $body);
+ $response->assertStatus(200)
+ ->assertHeader('Content-Type', 'text/xml; charset=utf-8');
+
+ $xml = $response->getContent();
+
+ $doc = new \DOMDocument('1.0', 'utf-8');
+ $doc->loadXML($xml);
+
+ $autodiscover = $doc->documentElement;
+ $this->assertSame(Discovery\MicrosoftXml::NS, $autodiscover->getAttribute('xmlns'));
+ $response = $autodiscover->getElementsByTagName('Response')->item(0);
+ $this->assertSame(Discovery\MicrosoftXml::OUTLOOK_NS, $response->getAttribute('xmlns'));
+ $user = $response->getElementsByTagName('User')->item(0);
+ $this->assertSame('', $user->getElementsByTagName('DisplayName')->item(0)->nodeValue);
+ $this->assertSame('john@kolab.org', $user->getElementsByTagName('AutoDiscoverSMTPAddress')->item(0)->nodeValue);
+ $account = $response->getElementsByTagName('Account')->item(0);
+ $this->assertSame('settings', $account->getElementsByTagName('Action')->item(0)->nodeValue);
+ $this->assertSame('email', $account->getElementsByTagName('AccountType')->item(0)->nodeValue);
+ $protocols = $account->getElementsByTagName('Protocol');
+ $this->assertSame('IMAP', $protocols->item(0)->getElementsByTagName('Type')->item(0)->nodeValue);
+ $this->assertSame("imap.{$host}", $protocols->item(0)->getElementsByTagName('Server')->item(0)->nodeValue);
+ $this->assertSame('993', $protocols->item(0)->getElementsByTagName('Port')->item(0)->nodeValue);
+ $this->assertSame('john@kolab.org', $protocols->item(0)->getElementsByTagName('LoginName')->item(0)->nodeValue);
+ $this->assertSame('off', $protocols->item(0)->getElementsByTagName('SPA')->item(0)->nodeValue);
+ $this->assertSame('SSL', $protocols->item(0)->getElementsByTagName('Encryption')->item(0)->nodeValue);
+ $this->assertSame('POP3', $protocols->item(1)->getElementsByTagName('Type')->item(0)->nodeValue);
+ $this->assertSame("pop3.{$host}", $protocols->item(1)->getElementsByTagName('Server')->item(0)->nodeValue);
+ $this->assertSame('995', $protocols->item(1)->getElementsByTagName('Port')->item(0)->nodeValue);
+ $this->assertSame('john@kolab.org', $protocols->item(1)->getElementsByTagName('LoginName')->item(0)->nodeValue);
+ $this->assertSame('off', $protocols->item(1)->getElementsByTagName('SPA')->item(0)->nodeValue);
+ $this->assertSame('SSL', $protocols->item(2)->getElementsByTagName('Encryption')->item(0)->nodeValue);
+ $this->assertSame('SMTP', $protocols->item(2)->getElementsByTagName('Type')->item(0)->nodeValue);
+ $this->assertSame("smtp.{$host}", $protocols->item(2)->getElementsByTagName('Server')->item(0)->nodeValue);
+ $this->assertSame('465', $protocols->item(2)->getElementsByTagName('Port')->item(0)->nodeValue);
+ $this->assertSame('john@kolab.org', $protocols->item(2)->getElementsByTagName('LoginName')->item(0)->nodeValue);
+ $this->assertSame('off', $protocols->item(2)->getElementsByTagName('SPA')->item(0)->nodeValue);
+ $this->assertSame('SSL', $protocols->item(2)->getElementsByTagName('Encryption')->item(0)->nodeValue);
+
+ // Test mobilesync schema
+ $body = <<<'EOF'
+ <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006">
+ <Request>
+ <EMailAddress>john@kolab.org</EMailAddress>
+ <AcceptableResponseSchema>
+ http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006
+ </AcceptableResponseSchema>
+ </Request>
+ </Autodiscover>
+ EOF;
+
+ $response = $this->call('POST', '/Autodiscover/Autodiscover.xml', [], [], [], [], $body);
+ $response->assertStatus(200);
+
+ $xml = $response->getContent();
+
+ $doc = new \DOMDocument('1.0', 'utf-8');
+ $doc->loadXML($xml);
+
+ $autodiscover = $doc->documentElement;
+ $this->assertSame(Discovery\MicrosoftXml::NS, $autodiscover->getAttribute('xmlns'));
+ $response = $autodiscover->getElementsByTagName('Response')->item(0);
+ $this->assertSame(Discovery\MicrosoftXml::MOBILESYNC_NS, $response->getAttribute('xmlns'));
+ $user = $response->getElementsByTagName('User')->item(0);
+ $this->assertSame('', $user->getElementsByTagName('DisplayName')->item(0)->nodeValue);
+ $this->assertSame('john@kolab.org', $user->getElementsByTagName('EMailAddress')->item(0)->nodeValue);
+ $action = $response->getElementsByTagName('Action')->item(0);
+ $settings = $action->getElementsByTagName('Settings')->item(0);
+ $server = $settings->getElementsByTagName('Server')->item(0);
+ $this->assertSame('MobileSync', $server->getElementsByTagName('Type')->item(0)->nodeValue);
+ $this->assertSame(
+ "https://activesync.{$host}/Microsoft-Server-ActiveSync",
+ $server->getElementsByTagName('Url')->item(0)->nodeValue
+ );
+
+ // Test the other route, i.e. /AutoDiscover/AutoDiscover.xml
+ $response = $this->call('POST', '/AutoDiscover/AutoDiscover.xml', [], [], [], [], $body);
+ $response->assertStatus(200);
+
+ // Test error responses (empty request body)
+ $response = $this->call('POST', '/Autodiscover/Autodiscover.xml', [], [], [], [], '');
+ $response->assertStatus(200)
+ ->assertHeader('Content-Type', 'text/xml; charset=utf-8');
+
+ $xml = $response->getContent();
+
+ $doc = new \DOMDocument('1.0', 'utf-8');
+ $doc->loadXML($xml);
+
+ $autodiscover = $doc->documentElement;
+ $this->assertSame(Discovery\MicrosoftXml::NS, $autodiscover->getAttribute('xmlns'));
+ $response = $autodiscover->getElementsByTagName('Response')->item(0);
+ $error = $response->getElementsByTagName('Error')->item(0);
+ $this->assertSame('600', $error->getElementsByTagName('ErrorCode')->item(0)->nodeValue);
+ $this->assertSame('Invalid input', $error->getElementsByTagName('Message')->item(0)->nodeValue);
+
+ // TODO: Test more error cases
+ }
+
+ /**
+ * Test Mozilla Thunderbird format
+ */
+ public function testMozilla()
+ {
+ $host = parse_url(\config('app.url'), \PHP_URL_HOST);
+
+ // Test .well-known URL
+ $response = $this->get('/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=john@kolab.org');
+ $response->assertStatus(200)
+ ->assertHeader('Content-Type', 'application/xml; charset=utf-8');
+
+ $xml = $response->getContent();
+
+ $doc = new \DOMDocument('1.0', 'utf-8');
+ $doc->loadXML($xml);
+
+ $clientConfig = $doc->documentElement;
+ $this->assertSame('1.1', $clientConfig->getAttribute('version'));
+ $emailProvider = $clientConfig->getElementsByTagName('emailProvider')->item(0);
+ $this->assertSame('kolab.org', $emailProvider->getAttribute('id'));
+ $domain = $emailProvider->getElementsByTagName('domain')->item(0);
+ $this->assertSame('kolab.org', $domain->nodeValue);
+ $displayName = $emailProvider->getElementsByTagName('displayName')->item(0);
+ $this->assertSame('Kolab', $displayName->nodeValue);
+ $servers = $emailProvider->getElementsByTagName('incomingServer');
+ $this->assertSame('imap', $servers->item(0)->getAttribute('type'));
+ $this->assertSame("imap.{$host}", $servers->item(0)->getElementsByTagName('hostname')->item(0)->nodeValue);
+ $this->assertSame('993', $servers->item(0)->getElementsByTagName('port')->item(0)->nodeValue);
+ $this->assertSame('SSL', $servers->item(0)->getElementsByTagName('socketType')->item(0)->nodeValue);
+ $this->assertSame('john@kolab.org', $servers->item(0)->getElementsByTagName('username')->item(0)->nodeValue);
+ $this->assertSame('password-cleartext', $servers->item(0)->getElementsByTagName('authentication')->item(0)->nodeValue);
+ $this->assertSame('pop3', $servers->item(1)->getAttribute('type'));
+ $this->assertSame("pop3.{$host}", $servers->item(1)->getElementsByTagName('hostname')->item(0)->nodeValue);
+ $this->assertSame('995', $servers->item(1)->getElementsByTagName('port')->item(0)->nodeValue);
+ $this->assertSame('SSL', $servers->item(1)->getElementsByTagName('socketType')->item(0)->nodeValue);
+ $this->assertSame('john@kolab.org', $servers->item(1)->getElementsByTagName('username')->item(0)->nodeValue);
+ $this->assertSame('password-cleartext', $servers->item(1)->getElementsByTagName('authentication')->item(0)->nodeValue);
+ $servers = $emailProvider->getElementsByTagName('outgoingServer');
+ $this->assertSame('smtp', $servers->item(0)->getAttribute('type'));
+ $this->assertSame("smtp.{$host}", $servers->item(0)->getElementsByTagName('hostname')->item(0)->nodeValue);
+ $this->assertSame('465', $servers->item(0)->getElementsByTagName('port')->item(0)->nodeValue);
+ $this->assertSame('SSL', $servers->item(0)->getElementsByTagName('socketType')->item(0)->nodeValue);
+ $this->assertSame('john@kolab.org', $servers->item(0)->getElementsByTagName('username')->item(0)->nodeValue);
+ $this->assertSame('password-cleartext', $servers->item(0)->getElementsByTagName('authentication')->item(0)->nodeValue);
+
+ // Test non-.well-known URL
+ $response = $this->get('/mail/config-v1.1.xml?emailaddress=john@kolab.org');
+ $response->assertStatus(200)
+ ->assertHeader('Content-Type', 'application/xml; charset=utf-8');
+
+ $xml = $response->getContent();
+
+ $doc = new \DOMDocument('1.0', 'utf-8');
+ $doc->loadXML($xml);
+
+ $clientConfig = $doc->documentElement;
+ $this->assertSame('1.1', $clientConfig->getAttribute('version'));
+ $emailProvider = $clientConfig->getElementsByTagName('emailProvider')->item(0);
+ $this->assertSame('kolab.org', $emailProvider->getAttribute('id'));
+
+ // Test error responses
+ $response = $this->get('/mail/config-v1.1.xml');
+ $response->assertNoContent(500);
+
+ $response = $this->get('/mail/config-v1.1.xml?emailaddress=john');
+ $response->assertNoContent(500);
+ }
+}
diff --git a/src/tests/Infrastructure/AutodiscoverTest.php b/src/tests/Infrastructure/AutodiscoverTest.php
deleted file mode 100644
--- a/src/tests/Infrastructure/AutodiscoverTest.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-
-namespace Tests\Infrastructure;
-
-use GuzzleHttp\Client;
-use Tests\TestCase;
-
-class AutodiscoverTest extends TestCase
-{
- private static ?Client $client = null;
-
- protected function setUp(): void
- {
- parent::setUp();
-
- if (!self::$client) {
- self::$client = new Client([
- 'http_errors' => false, // No exceptions
- 'base_uri' => \config('services.autodiscover.uri'),
- 'verify' => false,
- 'connect_timeout' => 10,
- 'timeout' => 10,
- ]);
- }
- }
-
- protected function tearDown(): void
- {
- parent::tearDown();
- }
-
- public function testWellKnownOutlook()
- {
- $body = <<<'EOF'
- <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
- <Request>
- <EMailAddress>admin@example.local</EMailAddress>
- <AcceptableResponseSchema>
- http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a
- </AcceptableResponseSchema>
- </Request>
- </Autodiscover>
- EOF;
- $response = self::$client->request('POST', 'autodiscover/autodiscover.xml', [
- 'headers' => [
- "Content-Type" => "text/xml; charset=utf-8",
- ],
- 'body' => $body,
- ]);
- $this->assertSame($response->getStatusCode(), 200);
- $data = $response->getBody();
- $this->assertTrue(str_contains($data, '<Server>example.local</Server>'));
- $this->assertTrue(str_contains($data, 'admin@example.local'));
- }
-
- public function testWellKnownActivesync()
- {
- $body = <<<'EOF'
- <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/mobilesync/requestschema/2006">
- <Request>
- <EMailAddress>admin@example.local</EMailAddress>
- <AcceptableResponseSchema>
- http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006
- </AcceptableResponseSchema>
- </Request>
- </Autodiscover>
- EOF;
- $response = self::$client->request('POST', 'autodiscover/autodiscover.xml', [
- 'headers' => [
- "Content-Type" => "text/xml; charset=utf-8",
- ],
- 'body' => $body,
- ]);
- $this->assertSame($response->getStatusCode(), 200);
- $data = $response->getBody();
- $this->assertTrue(str_contains($data, '<Url>https://example.local/Microsoft-Server-ActiveSync</Url>'));
- $this->assertTrue(str_contains($data, 'admin@example.local'));
- }
-
- public function testWellKnownMail()
- {
- $response = self::$client->request(
- 'GET',
- '.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=fred@example.com'
- );
- $this->assertSame($response->getStatusCode(), 200);
- }
-}

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 6:44 PM (2 h, 20 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18830790
Default Alt Text
D5256.1775328281.diff (41 KB)

Event Timeline