1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Mailer\Transport\Smtp; 13 14use Psr\Log\LoggerInterface; 15use Symfony\Component\Mailer\Exception\TransportException; 16use Symfony\Component\Mailer\Exception\TransportExceptionInterface; 17use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface; 18use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; 19use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; 20 21/** 22 * Sends Emails over SMTP with ESMTP support. 23 * 24 * @author Fabien Potencier <fabien@symfony.com> 25 * @author Chris Corbyn 26 */ 27class EsmtpTransport extends SmtpTransport 28{ 29 private $authenticators = []; 30 private $username = ''; 31 private $password = ''; 32 33 public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) 34 { 35 parent::__construct(null, $dispatcher, $logger); 36 37 // order is important here (roughly most secure and popular first) 38 $this->authenticators = [ 39 new Auth\CramMd5Authenticator(), 40 new Auth\LoginAuthenticator(), 41 new Auth\PlainAuthenticator(), 42 new Auth\XOAuth2Authenticator(), 43 ]; 44 45 /** @var SocketStream $stream */ 46 $stream = $this->getStream(); 47 48 if (null === $tls) { 49 if (465 === $port) { 50 $tls = true; 51 } else { 52 $tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host; 53 } 54 } 55 if (!$tls) { 56 $stream->disableTls(); 57 } 58 if (0 === $port) { 59 $port = $tls ? 465 : 25; 60 } 61 62 $stream->setHost($host); 63 $stream->setPort($port); 64 } 65 66 public function setUsername(string $username): self 67 { 68 $this->username = $username; 69 70 return $this; 71 } 72 73 public function getUsername(): string 74 { 75 return $this->username; 76 } 77 78 public function setPassword(string $password): self 79 { 80 $this->password = $password; 81 82 return $this; 83 } 84 85 public function getPassword(): string 86 { 87 return $this->password; 88 } 89 90 public function addAuthenticator(AuthenticatorInterface $authenticator): void 91 { 92 $this->authenticators[] = $authenticator; 93 } 94 95 protected function doHeloCommand(): void 96 { 97 try { 98 $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); 99 } catch (TransportExceptionInterface $e) { 100 parent::doHeloCommand(); 101 102 return; 103 } 104 105 $capabilities = $this->getCapabilities($response); 106 107 /** @var SocketStream $stream */ 108 $stream = $this->getStream(); 109 // WARNING: !$stream->isTLS() is right, 100% sure :) 110 // if you think that the ! should be removed, read the code again 111 // if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured 112 if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) { 113 $this->executeCommand("STARTTLS\r\n", [220]); 114 115 if (!$stream->startTLS()) { 116 throw new TransportException('Unable to connect with STARTTLS.'); 117 } 118 119 try { 120 $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); 121 $capabilities = $this->getCapabilities($response); 122 } catch (TransportExceptionInterface $e) { 123 parent::doHeloCommand(); 124 125 return; 126 } 127 } 128 129 if (\array_key_exists('AUTH', $capabilities)) { 130 $this->handleAuth($capabilities['AUTH']); 131 } 132 } 133 134 private function getCapabilities(string $ehloResponse): array 135 { 136 $capabilities = []; 137 $lines = explode("\r\n", trim($ehloResponse)); 138 array_shift($lines); 139 foreach ($lines as $line) { 140 if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) { 141 $value = strtoupper(ltrim($matches[2], ' =')); 142 $capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : []; 143 } 144 } 145 146 return $capabilities; 147 } 148 149 private function handleAuth(array $modes): void 150 { 151 if (!$this->username) { 152 return; 153 } 154 155 $authNames = []; 156 $errors = []; 157 $modes = array_map('strtolower', $modes); 158 foreach ($this->authenticators as $authenticator) { 159 if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) { 160 continue; 161 } 162 163 $authNames[] = $authenticator->getAuthKeyword(); 164 try { 165 $authenticator->authenticate($this); 166 167 return; 168 } catch (TransportExceptionInterface $e) { 169 try { 170 $this->executeCommand("RSET\r\n", [250]); 171 } catch (TransportExceptionInterface $_) { 172 // ignore this exception as it probably means that the server error was final 173 } 174 175 // keep the error message, but tries the other authenticators 176 $errors[$authenticator->getAuthKeyword()] = $e; 177 } 178 } 179 180 if (!$authNames) { 181 throw new TransportException('Failed to find an authenticator supported by the SMTP server.'); 182 } 183 184 $message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames)); 185 foreach ($errors as $name => $error) { 186 $message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error); 187 } 188 189 throw new TransportException($message); 190 } 191} 192