254 lines
8.0 KiB
PHP
254 lines
8.0 KiB
PHP
<?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;
|
|
}
|
|
}
|