resolved conflicts

This commit is contained in:
2025-03-31 03:46:05 +03:00
6454 changed files with 1520539 additions and 9 deletions
@@ -0,0 +1,124 @@
<?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;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
use Symfony\Component\Mailer\Transport\Smtp\Stream\ProcessStream;
use Symfony\Component\Mime\RawMessage;
/**
* SendmailTransport for sending mail through a Sendmail/Postfix (etc..) binary.
*
* Transport can be instantiated through SendmailTransportFactory or NativeTransportFactory:
*
* - SendmailTransportFactory to use most common sendmail path and recommended options
* - NativeTransportFactory when configuration is set via php.ini
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Chris Corbyn
*/
class SendmailTransport extends AbstractTransport
{
private string $command = '/usr/sbin/sendmail -bs';
private ProcessStream $stream;
private ?SmtpTransport $transport = null;
/**
* Constructor.
*
* Supported modes are -bs and -t, with any additional flags desired.
*
* The recommended mode is "-bs" since it is interactive and failure notifications are hence possible.
* Note that the -t mode does not support error reporting and does not support Bcc properly (the Bcc headers are not removed).
*
* If using -t mode, you are strongly advised to include -oi or -i in the flags (like /usr/sbin/sendmail -oi -t)
*
* -f<sender> flag will be appended automatically if one is not present.
*/
public function __construct(?string $command = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null)
{
parent::__construct($dispatcher, $logger);
if (null !== $command) {
if (!str_contains($command, ' -bs') && !str_contains($command, ' -t')) {
throw new \InvalidArgumentException(sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command));
}
$this->command = $command;
}
$this->stream = new ProcessStream();
if (str_contains($this->command, ' -bs')) {
$this->stream->setCommand($this->command);
$this->stream->setInteractive(true);
$this->transport = new SmtpTransport($this->stream, $dispatcher, $logger);
}
}
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
{
if ($this->transport) {
return $this->transport->send($message, $envelope);
}
return parent::send($message, $envelope);
}
public function __toString(): string
{
if ($this->transport) {
return (string) $this->transport;
}
return 'smtp://sendmail';
}
protected function doSend(SentMessage $message): void
{
$this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
$command = $this->command;
if ($recipients = $message->getEnvelope()->getRecipients()) {
$command = str_replace(' -t', '', $command);
}
if (!str_contains($command, ' -f')) {
$command .= ' -f'.escapeshellarg($message->getEnvelope()->getSender()->getEncodedAddress());
}
$chunks = AbstractStream::replace("\r\n", "\n", $message->toIterable());
if (!str_contains($command, ' -i') && !str_contains($command, ' -oi')) {
$chunks = AbstractStream::replace("\n.", "\n..", $chunks);
}
foreach ($recipients as $recipient) {
$command .= ' '.escapeshellarg($recipient->getEncodedAddress());
}
$this->stream->setCommand($command);
$this->stream->initialize();
foreach ($chunks as $chunk) {
$this->stream->write($chunk);
}
$this->stream->flush();
$this->stream->terminate();
$this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
}
}
@@ -0,0 +1,392 @@
<?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\Exception\UnexpectedResponseException;
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 $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;
}
$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;
}
$mtaResult = $this->executeCommand("\r\n.\r\n", [250]);
$message->appendDebug($this->stream->getDebug());
$this->lastMessageTime = microtime(true);
if ($mtaResult && $messageId = $this->parseMessageId($mtaResult)) {
$message->setMessageId($messageId);
}
} 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 UnexpectedResponseException(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__);
}
/**
* @return void
*/
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->stop();
}
}
@@ -0,0 +1,145 @@
<?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
{
/** @var resource|null */
protected $stream;
/** @var resource|null */
protected $in;
/** @var resource|null */
protected $out;
protected $err;
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->err = $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()));
}
if (false === $line) {
throw new TransportException(sprintf('Unable to read from connection to "%s": ', $this->getReadConnectionDescription()).error_get_last()['message']);
}
}
$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;
}
@@ -0,0 +1,81 @@
<?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;
private bool $interactive = false;
public function setCommand(string $command): void
{
$this->command = $command;
}
public function setInteractive(bool $interactive): void
{
$this->interactive = $interactive;
}
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];
$this->err = &$pipes[2];
}
public function terminate(): void
{
if (null !== $this->stream) {
fclose($this->in);
$out = stream_get_contents($this->out);
fclose($this->out);
$err = stream_get_contents($this->err);
fclose($this->err);
if (0 !== $exitCode = proc_close($this->stream)) {
$errorMessage = 'Process failed with exit code '.$exitCode.': '.$out.$err;
}
}
parent::terminate();
if (!$this->interactive && isset($errorMessage)) {
throw new TransportException($errorMessage);
}
}
protected function getReadConnectionDescription(): string
{
return 'process '.$this->command;
}
}