TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
@@ -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;
}
}
+253
View File
@@ -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;
}
}
+250
View File
@@ -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);
}
}
+255
View File
@@ -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);
}
}
}
+172
View File
@@ -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);
}
}
+34
View File
@@ -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;
}
+78
View File
@@ -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;
}
}
+125
View File
@@ -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';
}
}
+220
View File
@@ -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;
}
}
+232
View File
@@ -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;
}
}