Files
jabali-panel/app/Http/Controllers/AutoDiscoverController.php
2026-02-02 03:11:45 +02:00

245 lines
7.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\EmailDomain;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* AutoDiscover Controller for Microsoft Outlook and Exchange clients
*
* Handles the autodiscover protocol to automatically configure email clients.
* Supports both GET and POST requests for autodiscover.xml
*/
class AutoDiscoverController extends Controller
{
/**
* Handle autodiscover request (POST with XML body)
*/
public function discover(Request $request): Response
{
$xml = $request->getContent();
// Parse email address from request
$email = $this->normalizeEmail($this->extractEmailFromRequest($xml));
if (! $email) {
return $this->errorResponse('No email address provided');
}
// Extract domain from email
$parts = explode('@', $email);
if (count($parts) !== 2) {
return $this->errorResponse('Invalid email address');
}
$domain = $parts[1];
// Check if domain is managed by our mail server
$emailDomain = EmailDomain::whereHas('domain', function ($query) use ($domain) {
$query->where('domain_name', $domain);
})->where('is_active', true)->first();
if (! $emailDomain) {
return $this->errorResponse('Domain not configured for email');
}
// Get mail server hostname
$mailServer = $this->getMailServer($emailDomain);
return $this->autodiscoverResponse($email, $mailServer);
}
/**
* Handle GET request for autodiscover (some clients use this)
*/
public function discoverGet(Request $request): Response
{
$email = $this->normalizeEmail($request->query('email') ?? $request->query('Email'));
if (! $email) {
return response('Email parameter required', 400);
}
$parts = explode('@', $email);
if (count($parts) !== 2) {
return response('Invalid email address', 400);
}
$domain = $parts[1];
$emailDomain = EmailDomain::whereHas('domain', function ($query) use ($domain) {
$query->where('domain_name', $domain);
})->where('is_active', true)->first();
if (! $emailDomain) {
return response('Domain not configured', 404);
}
$mailServer = $this->getMailServer($emailDomain);
return $this->autodiscoverResponse($email, $mailServer);
}
/**
* Extract email from autodiscover XML request
*/
private function extractEmailFromRequest(string $xml): ?string
{
if (empty($xml)) {
return null;
}
// Try to parse XML
libxml_use_internal_errors(true);
$doc = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NONET);
if ($doc === false) {
return null;
}
// Register namespaces
$doc->registerXPathNamespace('a', 'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006');
// Try different XPath expressions
$results = $doc->xpath('//a:EMailAddress');
if (! empty($results)) {
return trim((string) $results[0]);
}
// Try without namespace
$results = $doc->xpath('//EMailAddress');
if (! empty($results)) {
return trim((string) $results[0]);
}
// Try AcceptableResponseSchema pattern
if (isset($doc->Request->EMailAddress)) {
return trim((string) $doc->Request->EMailAddress);
}
return null;
}
/**
* Get mail server hostname for the domain
*/
private function getMailServer(EmailDomain $emailDomain): string
{
// Use setting or fall back to domain-based hostname
$hostname = \App\Models\Setting::get('mail_hostname');
if ($hostname) {
return $hostname;
}
return 'mail.'.$emailDomain->domain->domain_name;
}
/**
* Generate autodiscover XML response
*/
private function autodiscoverResponse(string $email, string $mailServer): Response
{
$displayName = explode('@', $email)[0];
$escapedEmail = $this->escapeXml($email);
$escapedServer = $this->escapeXml($mailServer);
$escapedDisplay = $this->escapeXml($displayName);
$xml = <<<XML
<?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>{$escapedServer}</Server>
<Port>993</Port>
<SSL>on</SSL>
<SPA>off</SPA>
<AuthRequired>on</AuthRequired>
<LoginName>{$escapedEmail}</LoginName>
</Protocol>
<Protocol>
<Type>SMTP</Type>
<Server>{$escapedServer}</Server>
<Port>587</Port>
<SSL>on</SSL>
<Encryption>TLS</Encryption>
<SPA>off</SPA>
<AuthRequired>on</AuthRequired>
<LoginName>{$escapedEmail}</LoginName>
</Protocol>
<Protocol>
<Type>POP3</Type>
<Server>{$escapedServer}</Server>
<Port>995</Port>
<SSL>on</SSL>
<SPA>off</SPA>
<AuthRequired>on</AuthRequired>
<LoginName>{$escapedEmail}</LoginName>
</Protocol>
</Account>
</Response>
</Autodiscover>
XML;
return response($xml, 200)
->header('Content-Type', 'application/xml; charset=utf-8');
}
/**
* Generate error response
*/
private function errorResponse(string $message): Response
{
$escapedMessage = $this->escapeXml($message);
$xml = <<<XML
<?xml version="1.0" encoding="utf-8"?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<Response>
<Error Time="{$this->getTimestamp()}" Id="1">
<ErrorCode>600</ErrorCode>
<Message>{$escapedMessage}</Message>
<DebugData />
</Error>
</Response>
</Autodiscover>
XML;
return response($xml, 200)
->header('Content-Type', 'application/xml; charset=utf-8');
}
private function getTimestamp(): string
{
return date('H:i:s.v');
}
private function normalizeEmail(?string $email): ?string
{
if ($email === null) {
return null;
}
$email = trim($email);
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
return null;
}
return $email;
}
private function escapeXml(string $value): string
{
return htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}
}