One Hat Cyber Team
Your IP:
216.73.216.63
Server IP:
198.54.114.155
Server:
Linux server71.web-hosting.com 4.18.0-513.18.1.lve.el8.x86_64 #1 SMP Thu Feb 22 12:55:50 UTC 2024 x86_64
Server Software:
LiteSpeed
PHP Version:
5.6.40
Create File
|
Create Folder
Execute
Dir :
~
/
proc
/
thread-self
/
cwd
/
View File Name :
Smtp.tar
EsmtpTransport.php 0000644 00000016457 15111717742 0010305 0 ustar 00 <?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\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface; use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; /** * Sends Emails over SMTP with ESMTP support. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn */ class EsmtpTransport extends SmtpTransport { private array $authenticators = []; private string $username = ''; private string $password = ''; private array $capabilities; public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null, AbstractStream $stream = null, array $authenticators = null) { parent::__construct($stream, $dispatcher, $logger); if (null === $authenticators) { // fallback to default authenticators // order is important here (roughly most secure and popular first) $authenticators = [ new Auth\CramMd5Authenticator(), new Auth\LoginAuthenticator(), new Auth\PlainAuthenticator(), new Auth\XOAuth2Authenticator(), ]; } $this->setAuthenticators($authenticators); /** @var SocketStream $stream */ $stream = $this->getStream(); if (null === $tls) { if (465 === $port) { $tls = true; } else { $tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host; } } if (!$tls) { $stream->disableTls(); } if (0 === $port) { $port = $tls ? 465 : 25; } $stream->setHost($host); $stream->setPort($port); } /** * @return $this */ public function setUsername(string $username): static { $this->username = $username; return $this; } public function getUsername(): string { return $this->username; } /** * @return $this */ public function setPassword(#[\SensitiveParameter] string $password): static { $this->password = $password; return $this; } public function getPassword(): string { return $this->password; } public function setAuthenticators(array $authenticators): void { $this->authenticators = []; foreach ($authenticators as $authenticator) { $this->addAuthenticator($authenticator); } } public function addAuthenticator(AuthenticatorInterface $authenticator): void { $this->authenticators[] = $authenticator; } public function executeCommand(string $command, array $codes): string { return [250] === $codes && str_starts_with($command, 'HELO ') ? $this->doEhloCommand() : parent::executeCommand($command, $codes); } final protected function getCapabilities(): array { return $this->capabilities; } private function doEhloCommand(): string { try { $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); } catch (TransportExceptionInterface $e) { try { return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]); } catch (TransportExceptionInterface $ex) { if (!$ex->getCode()) { throw $e; } throw $ex; } } $this->capabilities = $this->parseCapabilities($response); /** @var SocketStream $stream */ $stream = $this->getStream(); // WARNING: !$stream->isTLS() is right, 100% sure :) // if you think that the ! should be removed, read the code again // if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $this->capabilities)) { $this->executeCommand("STARTTLS\r\n", [220]); if (!$stream->startTLS()) { throw new TransportException('Unable to connect with STARTTLS.'); } $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); $this->capabilities = $this->parseCapabilities($response); } if (\array_key_exists('AUTH', $this->capabilities)) { $this->handleAuth($this->capabilities['AUTH']); } return $response; } private function parseCapabilities(string $ehloResponse): array { $capabilities = []; $lines = explode("\r\n", trim($ehloResponse)); array_shift($lines); foreach ($lines as $line) { if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) { $value = strtoupper(ltrim($matches[2], ' =')); $capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : []; } } return $capabilities; } private function handleAuth(array $modes): void { if (!$this->username) { return; } $code = null; $authNames = []; $errors = []; $modes = array_map('strtolower', $modes); foreach ($this->authenticators as $authenticator) { if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) { continue; } $code = null; $authNames[] = $authenticator->getAuthKeyword(); try { $authenticator->authenticate($this); return; } catch (TransportExceptionInterface $e) { $code = $e->getCode(); try { $this->executeCommand("RSET\r\n", [250]); } catch (TransportExceptionInterface) { // ignore this exception as it probably means that the server error was final } // keep the error message, but tries the other authenticators $errors[$authenticator->getAuthKeyword()] = $e->getMessage(); } } if (!$authNames) { throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes)), $code ?: 504); } $message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames)); foreach ($errors as $name => $error) { $message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error); } throw new TransportException($message, $code ?: 535); } } EsmtpTransportFactory.php 0000644 00000004542 15111717742 0011625 0 ustar 00 <?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 Symfony\Component\Mailer\Transport\AbstractTransportFactory; use Symfony\Component\Mailer\Transport\Dsn; use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; use Symfony\Component\Mailer\Transport\TransportInterface; /** * @author Konstantin Myakshin <molodchick@gmail.com> */ final class EsmtpTransportFactory extends AbstractTransportFactory { public function create(Dsn $dsn): TransportInterface { $tls = 'smtps' === $dsn->getScheme() ? true : null; $port = $dsn->getPort(0); $host = $dsn->getHost(); $transport = new EsmtpTransport($host, $port, $tls, $this->dispatcher, $this->logger); if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOL)) { /** @var SocketStream $stream */ $stream = $transport->getStream(); $streamOptions = $stream->getStreamOptions(); $streamOptions['ssl']['verify_peer'] = false; $streamOptions['ssl']['verify_peer_name'] = false; $stream->setStreamOptions($streamOptions); } if ($user = $dsn->getUser()) { $transport->setUsername($user); } if ($password = $dsn->getPassword()) { $transport->setPassword($password); } if (null !== ($localDomain = $dsn->getOption('local_domain'))) { $transport->setLocalDomain($localDomain); } if (null !== ($maxPerSecond = $dsn->getOption('max_per_second'))) { $transport->setMaxPerSecond((float) $maxPerSecond); } if (null !== ($restartThreshold = $dsn->getOption('restart_threshold'))) { $transport->setRestartThreshold((int) $restartThreshold, (int) $dsn->getOption('restart_threshold_sleep', 0)); } if (null !== ($pingThreshold = $dsn->getOption('ping_threshold'))) { $transport->setPingThreshold((int) $pingThreshold); } return $transport; } protected function getSupportedSchemes(): array { return ['smtp', 'smtps']; } } Auth/AuthenticatorInterface.php 0000644 00000001477 15111717742 0012630 0 ustar 00 <?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\Auth; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * An Authentication mechanism. * * @author Chris Corbyn */ interface AuthenticatorInterface { /** * Tries to authenticate the user. * * @throws TransportExceptionInterface */ public function authenticate(EsmtpTransport $client): void; /** * Gets the name of the AUTH mechanism this Authenticator handles. */ public function getAuthKeyword(): string; } Auth/CramMd5Authenticator.php 0000644 00000003222 15111717742 0012146 0 ustar 00 <?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\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles CRAM-MD5 authentication. * * @author Chris Corbyn */ class CramMd5Authenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'CRAM-MD5'; } /** * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void { $challenge = $client->executeCommand("AUTH CRAM-MD5\r\n", [334]); $challenge = base64_decode(substr($challenge, 4)); $message = base64_encode($client->getUsername().' '.$this->getResponse($client->getPassword(), $challenge)); $client->executeCommand(sprintf("%s\r\n", $message), [235]); } /** * Generates a CRAM-MD5 response from a server challenge. */ private function getResponse(#[\SensitiveParameter] string $secret, string $challenge): string { if (\strlen($secret) > 64) { $secret = pack('H32', md5($secret)); } if (\strlen($secret) < 64) { $secret = str_pad($secret, 64, \chr(0)); } $kipad = substr($secret, 0, 64) ^ str_repeat(\chr(0x36), 64); $kopad = substr($secret, 0, 64) ^ str_repeat(\chr(0x5C), 64); $inner = pack('H32', md5($kipad.$challenge)); $digest = md5($kopad.$inner); return $digest; } } Auth/XOAuth2Authenticator.php 0000644 00000002005 15111717742 0012146 0 ustar 00 <?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\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles XOAUTH2 authentication. * * @author xu.li<AthenaLightenedMyPath@gmail.com> * * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol */ class XOAuth2Authenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'XOAUTH2'; } /** * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism */ public function authenticate(EsmtpTransport $client): void { $client->executeCommand('AUTH XOAUTH2 '.base64_encode('user='.$client->getUsername()."\1auth=Bearer ".$client->getPassword()."\1\1")."\r\n", [235]); } } Auth/PlainAuthenticator.php 0000644 00000001560 15111717742 0011764 0 ustar 00 <?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\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles PLAIN authentication. * * @author Chris Corbyn */ class PlainAuthenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'PLAIN'; } /** * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void { $client->executeCommand(sprintf("AUTH PLAIN %s\r\n", base64_encode($client->getUsername().\chr(0).$client->getUsername().\chr(0).$client->getPassword())), [235]); } } Auth/LoginAuthenticator.php 0000644 00000001703 15111717742 0011770 0 ustar 00 <?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\Auth; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; /** * Handles LOGIN authentication. * * @author Chris Corbyn */ class LoginAuthenticator implements AuthenticatorInterface { public function getAuthKeyword(): string { return 'LOGIN'; } /** * @see https://www.ietf.org/rfc/rfc4954.txt */ public function authenticate(EsmtpTransport $client): void { $client->executeCommand("AUTH LOGIN\r\n", [334]); $client->executeCommand(sprintf("%s\r\n", base64_encode($client->getUsername())), [334]); $client->executeCommand(sprintf("%s\r\n", base64_encode($client->getPassword())), [235]); } } error_log 0000644 00000002136 15111717742 0006471 0 ustar 00 [25-Nov-2025 20:01:13 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Mailer\Transport\AbstractTransport" not found in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php:32 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php on line 32 [25-Nov-2025 20:05:17 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Mailer\Transport\Smtp\SmtpTransport" not found in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/EsmtpTransport.php:28 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/EsmtpTransport.php on line 28 [25-Nov-2025 20:23:40 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Mailer\Transport\AbstractTransportFactory" not found in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php:22 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php on line 22 Stream/AbstractStream.php 0000644 00000006760 15111717742 0011446 0 ustar 00 <?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\Stream; use Symfony\Component\Mailer\Exception\TransportException; /** * A stream supporting remote sockets and local processes. * * @author Fabien Potencier <fabien@symfony.com> * @author Nicolas Grekas <p@tchwork.com> * @author Chris Corbyn * * @internal */ abstract class AbstractStream { protected $stream; protected $in; protected $out; private string $debug = ''; public function write(string $bytes, bool $debug = true): void { if ($debug) { foreach (explode("\n", trim($bytes)) as $line) { $this->debug .= sprintf("> %s\n", $line); } } $bytesToWrite = \strlen($bytes); $totalBytesWritten = 0; while ($totalBytesWritten < $bytesToWrite) { $bytesWritten = @fwrite($this->in, substr($bytes, $totalBytesWritten)); if (false === $bytesWritten || 0 === $bytesWritten) { throw new TransportException('Unable to write bytes on the wire.'); } $totalBytesWritten += $bytesWritten; } } /** * Flushes the contents of the stream (empty it) and set the internal pointer to the beginning. */ public function flush(): void { fflush($this->in); } /** * Performs any initialization needed. */ abstract public function initialize(): void; public function terminate(): void { $this->stream = $this->out = $this->in = null; } public function readLine(): string { if (feof($this->out)) { return ''; } $line = fgets($this->out); if ('' === $line || false === $line) { $metas = stream_get_meta_data($this->out); if ($metas['timed_out']) { throw new TransportException(sprintf('Connection to "%s" timed out.', $this->getReadConnectionDescription())); } if ($metas['eof']) { throw new TransportException(sprintf('Connection to "%s" has been closed unexpectedly.', $this->getReadConnectionDescription())); } } $this->debug .= sprintf('< %s', $line); return $line; } public function getDebug(): string { $debug = $this->debug; $this->debug = ''; return $debug; } public static function replace(string $from, string $to, iterable $chunks): \Generator { if ('' === $from) { yield from $chunks; return; } $carry = ''; $fromLen = \strlen($from); foreach ($chunks as $chunk) { if ('' === $chunk = $carry.$chunk) { continue; } if (str_contains($chunk, $from)) { $chunk = explode($from, $chunk); $carry = array_pop($chunk); yield implode($to, $chunk).$to; } else { $carry = $chunk; } if (\strlen($carry) > $fromLen) { yield substr($carry, 0, -$fromLen); $carry = substr($carry, -$fromLen); } } if ('' !== $carry) { yield $carry; } } abstract protected function getReadConnectionDescription(): string; } Stream/error_log 0000644 00000001404 15111717742 0007721 0 ustar 00 [26-Nov-2025 12:53:32 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream" not found in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php:24 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php on line 24 [26-Nov-2025 12:56:48 UTC] PHP Fatal error: Uncaught Error: Class "Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream" not found in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/Stream/SocketStream.php:24 Stack trace: #0 {main} thrown in /home/fluxyjvi/public_html/project/vendor/symfony/mailer/Transport/Smtp/Stream/SocketStream.php on line 24 Stream/SocketStream.php 0000644 00000010723 15111717742 0011125 0 ustar 00 <?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\Stream; use Symfony\Component\Mailer\Exception\TransportException; /** * A stream supporting remote sockets. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn * * @internal */ final class SocketStream extends AbstractStream { private string $url; private string $host = 'localhost'; private int $port = 465; private float $timeout; private bool $tls = true; private ?string $sourceIp = null; private array $streamContextOptions = []; /** * @return $this */ public function setTimeout(float $timeout): static { $this->timeout = $timeout; return $this; } public function getTimeout(): float { return $this->timeout ?? (float) \ini_get('default_socket_timeout'); } /** * Literal IPv6 addresses should be wrapped in square brackets. * * @return $this */ public function setHost(string $host): static { $this->host = $host; return $this; } public function getHost(): string { return $this->host; } /** * @return $this */ public function setPort(int $port): static { $this->port = $port; return $this; } public function getPort(): int { return $this->port; } /** * Sets the TLS/SSL on the socket (disables STARTTLS). * * @return $this */ public function disableTls(): static { $this->tls = false; return $this; } public function isTLS(): bool { return $this->tls; } /** * @return $this */ public function setStreamOptions(array $options): static { $this->streamContextOptions = $options; return $this; } public function getStreamOptions(): array { return $this->streamContextOptions; } /** * Sets the source IP. * * IPv6 addresses should be wrapped in square brackets. * * @return $this */ public function setSourceIp(string $ip): static { $this->sourceIp = $ip; return $this; } /** * Returns the IP used to connect to the destination. */ public function getSourceIp(): ?string { return $this->sourceIp; } public function initialize(): void { $this->url = $this->host.':'.$this->port; if ($this->tls) { $this->url = 'ssl://'.$this->url; } $options = []; if ($this->sourceIp) { $options['socket']['bindto'] = $this->sourceIp.':0'; } if ($this->streamContextOptions) { $options = array_merge($options, $this->streamContextOptions); } // do it unconditionally as it will be used by STARTTLS as well if supported $options['ssl']['crypto_method'] ??= \STREAM_CRYPTO_METHOD_TLS_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; $streamContext = stream_context_create($options); $timeout = $this->getTimeout(); set_error_handler(function ($type, $msg) { throw new TransportException(sprintf('Connection could not be established with host "%s": ', $this->url).$msg); }); try { $this->stream = stream_socket_client($this->url, $errno, $errstr, $timeout, \STREAM_CLIENT_CONNECT, $streamContext); } finally { restore_error_handler(); } stream_set_blocking($this->stream, true); stream_set_timeout($this->stream, $timeout); $this->in = &$this->stream; $this->out = &$this->stream; } public function startTLS(): bool { set_error_handler(function ($type, $msg) { throw new TransportException('Unable to connect with STARTTLS: '.$msg); }); try { return stream_socket_enable_crypto($this->stream, true); } finally { restore_error_handler(); } } public function terminate(): void { if (null !== $this->stream) { fclose($this->stream); } parent::terminate(); } protected function getReadConnectionDescription(): string { return $this->url; } } Stream/ProcessStream.php 0000644 00000003107 15111717742 0011311 0 ustar 00 <?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\Stream; use Symfony\Component\Mailer\Exception\TransportException; /** * A stream supporting local processes. * * @author Fabien Potencier <fabien@symfony.com> * @author Chris Corbyn * * @internal */ final class ProcessStream extends AbstractStream { private string $command; public function setCommand(string $command): void { $this->command = $command; } public function initialize(): void { $descriptorSpec = [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', '\\' === \DIRECTORY_SEPARATOR ? 'a' : 'w'], ]; $pipes = []; $this->stream = proc_open($this->command, $descriptorSpec, $pipes); stream_set_blocking($pipes[2], false); if ($err = stream_get_contents($pipes[2])) { throw new TransportException('Process could not be started: '.$err); } $this->in = &$pipes[0]; $this->out = &$pipes[1]; } public function terminate(): void { if (null !== $this->stream) { fclose($this->in); fclose($this->out); proc_close($this->stream); } parent::terminate(); } protected function getReadConnectionDescription(): string { return 'process '.$this->command; } } SmtpTransport.php 0000644 00000027761 15111717742 0010140 0 ustar 00 <?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(); } }