├── .gitignore ├── LICENSE ├── src ├── autoconfig.settings.sample.php └── autoconfig.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /src/autoconfig.settings.php 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Marcel Veldhuizen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/autoconfig.settings.sample.php: -------------------------------------------------------------------------------- 1 | add('example.com'); 13 | 14 | // Name and short name for the offered service, as used by Mozilla Thunderbird 15 | $cfg->name = 'Example mail services'; 16 | $cfg->nameShort = 'Example'; 17 | 18 | // Domains for which these settings apply, in lowercase. 19 | $cfg->domains = [ 'example.com', 'example.net', 'example.org' ]; 20 | 21 | // This is the username associated with the email address. If some kind of lookup 22 | // needs to occur to map the email address to a username, this can be done by 23 | // implementing the UsernameResolver interface, or by just embedding the code here. 24 | // One such UsernameResolvers is currently built in, to aliases files used on many Linux systems. 25 | // 26 | // Some examples: 27 | // "$localpart"; Use the localpart of the email address as username 28 | // new AliasesFileUsernameResolver(); Scan /etc/mail/aliases to obtain the username 29 | // new AliasesFileUsernameResolver("/etc/mail/$domain"); Same but with separate file per domain 30 | $cfg->username = "$localpart"; 31 | 32 | // Add available servers here. 33 | // addServer($type, $hostname) 34 | // $type Server type. Possible types are: imap, pop3, smtp 35 | // $hostname The host name or IP address of the server 36 | // 37 | // Each server should have one or more endpoints, defined by chaining one or more calls to withEndpoint: 38 | // withEndpoint($socketType, $port, $authenticatonType) 39 | // $socketType Required and can be one of: plain, STARTTLS, SSL 40 | // $port The port number on which the server will listen. Omit to use defaults. 41 | // $authentication The authentication scheme to use: 42 | // password-cleartext Default. Should be used only with STARTTLS or SSL 43 | // CRAM-MD5 Not supported by Outlook 44 | // SPA Not supported by Thunderbird 45 | // none Only valid for SMTP. Not recommended however. 46 | 47 | // Example IMAP server for incoming mail, running on port 143 (TLS) and 993 (SSL) 48 | $cfg->addServer('imap', 'imap.example.com') 49 | ->withEndpoint('STARTTLS') 50 | ->withEndpoint('SSL'); 51 | 52 | // Example POP3 server for incoming mail, running on port 110 (TLS) and 995 (SSL) 53 | $cfg->addServer('pop3', 'pop.example.com') 54 | ->withEndpoint('STARTTLS') 55 | ->withEndpoint('SSL'); 56 | 57 | // Example SMTP server for outgoing mail, running on port 587 (TLS) and 465 (SSL) 58 | $cfg->addServer('smtp', 'smtp.example.com') 59 | ->withEndpoint('STARTTLS', 587) 60 | ->withEndpoint('SSL'); 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MailClientAutoConfig 2 | PHP script to help serve Outlook [autodiscover.xml](https://msdn.microsoft.com/en-us/library/cc463896%28v=exchg.80%29.aspx) as well as Mozilla [autoconfig](https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) files. 3 | 4 | Instead of making your users manually configure their email clients, you can publish the necessary settings on a web server. Several popular mail clients support this feature and will automatically configure based on an email address and a password. 5 | 6 | This script aims to help you in this endeavor. It currently supports two different standards, used by different mail clients: 7 | * Microsoft Outlook 8 | * Mozilla Thunderbird 9 | * Evoluton 10 | * KMail 11 | * Kontact 12 | 13 | Things this script does for you: 14 | * Support multiple standards based on a single configuration file 15 | * Determine the username to use based on the email address.
16 | Some setups use the full mail address, others use the local part, yet others have no fixed relation between the two. 17 | This script can be especially helpful in the latter case, since it supports reading /etc/mail/aliases type files commonly found in Linux setups. You can also define your own mapping using for example a database, but this will require some additional programming. 18 | 19 | ## Requirements 20 | * A web server capable of running PHP scripts and URL rewriting 21 | * A **SSL certificate** for **autoconfig.yourdomain.com**, **autodiscover.yourdomain.com**, as well as any additional domains you use. The most crucial of these is autodiscover.yourdomain.com, which is used by Outlook, which will use HTTPS to obtain its settings by default. It may revert to HTTP if the user persists, but will complain loudly before doing so. 22 | * DNS entries for the above host names pointing to the web server 23 | 24 | While pretty much any web server on any platform will do, only Apache 2 will be covered here in detail for now. 25 | 26 | ## Setup using Apache 2 27 | 28 | In this example, we'll be setting things up for the example.com mail server, running Linux and Apache 2. The organization also uses a number of mail addresses on example.org, hosted on the same mail server. Some of the details are glanced over here because they differ per Linux distro. 29 | 30 | 1. Create a new directory that will contain the web site. In this example, we'll use `/var/www/autoconfig` 31 | 2. Place the autoconfig.php and autoconfig.settings.sample.php in the new directory 32 | 3. Copy or rename autoconfig.settings.sample.php to autoconfig.settings.php 33 | 4. Create a new VirtualHost configuration in Apache: 34 | ``` 35 | 36 | ServerAdmin postmaster@example.com 37 | DocumentRoot /var/www/autoconfig 38 | ServerName autoconfig.example.com 39 | ServerAlias autoconfig.example.org autodiscover.example.com autodiscover.example.org 40 | ErrorLog /var/log/apache2/autoconfig-error.log 41 | TransferLog /var/log/apache2/autoconfig-access.log 42 | 43 | RewriteEngine On 44 | RewriteRule ^/mail/config-.*\.xml$ /autoconfig.php 45 | RewriteRule ^/autodiscover/autodiscover.xml$ /autoconfig.php 46 | 47 | ``` 48 | 5. Repeat the same configuration for `` and add your SSL configuration (`SSLCertificateFile`, `SSLCertificateKeyFile`, etc.) Alternatively, if you use [Let's Encrypt](https://letsencrypt.org/) for your free SSL certificate, you can let the command line tool take care of this for you afterwards. 49 | 6. You may want to disable directory listing if it's not disabled by default: 50 | ``` 51 | 52 | Options -Indexes 53 | 54 | ``` 55 | 6. Edit the autoconfig.settings.php file using your favorite text editor. See below for configuration details. 56 | 7. Restart Apache 57 | 58 | ## Configuration 59 | 60 | The configuration file is really just a normal PHP source file that is included into the main source file. 61 | This means that there is nothing stopping you from adding custom code here, or from messing things up badly ;-) 62 | 63 | Most of the configuration is explained inside the sample configuration file itself, but there are a few things to note: 64 | * In the configuration file, you typically define at least two servers: one IMAP or POP3 server, and a SMTP server. 65 | * Each server can have one or more endpoints. An endpoint is a combination of: 66 | * The TCP port number 67 | * The transport security (unencrypted, TLS or SSL) 68 | * The authentication mechanism. 69 | * The order in which you define servers and their endpoints is significant. 70 | * Outlook will see only the first usable server for both incoming and outgoing mail. 71 | * Thunderbird will generally use the first one as well, although it appears to have a preference for SMTP with authentication over unauthenticated SMTP. 72 | 73 | 74 | To continue the example started above, let's assume users have mail address both on the example.com and example.org domain. 75 | Each of these domains have their own aliases file, `/etc/mail/domains/example.com/aliases` and `/etc/mail/domains/example.org/aliases`. We will use these files to determine the username for each mail alias. 76 | 77 | ``` 78 | add('example.com'); 81 | $cfg->name = 'Example mail services'; 82 | $cfg->nameShort = 'Example'; 83 | $cfg->domains = [ 'example.com', 'example.org' ]; 84 | $cfg->username = new AliasesFileUsernameResolver("/etc/mail/domains/$domain/aliases"); 85 | 86 | $cfg->addServer('imap', 'mail.example.com') 87 | ->withEndpoint('STARTTLS') 88 | ->withEndpoint('SSL'); 89 | 90 | $cfg->addServer('smtp', 'mail.example.com') 91 | ->withEndpoint('STARTTLS') 92 | ->withEndpoint('SSL'); 93 | ``` 94 | 95 | ## Testing the setup 96 | 97 | 1. Visit http://autoconfig.example.com/mail/config-v1.1.xml?emailaddress=info@example.com (where info@example.com is a valid email address delivered to a single local mailbox). 98 | * You should see an XML file in which you recognize the settings you've provided. 99 | * If you get an empty response, check the error log of your web server. 100 | 2. Visit https://autoconfig.example.com/mail/config-v1.1.xml?emailaddress=info@example.com to test your SSL setup. 101 | * You should see the same file. 102 | * If this does not work, check your SSL setup. 103 | 3. If everything appears to work, test by adding the mail account to Thunderbird and Outlook itself. 104 | 105 | -------------------------------------------------------------------------------- /src/autoconfig.php: -------------------------------------------------------------------------------- 1 | id = $id; 9 | array_push($this->items, $result); 10 | return $result; 11 | } 12 | 13 | public function getDomainConfig($domain) { 14 | foreach ($this->items as $domainConfig) { 15 | if (in_array($domain, $domainConfig->domains)) { 16 | return $domainConfig; 17 | } 18 | } 19 | 20 | throw new Exception('No configuration found for requested domain.'); 21 | } 22 | } 23 | 24 | class DomainConfiguration { 25 | public $domains; 26 | public $servers = array(); 27 | public $username; 28 | 29 | public function addServer($type, $hostname) { 30 | $server = $this->createServer($type, $hostname); 31 | $server->username = $this->username; 32 | array_push($this->servers, $server); 33 | return $server; 34 | } 35 | 36 | private function createServer($type, $hostname) { 37 | switch ($type) { 38 | case 'imap': 39 | return new Server($type, $hostname, 143, 993); 40 | case 'pop3': 41 | return new Server($type, $hostname, 110, 995); 42 | case 'smtp': 43 | return new Server($type, $hostname, 25, 465); 44 | default: 45 | throw new Exception("Unrecognized server type \"$type\""); 46 | } 47 | } 48 | } 49 | 50 | class Server { 51 | public $type; 52 | public $hostname; 53 | public $username; 54 | public $endpoints; 55 | public $samePassword; 56 | 57 | public function __construct($type, $hostname, $defaultPort, $defaultSslPort) { 58 | $this->type = $type; 59 | $this->hostname = $hostname; 60 | $this->defaultPort = $defaultPort; 61 | $this->defaultSslPort = $defaultSslPort; 62 | $this->endpoints = array(); 63 | $this->samePassword = true; 64 | } 65 | 66 | public function withUsername($username) { 67 | $this->username = $username; 68 | return $this; 69 | } 70 | 71 | public function withDifferentPassword() { 72 | $this->samePassword = false; 73 | return $this; 74 | } 75 | 76 | public function withEndpoint($socketType, $port = null, $authentication = 'password-cleartext') { 77 | if ($port === null) { 78 | $port = $socketType === 'SSL' ? $this->defaultSslPort : $this->defaultPort; 79 | } 80 | 81 | array_push($this->endpoints, (object)array( 82 | 'socketType' => $socketType, 83 | 'port' => $port, 84 | 'authentication' => $authentication)); 85 | 86 | return $this; 87 | } 88 | 89 | 90 | } 91 | 92 | interface UsernameResolver { 93 | public function findUsername($request); 94 | } 95 | 96 | class AliasesFileUsernameResolver implements UsernameResolver { 97 | private $fileName; 98 | 99 | function __construct($fileName = "/etc/mail/aliases") { 100 | $this->fileName = $fileName; 101 | } 102 | 103 | public function findUsername($request) { 104 | static $cachedEmail = null; 105 | static $cachedUsername = null; 106 | 107 | if ($request->email === $cachedEmail) { 108 | return $cachedUsername; 109 | } 110 | 111 | $fp = fopen($this->fileName, 'rb'); 112 | 113 | if ($fp === false) { 114 | throw new Exception("Unable to open aliases file \"$fileName\""); 115 | } 116 | 117 | $username = $this->findLocalPart($fp, $request->localpart); 118 | if (strpos($username, "@") !== false || strpos($username, ",") !== false) { 119 | $username = null; 120 | } 121 | 122 | $cachedEmail = $request->email; 123 | $cachedUsername = $username; 124 | return $username; 125 | } 126 | 127 | protected function findLocalPart($fp, $localPart) { 128 | while (($line = fgets($fp)) !== false) { 129 | $matches = array(); 130 | if (!preg_match("/^\s*" . preg_quote($localPart) . "\s*:\s*(\S+)\s*$/", $line, $matches)) continue; 131 | return $matches[1]; 132 | } 133 | } 134 | } 135 | 136 | abstract class RequestHandler { 137 | public function handleRequest() { 138 | $request = $this->parseRequest(); 139 | $this->expandRequest($request); 140 | $config = $this->getDomainConfig($request); 141 | $this->writeResponse($config, $request); 142 | } 143 | 144 | protected abstract function parseRequest(); 145 | protected abstract function writeResponse($config, $request); 146 | 147 | protected function expandRequest($request) { 148 | list($localpart, $domain) = explode('@', $request->email); 149 | 150 | if (!isset($request->localpart)) { 151 | $request->localpart = $localpart; 152 | } 153 | 154 | if (!isset($request->domain)) { 155 | $request->domain = strtolower($domain); 156 | } 157 | } 158 | 159 | protected function getDomainConfig($request) { 160 | static $cachedEmail = null; 161 | static $cachedConfig = null; 162 | 163 | if ($cachedEmail === $request->email) { 164 | return $cachedConfig; 165 | } 166 | 167 | $cachedConfig = $this->readConfig($request); 168 | $cachedEmail = $request->email; 169 | 170 | return $cachedConfig->getDomainConfig($request->domain); 171 | } 172 | 173 | protected function readConfig($vars) { 174 | foreach ($vars as $var => $value) { 175 | $$var = $value; 176 | } 177 | 178 | $config = new Configuration(); 179 | include './autoconfig.settings.php'; 180 | return $config; 181 | } 182 | 183 | protected function getUsername($server, $request) { 184 | if (is_string($server->username)) { 185 | return $server->username; 186 | } 187 | 188 | if ($server->username instanceof UsernameResolver) { 189 | $resolver = $server->username; 190 | return $resolver->findUsername($request); 191 | } 192 | } 193 | } 194 | 195 | class MozillaHandler extends RequestHandler { 196 | public function writeResponse($config, $request) { 197 | header("Content-Type: text/xml"); 198 | $writer = new XMLWriter(); 199 | $writer->openURI("php://output"); 200 | 201 | $this->writeXml($writer, $config, $request); 202 | $writer->flush(); 203 | } 204 | 205 | protected function parseRequest() { 206 | return (object)array('email' => $_GET['emailaddress']); 207 | } 208 | 209 | protected function writeXml($writer, $config, $request) { 210 | $writer->startDocument("1.0"); 211 | $writer->setIndent(4); 212 | $writer->startElement("clientConfig"); 213 | $writer->writeAttribute("version", "1.1"); 214 | 215 | $this->writeEmailProvider($writer, $config, $request); 216 | 217 | $writer->endElement(); 218 | $writer->endDocument(); 219 | } 220 | 221 | protected function writeEmailProvider($writer, $config, $request) { 222 | $writer->startElement("emailProvider"); 223 | $writer->writeAttribute("id", $config->id); 224 | 225 | foreach ($config->domains as $domain) { 226 | $writer->writeElement("domain", $domain); 227 | } 228 | 229 | $writer->writeElement("displayName", $config->name); 230 | $writer->writeElement("displayShortName", $config->nameShort); 231 | 232 | foreach ($config->servers as $server) { 233 | foreach ($server->endpoints as $endpoint) { 234 | $this->writeServer($writer, $server, $endpoint, $request); 235 | } 236 | } 237 | 238 | $writer->endElement(); 239 | } 240 | 241 | protected function writeServer($writer, $server, $endpoint, $request) { 242 | switch ($server->type) { 243 | case 'imap': 244 | case 'pop3': 245 | $this->writeIncomingServer($writer, $server, $endpoint, $request); 246 | break; 247 | case 'smtp': 248 | $this->writeSmtpServer($writer, $server, $endpoint, $request); 249 | break; 250 | } 251 | } 252 | 253 | protected function writeIncomingServer($writer, $server, $endpoint, $request) { 254 | $authentication = $this->mapAuthenticationType($endpoint->authentication); 255 | if (empty($authentication)) return; 256 | 257 | $writer->startElement("incomingServer"); 258 | $writer->writeAttribute("type", $server->type); 259 | $writer->writeElement("hostname", $server->hostname); 260 | $writer->writeElement("port", $endpoint->port); 261 | $writer->writeElement("socketType", $endpoint->socketType); 262 | $writer->writeElement("username", $this->getUsername($server, $request)); 263 | $writer->writeElement("authentication", $authentication); 264 | $writer->endElement(); 265 | } 266 | 267 | protected function writeSmtpServer($writer, $server, $endpoint, $request) { 268 | $authentication = $this->mapAuthenticationType($endpoint->authentication); 269 | if ($authentication === null) return; 270 | 271 | $writer->startElement("outgoingServer"); 272 | $writer->writeAttribute("type", "smtp"); 273 | $writer->writeElement("hostname", $server->hostname); 274 | $writer->writeElement("port", $endpoint->port); 275 | $writer->writeElement("socketType", $endpoint->socketType); 276 | 277 | if ($authentication !== false) { 278 | $writer->writeElement("username", $this->getUsername($server, $request)); 279 | $writer->writeElement("authentication", $authentication); 280 | } 281 | 282 | $writer->writeElement("addThisServer", "true"); 283 | $writer->writeElement("useGlobalPreferredServer", "true"); 284 | $writer->endElement(); 285 | } 286 | 287 | protected function mapAuthenticationType($authentication) { 288 | switch ($authentication) { 289 | case 'password-cleartext': 290 | return 'password-cleartext'; 291 | case 'CRAM-MD5': 292 | return 'password-encrypted'; 293 | case 'none': 294 | return false; 295 | default: 296 | return null; 297 | } 298 | } 299 | } 300 | 301 | class OutlookHandler extends RequestHandler { 302 | public function writeResponse($config, $request) { 303 | header("Content-Type: application/xml"); 304 | 305 | $writer = new XMLWriter(); 306 | $writer->openMemory(); 307 | 308 | $this->writeXml($writer, $config, $request); 309 | 310 | $response = $writer->outputMemory(true); 311 | echo $response; 312 | } 313 | 314 | protected function parseRequest() { 315 | $postdata = file_get_contents("php://input"); 316 | 317 | if (strlen($postdata) > 0) { 318 | $xml = simplexml_load_string($postdata); 319 | return (object)array('email' => $xml->Request->EMailAddress); 320 | } 321 | 322 | return null; 323 | } 324 | 325 | public function writeXml($writer, $config, $request) { 326 | $writer->startDocument("1.0", "utf-8"); 327 | $writer->setIndent(4); 328 | $writer->startElement("Autodiscover"); 329 | $writer->writeAttribute("xmlns", "http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"); 330 | $writer->startElement("Response"); 331 | $writer->writeAttribute("xmlns", "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"); 332 | 333 | $writer->startElement("Account"); 334 | $writer->writeElement("AccountType", "email"); 335 | $writer->writeElement("Action", "settings"); 336 | 337 | foreach ($config->servers as $server) { 338 | foreach ($server->endpoints as $endpoint) { 339 | if ($this->writeProtocol($writer, $server, $endpoint, $request)) 340 | break; 341 | } 342 | } 343 | 344 | $writer->endElement(); 345 | 346 | $writer->endElement(); 347 | $writer->endElement(); 348 | $writer->endDocument(); 349 | } 350 | 351 | protected function writeProtocol($writer, $server, $endpoint, $request) { 352 | switch ($endpoint->authentication) { 353 | case 'password-cleartext': 354 | case 'SPA': 355 | break; 356 | case 'none': 357 | if ($server->type !== 'smtp') return false; 358 | break; 359 | default: 360 | return false; 361 | } 362 | 363 | $writer->startElement('Protocol'); 364 | $writer->writeElement('Type', strtoupper($server->type)); 365 | $writer->writeElement('Server', $server->hostname); 366 | $writer->writeElement('Port', $endpoint->port); 367 | $writer->writeElement('LoginName', $this->getUsername($server, $request)); 368 | $writer->writeElement('DomainRequired', 'off'); 369 | $writer->writeElement('SPA', $endpoint->authentication === 'SPA' ? 'on' : 'off'); 370 | 371 | switch ($endpoint->socketType) { 372 | case 'plain': 373 | $writer->writeElement("SSL", "off"); 374 | break; 375 | case 'SSL': 376 | $writer->writeElement("SSL", "on"); 377 | $writer->writeElement("Encryption", "SSL"); 378 | break; 379 | case 'STARTTLS': 380 | $writer->writeElement("SSL", "on"); 381 | $writer->writeElement("Encryption", "TLS"); 382 | break; 383 | } 384 | 385 | $writer->writeElement("AuthRequired", $endpoint->authentication !== 'none' ? 'on' : 'off'); 386 | 387 | if ($server->type == 'smtp') { 388 | $writer->writeElement('UsePOPAuth', $server->samePassword ? 'on' : 'off'); 389 | $writer->writeElement('SMTPLast', 'off'); 390 | } 391 | 392 | $writer->endElement(); 393 | 394 | return true; 395 | } 396 | 397 | protected function mapAuthenticationType($authentication) { 398 | switch ($authentication) { 399 | case 'password-cleartext': 400 | return 'password-cleartext'; 401 | case 'CRAM-MD5': 402 | return 'password-encrypted'; 403 | case 'none': 404 | return false; 405 | default: 406 | return null; 407 | } 408 | } 409 | 410 | } 411 | 412 | if (strpos($_SERVER['SERVER_NAME'], "autoconfig.") === 0) { 413 | // Configuration for Mozilla Thunderbird, Evolution, KMail, Kontact 414 | $handler = new MozillaHandler(); 415 | } else if (strpos($_SERVER['SERVER_NAME'], "autodiscover.") === 0) { 416 | // Configuration for Outlook 417 | $handler = new OutlookHandler(); 418 | } 419 | 420 | $handler->handleRequest(); 421 | --------------------------------------------------------------------------------