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
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            $this->logger->error('An Exception occurred while flushing email queue: ' . $exception->getMessage());
90        }
91    }
92
93    /**
94     * @inheritdoc
95     */
96    public function flushQueue(TransportInterface $transport): int
97    {
98        if ($this->queuedMessages === []) {
99            return 0;
100        }
101
102        $retries = $this->retries;
103        $message = null;
104        $count = 0;
105        while ($retries--) {
106            try {
107                while ($message = array_pop($this->queuedMessages)) {
108                    $transport->send($message->getMessage(), $message->getEnvelope());
109                    $count++;
110                }
111            } catch (TransportExceptionInterface $exception) {
112                if ($retries && $message) {
113                    // re-queue the message at the end of the queue to give a chance
114                    // to the other messages to be sent, in case the failure was due to
115                    // this message and not just the transport failing
116                    array_unshift($this->queuedMessages, $message);
117
118                    // wait half a second before we try again
119                    usleep(500000);
120                } else {
121                    throw $exception;
122                }
123            }
124        }
125        return $count;
126    }
127
128    /**
129     * Stores a message in the queue.
130     * @param SentMessage $message
131     */
132    protected function doSend(SentMessage $message): void
133    {
134        $this->queuedMessages[] = $message;
135    }
136
137    public function __toString(): string
138    {
139        return 'MemorySpool';
140    }
141}
142