EsmtpTransport.php 6.0 KB

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