vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php line 44
<?php/** This file is part of the Symfony package.** (c) Fabien Potencier <fabien@symfony.com>** For the full copyright and license information, please view the LICENSE* file that was distributed with this source code.*/namespace Symfony\Component\Mailer\Transport\Smtp;use Psr\EventDispatcher\EventDispatcherInterface;use Psr\Log\LoggerInterface;use Symfony\Component\Mailer\Envelope;use Symfony\Component\Mailer\Exception\LogicException;use Symfony\Component\Mailer\Exception\TransportException;use Symfony\Component\Mailer\Exception\TransportExceptionInterface;use Symfony\Component\Mailer\SentMessage;use Symfony\Component\Mailer\Transport\AbstractTransport;use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;use Symfony\Component\Mime\RawMessage;/*** Sends emails over SMTP.** @author Fabien Potencier <fabien@symfony.com>* @author Chris Corbyn*/class SmtpTransport extends AbstractTransport{private bool $started = false;private int $restartThreshold = 100;private int $restartThresholdSleep = 0;private int $restartCounter = 0;private int $pingThreshold = 100;private float $lastMessageTime = 0;private AbstractStream $stream;private string $mtaResult = '';private string $domain = '[127.0.0.1]';public function __construct(AbstractStream $stream = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null){parent::__construct($dispatcher, $logger);$this->stream = $stream ?? new SocketStream();}public function getStream(): AbstractStream{return $this->stream;}/*** Sets the maximum number of messages to send before re-starting the transport.** By default, the threshold is set to 100 (and no sleep at restart).** @param int $threshold The maximum number of messages (0 to disable)* @param int $sleep The number of seconds to sleep between stopping and re-starting the transport** @return $this*/public function setRestartThreshold(int $threshold, int $sleep = 0): static{$this->restartThreshold = $threshold;$this->restartThresholdSleep = $sleep;return $this;}/*** Sets the minimum number of seconds required between two messages, before the server is pinged.* If the transport wants to send a message and the time since the last message exceeds the specified threshold,* the transport will ping the server first (NOOP command) to check if the connection is still alive.* Otherwise the message will be sent without pinging the server first.** Do not set the threshold too low, as the SMTP server may drop the connection if there are too many* non-mail commands (like pinging the server with NOOP).** By default, the threshold is set to 100 seconds.** @param int $seconds The minimum number of seconds between two messages required to ping the server** @return $this*/public function setPingThreshold(int $seconds): static{$this->pingThreshold = $seconds;return $this;}/*** Sets the name of the local domain that will be used in HELO.** This should be a fully-qualified domain name and should be truly the domain* you're using.** If your server does not have a domain name, use the IP address. This will* automatically be wrapped in square brackets as described in RFC 5321,* section 4.1.3.** @return $this*/public function setLocalDomain(string $domain): static{if ('' !== $domain && '[' !== $domain[0]) {if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {$domain = '['.$domain.']';} elseif (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {$domain = '[IPv6:'.$domain.']';}}$this->domain = $domain;return $this;}/*** Gets the name of the domain that will be used in HELO.** If an IP address was specified, this will be returned wrapped in square* brackets as described in RFC 5321, section 4.1.3.*/public function getLocalDomain(): string{return $this->domain;}public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage{try {$message = parent::send($message, $envelope);} catch (TransportExceptionInterface $e) {if ($this->started) {try {$this->executeCommand("RSET\r\n", [250]);} catch (TransportExceptionInterface) {// ignore this exception as it probably means that the server error was final}}throw $e;}if ($this->mtaResult && $messageId = $this->parseMessageId($this->mtaResult)) {$message->setMessageId($messageId);}$this->checkRestartThreshold();return $message;}protected function parseMessageId(string $mtaResult): string{$regexps = ['/250 Ok (?P<id>[0-9a-f-]+)\r?$/mis','/250 Ok:? queued as (?P<id>[A-Z0-9]+)\r?$/mis',];$matches = [];foreach ($regexps as $regexp) {if (preg_match($regexp, $mtaResult, $matches)) {return $matches['id'];}}return '';}public function __toString(): string{if ($this->stream instanceof SocketStream) {$name = sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost());$port = $this->stream->getPort();if (!(25 === $port || ($tls && 465 === $port))) {$name .= ':'.$port;}return $name;}return 'smtp://sendmail';}/*** Runs a command against the stream, expecting the given response codes.** @param int[] $codes** @throws TransportException when an invalid response if received*/public function executeCommand(string $command, array $codes): string{$this->stream->write($command);$response = $this->getFullResponse();$this->assertResponseCode($response, $codes);return $response;}protected function doSend(SentMessage $message): void{if (microtime(true) - $this->lastMessageTime > $this->pingThreshold) {$this->ping();}if (!$this->started) {$this->start();}try {$envelope = $message->getEnvelope();$this->doMailFromCommand($envelope->getSender()->getEncodedAddress());foreach ($envelope->getRecipients() as $recipient) {$this->doRcptToCommand($recipient->getEncodedAddress());}$this->executeCommand("DATA\r\n", [354]);try {foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) {$this->stream->write($chunk, false);}$this->stream->flush();} catch (TransportExceptionInterface $e) {throw $e;} catch (\Exception $e) {$this->stream->terminate();$this->started = false;$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));throw $e;}$this->mtaResult = $this->executeCommand("\r\n.\r\n", [250]);$message->appendDebug($this->stream->getDebug());$this->lastMessageTime = microtime(true);} catch (TransportExceptionInterface $e) {$e->appendDebug($this->stream->getDebug());$this->lastMessageTime = 0;throw $e;}}/*** @internal since version 6.1, to be made private in 7.0** @final since version 6.1, to be made private in 7.0*/protected function doHeloCommand(): void{$this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]);}private function doMailFromCommand(string $address): void{$this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]);}private function doRcptToCommand(string $address): void{$this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]);}public function start(): void{if ($this->started) {return;}$this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));$this->stream->initialize();$this->assertResponseCode($this->getFullResponse(), [220]);$this->doHeloCommand();$this->started = true;$this->lastMessageTime = 0;$this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__));}/*** Manually disconnect from the SMTP server.** In most cases this is not necessary since the disconnect happens automatically on termination.* In cases of long-running scripts, this might however make sense to avoid keeping an open* connection to the SMTP server in between sending emails.*/public function stop(): void{if (!$this->started) {return;}$this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__));try {$this->executeCommand("QUIT\r\n", [221]);} catch (TransportExceptionInterface) {} finally {$this->stream->terminate();$this->started = false;$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));}}private function ping(): void{if (!$this->started) {return;}try {$this->executeCommand("NOOP\r\n", [250]);} catch (TransportExceptionInterface) {$this->stop();}}/*** @throws TransportException if a response code is incorrect*/private function assertResponseCode(string $response, array $codes): void{if (!$codes) {throw new LogicException('You must set the expected response code.');}[$code] = sscanf($response, '%3d');$valid = \in_array($code, $codes);if (!$valid || !$response) {$codeStr = $code ? sprintf('code "%s"', $code) : 'empty code';$responseStr = $response ? sprintf(', with message "%s"', trim($response)) : '';throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0);}}private function getFullResponse(): string{$response = '';do {$line = $this->stream->readLine();$response .= $line;} while ($line && isset($line[3]) && ' ' !== $line[3]);return $response;}private function checkRestartThreshold(): void{// when using sendmail via non-interactive mode, the transport is never "started"if (!$this->started) {return;}++$this->restartCounter;if ($this->restartCounter < $this->restartThreshold) {return;}$this->stop();if (0 < $sleep = $this->restartThresholdSleep) {$this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep));sleep($sleep);}$this->start();$this->restartCounter = 0;}public function __sleep(): array{throw new \BadMethodCallException('Cannot serialize '.__CLASS__);}public function __wakeup(){throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);}public function __destruct(){$this->stop();}}