From fc02a7f10e3135bc50b93397498cc475e602df36 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Tue, 16 Apr 2024 16:44:51 +0200 Subject: [PATCH 01/17] chore: require symfony/mailer. change compatibility to neos >= 8.x --- README.md | 11 ++++++----- composer.json | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 18508ba..82b0802 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This package provides a service class intended to be used as base class for send In addition it counatins a debugging aspect for cutting off and/or redirecting all mails (sent by Swiftmailer) in a development environment. -## Kompatiblität +## Compatibilty Versioning scheme: @@ -24,6 +24,7 @@ Releases und compatibility: | Package-Version | Neos Flow Version | |-----------------|------------------------| +| 2.0.x | >= 8.x | | 1.1.x | >= 6.x | | 1.0.x | 4.x - 5.x | @@ -43,9 +44,9 @@ FormatD: Extend AbstractMailerService and add methods as needed following the example of sendTestMail(). -## intersept all mails in a dev environment +## Intercept all e-mails in a dev environment -Configure swiftmailer to intersept all mails send by your neos installation (not only by the service). +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 +61,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 +75,7 @@ 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 diff --git a/composer.json b/composer.json index 6679b53..36b49bf 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,9 @@ "type": "neos-package", "license": "MIT", "require": { - "neos/flow": "^6.0 || ^7.0 || ^8.0", - "neos/swiftmailer": "^7.3" + "php": ">=8.1", + "neos/flow": "^8.0 || ^9.0", + "symfony/mailer": "^7.0" }, "suggest": [], "autoload": { From b9fcc56d1d9c2291bfcb585852156e410a03d7bf Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Tue, 16 Apr 2024 19:22:07 +0200 Subject: [PATCH 02/17] chore: add factories for to symfony mailer. re-add interception functionality via config. --- Classes/Aspect/DebuggingAspect.php | 85 -------- Classes/Command/MailerCommandController.php | 23 +++ Classes/Factories/MailFactory.php | 79 ++++++++ Classes/Factories/MailerFactory.php | 35 ++++ Classes/Service/AbstractMailerService.php | 209 +++++--------------- Classes/Traits/InterceptionTrait.php | 81 +++++--- Configuration/Settings.yaml | 7 +- 7 files changed, 242 insertions(+), 277 deletions(-) delete mode 100644 Classes/Aspect/DebuggingAspect.php create mode 100644 Classes/Command/MailerCommandController.php create mode 100644 Classes/Factories/MailFactory.php create mode 100644 Classes/Factories/MailerFactory.php 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..3b972a6 --- /dev/null +++ b/Classes/Command/MailerCommandController.php @@ -0,0 +1,23 @@ +abstractMailerService->sendTest($to); + } +} 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/Service/AbstractMailerService.php b/Classes/Service/AbstractMailerService.php index 45b7361..ab9cc8a 100644 --- a/Classes/Service/AbstractMailerService.php +++ b/Classes/Service/AbstractMailerService.php @@ -1,189 +1,74 @@ defaultFrom = array($this->mailSettings['defaultFrom']['address'] => $this->mailSettings['defaultFrom']['name']); - } - - /** - * 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; - } + protected Address $defaultFromAddress; - /** - * 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; - } - - /** - * 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); - } - - /** - * Substitution function called by preg_replace_callback - * - * @param $match - * @return string - */ - public function attachHtmlInlineImage($match) { - $completeMatch = $match[0]; - $imgTagStart = $match[1]; - $path = $match[2]; - $imgTagEnd = $match[3]; - - // you can disable embedding with data attribute (useful for tracking pixel) - if (preg_match('#data-fdmailer-embed="disable"#', $completeMatch)) { - return $completeMatch; - } - - // only use local embed if nothing else can work (legacy mode) - if (!isset($this->mailSettings['localEmbed']) || $this->mailSettings['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']) { - 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); - } - if (!preg_match('#^http.*#', $path)) { - // if path is now relative to document root we prepend local path - $path = FLOW_PATH_WEB . $path; - } - } - - if ($this->mailSettings['attachEmbeddedImages']) { - $this->processedMessage->attach(\Swift_Attachment::fromPath(urldecode($path))); - } - - return $imgTagStart.$this->processedMessage->embed(\Swift_Image::fromPath(urldecode($path))).'"'.$imgTagEnd; + public function initializeObject() + { + $this->mailer = $this->mailerFactory->createMailer(); + $this->defaultFromAddress = new Address($this->configuration['defaultFrom']['address'], $this->configuration['defaultFrom']['name']); } - /** - * 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'); - } + protected function send($subject, $to, $from, $text, $html) { + $mail = $this->mailFactory->createMail( + $subject, + $to, + $from ? $from : $this->defaultFromAddress, + $text, + $html + ); + + if ($this->configuration['interceptAll']['active'] || $this->configuration['bccAll']['active']) { + $mail = $this->intercept($mail); + } - /** - * Sends a message - * - * @param \Neos\SwiftMailer\Message $message - */ - protected function sendMail(\Neos\SwiftMailer\Message $message) { - $message->send(); + $this->mailer->send($mail); } /** - * Sends test email to check the configuration - * - * @param string|array $to - * @return void + * @param array|Address|string $to */ - 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); + 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', + // get html from fusion template + ); + + $this->mailer->send($mail); } } - -?> \ No newline at end of file diff --git a/Classes/Traits/InterceptionTrait.php b/Classes/Traits/InterceptionTrait.php index 55a8a25..e41b76b 100644 --- a/Classes/Traits/InterceptionTrait.php +++ b/Classes/Traits/InterceptionTrait.php @@ -1,36 +1,63 @@ intercepted; - } - - /** - * @param bool $intercepted - */ - public function setIntercepted(bool $intercepted): void - { - $this->intercepted = $intercepted; - } - + #[Flow\InjectConfiguration(package: "FormatD.Mailer", path: "")] + 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@bla.com')); + $mail->bcc(new Address('somewhere@bla.com')); + return; + } + } + + # @todo check IF and HOW this needs to be adapted to work with job / mail queue + $interceptedRecipients = key($originalTo) . ($originalCc ? ' CC: ' . key($originalCc) : '') . ($originalBcc ? ' BCC: ' . key($originalBcc) : ''); + $mail->subject('[intercepted ' . $interceptedRecipients . '] ' . $mail->getSubject()); + + $mail->cc(new Address('somewhere@bla.com')); + $mail->bcc(new Address('somewhere@bla.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/Settings.yaml b/Configuration/Settings.yaml index 8630952..efacf77 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -1,8 +1,9 @@ FormatD: Mailer: - localEmbed: false - attachEmbeddedImages: false + dsn: 'smtp://user:pass@smtp.example.com:25' + #localEmbed: false + #attachEmbeddedImages: false interceptAll: active: false recipients: [] @@ -12,4 +13,4 @@ FormatD: recipients: [] defaultFrom: address: 'example@example.com' - name: 'Example' \ No newline at end of file + name: 'Example' From 58062b33fd136f44a9134d28d46dad98f2d72176 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Wed, 17 Apr 2024 17:24:23 +0200 Subject: [PATCH 03/17] chore: WIP add functionality to render e-mail html from nodes. --- Classes/Command/MailerCommandController.php | 1 + Classes/Service/AbstractMailerService.php | 110 +++++++++++++++++- Classes/Service/ContentRepositoryService.php | 103 ++++++++++++++++ Configuration/Settings.Neos.Neos.yaml | 5 + Configuration/Settings.yaml | 7 +- NodeTypes/Document/Email.fusion | 45 +++++++ NodeTypes/Document/Email.yaml | 16 +++ .../Private/Fusion/Fragments/Body.fusion | 107 +++++++++++++++++ Resources/Private/Fusion/Fragments/Body.mjml | 28 +++++ .../Private/Fusion/Fragments/Head.fusion | 10 ++ Resources/Private/Fusion/Root.fusion | 10 ++ .../Templates/Notifications/TestMail.html | 26 ----- 12 files changed, 434 insertions(+), 34 deletions(-) create mode 100644 Classes/Service/ContentRepositoryService.php create mode 100644 Configuration/Settings.Neos.Neos.yaml create mode 100644 NodeTypes/Document/Email.fusion create mode 100644 NodeTypes/Document/Email.yaml create mode 100644 Resources/Private/Fusion/Fragments/Body.fusion create mode 100644 Resources/Private/Fusion/Fragments/Body.mjml create mode 100644 Resources/Private/Fusion/Fragments/Head.fusion create mode 100644 Resources/Private/Fusion/Root.fusion delete mode 100644 Resources/Private/Templates/Notifications/TestMail.html diff --git a/Classes/Command/MailerCommandController.php b/Classes/Command/MailerCommandController.php index 3b972a6..28af6a0 100644 --- a/Classes/Command/MailerCommandController.php +++ b/Classes/Command/MailerCommandController.php @@ -15,6 +15,7 @@ class MailerCommandController extends CommandController /** * @param string $to + * ./flow mailer:sendTest --to someone@somewhere.com */ public function sendTestCommand($to) { diff --git a/Classes/Service/AbstractMailerService.php b/Classes/Service/AbstractMailerService.php index ab9cc8a..6884de8 100644 --- a/Classes/Service/AbstractMailerService.php +++ b/Classes/Service/AbstractMailerService.php @@ -1,15 +1,18 @@ contentRepositoryService->getContentRepository(); + $workspace = $this->contentRepositoryService->getWorkspace($contentRepository); + $contentGraph = $this->contentRepositoryService->getContentGraph($contentRepository); + + $testEmailNodes = $contentGraph->findNodeAggregateById($workspace->currentContentStreamId, NodeAggregateId::fromString($this->configuration['templateNodes']['test'])); + $mail = $this->mailFactory->createMail( 'Format D Mailer // Test E-Mail', $to, $this->defaultFromAddress, 'Hello guys, this is a test e-mail', - // get html from fusion template + $this->getHtml($testEmailNodes->getNodes()[0]) ); $this->mailer->send($mail); } + + protected function getHtml(Node $emailNode) + { + #$emailUri = $this->contentRepositoryService->getNodeUri($emailNode); + $emailUri = ''; + + try { + $response = $this->browser->request($emailUri); + } catch (CurlEngineException $e) { + // log $response->getStatusCode() + $response = $this->browser->request($emailUri); + } + + if ($response->getStatusCode() !== 200) { + // log $response->getStatusCode() + } + + $newsletterContent = $response->getBody()->getContents(); + + # @todo add handling for marker in template (?) + #$newsletterContent = preg_replace_callback('#\{[a-zA-Z]+\}#', function ($match) use ($recipientData) { + # return $recipientData->replaceMarker($match[0]); + #}, $newsletterContent); + + # 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) + { + $completeMatch = $match[0]; + $imgTagStart = $match[1]; + $path = $match[2]; + $imgTagEnd = $match[3]; + + // you can disable embedding with data attribute (useful for tracking pixel) + if (preg_match('#data-fdmailer-embed="disable"#', $completeMatch)) { + return $completeMatch; + } + + // only use local embed if nothing else can work (legacy mode) + 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->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); + } + if (!preg_match('#^http.*#', $path)) { + // if path is now relative to document root we prepend local path + $path = FLOW_PATH_WEB . $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 . ' ' . $imgTagEnd; + } } diff --git a/Classes/Service/ContentRepositoryService.php b/Classes/Service/ContentRepositoryService.php new file mode 100644 index 0000000..60930c2 --- /dev/null +++ b/Classes/Service/ContentRepositoryService.php @@ -0,0 +1,103 @@ +serverRequestFactory->createServerRequest('GET', new Uri($this->baseUri)); + + if (isset($this->baseUri) && is_string($this->baseUri) && !empty($this->baseUri)) { + // Sets requestUriHost like RequestUriHostMiddleware does + /** @var RouteParameters $routingParameters */ + $routingParameters = $httpRequest->getAttribute(ServerRequestAttributes::ROUTING_PARAMETERS) ?? RouteParameters::createEmpty(); + $routingParameters = $routingParameters->withParameter('requestUriHost', $this->baseUri); + + $httpRequestAttributes = $httpRequest->getAttributes(); + $httpRequestAttributes[ServerRequestAttributes::ROUTING_PARAMETERS] = $routingParameters; + $reflectedHttpRequest = new \ReflectionObject($httpRequest); + $reflectedHttpRequestAttributes = $reflectedHttpRequest->getProperty('attributes'); + $reflectedHttpRequestAttributes->setAccessible(true); + $reflectedHttpRequestAttributes->setValue($httpRequest, $httpRequestAttributes); + } + + $request = $this->actionRequestFactory->createActionRequest($httpRequest); + $uriBuilder = new UriBuilder(); + $uriBuilder->setRequest($request); + $this->uriBuilder = $uriBuilder; + } + + 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->getWorkspaceFinder()->findOneByName(WorkspaceName::fromString($workspaceName)); + } + + public function getContentGraph(ContentRepository $contentRepository): ContentGraphInterface + { + return $contentRepository->getContentGraph(); + } + + public function getNodeUri(Node $node, $arguments = [], $format = 'html') + { + # @todo fix this / make it work + return $this->linkingService->createNodeUri( + new \Neos\Flow\Mvc\Controller\ControllerContext( + $this->uriBuilder->getRequest(), + new ActionResponse(), + new \Neos\Flow\Mvc\Controller\Arguments([]), + $this->uriBuilder + ), + $node, + null, + $format, + true, + $arguments + ); + } +} 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 efacf77..e03bc9f 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -1,9 +1,8 @@ - FormatD: Mailer: dsn: 'smtp://user:pass@smtp.example.com:25' - #localEmbed: false - #attachEmbeddedImages: false + localEmbed: false + attachEmbeddedImages: false interceptAll: active: false recipients: [] @@ -14,3 +13,5 @@ FormatD: defaultFrom: address: 'example@example.com' name: 'Example' + templateNodes: + test: '' 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/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/Root.fusion b/Resources/Private/Fusion/Root.fusion new file mode 100644 index 0000000..7eded25 --- /dev/null +++ b/Resources/Private/Fusion/Root.fusion @@ -0,0 +1,10 @@ +include: Fragments/**/* +include: Templates/**/* +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 From dd4b9dafc90b49c84f047b57d196236597574632 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Wed, 17 Apr 2024 17:26:44 +0200 Subject: [PATCH 04/17] fix: remove obsolete fusion include path. --- Resources/Private/Fusion/Root.fusion | 1 - 1 file changed, 1 deletion(-) diff --git a/Resources/Private/Fusion/Root.fusion b/Resources/Private/Fusion/Root.fusion index 7eded25..f297248 100644 --- a/Resources/Private/Fusion/Root.fusion +++ b/Resources/Private/Fusion/Root.fusion @@ -1,5 +1,4 @@ include: Fragments/**/* -include: Templates/**/* include: nodetypes://FormatD.Mailer/**/*.fusion root { From 4bef2cbe6dcb4a60158e71f15df8d28798deb252 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Thu, 25 Apr 2024 11:37:00 +0200 Subject: [PATCH 05/17] chore: add functionality to get node's uri. --- Classes/Service/AbstractMailerService.php | 3 +- Classes/Service/ContentRepositoryService.php | 39 +++++++++----------- Configuration/Settings.yaml | 3 ++ 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Classes/Service/AbstractMailerService.php b/Classes/Service/AbstractMailerService.php index 6884de8..4f288f6 100644 --- a/Classes/Service/AbstractMailerService.php +++ b/Classes/Service/AbstractMailerService.php @@ -92,8 +92,7 @@ public function sendTest($to) protected function getHtml(Node $emailNode) { - #$emailUri = $this->contentRepositoryService->getNodeUri($emailNode); - $emailUri = ''; + $emailUri = $this->contentRepositoryService->getNodeUri($emailNode); try { $response = $this->browser->request($emailUri); diff --git a/Classes/Service/ContentRepositoryService.php b/Classes/Service/ContentRepositoryService.php index 60930c2..5e3ba32 100644 --- a/Classes/Service/ContentRepositoryService.php +++ b/Classes/Service/ContentRepositoryService.php @@ -12,12 +12,11 @@ use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use GuzzleHttp\Psr7\Uri; use Neos\Flow\Mvc\Routing\UriBuilder; -use Neos\Neos\Service\LinkingService; -use Neos\Flow\Mvc\ActionResponse; -use Neos\Flow\Mvc\ActionRequestFactory; use Psr\Http\Message\ServerRequestFactoryInterface; use Neos\Flow\Http\ServerRequestAttributes; +use Neos\Flow\Mvc\ActionRequestFactory; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; +use Neos\Neos\FrontendRouting\NodeAddressFactory; /** * Various helper for CR and nodes @@ -29,6 +28,9 @@ class ContentRepositoryService { #[Flow\InjectConfiguration(package: "Neos.Flow", path: "http.baseUri")] protected string $baseUri; + #[Flow\InjectConfiguration(package: "FormatD.Mailer", path: "site")] + protected array $siteConfig; + #[Flow\Inject] protected ContentRepositoryRegistry $contentRepositoryRegistry; @@ -38,9 +40,6 @@ class ContentRepositoryService { #[Flow\Inject] protected ServerRequestFactoryInterface $serverRequestFactory; - #[Flow\Inject] - protected LinkingService $linkingService; - protected UriBuilder $uriBuilder; public function initializeObject() @@ -48,10 +47,11 @@ public function initializeObject() $httpRequest = $this->serverRequestFactory->createServerRequest('GET', new Uri($this->baseUri)); if (isset($this->baseUri) && is_string($this->baseUri) && !empty($this->baseUri)) { - // Sets requestUriHost like RequestUriHostMiddleware does /** @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; @@ -83,21 +83,16 @@ public function getContentGraph(ContentRepository $contentRepository): ContentGr return $contentRepository->getContentGraph(); } - public function getNodeUri(Node $node, $arguments = [], $format = 'html') + public function getNodeUri(Node $node, $arguments = [], $absolute = true, $format = 'html') { - # @todo fix this / make it work - return $this->linkingService->createNodeUri( - new \Neos\Flow\Mvc\Controller\ControllerContext( - $this->uriBuilder->getRequest(), - new ActionResponse(), - new \Neos\Flow\Mvc\Controller\Arguments([]), - $this->uriBuilder - ), - $node, - null, - $format, - true, - $arguments - ); + $nodeAddressFactory = NodeAddressFactory::create($this->getContentRepository()); + $nodeAddress = $nodeAddressFactory->createFromNode($node); + + return $this->uriBuilder + ->setArguments($arguments) + ->setCreateAbsoluteUri($absolute) + ->setFormat($format) + ->uriFor('show', ['node' => $nodeAddress], 'Frontend\Node', 'Neos.Neos') + ; } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index e03bc9f..c8d8ec6 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -1,5 +1,8 @@ FormatD: Mailer: + site: + siteNodeName: 'format-d-website' + contentRepositoryId: 'default' dsn: 'smtp://user:pass@smtp.example.com:25' localEmbed: false attachEmbeddedImages: false From 133740344bd62c8dc55819d9925b52cd81699599 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Fri, 26 Apr 2024 09:28:39 +0200 Subject: [PATCH 06/17] chore: require guzzlehttp/guzzle. --- composer.json | 197 +++++++++++++++++++++++++------------------------- 1 file changed, 99 insertions(+), 98 deletions(-) diff --git a/composer.json b/composer.json index 36b49bf..71122e8 100644 --- a/composer.json +++ b/composer.json @@ -1,99 +1,100 @@ { - "name": "formatd/mailer", - "description": "Wrapper for Swiftmailer in Neos Flow Projects", - "type": "neos-package", - "license": "MIT", - "require": { - "php": ">=8.1", - "neos/flow": "^8.0 || ^9.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" - ] - } -} + "name": "formatd/mailer", + "description": "Wrapper for Swiftmailer in Neos Flow Projects", + "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 From 4ab7b83893534065cb4d97b3622e95e502e5ac2f Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Fri, 26 Apr 2024 10:26:39 +0200 Subject: [PATCH 07/17] chore: use guzzle client for requests. add logging. --- Classes/Service/AbstractMailerService.php | 63 +++++++++++++---------- Configuration/Objects.yaml | 8 +++ Configuration/Settings.Neos.Flow.yaml | 14 +++++ composer.json | 2 +- 4 files changed, 60 insertions(+), 27 deletions(-) create mode 100644 Configuration/Objects.yaml create mode 100644 Configuration/Settings.Neos.Flow.yaml diff --git a/Classes/Service/AbstractMailerService.php b/Classes/Service/AbstractMailerService.php index 4f288f6..3e992b0 100644 --- a/Classes/Service/AbstractMailerService.php +++ b/Classes/Service/AbstractMailerService.php @@ -1,17 +1,17 @@ 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; } - protected function send($subject, $to, $from, $text, $html) { + protected function send($subject, $to, $from, $text, $html) + { $mail = $this->mailFactory->createMail( $subject, $to, @@ -66,12 +78,12 @@ protected function send($subject, $to, $from, $text, $html) { } $this->mailer->send($mail); - } + } - /** - * @param array|Address|string $to - */ - public function sendTest($to) + /** + * @param array|Address|string $to + */ + public function sendTest($to) { $contentRepository = $this->contentRepositoryService->getContentRepository(); $workspace = $this->contentRepositoryService->getWorkspace($contentRepository); @@ -88,21 +100,20 @@ public function sendTest($to) ); $this->mailer->send($mail); - } + } protected function getHtml(Node $emailNode) { $emailUri = $this->contentRepositoryService->getNodeUri($emailNode); try { - $response = $this->browser->request($emailUri); - } catch (CurlEngineException $e) { - // log $response->getStatusCode() - $response = $this->browser->request($emailUri); + $response = $this->client->request('GET', $emailUri . 'sdkf'); + } catch (ClientException $e) { + $this->mailerLogger->error("MAILER_ERROR :: " . $e->getResponse()->getBody()->getContents()); } if ($response->getStatusCode() !== 200) { - // log $response->getStatusCode() + $this->mailerLogger->error("MAILER_ERROR :: " . $response->getStatusCode()); } $newsletterContent = $response->getBody()->getContents(); 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/composer.json b/composer.json index 71122e8..afe4ab8 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "formatd/mailer", - "description": "Wrapper for Swiftmailer in Neos Flow Projects", + "description": "Send mails in Neos with symfony mailer", "type": "neos-package", "license": "MIT", "require": { From 717d48e347ad55bd3b3f679323c343e43fe6974b Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Fri, 26 Apr 2024 18:55:37 +0200 Subject: [PATCH 08/17] chore: add functionality to send node based e-mail template with neos.form.builder and custom e-mail finisher. --- .../DataSource/EmailReferenceDataSource.php | 45 ++++++ Classes/Form/Finishers/EmailFinisher.php | 129 ++++++++++++++++++ Classes/Form/FormElements/AssetAttachment.php | 37 +++++ Classes/Service/AbstractMailerService.php | 39 ++++-- Classes/Traits/InterceptionTrait.php | 2 +- Configuration/Settings.Neos.Form.yaml | 13 ++ Configuration/Settings.yaml | 5 +- NodeTypes/Content/AssetAttachment.yaml | 23 ++++ NodeTypes/Content/EmailFinisher.yaml | 32 +++++ .../Overrides/Elements/AssetAttachment.fusion | 19 +++ .../Elements/AssetAttachment.fusion | 3 + .../Finisher/EmailFinisher.Definition.fusion | 20 +++ Resources/Private/Fusion/Root.fusion | 1 + 13 files changed, 353 insertions(+), 15 deletions(-) create mode 100644 Classes/DataSource/EmailReferenceDataSource.php create mode 100644 Classes/Form/Finishers/EmailFinisher.php create mode 100644 Classes/Form/FormElements/AssetAttachment.php create mode 100644 Configuration/Settings.Neos.Form.yaml create mode 100644 NodeTypes/Content/AssetAttachment.yaml create mode 100644 NodeTypes/Content/EmailFinisher.yaml create mode 100644 Resources/Private/Fusion/Overrides/Elements/AssetAttachment.fusion create mode 100644 Resources/Private/Fusion/Overrides/Neos.Form.Builder/Elements/AssetAttachment.fusion create mode 100644 Resources/Private/Fusion/Overrides/Neos.Form.Builder/Finisher/EmailFinisher.Definition.fusion diff --git a/Classes/DataSource/EmailReferenceDataSource.php b/Classes/DataSource/EmailReferenceDataSource.php new file mode 100644 index 0000000..ce61cd7 --- /dev/null +++ b/Classes/DataSource/EmailReferenceDataSource.php @@ -0,0 +1,45 @@ +parents('[instanceof FormatD.DesignSystem:Site]') + ->find('[instanceof FormatD.Mailer:Document.Email]') + ->get(); + + $data = []; + foreach ($emailNodes as $emailNode) { + $data[] = [ + 'label' => $emailNode->getLabel(), + 'value' => $emailNode->nodeAggregateId, + 'icon' => static::$icon + ]; + } + + return $data; + } +} diff --git a/Classes/Form/Finishers/EmailFinisher.php b/Classes/Form/Finishers/EmailFinisher.php new file mode 100644 index 0000000..f19f32d --- /dev/null +++ b/Classes/Form/Finishers/EmailFinisher.php @@ -0,0 +1,129 @@ + 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) + { + $markers = preg_match_all('#(\#\#\#)(.*?)(\#\#\#)#', $emailHtml, $matches); + + if(isset($matches[2])) { + foreach($matches[2] as $match) { + if(isset($formValues[$match])){ + $emailHtml = preg_replace('/(\#\#\#)('.$match.')(\#\#\#)/', $formValues[$match], $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 3e992b0..32fcc2f 100644 --- a/Classes/Service/AbstractMailerService.php +++ b/Classes/Service/AbstractMailerService.php @@ -63,7 +63,7 @@ protected function setSslVerification(array $clientOptions): array return $clientOptions; } - protected function send($subject, $to, $from, $text, $html) + public function send($subject, $to, $from, $text, $html, $replyTo = null, $cc = null, $bcc = null) { $mail = $this->mailFactory->createMail( $subject, @@ -73,6 +73,18 @@ protected function send($subject, $to, $from, $text, $html) $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); } @@ -85,24 +97,29 @@ protected function send($subject, $to, $from, $text, $html) */ public function sendTest($to) { - $contentRepository = $this->contentRepositoryService->getContentRepository(); - $workspace = $this->contentRepositoryService->getWorkspace($contentRepository); - $contentGraph = $this->contentRepositoryService->getContentGraph($contentRepository); - - $testEmailNodes = $contentGraph->findNodeAggregateById($workspace->currentContentStreamId, NodeAggregateId::fromString($this->configuration['templateNodes']['test'])); - $mail = $this->mailFactory->createMail( 'Format D Mailer // Test E-Mail', $to, $this->defaultFromAddress, 'Hello guys, this is a test e-mail', - $this->getHtml($testEmailNodes->getNodes()[0]) + $this->getHtml($this->getNodeById($this->configuration['templateNodes']['test'])) ); $this->mailer->send($mail); } - protected function getHtml(Node $emailNode) + public function getNodeById(string $id) + { + $contentRepository = $this->contentRepositoryService->getContentRepository(); + $workspace = $this->contentRepositoryService->getWorkspace($contentRepository); + $contentGraph = $this->contentRepositoryService->getContentGraph($contentRepository); + + $nodesById = $contentGraph->findNodeAggregateById($workspace->currentContentStreamId, NodeAggregateId::fromString($id)); + + return $nodesById->getNodes()[0]; + } + + public function getHtml(Node $emailNode) { $emailUri = $this->contentRepositoryService->getNodeUri($emailNode); @@ -118,10 +135,6 @@ protected function getHtml(Node $emailNode) $newsletterContent = $response->getBody()->getContents(); - # @todo add handling for marker in template (?) - #$newsletterContent = preg_replace_callback('#\{[a-zA-Z]+\}#', function ($match) use ($recipientData) { - # return $recipientData->replaceMarker($match[0]); - #}, $newsletterContent); # attach images if ($this->configuration['attachEmbeddedImages']) { diff --git a/Classes/Traits/InterceptionTrait.php b/Classes/Traits/InterceptionTrait.php index e41b76b..3324394 100644 --- a/Classes/Traits/InterceptionTrait.php +++ b/Classes/Traits/InterceptionTrait.php @@ -8,7 +8,7 @@ trait InterceptionTrait { - #[Flow\InjectConfiguration(package: "FormatD.Mailer", path: "")] + #[Flow\InjectConfiguration(package: "FormatD.Mailer")] protected array $configuration = []; /** 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.yaml b/Configuration/Settings.yaml index c8d8ec6..6dc44ca 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -15,6 +15,9 @@ FormatD: recipients: [] defaultFrom: address: 'example@example.com' - name: 'Example' + 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/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 index f297248..abc63d6 100644 --- a/Resources/Private/Fusion/Root.fusion +++ b/Resources/Private/Fusion/Root.fusion @@ -1,3 +1,4 @@ +include: Overrides/**/* include: Fragments/**/* include: nodetypes://FormatD.Mailer/**/*.fusion From 092e9867ac726a2a4d3f2b97a980fa7c0099c1a3 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Mon, 29 Apr 2024 12:32:34 +0200 Subject: [PATCH 09/17] chore: fix subject for intercepted e-mails. --- Classes/Traits/InterceptionTrait.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Classes/Traits/InterceptionTrait.php b/Classes/Traits/InterceptionTrait.php index 3324394..7d4439a 100644 --- a/Classes/Traits/InterceptionTrait.php +++ b/Classes/Traits/InterceptionTrait.php @@ -40,18 +40,19 @@ protected function interceptAll($mail) foreach ($this->configuration['interceptAll']['noInterceptPatterns'] as $pattern) { if (preg_match($pattern, key($originalTo))) { - $mail->to(new Address('somewhere@bla.com')); - $mail->bcc(new Address('somewhere@bla.com')); + $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 = key($originalTo) . ($originalCc ? ' CC: ' . key($originalCc) : '') . ($originalBcc ? ' BCC: ' . key($originalBcc) : ''); + $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@bla.com')); - $mail->bcc(new Address('somewhere@bla.com')); + $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); From 496477bb1fec4db804482a6e06892d6233acaa71 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Tue, 16 Jul 2024 15:13:20 +0200 Subject: [PATCH 10/17] chore: refactor `ContentRepositoryService` after `ContentRepositoryId` was moved to `SharedModel` namespace. --- Classes/Service/ContentRepositoryService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/ContentRepositoryService.php b/Classes/Service/ContentRepositoryService.php index 5e3ba32..d3dd2fd 100644 --- a/Classes/Service/ContentRepositoryService.php +++ b/Classes/Service/ContentRepositoryService.php @@ -5,7 +5,7 @@ use Neos\Flow\Annotations as Flow; use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; -use Neos\ContentRepository\Core\Factory\ContentRepositoryId; +use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId; use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphInterface; use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; From dcb2b84cf791c2b2f9a0030d1718b552b731de4e Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Tue, 16 Jul 2024 16:38:42 +0200 Subject: [PATCH 11/17] chore: fix nodeId references for neos 9 beta 10. --- Classes/DataSource/EmailReferenceDataSource.php | 10 ++++++---- Classes/Service/AbstractMailerService.php | 15 +++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Classes/DataSource/EmailReferenceDataSource.php b/Classes/DataSource/EmailReferenceDataSource.php index ce61cd7..547c105 100644 --- a/Classes/DataSource/EmailReferenceDataSource.php +++ b/Classes/DataSource/EmailReferenceDataSource.php @@ -6,7 +6,8 @@ use Neos\Neos\Service\DataSource\AbstractDataSource; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; -class EmailReferenceDataSource extends AbstractDataSource { +class EmailReferenceDataSource extends AbstractDataSource +{ /** * @var string @@ -24,18 +25,19 @@ class EmailReferenceDataSource extends AbstractDataSource { * @return array|mixed * @throws \Neos\Eel\Exception */ - public function getData(Node $node = null, array $arguments = []) { + public function getData(Node $node = null, array $arguments = []) + { $q = new FlowQuery([$node]); $emailNodes = $q ->parents('[instanceof FormatD.DesignSystem:Site]') - ->find('[instanceof FormatD.Mailer:Document.Email]') + ->find('[instanceof FormatD.Mailer:Document.Email]') ->get(); $data = []; foreach ($emailNodes as $emailNode) { $data[] = [ 'label' => $emailNode->getLabel(), - 'value' => $emailNode->nodeAggregateId, + 'value' => $emailNode->aggregateId, 'icon' => static::$icon ]; } diff --git a/Classes/Service/AbstractMailerService.php b/Classes/Service/AbstractMailerService.php index 32fcc2f..1d2055e 100644 --- a/Classes/Service/AbstractMailerService.php +++ b/Classes/Service/AbstractMailerService.php @@ -108,16 +108,15 @@ public function sendTest($to) $this->mailer->send($mail); } - public function getNodeById(string $id) - { - $contentRepository = $this->contentRepositoryService->getContentRepository(); - $workspace = $this->contentRepositoryService->getWorkspace($contentRepository); - $contentGraph = $this->contentRepositoryService->getContentGraph($contentRepository); + public function getNodeById(string $id) + { + $contentRepository = $this->contentRepositoryService->getContentRepository(); + $contentGraph = $this->contentRepositoryService->getContentGraph($contentRepository); - $nodesById = $contentGraph->findNodeAggregateById($workspace->currentContentStreamId, NodeAggregateId::fromString($id)); + $nodesById = $contentGraph->findNodeAggregateById(NodeAggregateId::fromString($id)); - return $nodesById->getNodes()[0]; - } + return $nodesById->getNodes()[0]; + } public function getHtml(Node $emailNode) { From f4746b612c0473a5efe26109171b2a70386317b6 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Wed, 24 Jul 2024 11:29:53 +0200 Subject: [PATCH 12/17] chore: `getContentGraph` not need the workspace name as an argument. --- Classes/Service/ContentRepositoryService.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Classes/Service/ContentRepositoryService.php b/Classes/Service/ContentRepositoryService.php index d3dd2fd..e8154d6 100644 --- a/Classes/Service/ContentRepositoryService.php +++ b/Classes/Service/ContentRepositoryService.php @@ -78,10 +78,10 @@ public function getWorkspace(ContentRepository $contentRepository, string $works return $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::fromString($workspaceName)); } - public function getContentGraph(ContentRepository $contentRepository): ContentGraphInterface - { - return $contentRepository->getContentGraph(); - } + public function getContentGraph(ContentRepository $contentRepository, string $workspaceName = 'live'): ContentGraphInterface + { + return $contentRepository->getContentGraph(WorkspaceName::fromString($workspaceName)); + } public function getNodeUri(Node $node, $arguments = [], $absolute = true, $format = 'html') { From 2541a4b1f61110951c3a6c5bf57197cb10a2c96f Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Mon, 19 Aug 2024 15:40:15 +0200 Subject: [PATCH 13/17] chore: changes for neos 9 beta-11. rewrite `uriForNode()`. See https://discuss.neos.io/t/neos-9-beta-11-release/6618 --- .../DataSource/EmailReferenceDataSource.php | 6 +++- Classes/Form/Finishers/EmailFinisher.php | 17 +++++++--- Classes/Service/AbstractMailerService.php | 4 +-- Classes/Service/ContentRepositoryService.php | 34 ++++++++++--------- 4 files changed, 37 insertions(+), 24 deletions(-) diff --git a/Classes/DataSource/EmailReferenceDataSource.php b/Classes/DataSource/EmailReferenceDataSource.php index 547c105..6c91d81 100644 --- a/Classes/DataSource/EmailReferenceDataSource.php +++ b/Classes/DataSource/EmailReferenceDataSource.php @@ -5,6 +5,7 @@ use Neos\Flow\Annotations as Flow; use Neos\Neos\Service\DataSource\AbstractDataSource; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\Neos\Domain\NodeLabel\NodeLabelGeneratorInterface; class EmailReferenceDataSource extends AbstractDataSource { @@ -19,6 +20,9 @@ class EmailReferenceDataSource extends AbstractDataSource */ protected static $icon = 'icon-envelope'; + #[Flow\Inject] + protected NodeLabelGeneratorInterface $nodeLabelGenerator; + /** * @param Node|null $node * @param array $arguments @@ -36,7 +40,7 @@ public function getData(Node $node = null, array $arguments = []) $data = []; foreach ($emailNodes as $emailNode) { $data[] = [ - 'label' => $emailNode->getLabel(), + 'label' => $this->nodeLabelGenerator->getLabel($emailNode), 'value' => $emailNode->aggregateId, 'icon' => static::$icon ]; diff --git a/Classes/Form/Finishers/EmailFinisher.php b/Classes/Form/Finishers/EmailFinisher.php index f19f32d..10e3cc7 100644 --- a/Classes/Form/Finishers/EmailFinisher.php +++ b/Classes/Form/Finishers/EmailFinisher.php @@ -114,13 +114,20 @@ protected function executeInternal() protected function replaceMarkerWithFormValues($formValues, $emailHtml) { - $markers = preg_match_all('#(\#\#\#)(.*?)(\#\#\#)#', $emailHtml, $matches); + preg_match_all('#\#\#\#(.*?)\#\#\##', $emailHtml, $matches); - if(isset($matches[2])) { - foreach($matches[2] as $match) { - if(isset($formValues[$match])){ - $emailHtml = preg_replace('/(\#\#\#)('.$match.')(\#\#\#)/', $formValues[$match], $emailHtml); + 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); } } diff --git a/Classes/Service/AbstractMailerService.php b/Classes/Service/AbstractMailerService.php index 1d2055e..b713472 100644 --- a/Classes/Service/AbstractMailerService.php +++ b/Classes/Service/AbstractMailerService.php @@ -120,10 +120,10 @@ public function getNodeById(string $id) public function getHtml(Node $emailNode) { - $emailUri = $this->contentRepositoryService->getNodeUri($emailNode); + $emailUri = $this->contentRepositoryService->uriForNode($emailNode); try { - $response = $this->client->request('GET', $emailUri . 'sdkf'); + $response = $this->client->request('GET', $emailUri); } catch (ClientException $e) { $this->mailerLogger->error("MAILER_ERROR :: " . $e->getResponse()->getBody()->getContents()); } diff --git a/Classes/Service/ContentRepositoryService.php b/Classes/Service/ContentRepositoryService.php index e8154d6..fff46e1 100644 --- a/Classes/Service/ContentRepositoryService.php +++ b/Classes/Service/ContentRepositoryService.php @@ -10,13 +10,16 @@ use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\SharedModel\Node\NodeAddress; use GuzzleHttp\Psr7\Uri; -use Neos\Flow\Mvc\Routing\UriBuilder; +use Neos\Flow\Mvc\ActionRequest; +use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; +use Neos\Neos\FrontendRouting\Options; use Psr\Http\Message\ServerRequestFactoryInterface; use Neos\Flow\Http\ServerRequestAttributes; use Neos\Flow\Mvc\ActionRequestFactory; use Neos\Flow\Mvc\Routing\Dto\RouteParameters; -use Neos\Neos\FrontendRouting\NodeAddressFactory; +use Psr\Http\Message\UriInterface; /** * Various helper for CR and nodes @@ -40,7 +43,10 @@ class ContentRepositoryService { #[Flow\Inject] protected ServerRequestFactoryInterface $serverRequestFactory; - protected UriBuilder $uriBuilder; + #[Flow\Inject] + protected NodeUriBuilderFactory $nodeUriBuilderFactory; + + protected ActionRequest $actionRequest; public function initializeObject() { @@ -61,10 +67,7 @@ public function initializeObject() $reflectedHttpRequestAttributes->setValue($httpRequest, $httpRequestAttributes); } - $request = $this->actionRequestFactory->createActionRequest($httpRequest); - $uriBuilder = new UriBuilder(); - $uriBuilder->setRequest($request); - $this->uriBuilder = $uriBuilder; + $this->actionRequest = $this->actionRequestFactory->createActionRequest($httpRequest); } public function getContentRepository(string $contentRepositoryId = 'default'): ContentRepository @@ -83,16 +86,15 @@ public function getContentGraph(ContentRepository $contentRepository, string $wo return $contentRepository->getContentGraph(WorkspaceName::fromString($workspaceName)); } - public function getNodeUri(Node $node, $arguments = [], $absolute = true, $format = 'html') + public function uriForNode(Node $node, ActionRequest $actionRequest = null, $absolute = true, $format = 'html'): UriInterface { - $nodeAddressFactory = NodeAddressFactory::create($this->getContentRepository()); - $nodeAddress = $nodeAddressFactory->createFromNode($node); - - return $this->uriBuilder - ->setArguments($arguments) - ->setCreateAbsoluteUri($absolute) - ->setFormat($format) - ->uriFor('show', ['node' => $nodeAddress], 'Frontend\Node', 'Neos.Neos') + $request = $actionRequest ? $actionRequest : $this->actionRequest; + return $this->nodeUriBuilderFactory + ->forActionRequest($request) + ->uriFor( + NodeAddress::fromNode($node), + $absolute ? Options::createEmpty()->withCustomFormat($format)->withForceAbsolute() : Options::createEmpty()->withCustomFormat($format) + ) ; } } From c3aa5706a41bcf95320c052469c90c872c268e22 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Mon, 16 Sep 2024 08:44:42 +0200 Subject: [PATCH 14/17] docs: update readme for new package version with symfony mailer. --- README.md | 65 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 82b0802..ddf7a18 100644 --- a/README.md +++ b/README.md @@ -1,41 +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. - - -## 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 | >= 8.x | -| 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' @@ -43,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### + +### 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. ## Intercept all e-mails in a dev environment -Configure mailer to intercept all mails send by your neos installation (not only by the service). +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: ``` @@ -78,4 +75,24 @@ FormatD: ## 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 | +|-----------------|------------------------| +| 3.0.x | >= 9.x | +| 2.0.x | >= 8.x < 9.x | +| 1.1.x | >= 6.x | +| 1.0.x | 4.x - 5.x | \ No newline at end of file From 6d4921152b3d97b8fa4aef06fce972a355399b40 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Mon, 16 Sep 2024 09:01:25 +0200 Subject: [PATCH 15/17] docs: fix release notes. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index ddf7a18..fe10000 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,6 @@ Releases und compatibility: | Package Version | Neos Flow Version | |-----------------|------------------------| -| 3.0.x | >= 9.x | -| 2.0.x | >= 8.x < 9.x | +| 2.0.x | >= 9.x | | 1.1.x | >= 6.x | | 1.0.x | 4.x - 5.x | \ No newline at end of file From bae1983c1fae5874f4b7301738d9a01b9b8342b8 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Mon, 28 Oct 2024 09:03:03 +0100 Subject: [PATCH 16/17] chore: Neos Beta14: replace `getWorkspaceFinder()` with `findWorkspaceByName()`. --- Classes/Service/ContentRepositoryService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Service/ContentRepositoryService.php b/Classes/Service/ContentRepositoryService.php index fff46e1..89982cb 100644 --- a/Classes/Service/ContentRepositoryService.php +++ b/Classes/Service/ContentRepositoryService.php @@ -78,7 +78,7 @@ public function getContentRepository(string $contentRepositoryId = 'default'): C public function getWorkspace(ContentRepository $contentRepository, string $workspaceName = 'live'): Workspace { - return $contentRepository->getWorkspaceFinder()->findOneByName(WorkspaceName::fromString($workspaceName)); + return $contentRepository->findWorkspaceByName(WorkspaceName::fromString($workspaceName)); } public function getContentGraph(ContentRepository $contentRepository, string $workspaceName = 'live'): ContentGraphInterface From 933bdf486421b58ab774e099c0d737999b265944 Mon Sep 17 00:00:00 2001 From: Sabrina Sauter Date: Wed, 20 Nov 2024 08:30:53 +0100 Subject: [PATCH 17/17] chore: getNodeById() now fetches node from subgraph in correct dimension --- Classes/Service/AbstractMailerService.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Classes/Service/AbstractMailerService.php b/Classes/Service/AbstractMailerService.php index b713472..1f6d9f3 100644 --- a/Classes/Service/AbstractMailerService.php +++ b/Classes/Service/AbstractMailerService.php @@ -4,6 +4,7 @@ use Neos\Flow\Core\Bootstrap; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; +use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; use Symfony\Component\Mime\Address; @@ -113,9 +114,15 @@ public function getNodeById(string $id) $contentRepository = $this->contentRepositoryService->getContentRepository(); $contentGraph = $this->contentRepositoryService->getContentGraph($contentRepository); - $nodesById = $contentGraph->findNodeAggregateById(NodeAggregateId::fromString($id)); + $generalizations = $contentRepository->getVariationGraph()->getRootGeneralizations(); + $dimensionSpacePoint = reset($generalizations); - return $nodesById->getNodes()[0]; + $subgraph = $contentGraph->getSubgraph( + $dimensionSpacePoint, + VisibilityConstraints::withoutRestrictions(), + ); + + return $subgraph->findNodeById(NodeAggregateId::fromString($id)); } public function getHtml(Node $emailNode) @@ -134,7 +141,6 @@ public function getHtml(Node $emailNode) $newsletterContent = $response->getBody()->getContents(); - # attach images if ($this->configuration['attachEmbeddedImages']) { $newsletterContent = $this->attachHtmlInlineImages($newsletterContent);