TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* Used to implement backwards-compatible spooling
|
||||
*/
|
||||
interface DelayedTransportInterface extends TransportInterface
|
||||
{
|
||||
/**
|
||||
* Sends messages using the given transport instance
|
||||
*
|
||||
* @return int the number of messages sent
|
||||
*/
|
||||
public function flushQueue(TransportInterface $transport): int;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail\Event;
|
||||
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
|
||||
/**
|
||||
* This event is fired once a Mailer has sent a message and allows listeners to execute
|
||||
* further code afterwards, depending on the result, e.g. the SentMessage.
|
||||
*
|
||||
* Note: Usually TYPO3\CMS\Core\Mail\Mailer is given to the event. This implementation
|
||||
* allows to retrieve the SentMessage using the getSentMessage() method. Depending
|
||||
* on the Transport, used to send the message, this might also be NULL.
|
||||
*/
|
||||
final readonly class AfterMailerSentMessageEvent
|
||||
{
|
||||
public function __construct(private MailerInterface $mailer) {}
|
||||
|
||||
public function getMailer(): MailerInterface
|
||||
{
|
||||
return $this->mailer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail\Event;
|
||||
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
/**
|
||||
* This event is fired before the Mailer has sent a message and
|
||||
* allows listeners to manipulate the RawMessage and the Envelope.
|
||||
*
|
||||
* Note: Usually TYPO3\CMS\Core\Mail\Mailer is given to the event. This implementation
|
||||
* allows to retrieve the TransportInterface using the getTransport() method.
|
||||
*/
|
||||
final class BeforeMailerSentMessageEvent
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MailerInterface $mailer,
|
||||
private RawMessage $message,
|
||||
private ?Envelope $envelope = null,
|
||||
) {}
|
||||
|
||||
public function getMessage(): RawMessage
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function setMessage(RawMessage $message): void
|
||||
{
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
public function getEnvelope(): ?Envelope
|
||||
{
|
||||
return $this->envelope;
|
||||
}
|
||||
|
||||
public function setEnvelope(?Envelope $envelope = null): void
|
||||
{
|
||||
$this->envelope = $envelope;
|
||||
}
|
||||
|
||||
public function getMailer(): MailerInterface
|
||||
{
|
||||
return $this->mailer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\Exception\TransportException;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Header\HeaderInterface;
|
||||
use Symfony\Component\Mime\Header\Headers;
|
||||
use Symfony\Component\Mime\Part\AbstractPart;
|
||||
use Symfony\Component\Mime\Part\File;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\Serializer\PolymorphicDeserializer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Inspired by SwiftMailer, adapted for TYPO3 and Symfony/Mailer
|
||||
*
|
||||
* @internal This class is handled internally in TransportFactory
|
||||
*/
|
||||
class FileSpool extends AbstractTransport implements DelayedTransportInterface
|
||||
{
|
||||
/**
|
||||
* File WriteRetry Limit.
|
||||
*/
|
||||
protected int $retryLimit = 10;
|
||||
|
||||
/**
|
||||
* The maximum number of messages to send per flush
|
||||
*/
|
||||
protected int $messageLimit;
|
||||
|
||||
/**
|
||||
* The time limit per flush
|
||||
*/
|
||||
protected int $timeLimit;
|
||||
|
||||
/**
|
||||
* Create a new FileSpool, storing messages in $path.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $path,
|
||||
?EventDispatcherInterface $dispatcher = null,
|
||||
protected readonly ?LoggerInterface $logger = null,
|
||||
protected readonly PolymorphicDeserializer $deserializer = new PolymorphicDeserializer(),
|
||||
) {
|
||||
parent::__construct($dispatcher, $logger);
|
||||
|
||||
if (!file_exists($this->path)) {
|
||||
GeneralUtility::mkdir_deep($this->path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a message in the queue.
|
||||
*/
|
||||
protected function doSend(SentMessage $message): void
|
||||
{
|
||||
$fileName = $this->path . '/' . $this->getRandomString(9);
|
||||
$i = 0;
|
||||
|
||||
// We try an exclusive creation of the file. This is an atomic
|
||||
// operation, it avoids a locking mechanism
|
||||
do {
|
||||
$fileName .= $this->getRandomString(1);
|
||||
$filePointer = @fopen($fileName . '.message', 'x');
|
||||
} while ($filePointer === false && ++$i < $this->retryLimit);
|
||||
|
||||
if ($filePointer === false) {
|
||||
throw new TransportException('Could not create file for spooling', 1602615347);
|
||||
}
|
||||
|
||||
try {
|
||||
$ser = serialize($message);
|
||||
if (fwrite($filePointer, $ser) === false) {
|
||||
throw new TransportException('Could not write file for spooling', 1602615348);
|
||||
}
|
||||
} finally {
|
||||
fclose($filePointer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow to manage the enqueuing retry limit.
|
||||
*
|
||||
* Default is ten and allows over 64^20 different fileNames
|
||||
*/
|
||||
public function setRetryLimit(int $limit): void
|
||||
{
|
||||
$this->retryLimit = $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a recovery if for any reason a process is sending for too long.
|
||||
*
|
||||
* @param int $timeout in second Defaults is for very slow smtp responses
|
||||
*/
|
||||
public function recover(int $timeout = 900): void
|
||||
{
|
||||
foreach (new \DirectoryIterator($this->path) as $file) {
|
||||
$file = (string)$file->getRealPath();
|
||||
|
||||
if (str_ends_with($file, '.message.sending')) {
|
||||
$lockedtime = filectime($file);
|
||||
if ((time() - $lockedtime) > $timeout) {
|
||||
rename($file, substr($file, 0, -8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function flushQueue(TransportInterface $transport): int
|
||||
{
|
||||
$directoryIterator = new \DirectoryIterator($this->path);
|
||||
|
||||
$count = 0;
|
||||
$time = time();
|
||||
foreach ($directoryIterator as $file) {
|
||||
$file = (string)$file->getRealPath();
|
||||
|
||||
if (!str_ends_with($file, '.message')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* We try a rename, it's an atomic operation, and avoid locking the file */
|
||||
if (rename($file, $file . '.sending')) {
|
||||
try {
|
||||
$message = $this->deserializer->deserialize(
|
||||
(string)file_get_contents($file . '.sending'),
|
||||
[
|
||||
SentMessage::class,
|
||||
RawMessage::class,
|
||||
Envelope::class,
|
||||
Address::class,
|
||||
AbstractPart::class,
|
||||
File::class, // This one does not extend AbstractPart
|
||||
Headers::class,
|
||||
HeaderInterface::class,
|
||||
]
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger?->error(
|
||||
'Serialized message from {fileName} was rejected, because it contains a disallowed class object.',
|
||||
['fileName' => $file, 'exception' => $e],
|
||||
);
|
||||
rename($file . '.sending', $file . '.invalid');
|
||||
continue;
|
||||
}
|
||||
if ($message instanceof SentMessage) {
|
||||
// This may throw an exception if something goes wrong.
|
||||
// That is expected and will cause the `.sending` file to remain within the spooler.
|
||||
$transport->send($message->getMessage(), $message->getEnvelope());
|
||||
$count++;
|
||||
unlink($file . '.sending');
|
||||
} else {
|
||||
$this->logger?->error(
|
||||
'Serialized message from {fileName} was rejected, because {className} is not an instance of SentMessage.',
|
||||
[
|
||||
'fileName' => $file,
|
||||
'className' => get_debug_type($message),
|
||||
],
|
||||
);
|
||||
rename($file . '.sending', $file . '.invalid');
|
||||
}
|
||||
} else {
|
||||
/* This message has just been caught by another process */
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->getMessageLimit() && $count >= $this->getMessageLimit()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->getTimeLimit() && ($GLOBALS['EXEC_TIME'] - $time) >= $this->getTimeLimit()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random string needed to generate a fileName for the queue.
|
||||
*/
|
||||
protected function getRandomString(int $count): string
|
||||
{
|
||||
// This string MUST stay FS safe, avoid special chars
|
||||
$base = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_';
|
||||
$ret = '';
|
||||
$strlen = strlen($base);
|
||||
for ($i = 0; $i < $count; ++$i) {
|
||||
$ret .= $base[random_int(0, $strlen - 1)];
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of messages to send per flush.
|
||||
*/
|
||||
public function setMessageLimit(int $limit): void
|
||||
{
|
||||
$this->messageLimit = $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum number of messages to send per flush.
|
||||
*/
|
||||
public function getMessageLimit(): int
|
||||
{
|
||||
return $this->messageLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time limit (in seconds) per flush.
|
||||
*/
|
||||
public function setTimeLimit(int $limit): void
|
||||
{
|
||||
$this->timeLimit = $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time limit (in seconds) per flush.
|
||||
*/
|
||||
public function getTimeLimit(): int
|
||||
{
|
||||
return $this->timeLimit;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'FileSpool:' . $this->path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\Header\Headers;
|
||||
use Symfony\Component\Mime\Part\AbstractPart;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
use TYPO3\CMS\Core\Information\Typo3Information;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryData;
|
||||
use TYPO3\CMS\Core\View\ViewFactoryInterface;
|
||||
use TYPO3\CMS\Fluid\View\FluidViewAdapter;
|
||||
use TYPO3\CMS\Fluid\View\TemplatePaths;
|
||||
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperVariableContainer;
|
||||
|
||||
/**
|
||||
* Send out templated HTML/plain text emails with Fluid.
|
||||
*
|
||||
* @todo: This construct needs an overhaul because it violates "Composition over inheritance".
|
||||
* This is obvious when looking at __construct() already. FluidEmail extends symfony Email
|
||||
* (which is a symfony Message), and thus has to deal with things it shouldn't at this point.
|
||||
* The repeated calls to $this->resetBody() are the proof something is really wrong here.
|
||||
* At first glance it looks as if something like this should happen: The class responsibility
|
||||
* should be rendering of the subject and body only. It should probably be created using a factory,
|
||||
* returning an instance of some interface, with the factory interface being injected to consumers.
|
||||
* This would allow rendering emails with some different template engine, by injecting a different
|
||||
* factory interface implementation that returns some different class. With subject and body being
|
||||
* created by this, a symfony email should be created (maybe with a facade or factory again). And
|
||||
* then hand the mail over to something that can send it.
|
||||
* Working on this may go along with a renaming of the class structure, and we may want to
|
||||
* ensure abstraction does not explode too much along the way ...
|
||||
*/
|
||||
class FluidEmail extends Email
|
||||
{
|
||||
public const FORMAT_HTML = 'html';
|
||||
public const FORMAT_PLAIN = 'plain';
|
||||
public const FORMAT_BOTH = 'both';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected array $format = ['html', 'plain'];
|
||||
protected string $templateName = 'Default';
|
||||
protected FluidViewAdapter $view;
|
||||
|
||||
/**
|
||||
* @internal use TemplatedEmailFactory instead
|
||||
*/
|
||||
public function __construct(?TemplatePaths $templatePaths = null, ?Headers $headers = null, ?AbstractPart $body = null)
|
||||
{
|
||||
parent::__construct($headers, $body);
|
||||
$viewFactory = GeneralUtility::makeInstance(ViewFactoryInterface::class);
|
||||
$view = $viewFactory->create(new ViewFactoryData());
|
||||
if (!$view instanceof FluidViewAdapter) {
|
||||
throw new \RuntimeException(
|
||||
'Class FluidEmail can only deal with Fluid views via FluidViewAdapter',
|
||||
1724686399
|
||||
);
|
||||
}
|
||||
$this->view = $view;
|
||||
// @todo: This is where the problem starts: TemplatePaths() is hardcoded fluid, and part of the
|
||||
// current FluidEmail API. We can not put this into ViewFactoryData() directly. While
|
||||
// we *could* unpack the paths and format to an array again, we should probably better
|
||||
// redesign this implementation and work on the main comment above along the way.
|
||||
// Also note methods like getViewHelperVariableContainer() are hard-bound to fluid, too.
|
||||
if ($templatePaths === null) {
|
||||
$templatePaths = new TemplatePaths();
|
||||
$templatePaths->setTemplateRootPaths($GLOBALS['TYPO3_CONF_VARS']['MAIL']['templateRootPaths'] ?? []);
|
||||
$templatePaths->setLayoutRootPaths($GLOBALS['TYPO3_CONF_VARS']['MAIL']['layoutRootPaths'] ?? []);
|
||||
$templatePaths->setPartialRootPaths($GLOBALS['TYPO3_CONF_VARS']['MAIL']['partialRootPaths'] ?? []);
|
||||
}
|
||||
|
||||
$this->view->getRenderingContext()->setTemplatePaths($templatePaths);
|
||||
$this->view->assignMultiple($this->getDefaultVariables());
|
||||
$this->format($GLOBALS['TYPO3_CONF_VARS']['MAIL']['format'] ?? self::FORMAT_BOTH);
|
||||
}
|
||||
|
||||
public function format(string $format): static
|
||||
{
|
||||
$this->format = match ($format) {
|
||||
self::FORMAT_BOTH => [self::FORMAT_HTML, self::FORMAT_PLAIN],
|
||||
self::FORMAT_HTML => [self::FORMAT_HTML],
|
||||
self::FORMAT_PLAIN => [self::FORMAT_PLAIN],
|
||||
default => throw new \InvalidArgumentException('Setting FluidEmail->format() must be either "html", "plain" or "both", no other formats are currently supported', 1580743847),
|
||||
};
|
||||
$this->resetBody();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setTemplate(string $templateName): static
|
||||
{
|
||||
$this->templateName = $templateName;
|
||||
$this->resetBody();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assign($key, $value): static
|
||||
{
|
||||
$this->view->assign($key, $value);
|
||||
$this->resetBody();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function assignMultiple(array $values): static
|
||||
{
|
||||
$this->view->assignMultiple($values);
|
||||
$this->resetBody();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/*
|
||||
* Shorthand setters
|
||||
*/
|
||||
public function setRequest(ServerRequestInterface $request): static
|
||||
{
|
||||
$this->view->getRenderingContext()->setAttribute(ServerRequestInterface::class, $request);
|
||||
$this->view->assign('request', $request);
|
||||
if ($request->getAttribute('normalizedParams') instanceof NormalizedParams) {
|
||||
$this->view->assign('normalizedParams', $request->getAttribute('normalizedParams'));
|
||||
} else {
|
||||
$this->view->assign('normalizedParams', NormalizedParams::createFromServerParams($_SERVER));
|
||||
}
|
||||
$this->resetBody();
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getDefaultVariables(): array
|
||||
{
|
||||
return [
|
||||
'typo3' => [
|
||||
'sitename' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
|
||||
'formats' => [
|
||||
'date' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'],
|
||||
'time' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
|
||||
],
|
||||
'systemConfiguration' => $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS'],
|
||||
'information' => GeneralUtility::makeInstance(Typo3Information::class),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function ensureValidity(): void
|
||||
{
|
||||
$this->generateTemplatedBody();
|
||||
parent::ensureValidity();
|
||||
}
|
||||
|
||||
public function getBody(): AbstractPart
|
||||
{
|
||||
$this->generateTemplatedBody();
|
||||
return parent::getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource|string|null
|
||||
*/
|
||||
public function getHtmlBody(bool $forceBodyGeneration = false)
|
||||
{
|
||||
if ($forceBodyGeneration) {
|
||||
$this->generateTemplatedBody('html');
|
||||
} elseif (parent::getHtmlBody() === null) {
|
||||
$this->generateTemplatedBody();
|
||||
}
|
||||
return parent::getHtmlBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource|string|null
|
||||
*/
|
||||
public function getTextBody(bool $forceBodyGeneration = false)
|
||||
{
|
||||
if ($forceBodyGeneration) {
|
||||
$this->generateTemplatedBody('plain');
|
||||
} elseif (parent::getTextBody() === null) {
|
||||
$this->generateTemplatedBody();
|
||||
}
|
||||
return parent::getTextBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only used for ext:form, not part of TYPO3 Core API.
|
||||
*/
|
||||
public function getViewHelperVariableContainer(): ViewHelperVariableContainer
|
||||
{
|
||||
// the variables are possibly modified in ext:form, so content must be rendered
|
||||
$this->resetBody();
|
||||
return $this->view->getRenderingContext()->getViewHelperVariableContainer();
|
||||
}
|
||||
|
||||
protected function generateTemplatedBody(string $forceFormat = ''): void
|
||||
{
|
||||
// Use a local variable to allow forcing a specific format
|
||||
$format = $forceFormat ? [$forceFormat] : $this->format;
|
||||
|
||||
$tryToRenderSubjectSection = false;
|
||||
if (in_array(static::FORMAT_HTML, $format, true) && ($forceFormat || parent::getHtmlBody() === null)) {
|
||||
$this->html($this->renderContent('html'));
|
||||
$tryToRenderSubjectSection = true;
|
||||
}
|
||||
if (in_array(static::FORMAT_PLAIN, $format, true) && ($forceFormat || parent::getTextBody() === null)) {
|
||||
$this->text(trim($this->renderContent('txt')));
|
||||
$tryToRenderSubjectSection = true;
|
||||
}
|
||||
|
||||
if ($tryToRenderSubjectSection) {
|
||||
$subjectFromTemplate = $this->view->renderSection(
|
||||
'Subject',
|
||||
$this->view->getRenderingContext()->getVariableProvider()->getAll(),
|
||||
true
|
||||
);
|
||||
if (!empty($subjectFromTemplate)) {
|
||||
$this->subject($subjectFromTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function renderContent(string $format): string
|
||||
{
|
||||
$this->view->getRenderingContext()->getTemplatePaths()->setFormat($format);
|
||||
return $this->view->render($this->templateName);
|
||||
}
|
||||
|
||||
public function getView(): FluidViewAdapter
|
||||
{
|
||||
return $this->view;
|
||||
}
|
||||
|
||||
protected function resetBody(): void
|
||||
{
|
||||
$this->html(null);
|
||||
$this->text(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
/**
|
||||
* Adapter for Symfony Mime to be used by TYPO3 extensions.
|
||||
*/
|
||||
class MailMessage extends Email
|
||||
{
|
||||
/**
|
||||
* compatibility methods to allow for associative arrays as [name => email address]
|
||||
* as it was possible in TYPO3 v9 / SwiftMailer.
|
||||
*
|
||||
* Also, ensure to switch to Address objects and the ->subject()/->from() methods directly
|
||||
* to directly use the new API.
|
||||
*/
|
||||
/**
|
||||
* Set the subject of the message.
|
||||
*
|
||||
* @param string $subject
|
||||
*/
|
||||
public function setSubject($subject): self
|
||||
{
|
||||
return $this->subject($subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the origination date of the message as a UNIX timestamp.
|
||||
*
|
||||
* @param int $date
|
||||
*/
|
||||
public function setDate($date): self
|
||||
{
|
||||
return $this->date((new \DateTime())->setTimestamp($date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the return-path (the bounce address) of this message.
|
||||
*
|
||||
* @param string $address
|
||||
*/
|
||||
public function setReturnPath($address): self
|
||||
{
|
||||
return $this->returnPath($address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sender of this message.
|
||||
*
|
||||
* This does not override the From field, but it has a higher significance.
|
||||
*
|
||||
* @param string $address
|
||||
* @param string $name optional
|
||||
*/
|
||||
public function setSender($address, $name = null): self
|
||||
{
|
||||
return $this->sender(...$this->convertNamedAddress($address, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the from address of this message.
|
||||
*
|
||||
* You may pass an array of addresses if this message is from multiple people.
|
||||
*
|
||||
* If $name is passed and the first parameter is a string, this name will be
|
||||
* associated with the address.
|
||||
* If $name is passed and the first parameter is not a string, an exception is thrown.
|
||||
*
|
||||
* @param string|array $addresses
|
||||
* @param string $name optional
|
||||
*/
|
||||
public function setFrom($addresses, $name = null): self
|
||||
{
|
||||
$this->checkArguments($addresses, $name);
|
||||
return $this->from(...$this->convertNamedAddress($addresses, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reply-to address of this message.
|
||||
*
|
||||
* You may pass an array of addresses if replies will go to multiple people.
|
||||
*
|
||||
* If $name is passed and the first parameter is a string, this name will be
|
||||
* associated with the address.
|
||||
* If $name is passed and the first parameter is not a string, an exception is thrown.
|
||||
*
|
||||
* @param string|array $addresses
|
||||
* @param string $name optional
|
||||
*/
|
||||
public function setReplyTo($addresses, $name = null): self
|
||||
{
|
||||
$this->checkArguments($addresses, $name);
|
||||
return $this->replyTo(...$this->convertNamedAddress($addresses, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the to addresses of this message.
|
||||
*
|
||||
* If multiple recipients will receive the message an array should be used.
|
||||
* Example: array('receiver@domain.org', 'other@domain.org' => 'A name')
|
||||
*
|
||||
* If $name is passed and the first parameter is a string, this name will be
|
||||
* associated with the address.
|
||||
* If $name is passed and the first parameter is not a string, an exception is thrown.
|
||||
*
|
||||
* @param string|array $addresses
|
||||
* @param string $name optional
|
||||
*/
|
||||
public function setTo($addresses, $name = null): self
|
||||
{
|
||||
$this->checkArguments($addresses, $name);
|
||||
return $this->to(...$this->convertNamedAddress($addresses, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Cc addresses of this message.
|
||||
*
|
||||
* If multiple recipients will receive the message an array should be used.
|
||||
* Example: array('receiver@domain.org', 'other@domain.org' => 'A name')
|
||||
*
|
||||
* If $name is passed and the first parameter is a string, this name will be
|
||||
* associated with the address.
|
||||
* If $name is passed and the first parameter is not a string, an exception is thrown.
|
||||
*
|
||||
* @param string|array $addresses
|
||||
* @param string $name optional
|
||||
*/
|
||||
public function setCc($addresses, $name = null): self
|
||||
{
|
||||
$this->checkArguments($addresses, $name);
|
||||
return $this->cc(...$this->convertNamedAddress($addresses, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Bcc addresses of this message.
|
||||
*
|
||||
* If multiple recipients will receive the message an array should be used.
|
||||
* Example: array('receiver@domain.org', 'other@domain.org' => 'A name')
|
||||
*
|
||||
* If $name is passed and the first parameter is a string, this name will be
|
||||
* associated with the address.
|
||||
* If $name is passed and the first parameter is not a string, an exception is thrown.
|
||||
*
|
||||
* @param string|array $addresses
|
||||
* @param string $name optional
|
||||
*/
|
||||
public function setBcc($addresses, $name = null): self
|
||||
{
|
||||
$this->checkArguments($addresses, $name);
|
||||
return $this->bcc(...$this->convertNamedAddress($addresses, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for a delivery receipt from the recipient to be sent to $addresses.
|
||||
*/
|
||||
public function setReadReceiptTo(string $address): self
|
||||
{
|
||||
$this->getHeaders()->addMailboxHeader('Disposition-Notification-To', $address);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts address from [email, name] into Address objects.
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @return Address[]
|
||||
*/
|
||||
protected function convertNamedAddress(...$args): array
|
||||
{
|
||||
if (isset($args[1])) {
|
||||
return [Address::create(sprintf('%s <%s>', $args[1], $args[0]))];
|
||||
}
|
||||
if (is_string($args[0]) || is_array($args[0])) {
|
||||
return $this->convertAddresses($args[0]);
|
||||
}
|
||||
return $this->convertAddresses($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Addresses into Address/NamedAddress objects.
|
||||
*
|
||||
* @param string|array $addresses
|
||||
* @return Address[]
|
||||
*/
|
||||
protected function convertAddresses($addresses): array
|
||||
{
|
||||
if (!is_array($addresses)) {
|
||||
return [Address::create($addresses)];
|
||||
}
|
||||
$newAddresses = [];
|
||||
foreach ($addresses as $email => $name) {
|
||||
if (is_numeric($email) || ctype_digit($email)) {
|
||||
$newAddresses[] = Address::create($name);
|
||||
} else {
|
||||
$newAddresses[] = Address::create(sprintf('%s <%s>', $name, $email));
|
||||
}
|
||||
}
|
||||
|
||||
return $newAddresses;
|
||||
}
|
||||
|
||||
//
|
||||
// Compatibility methods, as it was possible in TYPO3 v9 / SwiftMailer.
|
||||
//
|
||||
|
||||
public function addFrom(Address|string|array|null ...$addresses): static
|
||||
{
|
||||
return parent::addFrom(...$this->convertNamedAddress(...$addresses));
|
||||
}
|
||||
|
||||
public function addReplyTo(Address|string|array|null ...$addresses): static
|
||||
{
|
||||
return parent::addReplyTo(...$this->convertNamedAddress(...$addresses));
|
||||
}
|
||||
|
||||
public function addTo(Address|string|array|null ...$addresses): static
|
||||
{
|
||||
return parent::addTo(...$this->convertNamedAddress(...$addresses));
|
||||
}
|
||||
|
||||
public function addCc(Address|string|array|null ...$addresses): static
|
||||
{
|
||||
return parent::addCc(...$this->convertNamedAddress(...$addresses));
|
||||
}
|
||||
|
||||
public function addBcc(Address|string|array|null ...$addresses): static
|
||||
{
|
||||
return parent::addBcc(...$this->convertNamedAddress(...$addresses));
|
||||
}
|
||||
|
||||
protected function checkArguments($addresses, ?string $name = null): void
|
||||
{
|
||||
if ($name !== null && !is_string($addresses)) {
|
||||
throw new \InvalidArgumentException('The combination of a name and an array of addresses is invalid.', 1570543657);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
use TYPO3\CMS\Core\Exception as CoreException;
|
||||
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
|
||||
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MailUtility;
|
||||
|
||||
/**
|
||||
* Adapter for Symfony/Mailer to be used by TYPO3 extensions.
|
||||
*
|
||||
* This will use the setting in TYPO3_CONF_VARS to choose the correct transport
|
||||
* for it to work out-of-the-box.
|
||||
*/
|
||||
#[Autoconfigure(public: true), AsAlias(MailerInterface::class, public: true)]
|
||||
class Mailer implements MailerInterface
|
||||
{
|
||||
protected array $mailSettings = [];
|
||||
|
||||
protected ?SentMessage $sentMessage;
|
||||
|
||||
/**
|
||||
* This will be added as X-Mailer to all outgoing mails
|
||||
*/
|
||||
protected string $mailerHeader = 'TYPO3';
|
||||
|
||||
/**
|
||||
* When constructing, also initializes the Symfony Transport like configured
|
||||
*
|
||||
* @param TransportInterface|null $transport optionally pass a transport to the constructor.
|
||||
* @throws CoreException
|
||||
*/
|
||||
public function __construct(
|
||||
protected ?TransportInterface $transport = null,
|
||||
protected readonly ?EventDispatcherInterface $eventDispatcher = null,
|
||||
) {
|
||||
if (empty($this->mailSettings)) {
|
||||
$this->injectMailSettings();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->initializeTransport();
|
||||
} catch (\Exception $e) {
|
||||
throw new CoreException($e->getMessage(), 1291068569);
|
||||
}
|
||||
}
|
||||
|
||||
public function send(RawMessage $message, ?Envelope $envelope = null): void
|
||||
{
|
||||
if ($message instanceof Email) {
|
||||
// Ensure to always have a From: header set
|
||||
if (empty($message->getFrom())) {
|
||||
$address = MailUtility::getSystemFromAddress();
|
||||
if ($address) {
|
||||
$name = MailUtility::getSystemFromName();
|
||||
if ($name) {
|
||||
$from = new Address($address, $name);
|
||||
} else {
|
||||
$from = new Address($address);
|
||||
}
|
||||
$message->from($from);
|
||||
}
|
||||
}
|
||||
if (empty($message->getReplyTo())) {
|
||||
$replyTo = MailUtility::getSystemReplyTo();
|
||||
if (!empty($replyTo)) {
|
||||
$address = key($replyTo);
|
||||
if ($address === 0) {
|
||||
$replyTo = new Address($replyTo[$address]);
|
||||
} else {
|
||||
$replyTo = new Address((string)$address, reset($replyTo));
|
||||
}
|
||||
$message->replyTo($replyTo);
|
||||
}
|
||||
}
|
||||
// Only set X-Mailer header once, if message is re-used
|
||||
if (!$message->getHeaders()->has('X-Mailer')) {
|
||||
$message->getHeaders()->addTextHeader('X-Mailer', $this->mailerHeader);
|
||||
}
|
||||
}
|
||||
|
||||
// After static enrichment took place, allow listeners to further manipulate message and envelope
|
||||
$event = new BeforeMailerSentMessageEvent($this, $message, $envelope);
|
||||
$this->eventDispatcher?->dispatch($event);
|
||||
|
||||
// Send message using the defined transport, with message and envelope from the event
|
||||
$this->sentMessage = $this->transport->send($event->getMessage(), $event->getEnvelope());
|
||||
|
||||
// Finally, allow further processing by listeners after the message has been sent
|
||||
$this->eventDispatcher?->dispatch(new AfterMailerSentMessageEvent($this));
|
||||
}
|
||||
|
||||
public function getSentMessage(): ?SentMessage
|
||||
{
|
||||
return $this->sentMessage;
|
||||
}
|
||||
|
||||
public function getTransport(): TransportInterface
|
||||
{
|
||||
return $this->transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a transport using the TYPO3_CONF_VARS configuration
|
||||
*
|
||||
* Used options:
|
||||
* $TYPO3_CONF_VARS['MAIL']['transport'] = 'smtp' | 'sendmail' | 'null' | 'mbox'
|
||||
*
|
||||
* $TYPO3_CONF_VARS['MAIL']['transport_smtp_server'] = 'smtp.example.org:25';
|
||||
* $TYPO3_CONF_VARS['MAIL']['transport_smtp_encrypt'] = FALSE; # requires openssl in PHP
|
||||
* $TYPO3_CONF_VARS['MAIL']['transport_smtp_username'] = 'username';
|
||||
* $TYPO3_CONF_VARS['MAIL']['transport_smtp_password'] = 'password';
|
||||
*
|
||||
* $TYPO3_CONF_VARS['MAIL']['transport_sendmail_command'] = '/usr/sbin/sendmail -bs'
|
||||
*
|
||||
* @throws CoreException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
private function initializeTransport()
|
||||
{
|
||||
$this->transport ??= $this->getTransportFactory()->get($this->mailSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is only used in unit tests
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function injectMailSettings(?array $mailSettings = null)
|
||||
{
|
||||
$this->mailSettings = $mailSettings ?? (array)$GLOBALS['TYPO3_CONF_VARS']['MAIL'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the real transport (not a spool).
|
||||
*/
|
||||
public function getRealTransport(): TransportInterface
|
||||
{
|
||||
$mailSettings = !empty($this->mailSettings) ? $this->mailSettings : (array)$GLOBALS['TYPO3_CONF_VARS']['MAIL'];
|
||||
unset($mailSettings['transport_spool_type']);
|
||||
return $this->getTransportFactory()->get($mailSettings);
|
||||
}
|
||||
|
||||
protected function getTransportFactory(): TransportFactory
|
||||
{
|
||||
return GeneralUtility::makeInstance(TransportFactory::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* Interface for mailers for sending emails. This should be used when injecting or creating an instance of the Mailer
|
||||
* class, so it can be easily overridden.
|
||||
*/
|
||||
interface MailerInterface extends \Symfony\Component\Mailer\MailerInterface
|
||||
{
|
||||
public function getSentMessage(): ?SentMessage;
|
||||
|
||||
public function getTransport(): TransportInterface;
|
||||
|
||||
public function getRealTransport(): TransportInterface;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\Locking\Exception\LockAcquireException;
|
||||
use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
|
||||
use TYPO3\CMS\Core\Locking\Exception\LockCreateException;
|
||||
use TYPO3\CMS\Core\Locking\LockFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Additional Mbox Transport option
|
||||
*
|
||||
* @internal This class is handled internally in TransportFactory
|
||||
*/
|
||||
class MboxTransport extends AbstractTransport
|
||||
{
|
||||
/**
|
||||
* Create a new MailTransport
|
||||
*
|
||||
* @param string $mboxFile The file into which to write mail.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $mboxFile,
|
||||
?EventDispatcherInterface $dispatcher = null,
|
||||
protected readonly ?LoggerInterface $logger = null,
|
||||
) {
|
||||
parent::__construct($dispatcher, $logger);
|
||||
$this->setMaxPerSecond(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the mail to a text file according to RFC 4155.
|
||||
*
|
||||
* @throws LockAcquireException
|
||||
* @throws LockAcquireWouldBlockException
|
||||
* @throws LockCreateException
|
||||
*/
|
||||
protected function doSend(SentMessage $message): void
|
||||
{
|
||||
// Add the complete mail inclusive headers
|
||||
$lockFactory = GeneralUtility::makeInstance(LockFactory::class);
|
||||
$lockObject = $lockFactory->createLocker('mbox');
|
||||
$lockObject->acquire();
|
||||
// Write the mbox file
|
||||
$file = @fopen($this->mboxFile, 'a');
|
||||
if (!$file) {
|
||||
$lockObject->release();
|
||||
throw new \RuntimeException(sprintf('Could not write to file "%s" when sending an email to debug transport', $this->mboxFile), 1291064151);
|
||||
}
|
||||
@fwrite($file, $message->toString());
|
||||
@fclose($file);
|
||||
GeneralUtility::fixPermissions($this->mboxFile);
|
||||
$lockObject->release();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->mboxFile;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\SentMessage;
|
||||
use Symfony\Component\Mailer\Transport\AbstractTransport;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Because TYPO3 doesn't offer a terminate signal or hook,
|
||||
* and taking in account the risk that extensions do some redirects or even exit,
|
||||
* we simply use the destructor of a singleton class which should be pretty much
|
||||
* at the end of a request.
|
||||
*
|
||||
* To have only one memory spool per request seems to be more appropriate anyway.
|
||||
*
|
||||
* @internal This class is handled internally in TransportFactory
|
||||
*/
|
||||
class MemorySpool extends AbstractTransport implements DelayedTransportInterface
|
||||
{
|
||||
use BlockSerializationTrait;
|
||||
|
||||
/**
|
||||
* @var SentMessage[]
|
||||
*/
|
||||
protected array $queuedMessages = [];
|
||||
|
||||
/**
|
||||
* Maximum number of retries when the real transport has failed.
|
||||
*/
|
||||
protected int $retries = 3;
|
||||
|
||||
/**
|
||||
* Create a new MemorySpool
|
||||
*/
|
||||
public function __construct(
|
||||
?EventDispatcherInterface $dispatcher = null,
|
||||
protected readonly ?LoggerInterface $logger = null
|
||||
) {
|
||||
parent::__construct($dispatcher, $logger);
|
||||
|
||||
$this->setMaxPerSecond(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends out the messages in the memory
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
// TODO: DI should be used to inject the MailerInterface
|
||||
$mailer = GeneralUtility::makeInstance(MailerInterface::class);
|
||||
try {
|
||||
$this->flushQueue($mailer->getRealTransport());
|
||||
} catch (\Throwable $exception) {
|
||||
if ($this->logger instanceof LoggerInterface) {
|
||||
$this->logger->error('An Exception occurred while flushing email queue: {message}', ['exception' => $exception, 'message' => $exception->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function flushQueue(TransportInterface $transport): int
|
||||
{
|
||||
if ($this->queuedMessages === []) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$retries = $this->retries;
|
||||
$message = null;
|
||||
$count = 0;
|
||||
while ($retries--) {
|
||||
try {
|
||||
while ($message = array_pop($this->queuedMessages)) {
|
||||
$transport->send($message->getMessage(), $message->getEnvelope());
|
||||
$count++;
|
||||
}
|
||||
} catch (TransportExceptionInterface $exception) {
|
||||
if ($retries) {
|
||||
// re-queue the message at the end of the queue to give a chance
|
||||
// to the other messages to be sent, in case the failure was due to
|
||||
// this message and not just the transport failing
|
||||
array_unshift($this->queuedMessages, $message);
|
||||
|
||||
// wait half a second before we try again
|
||||
usleep(500000);
|
||||
} else {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a message in the queue.
|
||||
*/
|
||||
protected function doSend(SentMessage $message): void
|
||||
{
|
||||
$this->queuedMessages[] = $message;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return 'MemorySpool';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Fluid\View\TemplatePaths;
|
||||
|
||||
/**
|
||||
* Factory for creating FluidEmail instances.
|
||||
*
|
||||
* Provides three creation methods:
|
||||
* - create(): For backend/CLI usage with global configuration only
|
||||
* - createFromRequest(): For frontend usage with site-aware template paths
|
||||
* - createWithOverrides(): For extensions needing custom template path overrides
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class TemplatedEmailFactory
|
||||
{
|
||||
/**
|
||||
* Create a FluidEmail instance with template paths resolved from site settings.
|
||||
*
|
||||
* Use this method for frontend contexts (e.g., form submissions, felogin)
|
||||
* where site-specific email templates should be applied.
|
||||
*
|
||||
* The factory extracts the site from the request attribute and merges
|
||||
* site-specific email settings with global mail configuration.
|
||||
*
|
||||
* Site settings used:
|
||||
* - email.templateRootPaths: array of template root paths
|
||||
* - email.layoutRootPaths: array of layout root paths
|
||||
* - email.partialRootPaths: array of partial root paths
|
||||
* - email.format: email format (html, plain, both)
|
||||
*/
|
||||
public function createFromRequest(ServerRequestInterface $request): FluidEmail
|
||||
{
|
||||
return $this->createWithOverrides([], [], [], $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a FluidEmail instance using global configuration only.
|
||||
*
|
||||
* Use this method for backend/CLI contexts (e.g., login notifications,
|
||||
* scheduler tasks, install tool) where no site context is available
|
||||
* or site-specific templates are not desired.
|
||||
*
|
||||
* Template paths are read from $GLOBALS['TYPO3_CONF_VARS']['MAIL'].
|
||||
*/
|
||||
public function create(?ServerRequestInterface $request = null): FluidEmail
|
||||
{
|
||||
$templatePaths = $this->buildTemplatePathsFromGlobals();
|
||||
|
||||
$fluidEmail = new FluidEmail($templatePaths);
|
||||
|
||||
if ($request !== null) {
|
||||
$fluidEmail->setRequest($request);
|
||||
}
|
||||
|
||||
return $fluidEmail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a FluidEmail instance with custom template path overrides.
|
||||
*
|
||||
* Use this method when extensions need to provide their own template paths
|
||||
* that are merged on top of the base configuration. The base configuration
|
||||
* is built from global $GLOBALS['TYPO3_CONF_VARS']['MAIL'] with site settings
|
||||
* merged on top when a request with a site attribute is provided.
|
||||
*
|
||||
* The override paths are then merged using array_replace(), so higher numeric
|
||||
* keys in overrides will take precedence, while existing numeric keys will
|
||||
* be overwritten.
|
||||
*
|
||||
* @param string[] $templateRootPaths Additional template root paths to merge
|
||||
* @param string[] $layoutRootPaths Additional layout root paths to merge
|
||||
* @param string[] $partialRootPaths Additional partial root paths to merge
|
||||
* @param ServerRequestInterface|null $request Optional request for site resolution and ViewHelper context
|
||||
*/
|
||||
public function createWithOverrides(
|
||||
array $templateRootPaths = [],
|
||||
array $layoutRootPaths = [],
|
||||
array $partialRootPaths = [],
|
||||
?ServerRequestInterface $request = null,
|
||||
): FluidEmail {
|
||||
$site = $request?->getAttribute('site');
|
||||
$templatePaths = $this->buildTemplatePathsWithSiteOverrides($site);
|
||||
|
||||
if ($templateRootPaths !== []) {
|
||||
$templatePaths->setTemplateRootPaths(
|
||||
array_replace($templatePaths->getTemplateRootPaths(), $templateRootPaths)
|
||||
);
|
||||
}
|
||||
if ($layoutRootPaths !== []) {
|
||||
$templatePaths->setLayoutRootPaths(
|
||||
array_replace($templatePaths->getLayoutRootPaths(), $layoutRootPaths)
|
||||
);
|
||||
}
|
||||
if ($partialRootPaths !== []) {
|
||||
$templatePaths->setPartialRootPaths(
|
||||
array_replace($templatePaths->getPartialRootPaths(), $partialRootPaths)
|
||||
);
|
||||
}
|
||||
|
||||
$fluidEmail = new FluidEmail($templatePaths);
|
||||
|
||||
if ($request !== null) {
|
||||
$fluidEmail->setRequest($request);
|
||||
}
|
||||
|
||||
if ($site instanceof Site) {
|
||||
$format = $site->getSettings()->get('email.format', '');
|
||||
if ($format !== '' && is_string($format)) {
|
||||
$fluidEmail->format($format);
|
||||
}
|
||||
}
|
||||
|
||||
return $fluidEmail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build template paths from global config with site settings merged on top.
|
||||
*
|
||||
* Site settings take precedence and are merged using array_replace()
|
||||
* to allow overriding specific numeric keys.
|
||||
*/
|
||||
private function buildTemplatePathsWithSiteOverrides(?object $site): TemplatePaths
|
||||
{
|
||||
$templatePaths = $this->buildTemplatePathsFromGlobals();
|
||||
|
||||
if ($site instanceof Site && !$site->getSettings()->isEmpty()) {
|
||||
$settings = $site->getSettings();
|
||||
|
||||
$siteTemplateRootPaths = $settings->get('email.templateRootPaths', []);
|
||||
if (is_array($siteTemplateRootPaths) && $siteTemplateRootPaths !== []) {
|
||||
$templatePaths->setTemplateRootPaths(
|
||||
$this->mergeYamlSiteSettingsArrayWithCurrent($templatePaths->getTemplateRootPaths(), $siteTemplateRootPaths)
|
||||
);
|
||||
}
|
||||
|
||||
$siteLayoutRootPaths = $settings->get('email.layoutRootPaths', []);
|
||||
if (is_array($siteLayoutRootPaths) && $siteLayoutRootPaths !== []) {
|
||||
$templatePaths->setLayoutRootPaths(
|
||||
$this->mergeYamlSiteSettingsArrayWithCurrent($templatePaths->getLayoutRootPaths(), $siteLayoutRootPaths)
|
||||
);
|
||||
}
|
||||
|
||||
$sitePartialRootPaths = $settings->get('email.partialRootPaths', []);
|
||||
if (is_array($sitePartialRootPaths) && $sitePartialRootPaths !== []) {
|
||||
$templatePaths->setPartialRootPaths(
|
||||
$this->mergeYamlSiteSettingsArrayWithCurrent($templatePaths->getPartialRootPaths(), $sitePartialRootPaths)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $templatePaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* When using the Site Settings GUI, the entered "stringlist" arrays have running numerical
|
||||
* indexes:
|
||||
*
|
||||
* email.partialRootPaths:
|
||||
* - 'EXT:my_extension/Resources/Private/Partials/Email'
|
||||
* - 'EXT:my_extension/Resources/Private/Partials/Email2'
|
||||
*
|
||||
* This would resolve to an array with the keys "0" and "1". This would override the
|
||||
* global template paths that already use "0" as the EXT:core base template paths.
|
||||
*
|
||||
* For a manually maintained settings.yaml, integrators however might use named indexes.
|
||||
* This method here allows to deal with both:
|
||||
*
|
||||
* - if the input is a sequential list (PHP `is_array_list`), all array keys are APPENDED to the array
|
||||
* - if the input has specific array keys (100, 200, ...) the array keys are REPLACED
|
||||
*/
|
||||
private function mergeYamlSiteSettingsArrayWithCurrent(array $currentArray, array $yamlArray): array
|
||||
{
|
||||
if (array_is_list($yamlArray)) {
|
||||
// array_merge() would replace numerical array keys, which we do not want.
|
||||
// Data must be stacked on top of the existing structure, with higher priority than globals.
|
||||
$nextKey = max(array_keys($currentArray)) + 1;
|
||||
$outputArray = $currentArray;
|
||||
foreach ($yamlArray as $value) {
|
||||
$outputArray[$nextKey++] = $value;
|
||||
}
|
||||
return $outputArray;
|
||||
}
|
||||
return array_replace($currentArray, $yamlArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build template paths from global mail configuration.
|
||||
*/
|
||||
private function buildTemplatePathsFromGlobals(): TemplatePaths
|
||||
{
|
||||
$globalConfig = $GLOBALS['TYPO3_CONF_VARS']['MAIL'] ?? [];
|
||||
|
||||
$templatePaths = new TemplatePaths();
|
||||
$templatePaths->setTemplateRootPaths($globalConfig['templateRootPaths'] ?? []);
|
||||
$templatePaths->setLayoutRootPaths($globalConfig['layoutRootPaths'] ?? []);
|
||||
$templatePaths->setPartialRootPaths($globalConfig['partialRootPaths'] ?? []);
|
||||
|
||||
return $templatePaths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Mail;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Transport;
|
||||
use Symfony\Component\Mailer\Transport\NullTransport;
|
||||
use Symfony\Component\Mailer\Transport\SendmailTransport;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
use TYPO3\CMS\Core\Log\LogManagerInterface;
|
||||
use TYPO3\CMS\Core\Resource\Security\FileNameValidator;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
readonly class TransportFactory
|
||||
{
|
||||
public const SPOOL_MEMORY = 'memory';
|
||||
public const SPOOL_FILE = 'file';
|
||||
|
||||
public function __construct(
|
||||
protected SymfonyEventDispatcherInterface $dispatcher,
|
||||
protected LogManagerInterface $logManager,
|
||||
private LoggerInterface $logger,
|
||||
private FileNameValidator $fileNameValidator,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array $mailSettings Typically from $GLOBALS['TYPO3_CONF_VARS']['MAIL']
|
||||
* @throws Exception
|
||||
*/
|
||||
public function get(array $mailSettings): TransportInterface
|
||||
{
|
||||
if (!isset($mailSettings['transport'])) {
|
||||
throw new \InvalidArgumentException('Key "transport" must be set in the mail settings', 1469363365);
|
||||
}
|
||||
if ($mailSettings['transport'] === 'spool') {
|
||||
throw new \InvalidArgumentException('Mail transport can not be set to "spool"', 1469363238);
|
||||
}
|
||||
|
||||
$transportType = !empty($mailSettings['transport_spool_type'])
|
||||
? 'spool' : (string)$mailSettings['transport'];
|
||||
|
||||
switch ($transportType) {
|
||||
case 'spool':
|
||||
$transport = $this->createSpool($mailSettings);
|
||||
break;
|
||||
case 'smtp':
|
||||
// Get settings to be used when constructing the transport object
|
||||
if (
|
||||
isset($mailSettings['transport_smtp_server'])
|
||||
&& strpos($mailSettings['transport_smtp_server'], ':') > 0
|
||||
) {
|
||||
$parts = GeneralUtility::trimExplode(':', $mailSettings['transport_smtp_server'], true);
|
||||
$host = $parts[0];
|
||||
$port = $parts[1] ?? null;
|
||||
} else {
|
||||
$host = (string)($mailSettings['transport_smtp_server'] ?? '');
|
||||
$port = null;
|
||||
}
|
||||
|
||||
if ($host === '') {
|
||||
throw new Exception('$GLOBALS[\'TYPO3_CONF_VARS\'][\'MAIL\'][\'transport_smtp_server\'] needs to be set when transport is set to "smtp".', 1291068606);
|
||||
}
|
||||
if ($port === null) {
|
||||
$port = 25;
|
||||
} else {
|
||||
$port = (int)$port;
|
||||
}
|
||||
$useEncryption = (bool)($mailSettings['transport_smtp_encrypt'] ?? false) ?: null;
|
||||
// Create transport
|
||||
$transport = new EsmtpTransport(
|
||||
$host,
|
||||
$port,
|
||||
$useEncryption,
|
||||
$this->dispatcher,
|
||||
$this->logManager->getLogger(EsmtpTransport::class)
|
||||
);
|
||||
$streamOptions = (array)($mailSettings['transport_smtp_stream_options'] ?? []);
|
||||
if (!empty($streamOptions)) {
|
||||
$stream = $transport->getStream();
|
||||
if (method_exists($stream, 'setStreamOptions') && method_exists($stream, 'getStreamOptions')) {
|
||||
$stream->setStreamOptions(array_merge_recursive($stream->getStreamOptions(), $streamOptions));
|
||||
}
|
||||
}
|
||||
// Need authentication?
|
||||
$username = (string)($mailSettings['transport_smtp_username'] ?? '');
|
||||
if ($username !== '') {
|
||||
$transport->setUsername($username);
|
||||
}
|
||||
$password = (string)($mailSettings['transport_smtp_password'] ?? '');
|
||||
if ($password !== '') {
|
||||
$transport->setPassword($password);
|
||||
}
|
||||
$mailDomain = (string)($mailSettings['transport_smtp_domain'] ?? '');
|
||||
if ($mailDomain !== '') {
|
||||
$transport->setLocalDomain($mailDomain);
|
||||
}
|
||||
$restartThreshold = (int)($mailSettings['transport_smtp_restart_threshold'] ?? 0);
|
||||
$restartThresholdSleep = (int)($mailSettings['transport_smtp_restart_threshold_sleep'] ?? 0);
|
||||
if ($restartThreshold > 0) {
|
||||
if ($restartThresholdSleep < 0) {
|
||||
// invalid, use default for threshold sleep
|
||||
$restartThresholdSleep = 0;
|
||||
}
|
||||
$transport->setRestartThreshold($restartThreshold, $restartThresholdSleep);
|
||||
}
|
||||
$pingThreshold = (int)($mailSettings['transport_smtp_ping_threshold'] ?? 0);
|
||||
if ($pingThreshold > 0) {
|
||||
$transport->setPingThreshold($pingThreshold);
|
||||
}
|
||||
break;
|
||||
case 'sendmail':
|
||||
$sendmailCommand = $mailSettings['transport_sendmail_command'] ?? @ini_get('sendmail_path');
|
||||
if (empty($sendmailCommand)) {
|
||||
$sendmailCommand = '/usr/sbin/sendmail -bs';
|
||||
$this->logger->warning('Mailer transport "sendmail" was chosen without a specific command, using "{command}"', ['command' => $sendmailCommand]);
|
||||
}
|
||||
// Create transport
|
||||
$transport = new SendmailTransport(
|
||||
$sendmailCommand,
|
||||
$this->dispatcher,
|
||||
$this->logManager->getLogger(SendmailTransport::class)
|
||||
);
|
||||
break;
|
||||
case 'mbox':
|
||||
$mboxFile = (string)($mailSettings['transport_mbox_file'] ?? '');
|
||||
if ($mboxFile === '') {
|
||||
throw new Exception('$GLOBALS[\'TYPO3_CONF_VARS\'][\'MAIL\'][\'transport_mbox_file\'] needs to be set when transport is set to "mbox".', 1294586645);
|
||||
}
|
||||
if (!$this->fileNameValidator->isValid($mboxFile)) {
|
||||
throw new Exception('$GLOBALS[\'TYPO3_CONF_VARS\'][\'MAIL\'][\'transport_mbox_file\'] failed against deny-pattern', 1705312431);
|
||||
}
|
||||
// Create our transport
|
||||
$transport = GeneralUtility::makeInstance(
|
||||
MboxTransport::class,
|
||||
$mboxFile,
|
||||
$this->dispatcher,
|
||||
$this->logManager->getLogger(MboxTransport::class)
|
||||
);
|
||||
break;
|
||||
// Used for testing purposes
|
||||
case 'null':
|
||||
case NullTransport::class:
|
||||
$transport = new NullTransport(
|
||||
$this->dispatcher,
|
||||
$this->logManager->getLogger(NullTransport::class)
|
||||
);
|
||||
break;
|
||||
// Used by Symfony's Transport Factory
|
||||
case !empty($mailSettings['dsn']):
|
||||
case 'dsn':
|
||||
if (empty($mailSettings['dsn'])) {
|
||||
throw new Exception('$GLOBALS[\'TYPO3_CONF_VARS\'][\'MAIL\'][\'dsn\'] needs to be set when transport is set to "dsn".', 1615021869);
|
||||
}
|
||||
$transport = Transport::fromDsn(
|
||||
$mailSettings['dsn'],
|
||||
$this->dispatcher,
|
||||
null,
|
||||
$this->logManager->getLogger(Transport::class)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Custom mail transport
|
||||
$transport = GeneralUtility::makeInstance($mailSettings['transport'], $mailSettings);
|
||||
if (!$transport instanceof TransportInterface) {
|
||||
throw new \RuntimeException($mailSettings['transport'] . ' is not an implementation of Symfony\Mailer\TransportInterface,
|
||||
but must implement that interface to be used as a mail transport.', 1323006478);
|
||||
}
|
||||
}
|
||||
|
||||
return $transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a spool from mail settings.
|
||||
*/
|
||||
protected function createSpool(array $mailSettings): DelayedTransportInterface
|
||||
{
|
||||
$transportSpoolType = (string)($mailSettings['transport_spool_type'] ?? '');
|
||||
switch ($transportSpoolType) {
|
||||
case self::SPOOL_FILE:
|
||||
$path = $mailSettings['transport_spool_filepath'] ?? '';
|
||||
if (!GeneralUtility::isAllowedAbsPath($path)) {
|
||||
$path = GeneralUtility::getFileAbsFileName($path);
|
||||
}
|
||||
if (empty($path)) {
|
||||
throw new \RuntimeException('The Spool Type filepath must be configured for TYPO3 in order to be used. Be sure that it\'s not accessible via the web.', 1518558797);
|
||||
}
|
||||
$spool = GeneralUtility::makeInstance(
|
||||
FileSpool::class,
|
||||
$path,
|
||||
$this->dispatcher,
|
||||
$this->logManager->getLogger(FileSpool::class)
|
||||
);
|
||||
break;
|
||||
case self::SPOOL_MEMORY:
|
||||
$spool = GeneralUtility::makeInstance(
|
||||
MemorySpool::class,
|
||||
$this->dispatcher,
|
||||
$this->logManager->getLogger(MemorySpool::class)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
$spool = GeneralUtility::makeInstance($transportSpoolType, $mailSettings);
|
||||
if (!$spool instanceof DelayedTransportInterface) {
|
||||
throw new \RuntimeException(
|
||||
$mailSettings['transport_spool_type'] . ' is not an implementation of DelayedTransportInterface, but must implement that interface to be used as a mail spool.',
|
||||
1466799482
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return $spool;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user