EsmtpTransport.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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. public function __construct(string $host = 'localhost', int $port = 0, ?bool $tls = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null, ?AbstractStream $stream = null, ?array $authenticators = null)
  33. {
  34. parent::__construct($stream, $dispatcher, $logger);
  35. if (null === $authenticators) {
  36. // fallback to default authenticators
  37. // order is important here (roughly most secure and popular first)
  38. $authenticators = [
  39. new Auth\CramMd5Authenticator(),
  40. new Auth\LoginAuthenticator(),
  41. new Auth\PlainAuthenticator(),
  42. new Auth\XOAuth2Authenticator(),
  43. ];
  44. }
  45. $this->setAuthenticators($authenticators);
  46. /** @var SocketStream $stream */
  47. $stream = $this->getStream();
  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. $stream->setHost($host);
  62. $stream->setPort($port);
  63. }
  64. /**
  65. * @return $this
  66. */
  67. public function setUsername(string $username): static
  68. {
  69. $this->username = $username;
  70. return $this;
  71. }
  72. public function getUsername(): string
  73. {
  74. return $this->username;
  75. }
  76. /**
  77. * @return $this
  78. */
  79. public function setPassword(#[\SensitiveParameter] string $password): static
  80. {
  81. $this->password = $password;
  82. return $this;
  83. }
  84. public function getPassword(): string
  85. {
  86. return $this->password;
  87. }
  88. /**
  89. * @return $this
  90. */
  91. public function setAutoTls(bool $autoTls): static
  92. {
  93. $this->autoTls = $autoTls;
  94. return $this;
  95. }
  96. public function isAutoTls(): bool
  97. {
  98. return $this->autoTls;
  99. }
  100. public function setAuthenticators(array $authenticators): void
  101. {
  102. $this->authenticators = [];
  103. foreach ($authenticators as $authenticator) {
  104. $this->addAuthenticator($authenticator);
  105. }
  106. }
  107. public function addAuthenticator(AuthenticatorInterface $authenticator): void
  108. {
  109. $this->authenticators[] = $authenticator;
  110. }
  111. public function executeCommand(string $command, array $codes): string
  112. {
  113. return [250] === $codes && str_starts_with($command, 'HELO ') ? $this->doEhloCommand() : parent::executeCommand($command, $codes);
  114. }
  115. final protected function getCapabilities(): array
  116. {
  117. return $this->capabilities;
  118. }
  119. private function doEhloCommand(): string
  120. {
  121. try {
  122. $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
  123. } catch (TransportExceptionInterface $e) {
  124. try {
  125. return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]);
  126. } catch (TransportExceptionInterface $ex) {
  127. if (!$ex->getCode()) {
  128. throw $e;
  129. }
  130. throw $ex;
  131. }
  132. }
  133. $this->capabilities = $this->parseCapabilities($response);
  134. /** @var SocketStream $stream */
  135. $stream = $this->getStream();
  136. // WARNING: !$stream->isTLS() is right, 100% sure :)
  137. // if you think that the ! should be removed, read the code again
  138. // if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured
  139. if ($this->autoTls && !$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $this->capabilities)) {
  140. $this->executeCommand("STARTTLS\r\n", [220]);
  141. if (!$stream->startTLS()) {
  142. throw new TransportException('Unable to connect with STARTTLS.');
  143. }
  144. $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
  145. $this->capabilities = $this->parseCapabilities($response);
  146. }
  147. if (\array_key_exists('AUTH', $this->capabilities)) {
  148. $this->handleAuth($this->capabilities['AUTH']);
  149. }
  150. return $response;
  151. }
  152. private function parseCapabilities(string $ehloResponse): array
  153. {
  154. $capabilities = [];
  155. $lines = explode("\r\n", trim($ehloResponse));
  156. array_shift($lines);
  157. foreach ($lines as $line) {
  158. if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) {
  159. $value = strtoupper(ltrim($matches[2], ' ='));
  160. $capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : [];
  161. }
  162. }
  163. return $capabilities;
  164. }
  165. private function handleAuth(array $modes): void
  166. {
  167. if (!$this->username) {
  168. return;
  169. }
  170. $code = null;
  171. $authNames = [];
  172. $errors = [];
  173. $modes = array_map('strtolower', $modes);
  174. foreach ($this->authenticators as $authenticator) {
  175. if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) {
  176. continue;
  177. }
  178. $code = null;
  179. $authNames[] = $authenticator->getAuthKeyword();
  180. try {
  181. $authenticator->authenticate($this);
  182. return;
  183. } catch (UnexpectedResponseException $e) {
  184. $code = $e->getCode();
  185. try {
  186. $this->executeCommand("RSET\r\n", [250]);
  187. } catch (TransportExceptionInterface) {
  188. // ignore this exception as it probably means that the server error was final
  189. }
  190. // keep the error message, but tries the other authenticators
  191. $errors[$authenticator->getAuthKeyword()] = $e->getMessage();
  192. }
  193. }
  194. if (!$authNames) {
  195. throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes)), $code ?: 504);
  196. }
  197. $message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames));
  198. foreach ($errors as $name => $error) {
  199. $message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error);
  200. }
  201. throw new TransportException($message, $code ?: 535);
  202. }
  203. }