snappymail/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Domain/Autoconfig.php

199 lines
6.4 KiB
PHP

<?php
/**
* https://datatracker.ietf.org/doc/draft-bucksch-autoconfig/
*/
namespace RainLoop\Providers\Domain;
abstract class Autoconfig
{
public static function discover(string $emailaddress) : ?array
{
$domain = \explode('@', $emailaddress);
$domain = \array_pop($domain);
// First try
$autoconfig = static::resolve($domain, $emailaddress);
if ($autoconfig) {
return $autoconfig;
}
// Else try MX
$suffixes = static::publicsuffixes();
$hostnames = [];
foreach (\SnappyMail\DNS::MX($domain) as $hostname) {
// Extract only the second-level domain from the MX hostname
$mxbasedomain = \explode('.', $hostname);
$i = -2;
while (\in_array(\implode('.', \array_slice($mxbasedomain, $i)), $suffixes)) {
--$i;
}
$mxbasedomain = \implode('.', \array_slice($mxbasedomain, $i));
if ($mxbasedomain) {
$mxfulldomain = $mxbasedomain;
if (\substr_count($hostname, '.') > \substr_count($mxbasedomain, '.')) {
// Remove the first component from the MX hostname
$mxfulldomain = \explode('.', $hostname, 2)[1];
}
$hostnames[$mxfulldomain] = $mxbasedomain;
}
}
foreach ($hostnames as $mxfulldomain => $mxbasedomain) {
if ($domain != $mxfulldomain) {
$autoconfig = static::resolve($mxfulldomain, $emailaddress);
if (!$autoconfig && $mxfulldomain != $mxbasedomain && $domain != $mxbasedomain) {
$autoconfig = static::resolve($mxbasedomain, $emailaddress);
}
if ($autoconfig) {
return $autoconfig;
}
}
}
// Else try Microsoft autodiscover
return static::autodiscover($domain);
}
private static function resolve(string $domain, string $emailaddress) : ?array
{
$emailaddress = \urlencode($emailaddress);
foreach ([
// 4.1. Mail provider
"https://autoconfig.{$domain}/.well-known/mail-v1.xml?emailaddress={$emailaddress}",
"https://{$domain}/.well-known/autoconfig/mail/config-v1.1.xml",
"http://autoconfig.{$domain}/mail/config-v1.1.xml",
// 4.2. Central database
"https://autoconfig.thunderbird.net/v1.1/{$domain}"
] as $url) {
$data = \file_get_contents($url);
if ($data && \str_contains($data, '<clientConfig')) {
$data = \json_decode(
\json_encode(
\simplexml_load_string($data, 'SimpleXMLElement', \LIBXML_NOCDATA)
), true);
if (!empty($data['emailProvider'])) {
$data = $data['emailProvider'];
unset($data['documentation']);
$data['incomingServer'] = \array_filter(
isset($data['incomingServer'][0]) ? $data['incomingServer'] : [$data['incomingServer']],
fn($data) => 'imap' === $data['@attributes']['type']
);
$data['outgoingServer'] = \array_filter(
isset($data['outgoingServer'][0]) ? $data['outgoingServer'] : [$data['outgoingServer']],
fn($data) => 'smtp' === $data['@attributes']['type']
);
$data['canonical'] = $url;
return $data;
}
}
}
return null;
}
private static function publicsuffixes() : array
{
$oCache = \RainLoop\Api::Actions()->Cacher();
$list = $oCache->Get('public_suffix_list') ?: null;
if ($list) {
$list = \json_decode($list, true);
if ($list[1] < \time()) {
$list = null;
} else {
$list = $list[0];
}
}
if (null === $list) {
$list = [];
$data = \file_get_contents('https://publicsuffix.org/list/public_suffix_list.dat');
if ($data) {
$list = \array_filter(
\explode("\n", $data),
fn($text) => \strlen($text) && '/' !== $text[0] && '*' !== $text[0] && \substr_count($text, '.')
);
}
// Don't lookup for 24 hours
$oCache->Set('public_suffix_list', \json_encode([$list, time() + 86400]));
}
return $list ?: [];
}
/**
* This is Microsoft
*/
private static function autodiscover(string $domain) : ?array
{
foreach ([
"https://{$domain}",
"https://autodiscover.{$domain}",
"http://autodiscover.{$domain}"
] as $host) {
$result = static::autodiscover_resolve($host, $domain);
if ($result) {
return $result;
}
}
foreach (\SnappyMail\DNS::SRV("_autodiscover._tcp.{$domain}") as $record) {
if (443 == $record['port']) {
$result = static::autodiscover_resolve("https://{$record['target']}", $domain);
} else if (80 == $record['port']) {
$result = static::autodiscover_resolve("http://{$record['target']}", $domain);
} else {
$result = static::autodiscover_resolve("https://{$record['target']}:{$record['port']}", $domain);
}
if ($result) {
return $result;
}
}
return null;
}
private static function autodiscover_resolve(string $host, string $domain) : ?array
{
$email = "autodiscover@{$domain}";
$context = \stream_context_create(['http' => [
'method' => 'POST',
'header' => 'Content-Type: application/xml',
'content' => '<?xml version="1.0" encoding="utf-8" ?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
<Request>
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
<EMailAddress>'.$email.'</EMailAddress>
</Request>
</Autodiscover>'
]]);
$url = "{$host}/autodiscover/autodiscover.xml";
$xml = \file_get_contents($url, false, $context);
if ($xml) {
$data = \json_decode(
\json_encode(
\simplexml_load_string($xml, 'SimpleXMLElement', \LIBXML_NOCDATA)
), true);
if (!empty($data['Response']['Account']['Protocol']) && 'email' === $data['Response']['Account']['AccountType']) {
$result = [
'incomingServer' => [],
'outgoingServer' => []
];
foreach ($data['Response']['Account']['Protocol'] as $entry) {
if ('IMAP' === $entry['Type']) {
$result['incomingServer'][] = [
'hostname' => $entry['Server'],
'port' => $entry['Port'],
'socketType' => ('on' === $entry['SSL']) ? (993 == $entry['Port'] ? 'SSL' : 'STARTTLS') : '',
'authentication' => 'password-cleartext',
'username' => ($entry['LoginName'] === $email) ? '%EMAILADDRESS%' : ''
];
} else if ('SMTP' === $entry['Type']) {
$result['outgoingServer'][] = [
'hostname' => $entry['Server'],
'port' => $entry['Port'],
'socketType' => (587 == $entry['Port']) ? 'STARTTLS' : ('on' === $entry['SSL'] ? 'SSL' : ''),
'authentication' => 'password-cleartext',
'username' => ($entry['LoginName'] === $email) ? '%EMAILADDRESS%' : ''
];
}
}
$result['canonical'] = $url;
return $result;
}
}
return null;
}
}