1<?php
2
3declare(strict_types=1);
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18namespace TYPO3\CMS\Core\Mail;
19
20use Psr\Log\LoggerInterface;
21use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
22use Symfony\Component\Mailer\SentMessage;
23use Symfony\Component\Mailer\Transport\AbstractTransport;
24use Symfony\Component\Mailer\Transport\TransportInterface;
25use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
26use TYPO3\CMS\Core\Security\BlockSerializationTrait;
27use TYPO3\CMS\Core\SingletonInterface;
28use TYPO3\CMS\Core\Utility\GeneralUtility;
29
30/**
31 * Because TYPO3 doesn't offer a terminate signal or hook,
32 * and taking in account the risk that extensions do some redirects or even exit,
33 * we simply use the destructor of a singleton class which should be pretty much
34 * at the end of a request.
35 *
36 * To have only one memory spool per request seems to be more appropriate anyway.
37 *
38 * @internal This class is experimental and subject to change!
39 */
40class MemorySpool extends AbstractTransport implements SingletonInterface, DelayedTransportInterface
41{
42    use BlockSerializationTrait;
43
44    /**
45     * The logger instance.
46     *
47     * @var LoggerInterface|null
48     */
49    protected $logger;
50
51    /**
52     * @var SentMessage[]
53     */
54    protected $queuedMessages = [];
55
56    /**
57     * Maximum number of retries when the real transport has failed.
58     *
59     * @var int
60     */
61    protected $retries = 3;
62
63    /**
64     * Create a new MemorySpool
65     *
66     * @param EventDispatcherInterface $dispatcher
67     * @param LoggerInterface $logger
68     */
69    public function __construct(
70        EventDispatcherInterface $dispatcher = null,
71        LoggerInterface $logger = null
72    ) {
73        parent::__construct($dispatcher, $logger);
74
75        $this->logger = $logger;
76
77        $this->setMaxPerSecond(0);
78    }
79
80    /**
81     * Sends out the messages in the memory
82     */
83    public function __destruct()
84    {
85        $mailer = GeneralUtility::makeInstance(Mailer::class);
86        try {
87            $this->flushQueue($mailer->getRealTransport());
88        } catch (TransportExceptionInterface $exception) {
89            if ($this->logger instanceof LoggerInterface) {
90                $this->logger->error('An Exception occurred while flushing email queue: {message}', ['exception' => $exception, 'message' => $exception->getMessage()]);
91            }
92        }
93    }
94
95    /**
96     * @inheritdoc
97     */
98    public function flushQueue(TransportInterface $transport): int
99    {
100        if ($this->queuedMessages === []) {
101            return 0;
102        }
103
104        $retries = $this->retries;
105        $message = null;
106        $count = 0;
107        while ($retries--) {
108            try {
109                while ($message = array_pop($this->queuedMessages)) {
110                    $transport->send($message->getMessage(), $message->getEnvelope());
111                    $count++;
112                }
113            } catch (TransportExceptionInterface $exception) {
114                if ($retries && $message) {
115                    // re-queue the message at the end of the queue to give a chance
116                    // to the other messages to be sent, in case the failure was due to
117                    // this message and not just the transport failing
118                    array_unshift($this->queuedMessages, $message);
119
120                    // wait half a second before we try again
121                    usleep(500000);
122                } else {
123                    throw $exception;
124                }
125            }
126        }
127        return $count;
128    }
129
130    /**
131     * Stores a message in the queue.
132     * @param SentMessage $message
133     */
134    protected function doSend(SentMessage $message): void
135    {
136        $this->queuedMessages[] = $message;
137    }
138
139    public function __toString(): string
140    {
141        return 'MemorySpool';
142    }
143}
144