Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117871008
D5256.1775328281.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
41 KB
Referenced Files
None
Subscribers
None
D5256.1775328281.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D5256: Auto-discovery service
Attached
Detach File
Event Timeline