diff --git a/Classes/Aspect/DebuggingAspect.php b/Classes/Aspect/DebuggingAspect.php
deleted file mode 100644
index bdeae35..0000000
--- a/Classes/Aspect/DebuggingAspect.php
+++ /dev/null
@@ -1,85 +0,0 @@
-send())")
- * @return void
- */
- public function interceptEmails(\Neos\Flow\Aop\JoinPointInterface $joinPoint) {
-
- if ($this->settings['interceptAll']['active'] || $this->settings['bccAll']['active']) {
- /**
- * @var \Neos\SwiftMailer\Message $message
- */
- $message = $joinPoint->getProxy();
-
- if ($this->settings['interceptAll']['active']) {
-
- $oldTo = $message->getTo();
- $oldCc = $message->getCc();
- $oldBcc = $message->getBcc();
-
- foreach ($this->settings['interceptAll']['noInterceptPatterns'] as $pattern) {
- if (preg_match($pattern, key($oldTo))) {
- // let the mail through but clean all cc and bcc fields
- $message->setCc(array());
- $message->setBcc(array());
- return;
- }
- }
-
- // stop if this aspect is executed twice (happens if QueueAdaptor is installed)
- if ($message->isIntercepted()) {
- return;
- }
-
- $interceptedRecipients = key($oldTo) . ($oldCc ? ' CC: ' . key($oldCc) : '') . ($oldBcc ? ' BCC: ' . key($oldBcc) : '');
- $message->setSubject('[intercepted '.$interceptedRecipients.'] '.$message->getSubject());
- $message->setIntercepted(true);
-
- $message->setCc(array());
- $message->setBcc(array());
-
- $first = true;
- foreach ($this->settings['interceptAll']['recipients'] as $email) {
- if ($first) {
- $message->setTo($email);
- } else {
- $message->addCc($email);
- }
- $first = false;
- }
- }
-
- if ($this->settings['bccAll']['active']) {
- foreach ($this->settings['bccAll']['recipients'] as $email) {
- $message->addBcc($email);
- }
- }
- }
- }
-}
-
-?>
diff --git a/Classes/Command/MailerCommandController.php b/Classes/Command/MailerCommandController.php
new file mode 100644
index 0000000..28af6a0
--- /dev/null
+++ b/Classes/Command/MailerCommandController.php
@@ -0,0 +1,24 @@
+abstractMailerService->sendTest($to);
+ }
+}
diff --git a/Classes/DataSource/EmailReferenceDataSource.php b/Classes/DataSource/EmailReferenceDataSource.php
new file mode 100644
index 0000000..6c91d81
--- /dev/null
+++ b/Classes/DataSource/EmailReferenceDataSource.php
@@ -0,0 +1,51 @@
+parents('[instanceof FormatD.DesignSystem:Site]')
+ ->find('[instanceof FormatD.Mailer:Document.Email]')
+ ->get();
+
+ $data = [];
+ foreach ($emailNodes as $emailNode) {
+ $data[] = [
+ 'label' => $this->nodeLabelGenerator->getLabel($emailNode),
+ 'value' => $emailNode->aggregateId,
+ 'icon' => static::$icon
+ ];
+ }
+
+ return $data;
+ }
+}
diff --git a/Classes/Factories/MailFactory.php b/Classes/Factories/MailFactory.php
new file mode 100644
index 0000000..68c674c
--- /dev/null
+++ b/Classes/Factories/MailFactory.php
@@ -0,0 +1,79 @@
+from($from)
+ ->subject($subject);
+
+ if (is_array($to)) {
+ $mail->to(...$to);
+ } else {
+ $mail->to($to);
+ }
+
+ if ($replyTo) {
+ if (is_array($replyTo)) {
+ $mail->replyTo(...$replyTo);
+ } else {
+ $mail->replyTo($replyTo);
+ }
+ }
+
+ if ($cc) {
+ if (is_array($cc)) {
+ $mail->cc(...$cc);
+ } else {
+ $mail->cc($cc);
+ }
+ }
+
+ if ($bcc) {
+ if (is_array($bcc)) {
+ $mail->bcc(...$bcc);
+ } else {
+ $mail->bcc($bcc);
+ }
+ }
+
+ if ($text) {
+ $mail->text($text);
+ }
+
+ if ($html) {
+ $mail->html($html);
+ }
+
+ return $mail;
+ }
+}
diff --git a/Classes/Factories/MailerFactory.php b/Classes/Factories/MailerFactory.php
new file mode 100644
index 0000000..e58f977
--- /dev/null
+++ b/Classes/Factories/MailerFactory.php
@@ -0,0 +1,35 @@
+configuration['dsn'] ?? null;
+
+ if (is_array($dsn)) {
+ $transport = Transport::fromDsns($dsn);
+ } elseif (is_string($dsn)) {
+ $transport = Transport::fromDsn($dsn);
+ } else {
+ throw new \InvalidArgumentException("Mailer needs a transport dsn configuration");
+ }
+
+ return new Mailer($transport);
+ }
+}
diff --git a/Classes/Form/Finishers/EmailFinisher.php b/Classes/Form/Finishers/EmailFinisher.php
new file mode 100644
index 0000000..10e3cc7
--- /dev/null
+++ b/Classes/Form/Finishers/EmailFinisher.php
@@ -0,0 +1,136 @@
+ self::CONTENT_TYPE_HTML,
+ self::FORMAT_PLAINTEXT => self::CONTENT_TYPE_PLAINTEXT,
+ ];
+
+ protected $defaultOptions = array(
+ 'senderAddress' => '',
+ 'senderName' => '',
+ 'recipientAddress' => '',
+ 'recipientName' => '',
+ 'format' => self::FORMAT_HTML,
+ 'attachAllPersistentResources' => false,
+ 'attachments' => [],
+ );
+
+ /**
+ * @return void
+ */
+ public function initializeObject() {
+ $this->defaultOptions['senderAddress'] = $this->configuration['defaultFrom']['address'];
+ $this->defaultOptions['senderName'] = $this->configuration['defaultFrom']['name'];
+ $this->defaultOptions['recipientAddress'] = $this->configuration['defaultTo']['address'];
+ $this->defaultOptions['recipientName'] = $this->configuration['defaultTo']['name'];
+ }
+
+ /**
+ * Executes this finisher
+ * @see AbstractFinisher::execute()
+ *
+ * @return void
+ * @throws FinisherException
+ */
+ protected function executeInternal()
+ {
+ if (!class_exists(Mailer::class)) {
+ throw new FinisherException('"symfony/mailer" doesn\'t seem to be installed, but is required for the EmailFinisher to work!', 1714142034);
+ }
+
+ $formValues = $this->finisherContext->getFormValues();
+ $templateNode = $this->parseOption('templateNode');
+ $subject = $this->parseOption('subject');
+ $senderAddress = $this->parseOption('senderAddress');
+ $senderName = $this->parseOption('senderName');
+ $recipientAddress = $this->parseOption('recipientAddress');
+ $recipientName = $this->parseOption('recipientName');
+ $replyToAddress = $this->parseOption('replyToAddress');
+ $carbonCopyAddress = $this->parseOption('carbonCopyAddress');
+ $blindCarbonCopyAddress = $this->parseOption('blindCarbonCopyAddress');
+ $format = $this->parseOption('format');
+
+ if ($templateNode === null) {
+ throw new FinisherException('The option "templateNode" must be set for the EmailFinisher.', 1714141943);
+ }
+ if ($subject === null) {
+ throw new FinisherException('The option "subject" must be set for the EmailFinisher.', 1327060320);
+ }
+ if ($recipientAddress === null) {
+ throw new FinisherException('The option "recipientAddress" must be set for the EmailFinisher.', 1327060200);
+ }
+ if (is_array($recipientAddress) && !empty($recipientName)) {
+ throw new FinisherException('The option "recipientName" cannot be used with multiple recipients in the EmailFinisher.', 1483365977);
+ }
+ if ($senderAddress === null) {
+ throw new FinisherException('The option "senderAddress" must be set for the EmailFinisher.', 1327060210);
+ }
+
+ $emailHtml = $this->abstractMailerService->getHtml($this->abstractMailerService->getNodeById($templateNode));
+ $parsedHtml = $this->replaceMarkerWithFormValues($formValues, $emailHtml);
+
+ $this->abstractMailerService->send(
+ $subject,
+ new Address($recipientAddress, $recipientName),
+ new Address($senderAddress, $senderName),
+ '',
+ $parsedHtml,
+ $replyToAddress ? new Address($replyToAddress) : null,
+ $carbonCopyAddress ? new Address($carbonCopyAddress) : null,
+ $blindCarbonCopyAddress ? new Address($blindCarbonCopyAddress) : null
+ );
+ }
+
+ protected function replaceMarkerWithFormValues($formValues, $emailHtml)
+ {
+ preg_match_all('#\#\#\#(.*?)\#\#\##', $emailHtml, $matches);
+
+ if (isset($matches[1])) {
+ foreach ($matches[1] as $match) {
+ $nestedMatch = explode('.', $match);
+ $replacement = '';
+
+ if (count($nestedMatch) > 1 && isset($formValues[$nestedMatch[0]][$nestedMatch[1]])) {
+ $replacement = $formValues[$nestedMatch[0]][$nestedMatch[1]];
+ } elseif (isset($formValues[$match])) {
+ $replacement = $formValues[$match];
+ }
+
+ $emailHtml = str_replace("###{$match}###", $replacement, $emailHtml);
+ }
+ }
+
+ return $emailHtml;
+ }
+}
diff --git a/Classes/Form/FormElements/AssetAttachment.php b/Classes/Form/FormElements/AssetAttachment.php
new file mode 100644
index 0000000..f29111e
--- /dev/null
+++ b/Classes/Form/FormElements/AssetAttachment.php
@@ -0,0 +1,37 @@
+assetRepository->findByIdentifier($elementValue);
+ $elementValue = $asset->getResource();
+ }
+}
diff --git a/Classes/Service/AbstractMailerService.php b/Classes/Service/AbstractMailerService.php
index 45b7361..1f6d9f3 100644
--- a/Classes/Service/AbstractMailerService.php
+++ b/Classes/Service/AbstractMailerService.php
@@ -1,108 +1,169 @@
defaultFrom = array($this->mailSettings['defaultFrom']['address'] => $this->mailSettings['defaultFrom']['name']);
- }
+ use InterceptionTrait;
- /**
- * Creates new message object and stores it as processedMessage
- *
- * @return \Neos\SwiftMailer\Message
- */
- public function createMessage() {
- $message = $this->objectManager->get('Neos\SwiftMailer\Message');
- $message->setFrom($this->defaultFrom);
- $this->processedMessage = $message;
- return $message;
- }
+ #[Flow\Inject(name: "FormatD.Mailer:MailerLogger")]
+ protected $mailerLogger;
- /**
- * Creates StandaloneView from templatePath and assigns default variables
- *
- * @param $templatePath
- * @return \Neos\FluidAdaptor\View\StandaloneView
- */
- public function createStandaloneView($templatePath) {
- $view = $this->objectManager->get('Neos\FluidAdaptor\View\StandaloneView');
- $view->setTemplatePathAndFilename('resource://'.$templatePath);
- $view->setFormat('html');
- $view->assign('baseUri', $this->baseUri);
- return $view;
- }
+ #[Flow\InjectConfiguration(package: "Neos.Flow", path: "http.baseUri")]
+ protected string $baseUri;
+
+ #[Flow\Inject]
+ protected ContentRepositoryService $contentRepositoryService;
+
+ #[Flow\Inject]
+ protected MailerFactory $mailerFactory;
+
+ #[Flow\Inject]
+ protected MailFactory $mailFactory;
+
+ protected MailerInterface $mailer;
+
+ protected Address $defaultFromAddress;
+
+ protected $client;
+
+ public function initializeObject()
+ {
+ $this->mailer = $this->mailerFactory->createMailer();
+ $this->defaultFromAddress = new Address($this->configuration['defaultFrom']['address'], $this->configuration['defaultFrom']['name']);
+
+ $clientOptions = [];
+ $clientOptions = $this->setSslVerification($clientOptions);
+ $this->client = new Client($clientOptions);
+ }
+
+ protected function setSslVerification(array $clientOptions): array
+ {
+ $flowContext = Bootstrap::getEnvironmentConfigurationSetting('FLOW_CONTEXT');
+ $clientOptions['verify'] = str_contains($flowContext, 'Development') ? false : true;
+
+ return $clientOptions;
+ }
+
+ public function send($subject, $to, $from, $text, $html, $replyTo = null, $cc = null, $bcc = null)
+ {
+ $mail = $this->mailFactory->createMail(
+ $subject,
+ $to,
+ $from ? $from : $this->defaultFromAddress,
+ $text,
+ $html
+ );
+
+ if ($replyTo !== null) {
+ $mail->addReplyTo($replyTo);
+ }
+
+ if ($cc !== null) {
+ $mail->addCc($cc);
+ }
+
+ if ($bcc !== null) {
+ $mail->addBcc($bcc);
+ }
+
+ if ($this->configuration['interceptAll']['active'] || $this->configuration['bccAll']['active']) {
+ $mail = $this->intercept($mail);
+ }
+
+ $this->mailer->send($mail);
+ }
+
+ /**
+ * @param array|Address|string $to
+ */
+ public function sendTest($to)
+ {
+ $mail = $this->mailFactory->createMail(
+ 'Format D Mailer // Test E-Mail',
+ $to,
+ $this->defaultFromAddress,
+ 'Hello guys, this is a test e-mail',
+ $this->getHtml($this->getNodeById($this->configuration['templateNodes']['test']))
+ );
+
+ $this->mailer->send($mail);
+ }
- /**
- * Convert inline images in html to attached ones
- *
- * @param string $html
- * @return string
- */
- protected function attachHtmlInlineImages($html) {
- return preg_replace_callback('#(
]*[ ]?src=")([^"]+)("[^>]*>)#', array($this, 'attachHtmlInlineImage'), $html);
+ public function getNodeById(string $id)
+ {
+ $contentRepository = $this->contentRepositoryService->getContentRepository();
+ $contentGraph = $this->contentRepositoryService->getContentGraph($contentRepository);
+
+ $generalizations = $contentRepository->getVariationGraph()->getRootGeneralizations();
+ $dimensionSpacePoint = reset($generalizations);
+
+ $subgraph = $contentGraph->getSubgraph(
+ $dimensionSpacePoint,
+ VisibilityConstraints::withoutRestrictions(),
+ );
+
+ return $subgraph->findNodeById(NodeAggregateId::fromString($id));
}
+ public function getHtml(Node $emailNode)
+ {
+ $emailUri = $this->contentRepositoryService->uriForNode($emailNode);
+
+ try {
+ $response = $this->client->request('GET', $emailUri);
+ } catch (ClientException $e) {
+ $this->mailerLogger->error("MAILER_ERROR :: " . $e->getResponse()->getBody()->getContents());
+ }
+
+ if ($response->getStatusCode() !== 200) {
+ $this->mailerLogger->error("MAILER_ERROR :: " . $response->getStatusCode());
+ }
+
+ $newsletterContent = $response->getBody()->getContents();
+
+ # attach images
+ if ($this->configuration['attachEmbeddedImages']) {
+ $newsletterContent = $this->attachHtmlInlineImages($newsletterContent);
+ }
+
+ # @todo add attachment handling
+
+ return $newsletterContent;
+ }
+
+ protected function attachHtmlInlineImages($html)
+ {
+ return preg_replace_callback('#(
]*[ ]?src=")([^"]+)("[^>]*>)#', array($this, 'attachHtmlInlineImage'), $html);
+ }
+
/**
* Substitution function called by preg_replace_callback
*
* @param $match
* @return string
*/
- public function attachHtmlInlineImage($match) {
+ public function attachHtmlInlineImage($match)
+ {
$completeMatch = $match[0];
$imgTagStart = $match[1];
$path = $match[2];
@@ -114,12 +175,12 @@ public function attachHtmlInlineImage($match) {
}
// only use local embed if nothing else can work (legacy mode)
- if (!isset($this->mailSettings['localEmbed']) || $this->mailSettings['localEmbed'] === false) {
+ if (!isset($this->configuration['localEmbed']) || $this->configuration['localEmbed'] === false) {
// if in cli we do not know the baseurl so we request the file locally
if (FLOW_SAPITYPE == 'CLI' && !preg_match('#^http.*#', $path)) {
$path = FLOW_PATH_WEB . $path;
}
- } else if ($this->mailSettings['localEmbed']) {
+ } else if ($this->configuration['localEmbed']) {
if (preg_match('#^http.*#', $path) && $this->baseUri) {
// if we know the baseUri we remove it to be able to convert the path to a local path
$path = str_replace($this->baseUri, "", $path, $replaceCount);
@@ -130,60 +191,12 @@ public function attachHtmlInlineImage($match) {
}
}
- if ($this->mailSettings['attachEmbeddedImages']) {
- $this->processedMessage->attach(\Swift_Attachment::fromPath(urldecode($path)));
- }
+ if ($this->configuration['attachEmbeddedImages']) {
+ #$this->processedMessage->attach(\Swift_Attachment::fromPath(urldecode($path)));
+ # @todo attach resource to symfony mail
+ }
- return $imgTagStart.$this->processedMessage->embed(\Swift_Image::fromPath(urldecode($path))).'"'.$imgTagEnd;
+ #return $imgTagStart . $this->processedMessage->embed(\Swift_Image::fromPath(urldecode($path))) . '"' . $imgTagEnd;
+ return $imgTagStart . ' ' . $imgTagEnd;
}
-
- /**
- * Sets the mailcontent from a standalone view
- *
- * @param \Neos\SwiftMailer\Message $message
- * @param \Neos\FluidAdaptor\View\StandaloneView $view
- * @param bool $embedImages
- */
- protected function setMailContentFromStandaloneView(\Neos\SwiftMailer\Message $message, \Neos\FluidAdaptor\View\StandaloneView $view, $embedImages = false) {
-
- $subject = trim($view->renderSection('subject'));
- $html = trim($view->renderSection('html'));
- $plain = trim($view->renderSection('plain', [], true));
-
- if($embedImages) $html = $this->attachHtmlInlineImages($html);
-
- $message->setSubject($subject);
- if ($html) $message->addPart($html,'text/html','utf-8');
- if ($plain) $message->addPart($plain,'text/plain','utf-8');
- }
-
- /**
- * Sends a message
- *
- * @param \Neos\SwiftMailer\Message $message
- */
- protected function sendMail(\Neos\SwiftMailer\Message $message) {
- $message->send();
- }
-
- /**
- * Sends test email to check the configuration
- *
- * @param string|array $to
- * @return void
- */
- public function sendTestMail($to) {
-
- $mail = $this->createMessage();
-
- $view = $this->createStandaloneView('FormatD.Mailer/Private/Templates/Notifications/TestMail.html');
- $view->assign('teststring', 'HelloWorld');
-
- $this->setMailContentFromStandaloneView($mail, $view, true);
- $mail->setTo($to);
-
- $this->sendMail($mail);
- }
}
-
-?>
\ No newline at end of file
diff --git a/Classes/Service/ContentRepositoryService.php b/Classes/Service/ContentRepositoryService.php
new file mode 100644
index 0000000..89982cb
--- /dev/null
+++ b/Classes/Service/ContentRepositoryService.php
@@ -0,0 +1,100 @@
+serverRequestFactory->createServerRequest('GET', new Uri($this->baseUri));
+
+ if (isset($this->baseUri) && is_string($this->baseUri) && !empty($this->baseUri)) {
+ /** @var RouteParameters $routingParameters */
+ $routingParameters = $httpRequest->getAttribute(ServerRequestAttributes::ROUTING_PARAMETERS) ?? RouteParameters::createEmpty();
+ $routingParameters = $routingParameters->withParameter('requestUriHost', $this->baseUri);
+ $routingParameters = $routingParameters->withParameter('siteNodeName', $this->siteConfig['siteNodeName']);
+ $routingParameters = $routingParameters->withParameter('contentRepositoryId', $this->siteConfig['contentRepositoryId']);
+
+ $httpRequestAttributes = $httpRequest->getAttributes();
+ $httpRequestAttributes[ServerRequestAttributes::ROUTING_PARAMETERS] = $routingParameters;
+ $reflectedHttpRequest = new \ReflectionObject($httpRequest);
+ $reflectedHttpRequestAttributes = $reflectedHttpRequest->getProperty('attributes');
+ $reflectedHttpRequestAttributes->setAccessible(true);
+ $reflectedHttpRequestAttributes->setValue($httpRequest, $httpRequestAttributes);
+ }
+
+ $this->actionRequest = $this->actionRequestFactory->createActionRequest($httpRequest);
+ }
+
+ public function getContentRepository(string $contentRepositoryId = 'default'): ContentRepository
+ {
+ $contentRepositoryId = ContentRepositoryId::fromString($contentRepositoryId);
+ return $this->contentRepositoryRegistry->get($contentRepositoryId);
+ }
+
+ public function getWorkspace(ContentRepository $contentRepository, string $workspaceName = 'live'): Workspace
+ {
+ return $contentRepository->findWorkspaceByName(WorkspaceName::fromString($workspaceName));
+ }
+
+ public function getContentGraph(ContentRepository $contentRepository, string $workspaceName = 'live'): ContentGraphInterface
+ {
+ return $contentRepository->getContentGraph(WorkspaceName::fromString($workspaceName));
+ }
+
+ public function uriForNode(Node $node, ActionRequest $actionRequest = null, $absolute = true, $format = 'html'): UriInterface
+ {
+ $request = $actionRequest ? $actionRequest : $this->actionRequest;
+ return $this->nodeUriBuilderFactory
+ ->forActionRequest($request)
+ ->uriFor(
+ NodeAddress::fromNode($node),
+ $absolute ? Options::createEmpty()->withCustomFormat($format)->withForceAbsolute() : Options::createEmpty()->withCustomFormat($format)
+ )
+ ;
+ }
+}
diff --git a/Classes/Traits/InterceptionTrait.php b/Classes/Traits/InterceptionTrait.php
index 55a8a25..7d4439a 100644
--- a/Classes/Traits/InterceptionTrait.php
+++ b/Classes/Traits/InterceptionTrait.php
@@ -1,36 +1,64 @@
intercepted;
- }
-
- /**
- * @param bool $intercepted
- */
- public function setIntercepted(bool $intercepted): void
- {
- $this->intercepted = $intercepted;
- }
+ #[Flow\InjectConfiguration(package: "FormatD.Mailer")]
+ protected array $configuration = [];
-}
+ /**
+ * @param Email $mail
+ */
+ protected function intercept($mail)
+ {
+ if ($this->configuration['interceptAll']['active']) {
+ $mail = $this->interceptAll($mail);
+ }
+
+ if ($this->configuration['bccAll']['active']) {
+ foreach ($this->configuration['bccAll']['recipients'] as $email) {
+ $mail = $mail->addBcc($email);
+ }
+ }
+
+ return $mail;
+ }
+
+ /**
+ * @param Email $mail
+ */
+ protected function interceptAll($mail)
+ {
+ $originalTo = $mail->getTo();
+ $originalCc = $mail->getCc();
+ $originalBcc = $mail->getBcc();
-?>
+ foreach ($this->configuration['interceptAll']['noInterceptPatterns'] as $pattern) {
+ if (preg_match($pattern, key($originalTo))) {
+ $mail->to(new Address('somewhere@example.com'));
+ $mail->bcc(new Address('somewhere@example.com'));
+ return;
+ }
+ }
+
+ # @todo check IF and HOW this needs to be adapted to work with job / mail queue
+ $interceptedRecipients = $originalTo[0]->getAddress() . ($originalCc ? ' CC: ' . $originalCc[0]->getAddress() : '') . ($originalBcc ? ' BCC: ' . $originalBcc[0]->getAddress() : '');
+ $mail->subject('[intercepted ' . $interceptedRecipients . '] ' . $mail->getSubject());
+
+ $mail->cc(new Address('somewhere@example.com'));
+ $mail->bcc(new Address('somewhere@example.com'));
+
+ $first = true;
+ foreach ($this->configuration['interceptAll']['recipients'] as $email) {
+ $first ? $mail->to($email) : $mail->addCc($email);
+ $first = false;
+ }
+
+ return $mail;
+ }
+}
diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml
new file mode 100644
index 0000000..22cb4d0
--- /dev/null
+++ b/Configuration/Objects.yaml
@@ -0,0 +1,8 @@
+'FormatD.Mailer:MailerLogger':
+ className: Psr\Log\LoggerInterface
+ scope: singleton
+ factoryObjectName: Neos\Flow\Log\PsrLoggerFactoryInterface
+ factoryMethodName: get
+ arguments:
+ 1:
+ value: MailerLogger
diff --git a/Configuration/Settings.Neos.Flow.yaml b/Configuration/Settings.Neos.Flow.yaml
new file mode 100644
index 0000000..be02e8b
--- /dev/null
+++ b/Configuration/Settings.Neos.Flow.yaml
@@ -0,0 +1,14 @@
+Neos:
+ Flow:
+ log:
+ psr3:
+ Neos\Flow\Log\PsrLoggerFactory:
+ MailerLogger:
+ default:
+ class: Neos\Flow\Log\Backend\FileBackend
+ options:
+ logFileURL: '%FLOW_PATH_DATA%Logs/Mailer.log'
+ severityThreshold: '%LOG_INFO%'
+ createParentDirectories: true
+ maximumLogFileSize: 1048576
+ logFilesToKeep: 1
diff --git a/Configuration/Settings.Neos.Form.yaml b/Configuration/Settings.Neos.Form.yaml
new file mode 100644
index 0000000..954c6aa
--- /dev/null
+++ b/Configuration/Settings.Neos.Form.yaml
@@ -0,0 +1,13 @@
+Neos:
+ Form:
+ presets:
+ default:
+ formElementTypes:
+ 'FormatD.Mailer:AssetAttachment':
+ superTypes:
+ 'Neos.Form:FormElement': true
+ implementationClassName: FormatD\Mailer\Form\FormElements\AssetAttachment
+ finisherPresets:
+ 'FormatD.Mailer:EmailFinisher':
+ implementationClassName: FormatD\Mailer\Form\Finishers\EmailFinisher
+ options: { }
diff --git a/Configuration/Settings.Neos.Neos.yaml b/Configuration/Settings.Neos.Neos.yaml
new file mode 100644
index 0000000..f885bba
--- /dev/null
+++ b/Configuration/Settings.Neos.Neos.yaml
@@ -0,0 +1,5 @@
+Neos:
+ Neos:
+ fusion:
+ autoInclude:
+ FormatD.Mailer: true
diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml
index 8630952..6dc44ca 100644
--- a/Configuration/Settings.yaml
+++ b/Configuration/Settings.yaml
@@ -1,6 +1,9 @@
-
FormatD:
Mailer:
+ site:
+ siteNodeName: 'format-d-website'
+ contentRepositoryId: 'default'
+ dsn: 'smtp://user:pass@smtp.example.com:25'
localEmbed: false
attachEmbeddedImages: false
interceptAll:
@@ -12,4 +15,9 @@ FormatD:
recipients: []
defaultFrom:
address: 'example@example.com'
- name: 'Example'
\ No newline at end of file
+ name: 'Example From'
+ defaultTo:
+ address: 'example@example.com'
+ name: 'Example To'
+ templateNodes:
+ test: ''
diff --git a/NodeTypes/Content/AssetAttachment.yaml b/NodeTypes/Content/AssetAttachment.yaml
new file mode 100644
index 0000000..e5e730a
--- /dev/null
+++ b/NodeTypes/Content/AssetAttachment.yaml
@@ -0,0 +1,23 @@
+FormatD.Mailer:AssetAttachment:
+ superTypes:
+ Neos.Form.Builder:FormElement: true
+ ui:
+ label: 'Attachment'
+ icon: 'icon-paperclip'
+ position: 90
+ inspector:
+ groups:
+ 'formElement':
+ icon: 'icon-paperclip'
+ group: 'form.custom'
+ properties:
+ asset:
+ type: 'Neos\Media\Domain\Model\Asset'
+ ui:
+ label: 'Asset'
+ reloadIfChanged: true
+ inspector:
+ group: 'formElement'
+ editorOptions:
+ constraints:
+ mediaTypes: [ 'application/zip', 'application/pdf', 'application/msword', 'application/msexcel', 'application/mspowerpoint', 'application/powerpoint', 'application/vnd.ms-powerpoint', 'application/x-mspowerpoint', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.oasis.opendocument.presentation', 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ]
diff --git a/NodeTypes/Content/EmailFinisher.yaml b/NodeTypes/Content/EmailFinisher.yaml
new file mode 100644
index 0000000..1bebf65
--- /dev/null
+++ b/NodeTypes/Content/EmailFinisher.yaml
@@ -0,0 +1,32 @@
+FormatD.Mailer:EmailFinisher:
+ superTypes:
+ Neos.Form.Builder:EmailFinisher: true
+ ui:
+ label: 'Email Finisher'
+ help:
+ message: 'Configure e-mail options and select a node based e-mail template.'
+ properties:
+ recipientAddress:
+ validation: ~
+ senderAddress:
+ validation: ~
+ templateSource: [ ]
+ templateNode:
+ type: string
+ ui:
+ label: 'E-Mail Template'
+ inspector:
+ group: 'finisher'
+ position: 'start'
+ editor: 'Neos.Neos/Inspector/Editors/SelectBoxEditor'
+ editorOptions:
+ allowEmpty: true
+ dataSourceIdentifier: formatd-mailer-email-reference
+ format:
+ defaultValue: 'html'
+ ui:
+ inspector:
+ editorOptions:
+ values:
+ 'plaintext': []
+ 'multipart': []
diff --git a/NodeTypes/Document/Email.fusion b/NodeTypes/Document/Email.fusion
new file mode 100644
index 0000000..1f78168
--- /dev/null
+++ b/NodeTypes/Document/Email.fusion
@@ -0,0 +1,45 @@
+
+prototype(FormatD.Mailer:Document.Email) < prototype(Neos.Neos:Page) {
+
+ head {
+ metaDescriptionTag >
+ metaKeywordsTag >
+ metaRobotsTag >
+ canonicalLink >
+ alternateLanguageLinks >
+ twitterCard >
+ openGraphMetaTags >
+ facebookMetaTags >
+ structuredData >
+ formatDComponentLoaderRegistry >
+
+ email = FormatD.Mailer:Fragment.Head
+ }
+
+ bodyTag.attributes.style = 'word-spacing:normal;'
+
+ body = Neos.Fusion:Component {
+
+ header = Neos.Neos:ContentCollection {
+ nodePath = 'header'
+ }
+
+ main = Neos.Neos:ContentCollection {
+ nodePath = 'main'
+ }
+
+ footer = Neos.Neos:ContentCollection {
+ nodePath = 'footer'
+ }
+
+ renderer = Neos.Fusion:Join {
+ main = afx`
+
+ `
+ }
+ }
+}
diff --git a/NodeTypes/Document/Email.yaml b/NodeTypes/Document/Email.yaml
new file mode 100644
index 0000000..d24de84
--- /dev/null
+++ b/NodeTypes/Document/Email.yaml
@@ -0,0 +1,16 @@
+FormatD.Mailer:Document.Email:
+ superTypes:
+ 'Neos.Neos:Document': true
+ ui:
+ label: 'Email'
+ icon: 'envelope'
+ position: 99
+ childNodes:
+ header:
+ type: 'Neos.Neos:ContentCollection'
+ position: 'before main'
+ main:
+ type: 'Neos.Neos:ContentCollection'
+ footer:
+ type: 'Neos.Neos:ContentCollection'
+ position: 'after main'
diff --git a/README.md b/README.md
index 18508ba..fe10000 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,23 @@
# FormatD.Mailer
-A Template Mailer for Neos Flow or/and CMS Projects.
+A template mailer using Symfony Mailer to configure, sent and intercept e-mails in Flow and Neos.
## What does it do?
This package provides a service class intended to be used as base class for sending fusion templates as mails.
-In addition it counatins a debugging aspect for cutting off and/or redirecting all mails (sent by Swiftmailer) in a development environment.
-
-
-## Kompatiblität
-
-Versioning scheme:
-
- 1.0.0
- | | |
- | | Bugfix Releases (non breaking)
- | Neos Compatibility Releases (non breaking except framework dependencies)
- Feature Releases (breaking)
-
-Releases und compatibility:
-
-| Package-Version | Neos Flow Version |
-|-----------------|------------------------|
-| 1.1.x | >= 6.x |
-| 1.0.x | 4.x - 5.x |
+In addition it contains a debugging aspect for cutting off and/or redirecting all mails in a development environment.
## Using the service in you own plugins to use fluid templates for mails
-Configure default from address:
+Configure smtp data (`dsn`) and default from address (`defaultFrom`):
```
FormatD:
Mailer:
+ dsn: 'smtp://user:pass@smtp.example.com:25'
defaultFrom:
address: 'example@example.com'
name: 'Example'
@@ -42,10 +25,25 @@ FormatD:
Extend AbstractMailerService and add methods as needed following the example of sendTestMail().
+## Use `FormatD.Mailer:Document.Email` node type as e-mail templates
+You can create e-mail templates from the Neos backend by choosing and creating and "E-Mail" document node. This node can either
+* be referenced by its ID in the Settings.yaml to be sent via some controller action
+* or chosen in the Form Finisher `FormatD.Mailer:EmailFinisher` under "E-Mail Template" to select an e-mail template for sending in the Form Builder
+
+### Get form values in e-mail templates
+In the e-mail node in Neos, you can set markers to reflect the form values you want to submit.
+E.g., you have a form builder form with the elements (ids!) "firstName" and "lastName" and want to display the values in your e-mail. You can write any text and set markers, like so:
+First name: ###firstName###
+Last name: ###lastName###
+Topics: ###i-need-help-with.topic###
-## intersept all mails in a dev environment
+### Overriding e-mail template
+The e-mail templates consist of various fusion prototypes which you can extend or completely override in your site package as you see fit.
+`FormatD.Mailer:Document.Email` is a `Neos.Neos:Page` with multiple fragments for the e-mail head and body.
-Configure swiftmailer to intersept all mails send by your neos installation (not only by the service).
+## Intercept all e-mails in a dev environment
+
+Configure mailer to intercept all mails send by your Neos installation (not only by the service).
This is an example which intercepts all mails and redirects them to example@example.com and secondexample@example.com:
```
@@ -60,7 +58,7 @@ FormatD:
```
-## Handling Embedded Images
+## Handling embedded images
The method `AbstractMailerService->setMailContentFromStandaloneView()` has a parameter to embed all images into the mail body.
This is handy if you have an installation that is protected by a .htaccess file for example, or the user may not have access to the internet when reading the email. GMail also cannot display images if they are included from a local domain.
@@ -74,7 +72,26 @@ FormatD:
localEmbed: true
```
-## Disable embed for specific images
+## Disable embedding for specific images
You can disable image embedding for specific images by adding `data-fdmailer-embed="disable"` as data attribute to the image tag.
-This is useful for tracking pixels where you dont want the local embedding.
\ No newline at end of file
+This is useful for tracking pixels where you dont want the local embedding.
+
+
+## Compatibilty
+
+Versioning scheme:
+
+ 1.0.0
+ | | |
+ | | Bugfix Releases (non breaking)
+ | Neos Compatibility Releases (non breaking except framework dependencies)
+ Feature Releases (breaking)
+
+Releases und compatibility:
+
+| Package Version | Neos Flow Version |
+|-----------------|------------------------|
+| 2.0.x | >= 9.x |
+| 1.1.x | >= 6.x |
+| 1.0.x | 4.x - 5.x |
\ No newline at end of file
diff --git a/Resources/Private/Fusion/Fragments/Body.fusion b/Resources/Private/Fusion/Fragments/Body.fusion
new file mode 100644
index 0000000..011e96f
--- /dev/null
+++ b/Resources/Private/Fusion/Fragments/Body.fusion
@@ -0,0 +1,107 @@
+
+prototype(FormatD.Mailer:Fragment.Body) < prototype(Neos.Fusion:Component) {
+
+ @propTypes {
+ headerContent = ${propTypes.string}
+ mainContent = ${propTypes.string}
+ footerContent = ${propTypes.string}
+ }
+
+ headerContent = 'Hello World'
+ mainContent = 'Content'
+ footerContent = '© Format D GmbH 2024'
+
+ renderer = afx`
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+ |
+ {props.headerContent}
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {props.mainContent}
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {props.footerContent}
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+ `
+}
diff --git a/Resources/Private/Fusion/Fragments/Body.mjml b/Resources/Private/Fusion/Fragments/Body.mjml
new file mode 100644
index 0000000..d6db11f
--- /dev/null
+++ b/Resources/Private/Fusion/Fragments/Body.mjml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+ {props.headerContent}
+
+
+
+
+
+
+
+ {props.mainContent}
+
+
+
+
+
+
+
+ {props.footerContent}
+
+
+
+
+
diff --git a/Resources/Private/Fusion/Fragments/Head.fusion b/Resources/Private/Fusion/Fragments/Head.fusion
new file mode 100644
index 0000000..c73b5a9
--- /dev/null
+++ b/Resources/Private/Fusion/Fragments/Head.fusion
@@ -0,0 +1,10 @@
+prototype(FormatD.Mailer:Fragment.Head) < prototype(Neos.Fusion:Component) {
+
+ renderer = afx`
+
+
+
+
+
+ `
+}
diff --git a/Resources/Private/Fusion/Overrides/Elements/AssetAttachment.fusion b/Resources/Private/Fusion/Overrides/Elements/AssetAttachment.fusion
new file mode 100644
index 0000000..921035f
--- /dev/null
+++ b/Resources/Private/Fusion/Overrides/Elements/AssetAttachment.fusion
@@ -0,0 +1,19 @@
+prototype(FormatD.Mailer:AssetAttachment) < prototype(Neos.Form:HiddenField) {
+ label >
+ @process.wrap.attributes.class = ${elementHasValidationErrors ? 'form--group-hidden error' : 'form--group-hidden'}
+ fieldContainer.field.attributes.name = "asset-attachment"
+ fieldContainer.field.attributes.value = ${element.properties.asset.identifier}
+
+ validationErrors = Neos.Fusion:Tag {
+ tagName = 'span'
+ attributes {
+ class = 'help-inline'
+ }
+ content = Neos.Fusion:Loop {
+ items = ${elementValidationErrors}
+ itemName = 'error'
+ itemRenderer = ${error.message}
+ }
+ @if.hasValidationErrors = ${elementHasValidationErrors}
+ }
+}
diff --git a/Resources/Private/Fusion/Overrides/Neos.Form.Builder/Elements/AssetAttachment.fusion b/Resources/Private/Fusion/Overrides/Neos.Form.Builder/Elements/AssetAttachment.fusion
new file mode 100644
index 0000000..420f06f
--- /dev/null
+++ b/Resources/Private/Fusion/Overrides/Neos.Form.Builder/Elements/AssetAttachment.fusion
@@ -0,0 +1,3 @@
+prototype(FormatD.Mailer:AssetAttachment.Definition) < prototype(Neos.Form.Builder:FormElement.Definition) {
+ formElementType = 'FormatD.Mailer:AssetAttachment'
+}
diff --git a/Resources/Private/Fusion/Overrides/Neos.Form.Builder/Finisher/EmailFinisher.Definition.fusion b/Resources/Private/Fusion/Overrides/Neos.Form.Builder/Finisher/EmailFinisher.Definition.fusion
new file mode 100644
index 0000000..be9a063
--- /dev/null
+++ b/Resources/Private/Fusion/Overrides/Neos.Form.Builder/Finisher/EmailFinisher.Definition.fusion
@@ -0,0 +1,20 @@
+prototype(FormatD.Mailer:EmailFinisher.Definition) < prototype(Neos.Form.Builder:Finisher.Definition) {
+ formElementType = 'FormatD.Mailer:EmailFinisher'
+
+ options {
+ templateNode = ${null}
+ subject = ${null}
+ recipientAddress = ${null}
+ recipientName = ''
+ senderAddress = ${null}
+ senderName = ''
+ replyToAddress = ${null}
+ carbonCopyAddress = ${null}
+ blindCarbonCopyAddress = ${null}
+ format = 'html'
+ variables = Neos.Fusion:DataStructure {
+ baseUri = ${Configuration.setting('Neos.Flow.http.baseUri')}
+ domainName = ${String.pregReplace(Configuration.setting('Neos.Flow.http.baseUri'), "/https?:\/\/(.*)\//", "$1")}
+ }
+ }
+}
\ No newline at end of file
diff --git a/Resources/Private/Fusion/Root.fusion b/Resources/Private/Fusion/Root.fusion
new file mode 100644
index 0000000..abc63d6
--- /dev/null
+++ b/Resources/Private/Fusion/Root.fusion
@@ -0,0 +1,10 @@
+include: Overrides/**/*
+include: Fragments/**/*
+include: nodetypes://FormatD.Mailer/**/*.fusion
+
+root {
+ emailTemplates {
+ condition = ${q(documentNode).is('[instanceof FormatD.Mailer:Document.Email]')}
+ renderer = FormatD.Mailer:Document.Email
+ }
+}
diff --git a/Resources/Private/Templates/Notifications/TestMail.html b/Resources/Private/Templates/Notifications/TestMail.html
deleted file mode 100644
index d814bba..0000000
--- a/Resources/Private/Templates/Notifications/TestMail.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-This is a Test E-Mail
-
-
-
-
-
-
- |
-
- This is a test e-mail formatted with HTML
- {teststring}
- Baseuri: {baseUri}
-
- |
-
-
-
-
-
-
-
- This is a test e-mail formatted with plaintext
- {teststring}
- Baseuri: {baseUri}
-
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 6679b53..afe4ab8 100644
--- a/composer.json
+++ b/composer.json
@@ -1,98 +1,100 @@
{
- "name": "formatd/mailer",
- "description": "Wrapper for Swiftmailer in Neos Flow Projects",
- "type": "neos-package",
- "license": "MIT",
- "require": {
- "neos/flow": "^6.0 || ^7.0 || ^8.0",
- "neos/swiftmailer": "^7.3"
- },
- "suggest": [],
- "autoload": {
- "psr-4": {
- "FormatD\\Mailer\\": "Classes/"
- }
- },
- "extra": {
- "neos": {
- "package-key": "FormatD.Mailer"
- },
- "applied-flow-migrations": [
- "TYPO3.FLOW3-201201261636",
- "TYPO3.Fluid-201205031303",
- "TYPO3.FLOW3-201205292145",
- "TYPO3.FLOW3-201206271128",
- "TYPO3.FLOW3-201209201112",
- "TYPO3.Flow-201209251426",
- "TYPO3.Flow-201211151101",
- "TYPO3.Flow-201212051340",
- "TYPO3.TypoScript-130516234520",
- "TYPO3.TypoScript-130516235550",
- "TYPO3.TYPO3CR-130523180140",
- "TYPO3.Neos.NodeTypes-201309111655",
- "TYPO3.Flow-201310031523",
- "TYPO3.Flow-201405111147",
- "TYPO3.Neos-201407061038",
- "TYPO3.Neos-201409071922",
- "TYPO3.TYPO3CR-140911160326",
- "TYPO3.Neos-201410010000",
- "TYPO3.TYPO3CR-141101082142",
- "TYPO3.Neos-20141113115300",
- "TYPO3.Fluid-20141113120800",
- "TYPO3.Flow-20141113121400",
- "TYPO3.Fluid-20141121091700",
- "TYPO3.Neos-20141218134700",
- "TYPO3.Fluid-20150214130800",
- "TYPO3.Neos-20150303231600",
- "TYPO3.TYPO3CR-20150510103823",
- "TYPO3.Flow-20151113161300",
- "TYPO3.Form-20160601101500",
- "TYPO3.Flow-20161115140400",
- "TYPO3.Flow-20161115140430",
- "Neos.Flow-20161124204700",
- "Neos.Flow-20161124204701",
- "Neos.Twitter.Bootstrap-20161124204912",
- "Neos.Form-20161124205254",
- "Neos.Flow-20161124224015",
- "Neos.Party-20161124225257",
- "Neos.Eel-20161124230101",
- "Neos.Imagine-20161124231742",
- "Neos.Media-20161124233100",
- "Neos.NodeTypes-20161125002300",
- "Neos.Neos-20161125002322",
- "Neos.ContentRepository-20161125012000",
- "Neos.Fusion-20161125013710",
- "Neos.Fusion-20161125104701",
- "Neos.NodeTypes-20161125104800",
- "Neos.Neos-20161125104802",
- "Neos.Neos-20161125122412",
- "Neos.Flow-20161125124112",
- "Neos.SwiftMailer-20161130105617",
- "TYPO3.FluidAdaptor-20161130112935",
- "Neos.Fusion-20161201202543",
- "Neos.Neos-20161201222211",
- "Neos.Fusion-20161202215034",
- "Neos.ContentRepository.Search-20161210231100",
- "Neos.Fusion-20161219092345",
- "Neos.ContentRepository-20161219093512",
- "Neos.Media-20161219094126",
- "Neos.Neos-20161219094403",
- "Neos.Neos-20161219122512",
- "Neos.Fusion-20161219130100",
- "Neos.Neos-20161220163741",
- "Neos.Neos-20170115114620",
- "Neos.Fusion-20170120013047",
- "Neos.Flow-20170125103800",
- "Neos.Seo-20170127154600",
- "Neos.Flow-20170127183102",
- "Neos.Fusion-20180211175500",
- "Neos.Fusion-20180211184832",
- "Neos.Flow-20180415105700",
- "Neos.Neos-20180907103800",
- "Neos.Neos.Ui-20190319094900",
- "Neos.Flow-20190425144900",
- "Neos.Flow-20190515215000",
- "Neos.NodeTypes-20190917101945"
- ]
- }
-}
+ "name": "formatd/mailer",
+ "description": "Send mails in Neos with symfony mailer",
+ "type": "neos-package",
+ "license": "MIT",
+ "require": {
+ "php": ">=8.1",
+ "neos/flow": "^8.0 || ^9.0",
+ "guzzlehttp/guzzle": "^6.0 || ^7.0",
+ "symfony/mailer": "^7.0"
+ },
+ "suggest": [],
+ "autoload": {
+ "psr-4": {
+ "FormatD\\Mailer\\": "Classes/"
+ }
+ },
+ "extra": {
+ "neos": {
+ "package-key": "FormatD.Mailer"
+ },
+ "applied-flow-migrations": [
+ "TYPO3.FLOW3-201201261636",
+ "TYPO3.Fluid-201205031303",
+ "TYPO3.FLOW3-201205292145",
+ "TYPO3.FLOW3-201206271128",
+ "TYPO3.FLOW3-201209201112",
+ "TYPO3.Flow-201209251426",
+ "TYPO3.Flow-201211151101",
+ "TYPO3.Flow-201212051340",
+ "TYPO3.TypoScript-130516234520",
+ "TYPO3.TypoScript-130516235550",
+ "TYPO3.TYPO3CR-130523180140",
+ "TYPO3.Neos.NodeTypes-201309111655",
+ "TYPO3.Flow-201310031523",
+ "TYPO3.Flow-201405111147",
+ "TYPO3.Neos-201407061038",
+ "TYPO3.Neos-201409071922",
+ "TYPO3.TYPO3CR-140911160326",
+ "TYPO3.Neos-201410010000",
+ "TYPO3.TYPO3CR-141101082142",
+ "TYPO3.Neos-20141113115300",
+ "TYPO3.Fluid-20141113120800",
+ "TYPO3.Flow-20141113121400",
+ "TYPO3.Fluid-20141121091700",
+ "TYPO3.Neos-20141218134700",
+ "TYPO3.Fluid-20150214130800",
+ "TYPO3.Neos-20150303231600",
+ "TYPO3.TYPO3CR-20150510103823",
+ "TYPO3.Flow-20151113161300",
+ "TYPO3.Form-20160601101500",
+ "TYPO3.Flow-20161115140400",
+ "TYPO3.Flow-20161115140430",
+ "Neos.Flow-20161124204700",
+ "Neos.Flow-20161124204701",
+ "Neos.Twitter.Bootstrap-20161124204912",
+ "Neos.Form-20161124205254",
+ "Neos.Flow-20161124224015",
+ "Neos.Party-20161124225257",
+ "Neos.Eel-20161124230101",
+ "Neos.Imagine-20161124231742",
+ "Neos.Media-20161124233100",
+ "Neos.NodeTypes-20161125002300",
+ "Neos.Neos-20161125002322",
+ "Neos.ContentRepository-20161125012000",
+ "Neos.Fusion-20161125013710",
+ "Neos.Fusion-20161125104701",
+ "Neos.NodeTypes-20161125104800",
+ "Neos.Neos-20161125104802",
+ "Neos.Neos-20161125122412",
+ "Neos.Flow-20161125124112",
+ "Neos.SwiftMailer-20161130105617",
+ "TYPO3.FluidAdaptor-20161130112935",
+ "Neos.Fusion-20161201202543",
+ "Neos.Neos-20161201222211",
+ "Neos.Fusion-20161202215034",
+ "Neos.ContentRepository.Search-20161210231100",
+ "Neos.Fusion-20161219092345",
+ "Neos.ContentRepository-20161219093512",
+ "Neos.Media-20161219094126",
+ "Neos.Neos-20161219094403",
+ "Neos.Neos-20161219122512",
+ "Neos.Fusion-20161219130100",
+ "Neos.Neos-20161220163741",
+ "Neos.Neos-20170115114620",
+ "Neos.Fusion-20170120013047",
+ "Neos.Flow-20170125103800",
+ "Neos.Seo-20170127154600",
+ "Neos.Flow-20170127183102",
+ "Neos.Fusion-20180211175500",
+ "Neos.Fusion-20180211184832",
+ "Neos.Flow-20180415105700",
+ "Neos.Neos-20180907103800",
+ "Neos.Neos.Ui-20190319094900",
+ "Neos.Flow-20190425144900",
+ "Neos.Flow-20190515215000",
+ "Neos.NodeTypes-20190917101945"
+ ]
+ }
+}
\ No newline at end of file