MailTemplateService.php 35.3 KB
Newer Older
1
<?php
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/***************************************************************
 *  Copyright notice
 *
 *  (c) sgalinski Internet Services (https://www.sgalinski.de)
 *
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/
25
namespace SGalinski\SgMail\Service;
26

27
use DateTime;
28
29
use SGalinski\SgMail\Domain\Model\Mail;
use SGalinski\SgMail\Domain\Model\Template;
Paul Ilea's avatar
Paul Ilea committed
30
use SGalinski\SgMail\Domain\Repository\LayoutRepository;
31
use SGalinski\SgMail\Domain\Repository\MailRepository;
32
use SGalinski\SgMail\Domain\Repository\TemplateRepository;
33
use Swift_Attachment;
34
use Symfony\Component\Mailer\Exception\TransportException;
35
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
36
use TYPO3\CMS\Core\Context\Context;
37
use TYPO3\CMS\Core\Mail\MailMessage;
38
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
39
use TYPO3\CMS\Core\Resource\File;
40
41
use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\FileReference as CoreFileReference;
42
use TYPO3\CMS\Core\Resource\ResourceFactory;
43
use TYPO3\CMS\Core\SingletonInterface;
44
45
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Site\SiteFinder;
46
use TYPO3\CMS\Core\Utility\GeneralUtility;
47
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
48
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
49
use TYPO3\CMS\Extbase\Object\Exception;
50
use TYPO3\CMS\Extbase\Object\ObjectManager;
51
52
use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException;
use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException;
53
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
54
use TYPO3\CMS\Fluid\View\StandaloneView;
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
use function array_key_exists;
use function array_merge;
use function array_merge_recursive;
use function array_reverse;
use function count;
use function file_exists;
use function file_get_contents;
use function filter_var;
use function is_array;
use function is_bool;
use function is_object;
use function is_string;
use function method_exists;
use function nl2br;
use function preg_replace;
use function str_replace;
use function trim;
72

73
74
75
/**
 * MailTemplateService
 */
76
class MailTemplateService implements SingletonInterface {
77
78
79
80
81
82
83
84
85
86
	public const MARKER_TYPE_STRING = 'String';
	public const MARKER_TYPE_ARRAY = 'Array';
	public const MARKER_TYPE_OBJECT = 'Object';
	public const MARKER_TYPE_FILE = 'File';
	public const DEFAULT_LANGUAGE = 'default';
	public const DEFAULT_TEMPLATE_PATH = 'Resources/Private/Templates/SgMail/';
	public const CACHE_NAME = 'sg_mail_registerArrayCache';
	public const CACHE_LIFETIME_IN_SECONDS = 86400;
	public const REGISTER_FILE = 'Register.php';
	public const CONFIG_PATH = 'Configuration/MailTemplates';
87

88
	/**
89
	 * @var array
90
	 */
91
	private static $templateObjectCache = [];
92
93
94
95
96

	/**
	 * @var array
	 */
	private static $mailObjectCache = [];
97

98
	/**
99
	 * @var TemplateRepository
100
101
	 */
	protected $templateRepository;
102

103
	/**
104
	 * @var LayoutRepository
105
106
	 */
	protected $layoutRepository;
107
108
109
110
111
112

	/**
	 * @var MailRepository
	 */
	protected $mailRepository;

113
	/**
114
	 * @var ObjectManager
115
	 * @deprecated Not used since TYPO3 10
116
117
	 */
	protected $objectManager;
118
119
120
121
122
123

	/**
	 * @var RegisterService
	 */
	protected $registerService;

124
	/**
125
	 * @var string $toAddresses
126
	 */
127
	private $toAddresses = '';
128

129
	/**
130
	 * @var string $fromAddress
131
	 */
132
	private $fromAddress = '';
133

134
	/**
135
	 * @var string $ccAddresses
136
	 */
137
	private $ccAddresses = '';
138

139
	/**
140
	 * @var string $replyToAddress
141
	 */
142
	private $replyToAddress = '';
143

144
	/**
145
	 * @var SiteLanguage
146
	 */
147
148
	private $siteLanguage;

149
	/**
150
	 * @var boolean $ignoreMailQueue
151
	 */
152
	private $ignoreMailQueue = TRUE;
153

154
	/**
155
	 * @var string $templateName
156
157
	 */
	private $templateName;
158

159
160
161
162
	/**
	 * @var string $subject
	 */
	private $subject;
163

164
165
166
167
	/**
	 * @var string $overwrittenEmailBody
	 */
	private $overwrittenEmailBody = '';
168

169
	/**
170
	 * @var string $extensionKey
171
172
	 */
	private $extensionKey;
173

174
	/**
175
	 * @var array $markers
176
	 */
Torsten Oppermann's avatar
Torsten Oppermann committed
177
	private $markers;
178

179
180
181
182
	/**
	 * @var array $markerLabels
	 */
	private $markerLabels;
183

184
	/**
185
	 * @var string $bccAddresses
186
	 */
187
	private $bccAddresses = '';
188

189
190
191
192
	/**
	 * @var int
	 */
	private $priority = Mail::PRIORITY_LOWEST;
193

194
195
196
197
	/**
	 * @var int
	 */
	private $pid;
198

199
200
201
	/**
	 * @var string
	 */
202
	private $fromName = '';
203

204
	/**
205
206
	 * @var string
	 */
207
	private $mailBodyToSend = '';
208
209
210
211

	/**
	 * @var string
	 */
212
	private $subjectToSend;
213

214
215
	/**
	 * MailTemplateService constructor.
Paul Ilea's avatar
Paul Ilea committed
216
	 *
217
218
	 * @param string $templateName
	 * @param string $extensionKey
219
	 * @param array $markers
220
	 * @param array $markerLabels
221
	 * @throws Exception
222
	 */
223
	public function __construct(string $templateName = '', string $extensionKey = '', array $markers = [], array $markerLabels = []) {
224
225
226
		$this->templateName = $templateName;
		$this->extensionKey = $extensionKey;
		$this->markers = $markers;
227
		$this->markerLabels = $markerLabels;
228

229
		$this->registerService = GeneralUtility::makeInstance(RegisterService::class);
230
231
232
233
234
235
236
		if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '10.4.0')) {
			$this->objectManager = GeneralUtility::makeInstance(ObjectManager::class);
			/** @var TypoScriptSettingsService $typoScriptSettingsService */
			$typoScriptSettingsService = $this->objectManager->get(TypoScriptSettingsService::class);
		} else {
			$typoScriptSettingsService = GeneralUtility::makeInstance(TypoScriptSettingsService::class);
		}
237

238
		$tsSettings = $typoScriptSettingsService->getSettings(0, 'tx_sgmail');
239
		// use defaultMailFromAddress if it is provided in LocalConfiguration.php; use the sg_mail TS setting as fallback
240
		if (filter_var($GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'], FILTER_VALIDATE_EMAIL)) {
241
			$this->fromAddress = $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'];
242
243
		} else if (!filter_var($tsSettings['mail']['default']['from'], FILTER_VALIDATE_EMAIL)) {
			$this->fromAddress = 'noreply@example.org';
244
		} else {
245
			$this->fromAddress = $tsSettings['mail']['default']['from'];
246
247
		}

248
249
250
251
		if ($GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName']) {
			$this->fromName = $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName'];
		}

252
253
254
		$bccAddresses = GeneralUtility::trimExplode(',', $tsSettings['mail']['default']['bcc']);
		$ccAddresses = GeneralUtility::trimExplode(',', $tsSettings['mail']['default']['cc']);
		foreach ($bccAddresses as $index => $email) {
255
			if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
256
				unset($bccAddresses[$index]);
257
258
259
			}
		}

260
		foreach ($ccAddresses as $index => $email) {
261
			if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
262
				unset($ccAddresses[$index]);
263
264
			}
		}
265
266
267

		$this->bccAddresses = implode(',', $bccAddresses);
		$this->ccAddresses = implode(',', $ccAddresses);
268
269
270
	}

	/**
271
272
273
274
275
276
	 * Return default markers for sg_mail
	 *
	 * @param string $translationKey
	 * @param array $marker
	 * @param string $extensionKey
	 * @return array
277
	 */
278
	public static function getDefaultTemplateMarker(string $translationKey, array $marker, $extensionKey = 'sg_mail'): array {
279
280
		$languagePath = 'LLL:EXT:' . $extensionKey . '/Resources/Private/Language/locallang.xlf:' . $translationKey;
		// Need the key for translations
281
		if (trim($extensionKey) === '') {
282
			return [];
283
284
		}

285
286
287
288
289
290
291
292
293
		$generatedMarker = [];
		foreach ($marker as $markerName) {
			$generatedMarker[] = [
				'marker' => $markerName,
				'value' => $languagePath . '.example.' . $markerName,
				'description' => $languagePath . '.description.' . $markerName,
				'backend_translation_key' => $translationKey . '.example.' . $markerName,
				'extension_key' => $extensionKey
			];
294
295
		}

296
		return $generatedMarker;
297
298
299
	}

	/**
300
301
	 * Adds a file resource as attachment
	 *
302
303
	 * @param FileReference|null $fileReference
	 * @param File|null $file
304
305
306
	 *
	 * @return MailTemplateService
	 */
307
	public function addFileResourceAttachment(
308
		FileReference $fileReference = NULL, FileInterface $file = NULL
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
	): MailTemplateService {
		if (!$file) {
			if (!$fileReference) {
				return $this;
			}

			$originalResource = $fileReference->getOriginalResource();
			if (!$originalResource) {
				return $this;
			}

			$file = $originalResource->getOriginalFile();
			if (!$file) {
				return $this;
			}
324
325
		}

326
327
		$coreFileReferenceMailFile = GeneralUtility::makeInstance(ResourceFactory::class)
			->createFileReferenceObject(
328
			[
329
				'uid_local' => $file->getUid(),
330
331
332
333
334
335
336
337
338
				'table_local' => 'sys_file',
				'uid' => uniqid('NEW_MAIL', TRUE)
			]
		);

		$newFileReference = GeneralUtility::makeInstance(FileReference::class);
		$newFileReference->setOriginalResource($coreFileReferenceMailFile);

		$this->markers[] = $newFileReference;
339
340
341
		return $this;
	}

342
343
344
	/**
	 * Set preview markers for the template editor
	 *
345
	 * @throws NoSuchCacheException
346
	 */
347
348
	public function setPreviewMarkers(): void {
		$previewMarkers = [];
349
350

		/** @var array $markerArray */
351
		$markerArray = $this->registerService->getRegisterArray()[$this->extensionKey][$this->templateName]['marker'];
352
353
354
		foreach ($markerArray as $marker) {
			$markerPath = GeneralUtility::trimExplode('.', $marker['marker']);
			$temporaryMarkerArray = [];
355
			foreach (array_reverse($markerPath) as $index => $markerPathSegment) {
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
				if ($index === 0) {
					if ($marker['markerLabel']) {
						$markerPathSegment = $marker['markerLabel'];
					}

					if ($marker['backend_translation_key']) {
						$temporaryMarkerArray[$markerPathSegment] = LocalizationUtility::translate(
							$marker['backend_translation_key'], $marker['extension_key']
						);
					} else {
						$temporaryMarkerArray[$markerPathSegment] = $marker['value'];
					}
				} else {
					$temporaryMarkerArray = [$markerPathSegment => $temporaryMarkerArray];
				}
			}
372
373

			$previewMarkers[] = $temporaryMarkerArray;
374
		}
375
376

		$this->setMarkers(array_merge_recursive(...$previewMarkers));
377
378
	}

379
	/**
380
	 * Send the Email
381
	 *
382
	 * @param bool $isPreview
383
	 * @param bool $isNewsletter
384
	 * @return bool email was sent or added to mail queue successfully?
385
386
387
388
389
390
	 * @throws NoSuchCacheException
	 * @throws ResourceDoesNotExistException
	 * @throws IllegalObjectTypeException
	 * @throws UnknownObjectException
	 */
	public function sendEmail(bool $isPreview = FALSE, bool $isNewsletter = FALSE): bool {
391
		try {
392
			$template = $this->getTemplate();
393
394
395
396
397
398
399
400
401
402
		} catch (\Exception $e) {
			return FALSE;
		}

		// Load the values from the template if they are set
		if (!$isNewsletter && $template !== NULL) {
			$this->loadTemplateValues($template);
		}

		// get default template content from register array
403
		$defaultTemplateContent = $this->getDefaultTemplateContent($template);
404

405
		$this->parseValuesForMail($template, $defaultTemplateContent);
406

407
		$mail = $this->addMailToMailQueue();
408
409
		self::$mailObjectCache[$mail->getUid()] = $mail; // add it to cache to avoid extra DB queries
		if ($this->ignoreMailQueue) {
410
			return $this->sendMailFromQueue($mail->getUid());
411
412
		}

413
		return TRUE;
414
415
	}

416
417
418
	/**
	 * Get the template object
	 *
419
	 * @return null|Template
420
421
	 * @throws \Exception
	 */
422
423
424
425
426
427
	private function getTemplate(): ?Template {
		$isTemplateBlacklisted = $this->registerService->isTemplateBlacklisted(
			$this->extensionKey,
			$this->templateName,
			$this->pid
		);
428
		if ($isTemplateBlacklisted) {
429
			throw new \Exception('The template is blacklisted');
430
431
		}

432
433
434
435
436
		$templateHash = $this->getTemplateHash();
		if (
			isset(self::$templateObjectCache[$templateHash])
			&& self::$templateObjectCache[$templateHash] instanceof Template
		) {
437
438
			return self::$templateObjectCache[$templateHash];
		}
439

440
		/** @var Template $template */
441
442
443
		$template = $this->getTemplateRepository()->findByTemplateProperties(
			$this->extensionKey, $this->templateName, [$this->siteLanguage], $this->pid
		)->getFirst();
444

445
		if ($template === NULL && !empty($this->siteLanguage->getFallbackLanguageIds())) {
446
447
448
449
450
451
452
453
454
			$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($this->pid);
			$fallbackLanguages = [];
			foreach ($this->siteLanguage->getFallbackLanguageIds() as $fallbackLanguageId) {
				$fallbackLanguages[] = $site->getLanguageById($fallbackLanguageId);
			}

			$template = $this->getTemplateRepository()->findByTemplateProperties(
				$this->extensionKey, $this->templateName, $fallbackLanguages, $this->pid
			)->getFirst();
455
456
457
458
459
460
		} elseif($template === NULL) {
			$templateArray = $this->registerService->findTemplate(
				$this->extensionKey,
				$this->templateName,
				$this->siteLanguage
			);
461
			$templateArray['pid'] = $this->pid;
462
			$template = $this->getTemplateRepository()->create($templateArray);
463
		}
464

465
		self::$templateObjectCache[$templateHash] = $template;
466
467
		return $template;
	}
468

469
470
471
472
473
	/**
	 * Get the hash for the object cache
	 *
	 * @return string
	 */
474
475
476
477
478
479
480
	private function getTemplateHash(): string {
		return md5(
			$this->extensionKey . '_' .
			$this->templateName . '_' .
			$this->pid . '_' .
			$this->siteLanguage->getLanguageId()
		);
481
482
483
484
485
486
487
	}

	/**
	 * use all values from the given template
	 *
	 * @param Template $template
	 */
488
489
	public function loadTemplateValues(Template $template): void {
		$this->pid = $template->getPid();
490
491
		$this->extensionKey = $template->getExtensionKey();
		$this->templateName = $template->getTemplateName();
492
		$this->siteLanguage = $template->getSiteLanguage();
493
494

		$fromName = trim($template->getFromName());
495
496
497
		if ($fromName === '') {
			$fromName = $this->fromName;
		}
498

499
500
501
502
		if ($fromName === '' && $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName']) {
			$fromName = $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName'];
		}

503
		$fromMail = $this->getValidFromMail(trim($template->getFromMail()));
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
		// The setters check if the value is empty or not
		$this->setFromAddress($fromMail, $fromName);
		$this->setCcAddresses($template->getCc());
		$this->setBccAddresses($template->getBcc());
		$this->setReplyToAddress($template->getReplyTo());
		$this->setFromName($fromName);
	}

	/**
	 * Sets the fromMail property of the mailTemplateService.
	 * Checks validity and uses all available fallbacks
	 *
	 * @param string $fromMail
	 * @return string
	 */
519
520
521
	private function getValidFromMail(string $fromMail): string {
		$fromMail = trim($fromMail);
		if (!filter_var($fromMail, FILTER_VALIDATE_EMAIL)) {
522
523
			$fromMail = $this->fromAddress;
		}
524
525

		if (!filter_var($fromMail, FILTER_VALIDATE_EMAIL)) {
526
			$fromMail = $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'];
527
			if (!filter_var($GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'], FILTER_VALIDATE_EMAIL)) {
528
529
530
531
532
533
534
535
536
				$fromMail = 'noreply@example.com';
			}
		}

		return $fromMail;
	}



537
538
539
	/**
	 * Get the default content for this template
	 *
540
	 * @param Template|null $template
541
542
543
	 * @return bool|string
	 * @throws Exception
	 * @throws NoSuchCacheException
544
	 */
545
	protected function getDefaultTemplateContent(Template $template = NULL) {
546
		$defaultTemplateContent =
547
			$this->registerService->getRegisterArray()[$this->extensionKey][$this->templateName]['templateContent'];
548

549
		// If there is no template for this language, use the default template
550
		if ($template === NULL && $defaultTemplateContent === NULL) {
551
			$templatePath =
552
				$this->registerService->getRegisterArray()[$this->extensionKey][$this->templateName]['templatePath'];
553
554
555

			// only standard template file is considered since version 4.1
			$defaultTemplateFile = $templatePath . 'template.html';
556
557
			if (file_exists($defaultTemplateFile)) {
				$defaultTemplateContent = file_get_contents($defaultTemplateFile);
558
559
			} else {
				// use configured default html template
560
561
562
563
564
565
566
				if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '10.4.0', '<')) {
					/** @var TypoScriptSettingsService $typoScriptSettingsService */
					$typoScriptSettingsService = $this->objectManager->get(TypoScriptSettingsService::class);
				} else {
					$typoScriptSettingsService = GeneralUtility::makeInstance(TypoScriptSettingsService::class);
				}

567
568
569
570
				$tsSettings = $typoScriptSettingsService->getSettings(0, 'tx_sgmail');
				$defaultTemplateFile = GeneralUtility::getFileAbsFileName(
					$tsSettings['mail']['defaultHtmlTemplate']
				);
571

572
573
				if (file_exists($defaultTemplateFile)) {
					$defaultTemplateContent = file_get_contents($defaultTemplateFile);
574
				} else {
575
					return FALSE;
576
				}
577
			}
578
579
580
581
		} elseif ($template !== NULL) {
			$defaultTemplateContent = $template->getContent();
		} else {
			$defaultTemplateContent = '';
Torsten Oppermann's avatar
Torsten Oppermann committed
582
		}
583

584
585
		return $defaultTemplateContent;
	}
Torsten Oppermann's avatar
Torsten Oppermann committed
586

587
588
589
590
	/**
	 * @param string $toAddresses
	 * @return MailTemplateService
	 */
591
	public function setToAddresses(string $toAddresses): MailTemplateService {
592
593
594
595
596
		$normalizedToAddresses = trim(preg_replace('~\x{00a0}~iu', ' ', $toAddresses));
		$this->toAddresses = $normalizedToAddresses;
		return $this;
	}

597
	/**
598
599
600
	 * Sets the values to send the mail with from the template or register service
	 *
	 * @param Template|null $template
601
	 * @param string $defaultTemplateContent
602
603
	 * @throws Exception
	 * @throws NoSuchCacheException
604
	 */
605
	protected function parseValuesForMail(
606
607
		Template $template = NULL, string $defaultTemplateContent = ''
	): void {
608
609
610
611
612
613
614
		if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '10.4.0', '<')) {
			/** @var StandaloneView $emailView */
			$emailView = $this->objectManager->get(StandaloneView::class);
		} else {
			$emailView = GeneralUtility::makeInstance(StandaloneView::class);
		}

615
		$emailView->assignMultiple($this->markers);
Torsten Oppermann's avatar
Torsten Oppermann committed
616
		$emailView->assign('all_fields', $this->getAllMarker($this->markers));
617
		$emailView->assign('all_fields_html', $this->getAllMarkerHTML($this->markers));
618

619
		//TODO: make this as the lines below the next block
620
621
622
623
624
625
		$overwrittenEmailBody = $this->getOverwrittenEmailBody();
		$overwrittenSubject = '';
		if ($this->subject !== '' && $this->subject !== NULL) {
			$overwrittenSubject = $this->subject;
		}

626
		// parse markers
Paul Ilea's avatar
Paul Ilea committed
627
		if ($template !== NULL) {
628
629
630
			$subject = $this->parseMarkers(
				trim(empty($overwrittenSubject) ? $template->getSubject() : $overwrittenSubject),
				$emailView
631
			);
632
633
			$layoutId = $template->getLayout();
			$templateContent = $template->getContent();
634
			$this->setSubjectToSend($subject);
635
		} else {
636
			$subject = $this->registerService->getRegisterArray()[$this->extensionKey][$this->templateName]['subject'];
637
638
			if (is_array($subject)) {
				$subject = trim(
639
					$this->registerService->getRegisterArray()
640
					[$this->extensionKey][$this->templateName]['subject'][$this->siteLanguage->getTypo3Language()]
641
				);
642
			}
643

644
645
646
647
			$subject = $this->parseMarkers(
				(empty($overwrittenSubject) ? $subject : $overwrittenSubject),
				$emailView
			);
648

649
650
651
			$layoutId = 0;
			$templateContent = $defaultTemplateContent;
		}
652

653
		$this->setSubjectToSend($subject);
654

655
656
		// Parse the markers
		if ($this->fromName) {
657
			$this->setFromName($this->parseMarkers($this->fromName, $emailView));
658
659
		}

660
		if ($this->fromAddress) {
661
			$this->setFromAddress($this->parseMarkers($this->fromAddress, $emailView));
662
663
		}

664
		if ($this->replyToAddress) {
665
			$this->setReplyToAddress($this->parseMarkers($this->replyToAddress, $emailView));
666
667
		}

668
669
		if ($this->ccAddresses) {
			$this->setCcAddresses($this->parseMarkers($this->ccAddresses, $emailView));
670
671
		}

672
		if ($this->bccAddresses) {
673
			$this->setBccAddresses($this->parseMarkers($this->bccAddresses, $emailView));
674
675
		}

676
		if ($this->toAddresses) {
677
			$this->setToAddresses($this->parseMarkers($this->toAddresses, $emailView));
678
679
680
681
		}

		// reset template source back to default
		$emailView->setTemplateSource(
682
			empty($overwrittenEmailBody) ? $templateContent : $overwrittenEmailBody
683
		);
684

685
		// insert <br> tags, but replace every instance of three or more successive breaks with just two.
686
		$emailBody = $emailView->render();
687
688
		$emailBody = nl2br($emailBody);
		$emailBody = preg_replace('/(<br[\s]?[\/]?>[\s]*){3,}/', '<br><br>', $emailBody);
689

690
691
692
693
694
		$layout = $this->getLayoutRepository()->findByUidOrDefault(
			$layoutId,
			$this->pid,
			$this->siteLanguage->getLanguageId()
		);
695
696
697
		$emailHTMLHead = '';
		if ($layout) {
			$emailHTMLHead = $layout->getHeadContent();
698
			$emailBody = str_replace('###CONTENT###', $emailBody, $layout->getContent());
699
		}
700

701
		$this->mailBodyToSend = '<html><head>' . $emailHTMLHead . '</head><body>' . $emailBody . '</body></html>';
702
703
704
	}

	/**
705
	 * Get a single variable containing a list of all markers
706
	 *
707
708
	 * @param array $markers
	 * @return string
709
	 */
710
711
712
	private function getAllMarker(array $markers): string {
		$allMarker = '';
		foreach ($markers as $key => $value) {
713
			if (array_key_exists($key, $this->markerLabels) && $this->markerLabels[$key] !== NULL) {
714
715
				$key = $this->markerLabels[$key];
			}
716

717
			if (is_string($value)) {
718
				$allMarker .= $key . ': ' . $value . PHP_EOL;
719
			} elseif (is_array($value)) {
720
721
722
				foreach ($value as $innerKey => $innerValue) {
					$allMarker .= $key . '.' . $innerKey . ': ' . $innerValue . PHP_EOL;
				}
723
			} elseif (is_bool($value)) {
724
725
				$valueAsString = $value ? 'true' : 'false';
				$allMarker .= $key . ': ' . $valueAsString . PHP_EOL;
726
727
			} elseif (is_object($value)) {
				if (method_exists($value, '__toString')) {
728
729
730
					$allMarker .= $key . ': ' . $value->__toString() . PHP_EOL;
				}
			}
731
732
		}

733
734
		return $allMarker;
	}
735

736
737
738
739
740
741
742
743
744
745
	/**
	 * Get a single variable containing a list of all markers in table markup
	 *
	 * @param array $markers
	 * @return string
	 */
	private function getAllMarkerHTML(array $markers): string {
		$allMarker = '<table>';
		$allMarker .= '<style> table { text-align: left; } table tr th, table tr td { border-bottom: 1px solid rgba(0,0,0,0.2); padding: 2px 6px 4px;} </style>';
		foreach ($markers as $key => $value) {
746
			if (array_key_exists($key, $this->markerLabels) && $this->markerLabels[$key] !== NULL) {
747
748
749
				$key = $this->markerLabels[$key];
			}

750
			if (is_string($value)) {
751
				$allMarker .= '<tr><th>' . $key . ' </th><td> ' . $value . '</td></tr>';
752
			} elseif (is_array($value)) {
753
				foreach ($value as $innerKey => $innerValue) {
754
					$allMarker .= '<tr><th>' . $key . '.' . $innerKey . ' </th><td> ' . $innerValue . '</td></tr>';
755
				}
756
			} elseif (is_bool($value)) {
757
				$valueAsString = $value ? 'true' : 'false';
758
				$allMarker .= '<tr><th>' . $key . ' </th><td> ' . $valueAsString . '</td></tr>';
759
760
			} elseif (is_object($value)) {
				if (method_exists($value, '__toString')) {
761
					$allMarker .= '<tr><th>' . $key . ' </th><td> ' . $value->__toString() . '</td></tr>';
762
763
764
765
				}
			}
		}

766
		return $allMarker . '</table>';
767
768
	}

769
	/**
770
771
772
773
774
	 * Parses markers in an email View.
	 * !!! CHANGES THE SOURCE PATH AND IT SHOULD BE RESET BACK TO THE ORIGINAL!!!
	 *
	 * @param string $text
	 * @param StandaloneView $emailView
775
776
	 * @return string
	 */
777
	protected function parseMarkers(string $text, StandaloneView $emailView): string {
778
779
780
		if (strpos($text, '{') !== FALSE) {
			$emailView->setTemplateSource($text);
			return $emailView->render();
781
		}
782

783
		return $text;
784
785
786
	}

	/**
787
	 * Adds a new mail to the mail queue.
788
	 *
789
	 * @return Mail
790
791
792
793
	 * @throws Exception
	 * @throws IllegalObjectTypeException
	 * @throws NoSuchCacheException
	 */
794
	public function addMailToMailQueue(): Mail {
795
796
797
798
		$mail = GeneralUtility::makeInstance(Mail::class);
		$mail->setPid($this->pid);
		$mail->setExtensionKey($this->extensionKey);
		$mail->setTemplateName($this->templateName);
799
		$mail->setSiteLanguage($this->siteLanguage);
800
801
802
803
804
805
806
		$mail->setBlacklisted(
			$this->registerService->isTemplateBlacklisted(
				$this->extensionKey,
				$this->templateName,
				$this->pid
			)
		);
807
		$mail->setFromAddress($this->fromAddress);
808
		$mail->setFromName($this->fromName);
809
		$mail->setToAddress($this->toAddresses);
810
811
812
		$mail->setMailSubject($this->getSubjectToSend());
		$mail->setMailBody($this->getMailBodyToSend());
		$mail->setPriority($this->priority);
813
814
		$mail->setBccAddresses($this->bccAddresses);
		$mail->setCcAddresses($this->ccAddresses);
815
816
		$mail->setSendingTime(0);
		$mail->setLastSendingTime(0);
817
		$mail->setReplyTo($this->replyToAddress);
818
819
		foreach ($this->markers as $marker) {
			if ($marker instanceof FileReference) {
820
821
822
				// we need to create proper copies of the attachment so that the original file reference does not get
				// moved over to the mail model and worst case, the original model loses the reference because of this
				$originalResource = $marker->getOriginalResource();
823
824
825
				if ($originalResource instanceof CoreFileReference) {
					$coreFileReferenceMailFile = GeneralUtility::makeInstance(ResourceFactory::class)
						->createFileReferenceObject(
826
827
828
829
830
831
						[
							'uid_local' => $originalResource->getOriginalFile()->getUid(),
							'table_local' => 'sys_file',
							'uid' => uniqid('NEW_MAIL', TRUE)
						]
					);
832
					$newFileReference = GeneralUtility::makeInstance(FileReference::class);
833
834
835
					$newFileReference->setOriginalResource($coreFileReferenceMailFile);
					$mail->addAttachment($newFileReference);
				}
836
837
			}
		}
838

839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
		$mailRepository = $this->getMailRepository();
		$mailRepository->add($mail);
		$mailRepository->persist();
		return $mail;
	}

	/**
	 * Send a Mail from the queue, identified by its id
	 *
	 * @param int $uid
	 * @return bool
	 * @throws Exception
	 */
	public function sendMailFromQueue(int $uid): bool {
		if (!isset(self::$mailObjectCache[$uid]) || !self::$mailObjectCache[$uid] instanceof Mail) {
			$mailToSend = $this->getMailObjectByUid($uid);
			if ($mailToSend === FALSE) {
				return FALSE;
			}
		} else {
			$mailToSend = self::$mailObjectCache[$uid];
		}

		$this->sendMailsFromQueue([$mailToSend]);
		$success = FALSE;
		if ($mailToSend->getStatus() === Mail::STATUS_SENT) {
			$success = TRUE;
		}

		unset(self::$mailObjectCache[$uid]); // free the memory
		return $success;
	}

	/**
	 * Get the mail object by uid and check if it's blacklisted
	 *
	 * @param int $uid
	 * @return bool|object
	 * @throws Exception
	 */
	protected function getMailObjectByUid(int $uid): ?Mail {
880
881
882
883
884
885
886
		if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '10.4.0', '<')) {
			/** @var MailRepository $mailRepository */
			$mailRepository = $this->objectManager->get(MailRepository::class);
		} else {
			$mailRepository = GeneralUtility::makeInstance(MailRepository::class);
		}

887
888
889
890
891
892
		$mailObject = $mailRepository->findOneByUid($uid);
		if (!$mailObject || $mailObject->getBlacklisted()) {
			return NULL;
		}

		return $mailObject;
893
	}
894

895
	/**
896
897
898
899
900
	 * Add html and plain body text to the mail message object
	 *
	 * @param MailMessage $mailMessage
	 * @param string $htmlBody
	 * @param string $plainBody
901
	 */
902
903
904
905
906
907
908
909
	protected static function addBodyToMailMessage(MailMessage $mailMessage, string $htmlBody = '', string $plainBody = ''): void {
		if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '10.4.0', '<')) {
			$mailMessage->setBody($htmlBody, 'text/html');
			$mailMessage->addPart($plainBody, 'text/plain');
		} else {
			$mailMessage->html($htmlBody);
			$mailMessage->text($plainBody);
		}
910
911
912
	}

	/**
913
914
915
	 * Attach a file
	 * @param MailMessage $mailMessage
	 * @param FileInterface $file
916
	 */
917
918
919
920
921
922
923
924
925
926
	protected static function attachToMailMessage(MailMessage $mailMessage, FileInterface $file): void {
		if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '10.4.0', '<')) {
			$mailMessage->attach(
				Swift_Attachment::newInstance($file->getContents(), $file->getName(), $file->getMimeType())
			);
		} else {
			$mailMessage->attach(
				$file->getContents(), $file->getName(), $file->getMimeType()
			);
		}
927
928
929
	}

	/**
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
	 *
	 * @param Mail[] $mails
	 * @throws Exception
	 * @throws IllegalObjectTypeException
	 * @throws UnknownObjectException
	 */
	public function sendMailsFromQueue(array $mails): void {
		foreach ($mails as $mail) {
			$mailMessage = GeneralUtility::makeInstance(MailMessage::class);
			$toAddresses = trim($mail->getToAddress());
			$addressesArray = GeneralUtility::trimExplode(',', $mail->getToAddress(), TRUE);
			if (count($addressesArray) > 1) {
				$toAddresses = $addressesArray;
			}

			$mailMessage->setTo($toAddresses);
			$mailMessage->setFrom($mail