From 8aa35d5e9e1fe8975dd37051a9ccb49eafdcd281 Mon Sep 17 00:00:00 2001 From: Marius Klocke Date: Wed, 15 Jan 2025 19:50:57 +0100 Subject: [PATCH] Add HtmlMailRenderer --- src/Application/Email/HtmlMailRenderer.php | 154 ++++++++++++++++++ .../Handler/SendPasswordResetMailHandler.php | 32 +++- 2 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 src/Application/Email/HtmlMailRenderer.php diff --git a/src/Application/Email/HtmlMailRenderer.php b/src/Application/Email/HtmlMailRenderer.php new file mode 100644 index 0000000..2e66a05 --- /dev/null +++ b/src/Application/Email/HtmlMailRenderer.php @@ -0,0 +1,154 @@ + [ + 'margin' => '0', + 'padding' => '0', + 'font-family' => 'Arial, sans-serif', + ], + 'table' => [ + 'width' => '100%', + 'border-collapse' => 'collapse', + ], + 'img' => [ + 'display' => 'block', + 'max-width' => '100%', + 'height' => 'auto', + ], + '.container' => [ + 'width' => '100%', + 'max-width' => '600px', + 'margin' => '0 auto', + 'padding' => '20px' + ], + '.header' => [ + 'text-align' => 'center', + 'padding' => '40px 0', + ], + '.content' => [ + 'background-color' => '#ffffff', + 'padding' => '20px', + 'text-align' => 'center', + ], + '.cta-button' => [ + 'background-color' => '#ffc700', + 'color' => '#000000', + 'padding' => '15px 25px', + 'text-decoration' => 'none', + 'font-weight' => 'bold', + 'border-radius' => '5px', + 'display' => 'inline-block', + 'margin-top' => '20px', + ], + '.footer' => [ + 'text-align' => 'center', + 'padding' => '20px', + 'font-size' => '12px', + 'color' => '#888888' + ] + ]; + + public function render(array $data): string + { + $document = new DOMDocument(); + $html = $this->addElement($document, $document, 'html'); + + $this->addHeadElement($document, $html, $data); + $this->addBodyElement($document, $html, $data); + + return $document->saveHTML(); + } + + private function addHeadElement(DOMDocument $document, DOMNode $parent, array $data): DOMElement + { + $head = $this->addElement($document, $parent, 'head'); + + $this->addElement($document, $head, 'meta', ['charset' => 'UTF-8']); + $this->addElement($document, $head, 'meta', ['name' => 'viewport', 'value' => 'width=device-width, initial-scale=1.0']); + $this->addElement($document, $head, 'title', [], $data['subject']); + + return $head; + } + + private function addBodyElement(DOMDocument $document, DOMNode $parent, array $data): DOMElement + { + $body = $this->addElement($document, $parent, 'body'); + + // Table + Container + $table = $this->addElement($document, $body, 'table', [ + 'role' => 'presentation', + 'width' => '100%', + 'cellspacing' => '0', + 'cellpadding' => '0', + 'border' => '0' + ]); + $tableRow = $this->addElement($document, $table, 'tr'); + $tableCol = $this->addElement($document, $tableRow, 'td', ['align' => 'center']); + $container = $this->addElement($document, $tableCol, 'div', ['class' => 'container']); + + // Header + $header = $this->addElement($document, $container, 'div', ['class' => 'header']); + $this->addElement($document, $header, 'img', [ + 'src' => $data['logo']['src'], + 'alt' => $data['logo']['alt'] + ]); + + // Content + $content = $this->addElement($document, $container, 'div', ['class' => 'content']); + $this->addElement($document, $content, 'h1', [], $data['subject']); + $this->addElement($document, $content, 'p', [], $data['content']['text']); + $this->addElement($document, $content, 'a', [ + 'class' => 'cta-button', + 'href' => $data['content']['action']['href'] + ], $data['content']['action']['label']); + + // Footer + $footer = $this->addElement($document, $container, 'div', ['class' => 'footer']); + foreach ($data['footer']['hints'] as $hint) { + $this->addElement($document, $footer, 'p', [], $hint); + } + + return $body; + } + + private function addElement(DOMDocument $document, DOMNode $parent, string $tag, array $attributes = [], ?string $text = null): DOMElement + { + $element = $document->createElement($tag); + + $styles = []; + $styles = array_merge($styles, $this->styles[$element->tagName] ?? []); + + if (isset($attributes['class'])) { + $styles = array_merge($styles, $this->styles['.' . $attributes['class']] ?? []); + unset($attributes['class']); + } + + foreach ($attributes as $name => $value) { + $element->setAttribute($name, $value); + } + + if (count($styles) > 0) { + $values = []; + foreach ($styles as $name => $value) { + $values[] = "$name:$value"; + } + $element->setAttribute('style', implode(';', $values)); + } + + if ($text !== null) { + $element->appendChild($document->createTextNode($text)); + } + + $parent->appendChild($element); + + return $element; + } +} \ No newline at end of file diff --git a/src/Application/Handler/SendPasswordResetMailHandler.php b/src/Application/Handler/SendPasswordResetMailHandler.php index dc5db4c..da0120e 100644 --- a/src/Application/Handler/SendPasswordResetMailHandler.php +++ b/src/Application/Handler/SendPasswordResetMailHandler.php @@ -5,6 +5,7 @@ use DateTimeImmutable; use HexagonalPlayground\Application\Command\SendPasswordResetMailCommand; +use HexagonalPlayground\Application\Email\HtmlMailRenderer; use HexagonalPlayground\Application\Email\HtmlUtilsTrait; use HexagonalPlayground\Application\Email\MailerInterface; use HexagonalPlayground\Application\Security\AccessLinkGeneratorInterface; @@ -42,6 +43,8 @@ public function __construct(UserRepositoryInterface $userRepository, TemplateRen */ public function __invoke(SendPasswordResetMailCommand $command): array { + $renderer = new HtmlMailRenderer(); + try { $user = $this->userRepository->findByEmail($command->getEmail()); } catch (NotFoundException $e) { @@ -53,12 +56,29 @@ public function __invoke(SendPasswordResetMailCommand $command): array $recipient = [$user->getEmail() => $user->getFullName()]; $subject = 'Reset your password'; - $mailBody = $this->templateRenderer->render('PasswordReset.html.php', [ - 'receiver' => $user->getFirstName(), - 'targetLink' => $targetLink, - 'validUntil' => $expiresAt - ]); - $subject = $this->extractTitle($mailBody); + $mailData = [ + 'subject' => 'Passwort zurücksetzen', + 'logo' => [ + 'src' => 'https://www.wildeligabremen.de/wp-content/uploads/2023/05/cropped-Logo-mit-Schrift_30-Jahre-Kopie_2-e1683381765583.jpg', + 'alt' => 'Wilde Liga Bremen', + ], + 'content' => [ + 'text' => sprintf('Hey %s, nutze den folgenden Link um ein neues Passwort zu vergeben.', $user->getFirstName()), + 'action' => [ + 'href' => $targetLink, + 'label' => 'Neues Passwort setzen' + ] + ], + 'footer' => [ + 'hints' => [ + sprintf('Der Link ist gültig bis: %s', $expiresAt->format('d.m.Y H:i')), + 'Bitte leite diese E-Mail nicht an eine andere Person weiter.', + 'Wenn du diese E-Mail wiederholt bekommst ohne sie selbst angefordert zu haben, melde dich bitte beim Admin-Team.' + ] + ] + ]; + $mailBody = $renderer->render($mailData); + $subject = $mailData['subject']; $message = $this->mailer->createMessage($recipient, $subject, $mailBody); $this->mailer->send($message);