• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CPS-IT / mailqueue / 20021782204

08 Dec 2025 08:36AM UTC coverage: 16.692% (+16.7%) from 0.0%
20021782204

push

github

web-flow
Merge pull request #185 from CPS-IT/task/tests

109 of 653 relevant lines covered (16.69%)

0.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/Classes/Mail/Transport/QueueableFileTransport.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS extension "mailqueue".
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

18
namespace CPSIT\Typo3Mailqueue\Mail\Transport;
19

20
use CPSIT\Typo3Mailqueue\Enums;
21
use CPSIT\Typo3Mailqueue\Exception;
22
use CPSIT\Typo3Mailqueue\Iterator;
23
use CPSIT\Typo3Mailqueue\Mail;
24
use Psr\Log;
25
use Symfony\Component\Mailer;
26
use Symfony\Component\Mime;
27
use Symfony\Contracts\EventDispatcher;
28
use TYPO3\CMS\Core;
29

30
/**
31
 * QueueableFileTransport
32
 *
33
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
34
 * @license GPL-2.0-or-later
35
 */
36
final class QueueableFileTransport extends Core\Mail\FileSpool implements RecoverableTransport
37
{
38
    private const FILE_SUFFIX_QUEUED = '.message';
39
    private const FILE_SUFFIX_SENDING = '.message.sending';
40
    private const FILE_SUFFIX_FAILURE_DATA = '.message.failure';
41

42
    private readonly Core\Context\Context $context;
43

44
    public function __construct(
×
45
        string $path,
46
        ?EventDispatcher\EventDispatcherInterface $dispatcher = null,
47
        ?Log\LoggerInterface $logger = null,
48
    ) {
49
        parent::__construct($path, $dispatcher, $logger);
×
50
        $this->context = Core\Utility\GeneralUtility::makeInstance(Core\Context\Context::class);
×
51
    }
52

53
    /**
54
     * @throws Exception\SerializedMessageIsInvalid
55
     * @throws Mailer\Exception\TransportExceptionInterface
56
     */
57
    public function flushQueue(Mailer\Transport\TransportInterface $transport): int
×
58
    {
59
        $directoryIterator = new \DirectoryIterator($this->path);
×
60
        /** @var positive-int $execTime */
61
        $execTime = $this->context->getPropertyFromAspect('date', 'timestamp');
×
62
        $time = time();
×
63
        $count = 0;
×
64

65
        foreach ($directoryIterator as $file) {
×
66
            $path = (string)$file->getRealPath();
×
67

68
            if (!str_ends_with($path, self::FILE_SUFFIX_QUEUED)) {
×
69
                continue;
×
70
            }
71

72
            $item = $this->restoreItem($file);
×
73

74
            if ($this->dequeue($item, $transport)) {
×
75
                $count++;
×
76
            } else {
77
                // This message has just been caught by another process
78
                continue;
×
79
            }
80

81
            if ($this->getMessageLimit() && $count >= $this->getMessageLimit()) {
×
82
                break;
×
83
            }
84

85
            if ($this->getTimeLimit() && ($execTime - $time) >= $this->getTimeLimit()) {
×
86
                break;
×
87
            }
88
        }
89

90
        return $count;
×
91
    }
92

93
    public function enqueue(Mime\RawMessage $message, ?Mailer\Envelope $envelope = null): ?Mail\Queue\MailQueueItem
×
94
    {
95
        $sentMessage = $this->send($message, $envelope);
×
96

97
        // Early return if message was rejected
98
        if ($sentMessage === null) {
×
99
            return null;
×
100
        }
101

102
        // Look up mail in queue
103
        foreach ($this->getMailQueue() as $mailQueueItem) {
×
104
            // Loose comparison is intended
105
            if ($mailQueueItem->message == $sentMessage) {
×
106
                return $mailQueueItem;
×
107
            }
108
        }
109

110
        return null;
×
111
    }
112

113
    public function dequeue(Mail\Queue\MailQueueItem $item, Mailer\Transport\TransportInterface $transport): bool
×
114
    {
115
        $path = $this->path . DIRECTORY_SEPARATOR . $item->id;
×
116
        $sendingPath = $this->getFileVariant($path, self::FILE_SUFFIX_SENDING);
×
117
        $failurePath = $this->getFileVariant($path, self::FILE_SUFFIX_FAILURE_DATA);
×
118

119
        // We try a rename, it's an atomic operation, and avoid locking the file
120
        if ($path !== $sendingPath && !rename($path, $sendingPath)) {
×
121
            return false;
×
122
        }
123

124
        try {
125
            $transport->send($item->message->getMessage(), $item->message->getEnvelope());
×
126
        } catch (Mailer\Exception\TransportExceptionInterface $exception) {
×
127
            // Store failure metadata
128
            file_put_contents($failurePath, serialize(Mail\TransportFailure::fromException($exception)));
×
129

130
            throw $exception;
×
131
        }
132

133
        // Remove message from queue
134
        unlink($sendingPath);
×
135

136
        // Remove failure metadata
137
        if (file_exists($failurePath)) {
×
138
            unlink($failurePath);
×
139
        }
140

141
        return true;
×
142
    }
143

144
    public function delete(Mail\Queue\MailQueueItem $item): bool
×
145
    {
146
        $path = $this->path . DIRECTORY_SEPARATOR . $item->id;
×
147
        $failurePath = $this->getFileVariant($path, self::FILE_SUFFIX_FAILURE_DATA);
×
148

149
        // Early return if message no longer exists in queue
150
        if (!file_exists($path)) {
×
151
            return false;
×
152
        }
153

154
        // Remove failure metadata
155
        if (file_exists($failurePath)) {
×
156
            unlink($failurePath);
×
157
        }
158

159
        return unlink($path);
×
160
    }
161

162
    public function getMailQueue(): Mail\Queue\MailQueue
×
163
    {
164
        return new Mail\Queue\MailQueue(
×
165
            $this->initializeQueueFromFilePath(...),
×
166
        );
×
167
    }
168

169
    /**
170
     * @return \Generator<Mail\Queue\MailQueueItem>
171
     */
172
    private function initializeQueueFromFilePath(): \Generator
×
173
    {
174
        $iterator = new Iterator\LimitedFileIterator(
×
175
            new \DirectoryIterator($this->path),
×
176
            [
×
177
                self::FILE_SUFFIX_QUEUED,
×
178
                self::FILE_SUFFIX_SENDING,
×
179
            ],
×
180
        );
×
181

182
        foreach ($iterator as $file) {
×
183
            yield $this->restoreItem($file);
×
184
        }
185
    }
186

187
    /**
188
     * @throws Exception\SerializedMessageIsInvalid
189
     */
190
    private function restoreItem(\SplFileInfo $file): Mail\Queue\MailQueueItem
×
191
    {
192
        $path = (string)$file->getRealPath();
×
193
        $lastChanged = $file->getMTime();
×
194

195
        // Unserialize message
196
        $message = unserialize((string)file_get_contents($path), [
×
197
            'allowedClasses' => [
×
198
                Mime\RawMessage::class,
×
199
                Mime\Message::class,
×
200
                Mime\Email::class,
×
201
                Mailer\DelayedEnvelope::class,
×
202
                Mailer\Envelope::class,
×
203
            ],
×
204
        ]);
×
205

206
        if (!($message instanceof Mailer\SentMessage)) {
×
207
            throw new Exception\SerializedMessageIsInvalid($path);
×
208
        }
209

210
        // Define mail state
211
        if (str_ends_with($path, self::FILE_SUFFIX_SENDING)) {
×
212
            $state = Enums\MailState::Sending;
×
213
            $failure = $this->findFailureMetadata($path);
×
214
        } else {
215
            $state = Enums\MailState::Queued;
×
216
            $failure = null;
×
217
        }
218

219
        // Enforce failure if failure metadata were found
220
        if ($failure !== null) {
×
221
            $state = Enums\MailState::Failed;
×
222
        }
223

224
        // Add last modification date
225
        if ($lastChanged !== false) {
×
226
            $date = new \DateTimeImmutable('@' . $lastChanged, $this->getCurrentTimezone());
×
227
        } else {
228
            $date = null;
×
229
        }
230

231
        return new Mail\Queue\MailQueueItem($file->getFilename(), $message, $state, $date, $failure);
×
232
    }
233

234
    private function findFailureMetadata(string $file): ?Mail\TransportFailure
×
235
    {
236
        $failurePath = $this->getFileVariant($file, self::FILE_SUFFIX_FAILURE_DATA);
×
237

238
        try {
239
            return Mail\TransportFailure::fromFile($failurePath);
×
240
        } catch (Exception\FileDoesNotExist|Exception\SerializedFailureMetadataIsInvalid) {
×
241
            return null;
×
242
        }
243
    }
244

245
    private function getFileVariant(string $file, string $suffix): string
×
246
    {
247
        $variants = array_diff(
×
248
            [
×
249
                self::FILE_SUFFIX_FAILURE_DATA,
×
250
                self::FILE_SUFFIX_QUEUED,
×
251
                self::FILE_SUFFIX_SENDING,
×
252
            ],
×
253
            [$suffix],
×
254
        );
×
255

256
        foreach ($variants as $variant) {
×
257
            if (str_ends_with($file, $variant)) {
×
258
                return substr_replace($file, $suffix, -mb_strlen($variant));
×
259
            }
260
        }
261

262
        return $file;
×
263
    }
264

265
    private function getCurrentTimezone(): ?\DateTimeZone
×
266
    {
267
        $date = $this->context->getPropertyFromAspect('date', 'full');
×
268

269
        if (!($date instanceof \DateTimeInterface)) {
×
270
            return null;
×
271
        }
272

273
        return $date->getTimezone();
×
274
    }
275
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc