EsmtpTransport.php 8.3 KB

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