diff --git a/.env.example b/.env.example
index c59a2a1..8740ee5 100644
--- a/.env.example
+++ b/.env.example
@@ -17,4 +17,12 @@ BEE_API_KEY=
# Bee Setup
INIT_USER_NAME=admin
INIT_USER_PASSWORD=password
-INIT_USER_MAIL=admin@test.com
\ No newline at end of file
+INIT_USER_MAIL=admin@test.com
+
+# Mail
+MAIL_DEFAULT_SENDER=info@bee.local
+SMTP_USERNAME=smtp@stack-up.de
+SMTP_PASSWORD=
+SMTP_HOST= #srv-mail-01.lab.jonasf.de
+SMTP_PORT=465
+SMTP_ENCRYPTION=ssl
\ No newline at end of file
diff --git a/composer.json b/composer.json
index cf53759..997adfe 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,9 @@
"monolog/monolog": "^3.4",
"laminas/laminas-mail": "^2.23",
"teewurst/pipeline": "^3.0",
- "guzzlehttp/guzzle": "^7.8"
+ "guzzlehttp/guzzle": "^7.8",
+ "nette/mail": "^4.0",
+ "latte/latte": "^3.0"
},
"autoload": {
"psr-4": {
diff --git a/config/autoload/mail.global.php b/config/autoload/mail.global.php
new file mode 100644
index 0000000..b7b8ca3
--- /dev/null
+++ b/config/autoload/mail.global.php
@@ -0,0 +1,16 @@
+ [
+ 'default' => [
+ 'sender' => $_ENV['MAIL_DEFAULT_SENDER']
+ ],
+ 'smtp-server' => [
+ 'host' => $_ENV['SMTP_HOST'],
+ 'port' => (int)$_ENV['SMTP_PORT'] ?? 25,
+ 'encryption' => $_ENV['SMTP_ENCRYPTION'],
+ 'username' => $_ENV['SMTP_USERNAME'],
+ 'password' => $_ENV['SMTP_PASSWORD'],
+ ]
+ ]
+];
\ No newline at end of file
diff --git a/config/config.php b/config/config.php
index a4c18d7..1031b20 100644
--- a/config/config.php
+++ b/config/config.php
@@ -52,6 +52,7 @@ $aggregator = new ConfigAggregator([
\Bee\Infrastructure\Rbac\ConfigProvider::class,
\Bee\Infrastructure\Request\ConfigProvider::class,
\Bee\Infrastructure\Session\ConfigProvider::class,
+ \Bee\Infrastructure\Mail\ConfigProvider::class,
// HandlingDomain
\Bee\Handling\User\ConfigProvider::class,
diff --git a/data/mails/registration/assets/icon.png b/data/mails/registration/assets/icon.png
new file mode 100644
index 0000000..07e8b9d
Binary files /dev/null and b/data/mails/registration/assets/icon.png differ
diff --git a/data/mails/registration/template.latte b/data/mails/registration/template.latte
new file mode 100644
index 0000000..565a7f5
--- /dev/null
+++ b/data/mails/registration/template.latte
@@ -0,0 +1,63 @@
+
+
+
+ Willkommen beim Beekeeper!
+
+
+
+
+
+
Hallo {$username},
+
+
Herzlich willkommen beim Beekeeper!
+
+
Bitte klicke auf diesen Link um dein Passwort festzulegen.
+
Danach ist deine Registierung abgeschlossen und du kannst loslegen!
+
+
Mit fleißigen Grüßen,
+
Der Beekeeper
+
+
+
diff --git a/src/ApiDomain/External/Authentication/config/service_manager.php b/src/ApiDomain/External/Authentication/config/service_manager.php
index d90ba16..61124b9 100644
--- a/src/ApiDomain/External/Authentication/config/service_manager.php
+++ b/src/ApiDomain/External/Authentication/config/service_manager.php
@@ -1,5 +1,7 @@
[
+ // Formatter
+ ConfirmRegistrationFormatter::class => AutoWiringFactory::class,
+ LoginUserFormatter::class => AutoWiringFactory::class,
+
// Handler
LoginUserHandler::class => AutoWiringFactory::class,
LogoutUserHandler::class => AutoWiringFactory::class,
diff --git a/src/ApiDomain/External/Authentication/src/Formatter/ConfirmRegistrationFormatter.php b/src/ApiDomain/External/Authentication/src/Formatter/ConfirmRegistrationFormatter.php
new file mode 100644
index 0000000..daa01f8
--- /dev/null
+++ b/src/ApiDomain/External/Authentication/src/Formatter/ConfirmRegistrationFormatter.php
@@ -0,0 +1,27 @@
+ $user->getId()->toString(),
+ 'username' => $user->getUsername(),
+ 'roleIdentifier' => $user->getRole()->getIdentifier(),
+ 'createdAt' => $user->getCreatedAt()->format(DateTime::ATOM),
+ 'updatedAt' => $user->getUpdatedAt()->format(DateTime::ATOM)
+ ];
+
+ $userArray['permissions'] = [];
+
+ foreach ($user->getRole()->getPermissions()->toArray() as $permission) {
+ $userArray['permissions'][] = $permission->getIdentifier();
+ }
+
+ return $userArray;
+ }
+}
diff --git a/src/ApiDomain/External/Authentication/src/Formatter/LoginUserFormatter.php b/src/ApiDomain/External/Authentication/src/Formatter/LoginUserFormatter.php
new file mode 100644
index 0000000..772dd23
--- /dev/null
+++ b/src/ApiDomain/External/Authentication/src/Formatter/LoginUserFormatter.php
@@ -0,0 +1,14 @@
+ $session->getId()->toString()
+ ];
+ }
+}
diff --git a/src/ApiDomain/External/Authentication/src/Handler/ConfirmRegistrationHandler.php b/src/ApiDomain/External/Authentication/src/Handler/ConfirmRegistrationHandler.php
index 9520d11..15b7ee4 100644
--- a/src/ApiDomain/External/Authentication/src/Handler/ConfirmRegistrationHandler.php
+++ b/src/ApiDomain/External/Authentication/src/Handler/ConfirmRegistrationHandler.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Bee\API\External\Authentication\Handler;
+use Bee\API\External\Authentication\Formatter\ConfirmRegistrationFormatter;
use Bee\API\External\User\Formatter\UserFormatter;
use Bee\Handling\Registration\Handler\Command\ConfirmRegistration\ConfirmRegistrationCommandBuilder;
use Bee\Handling\Registration\Handler\Command\ConfirmRegistration\ConfirmRegistrationCommandHandler;
@@ -20,7 +21,7 @@ class ConfirmRegistrationHandler implements RequestHandlerInterface
public function __construct(
private readonly ConfirmRegistrationCommandHandler $handler,
private readonly ConfirmRegistrationCommandBuilder $builder,
- private readonly UserFormatter $userFormatter
+ private readonly ConfirmRegistrationFormatter $formatter
) {
}
@@ -36,7 +37,7 @@ class ConfirmRegistrationHandler implements RequestHandlerInterface
$result = $this->handler->execute($query);
return new JsonResponse(
- $this->userFormatter->format($result)
+ $this->formatter->format($result)
);
}
}
diff --git a/src/ApiDomain/External/Authentication/src/Handler/LoginUserHandler.php b/src/ApiDomain/External/Authentication/src/Handler/LoginUserHandler.php
index 0d60330..f92f5c1 100644
--- a/src/ApiDomain/External/Authentication/src/Handler/LoginUserHandler.php
+++ b/src/ApiDomain/External/Authentication/src/Handler/LoginUserHandler.php
@@ -4,9 +4,18 @@ declare(strict_types=1);
namespace Bee\API\External\Authentication\Handler;
+use Bee\API\External\Authentication\Formatter\LoginUserFormatter;
+use Bee\Handling\User\Exception\UserNotFoundByIdentifierException;
+use Bee\Handling\User\Exception\UserWrongPasswordException;
use Bee\Handling\UserSession\Handler\Command\LoginUser\LoginUserCommandBuilder;
use Bee\Handling\UserSession\Handler\Command\LoginUser\LoginUserCommandHandler;
+use Bee\Infrastructure\Exception\ErrorCode;
+use Bee\Infrastructure\Exception\ErrorDomain;
+use Bee\Infrastructure\Exception\Exception\Exception;
+use Bee\Infrastructure\Logging\Logger\Logger;
use Bee\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
+use Bee\Infrastructure\Response\ErrorResponse;
+use Bee\Infrastructure\Response\UnauthorizedResponse;
use Bee\Infrastructure\Session\Middleware\SessionMiddleware;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
@@ -17,10 +26,16 @@ class LoginUserHandler implements RequestHandlerInterface
{
public function __construct(
private readonly LoginUserCommandHandler $handler,
- private readonly LoginUserCommandBuilder $builder
+ private readonly LoginUserCommandBuilder $builder,
+ private readonly LoginUserFormatter $formatter,
+ private readonly Logger $logger,
) {
}
+ /**
+ * @throws UserWrongPasswordException
+ * @throws UserNotFoundByIdentifierException
+ */
public function handle(ServerRequestInterface $request): ResponseInterface
{
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
@@ -32,9 +47,12 @@ class LoginUserHandler implements RequestHandlerInterface
$data['password'],
);
- $result = $this->handler->execute($query);
- return new JsonResponse([
- 'sessionId' => $result->getId()->toString()
- ]);
+ try {
+ $result = $this->handler->execute($query);
+ return new JsonResponse($this->formatter->format($result));
+ } catch (Exception $e) {
+ $this->logger->exception($e);
+ return new ErrorResponse(ErrorDomain::Login, ErrorCode::SomethingWentWrong);
+ }
}
}
diff --git a/src/ApiDomain/External/Authentication/src/Handler/LogoutUserHandler.php b/src/ApiDomain/External/Authentication/src/Handler/LogoutUserHandler.php
index 0ca164b..81c4de0 100644
--- a/src/ApiDomain/External/Authentication/src/Handler/LogoutUserHandler.php
+++ b/src/ApiDomain/External/Authentication/src/Handler/LogoutUserHandler.php
@@ -30,6 +30,6 @@ class LogoutUserHandler implements RequestHandlerInterface
);
$this->handler->execute($query);
- return new JsonResponse('OK');
+ return new SuccessResponse();
}
}
diff --git a/src/ApiDomain/External/Authentication/src/Handler/RegisterUserHandler.php b/src/ApiDomain/External/Authentication/src/Handler/RegisterUserHandler.php
index 124c17e..33e110c 100644
--- a/src/ApiDomain/External/Authentication/src/Handler/RegisterUserHandler.php
+++ b/src/ApiDomain/External/Authentication/src/Handler/RegisterUserHandler.php
@@ -4,7 +4,13 @@ declare(strict_types=1);
namespace Bee\API\External\Authentication\Handler;
+use Bee\Handling\Registration\Exception\RegistrationWithIdentifierAlreadyExistsException;
+use Bee\Handling\User\Exception\UserWithIdentifierAlreadyExistsException;
+use Bee\Infrastructure\Exception\ErrorCode;
+use Bee\Infrastructure\Exception\ErrorDomain;
+use Bee\Infrastructure\Mail\Exception\SendMailFailedException;
use Bee\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
+use Bee\Infrastructure\Response\ErrorResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -18,7 +24,8 @@ class RegisterUserHandler implements RequestHandlerInterface
{
public function __construct(
private readonly RegisterUserCommandHandler $handler,
- private readonly RegisterUserCommandBuilder $builder
+ private readonly RegisterUserCommandBuilder $builder,
+ private readonly Logger $logger,
) {
}
@@ -33,7 +40,19 @@ class RegisterUserHandler implements RequestHandlerInterface
$host
);
- $this->handler->execute($query);
+ try {
+ $this->handler->execute($query);
+ } catch (RegistrationWithIdentifierAlreadyExistsException $e) {
+ $this->logger->exception($e);
+ return new ErrorResponse(ErrorDomain::Registration, ErrorCode::SomethingWentWrong);
+ } catch (UserWithIdentifierAlreadyExistsException $e) {
+ $this->logger->exception($e);
+ return new ErrorResponse(ErrorDomain::Registration, ErrorCode::SomethingWentWrong);
+ } catch (SendMailFailedException $e) {
+ $this->logger->exception($e);
+ return new ErrorResponse(ErrorDomain::Registration, ErrorCode::SomethingWentWrong);
+ }
+
return new SuccessResponse();
}
}
diff --git a/src/ApiDomain/External/User/config/service_manager.php b/src/ApiDomain/External/User/config/service_manager.php
index 912487c..8a7dcb9 100644
--- a/src/ApiDomain/External/User/config/service_manager.php
+++ b/src/ApiDomain/External/User/config/service_manager.php
@@ -1,6 +1,7 @@
[
// Formatter
- UserFormatter::class => AutoWiringFactory::class,
+ UserStateFormatter::class => AutoWiringFactory::class,
+ CreateUserFormatter::class => AutoWiringFactory::class,
// Handler
CreateUserHandler::class => AutoWiringFactory::class,
diff --git a/src/ApiDomain/External/User/src/Formatter/UserFormatter.php b/src/ApiDomain/External/User/src/Formatter/CreateUserFormatter.php
similarity index 65%
rename from src/ApiDomain/External/User/src/Formatter/UserFormatter.php
rename to src/ApiDomain/External/User/src/Formatter/CreateUserFormatter.php
index 5888485..3c4f95c 100644
--- a/src/ApiDomain/External/User/src/Formatter/UserFormatter.php
+++ b/src/ApiDomain/External/User/src/Formatter/CreateUserFormatter.php
@@ -5,15 +5,15 @@ namespace Bee\API\External\User\Formatter;
use DateTime;
use Bee\Data\Business\Entity\User;
-class UserFormatter {
+class CreateUserFormatter {
public function format(User $user): array {
$userArray = [
'id' => $user->getId()->toString(),
'username' => $user->getUsername(),
- 'role' => $user->getRole()->getIdentifier(),
- 'created' => $user->getCreatedAt()->format(DateTime::ATOM),
- 'updated' => $user->getUpdatedAt()->format(DateTime::ATOM)
+ 'roleIdentifier' => $user->getRole()->getIdentifier(),
+ 'createdAt' => $user->getCreatedAt()->format(DateTime::ATOM),
+ 'updatedAt' => $user->getUpdatedAt()->format(DateTime::ATOM)
];
$userArray['permissions'] = [];
diff --git a/src/ApiDomain/External/User/src/Formatter/UserStateFormatter.php b/src/ApiDomain/External/User/src/Formatter/UserStateFormatter.php
new file mode 100644
index 0000000..124ec6b
--- /dev/null
+++ b/src/ApiDomain/External/User/src/Formatter/UserStateFormatter.php
@@ -0,0 +1,28 @@
+ $user->getId()->toString(),
+ 'sessionId' => $user->getSession()->getId()->toString(),
+ 'username' => $user->getUsername(),
+ 'roleIdentifier' => $user->getRole()->getIdentifier(),
+ 'createdAt' => $user->getCreatedAt()->format(DateTime::ATOM),
+ 'updatedAt' => $user->getUpdatedAt()->format(DateTime::ATOM)
+ ];
+
+ $userArray['permissions'] = [];
+
+ foreach ($user->getRole()->getPermissions()->toArray() as $permission) {
+ $userArray['permissions'][] = $permission->getIdentifier();
+ }
+
+ return $userArray;
+ }
+}
diff --git a/src/ApiDomain/External/User/src/Handler/ChangeUsernameHandler.php b/src/ApiDomain/External/User/src/Handler/ChangeUsernameHandler.php
index afffb0e..21373d3 100644
--- a/src/ApiDomain/External/User/src/Handler/ChangeUsernameHandler.php
+++ b/src/ApiDomain/External/User/src/Handler/ChangeUsernameHandler.php
@@ -7,6 +7,7 @@ namespace Bee\API\External\User\Handler;
use Bee\Data\Business\Entity\User;
use Bee\Handling\User\Handler\Command\ChangeUsername\ChangeUsernameCommandBuilder;
use Bee\Handling\User\Handler\Command\ChangeUsername\ChangeUsernameCommandHandler;
+use Bee\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use Bee\Infrastructure\Response\SuccessResponse;
use Bee\Infrastructure\Session\Middleware\LoggedInUserMiddleware;
use Psr\Http\Message\ResponseInterface;
@@ -25,7 +26,6 @@ class ChangeUsernameHandler implements RequestHandlerInterface
{
/** @var User $user */
$user = $request->getAttribute(LoggedInUserMiddleware::USER_KEY);
-
$data = $request->getAttribute(AnalyzeBodyMiddleware::JSON_DATA);
$query = $this->builder->build(
diff --git a/src/ApiDomain/External/User/src/Handler/CreateUserHandler.php b/src/ApiDomain/External/User/src/Handler/CreateUserHandler.php
index 6301366..a0f13fc 100644
--- a/src/ApiDomain/External/User/src/Handler/CreateUserHandler.php
+++ b/src/ApiDomain/External/User/src/Handler/CreateUserHandler.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Bee\API\External\User\Handler;
+use Bee\API\External\User\Formatter\CreateUserFormatter;
use Bee\API\External\User\Formatter\UserFormatter;
use Bee\Handling\User\Handler\Command\CreateUser\CreateUserCommandBuilder;
use Bee\Handling\User\Handler\Command\CreateUser\CreateUserCommandHandler;
@@ -18,7 +19,7 @@ class CreateUserHandler implements RequestHandlerInterface
public function __construct(
private readonly CreateUserCommandHandler $handler,
private readonly CreateUserCommandBuilder $builder,
- private readonly UserFormatter $userFormatter,
+ private readonly CreateUserFormatter $formatter,
) {
}
@@ -34,7 +35,7 @@ class CreateUserHandler implements RequestHandlerInterface
$result = $this->handler->execute($query);
return new JsonResponse(
- $this->userFormatter->format($result)
+ $this->formatter->format($result)
);
}
}
diff --git a/src/ApiDomain/External/User/src/Handler/UserStateHandler.php b/src/ApiDomain/External/User/src/Handler/UserStateHandler.php
index 0e09b4f..e605bbe 100644
--- a/src/ApiDomain/External/User/src/Handler/UserStateHandler.php
+++ b/src/ApiDomain/External/User/src/Handler/UserStateHandler.php
@@ -4,10 +4,8 @@ declare(strict_types=1);
namespace Bee\API\External\User\Handler;
-use Bee\API\External\User\Formatter\UserFormatter;
+use Bee\API\External\User\Formatter\UserStateFormatter;
use Bee\Data\Business\Entity\User;
-use Bee\Handling\User\Handler\Command\CreateUser\CreateUserCommandBuilder;
-use Bee\Handling\User\Handler\Command\CreateUser\ChangePasswordCommandHandler;
use Bee\Infrastructure\Session\Middleware\LoggedInUserMiddleware;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
@@ -17,7 +15,7 @@ use Psr\Http\Server\RequestHandlerInterface;
class UserStateHandler implements RequestHandlerInterface
{
public function __construct(
- private readonly UserFormatter $userFormatter,
+ private readonly UserStateFormatter $userFormatter,
) {
}
@@ -27,7 +25,6 @@ class UserStateHandler implements RequestHandlerInterface
$user = $request->getAttribute(LoggedInUserMiddleware::USER_KEY);
$response = $this->userFormatter->format($user);
- $response['sessionId'] = $user->getSession()->getId()->toString();
return new JsonResponse($response);
}
diff --git a/src/HandlingDomain/Registration/src/Pipeline/RegisterUser/Step/SendMailStep.php b/src/HandlingDomain/Registration/src/Pipeline/RegisterUser/Step/SendMailStep.php
index 31b2ea7..b99f9b8 100644
--- a/src/HandlingDomain/Registration/src/Pipeline/RegisterUser/Step/SendMailStep.php
+++ b/src/HandlingDomain/Registration/src/Pipeline/RegisterUser/Step/SendMailStep.php
@@ -4,7 +4,8 @@ declare(strict_types=1);
namespace Bee\Handling\Registration\Pipeline\RegisterUser\Step;
-use Bee\Infrastructure\Request\Service\RequestService;
+use Bee\Infrastructure\Mail\Exception\SendMailFailedException;
+use Bee\Infrastructure\Mail\Service\MailService;
use teewurst\Pipeline\PipelineInterface;
use teewurst\Pipeline\TaskInterface;
@@ -13,10 +14,13 @@ class SendMailStep implements TaskInterface
private const CONFIRM_LINK = 'https://%s/auth/registration/%s';
public function __construct(
- private RequestService $requestService
+ private MailService $mailService,
) {
}
+ /**
+ * @throws SendMailFailedException
+ */
public function __invoke(
$payload,
PipelineInterface $pipeline
@@ -25,23 +29,19 @@ class SendMailStep implements TaskInterface
$command = $payload->getCommand();
$registration = $payload->getRegistration();
- $this->requestService->request(
- 'notification',
- 'send-mail',
- [
- 'bee-identifier' => 'new-account',
- 'sender' => 'info@stack-up.de',
- 'recipient' => $command->getMail(),
- 'data' => [
- 'username' => $command->getUsername(),
- 'confirmationLink' =>
- sprintf(
- self::CONFIRM_LINK,
- $command->getHost(),
- $registration->getId()->toString()
- )
- ]
- ]
+ $this->mailService->send(
+ template: 'registration',
+ templateData: [
+ 'username' => $command->getUsername(),
+ 'confirmationLink' =>
+ sprintf(
+ self::CONFIRM_LINK,
+ $command->getHost(),
+ $registration->getId()->toString()
+ )
+ ],
+ recipient: $command->getMail(),
+ senderName: "Beekeeper"
);
$pipeline->next()($payload, $pipeline);
diff --git a/src/Infrastructure/Exception/src/ErrorCode.php b/src/Infrastructure/Exception/src/ErrorCode.php
index a72072a..456cc08 100644
--- a/src/Infrastructure/Exception/src/ErrorCode.php
+++ b/src/Infrastructure/Exception/src/ErrorCode.php
@@ -9,4 +9,5 @@ enum ErrorCode : string {
case AlreadyExists = 'AlreadyExists';
case WrongCredentials = 'WrongCredentials';
case Mismatch = 'Mismatch';
+ case Failed = 'Failed';
}
\ No newline at end of file
diff --git a/src/Infrastructure/Exception/src/ErrorDomain.php b/src/Infrastructure/Exception/src/ErrorDomain.php
index 6c439be..574d2ea 100644
--- a/src/Infrastructure/Exception/src/ErrorDomain.php
+++ b/src/Infrastructure/Exception/src/ErrorDomain.php
@@ -10,4 +10,6 @@ enum ErrorDomain : string {
case UserPassword = 'UserPassword';
case Registration = 'Registration';
case Product = 'Product';
+ case Mail = 'Mail';
+ case Login = 'Login';
}
\ No newline at end of file
diff --git a/src/Infrastructure/Mail/config/service_manager.php b/src/Infrastructure/Mail/config/service_manager.php
new file mode 100644
index 0000000..47bff82
--- /dev/null
+++ b/src/Infrastructure/Mail/config/service_manager.php
@@ -0,0 +1,12 @@
+ [
+ MailService::class => AutoWiringFactory::class,
+ ],
+];
diff --git a/src/Infrastructure/Mail/src/ConfigProvider.php b/src/Infrastructure/Mail/src/ConfigProvider.php
new file mode 100644
index 0000000..7bc4a8d
--- /dev/null
+++ b/src/Infrastructure/Mail/src/ConfigProvider.php
@@ -0,0 +1,15 @@
+ require __DIR__ . './../config/service_manager.php',
+ ];
+ }
+}
diff --git a/src/Infrastructure/Mail/src/Exception/SendMailFailedException.php b/src/Infrastructure/Mail/src/Exception/SendMailFailedException.php
new file mode 100644
index 0000000..dfe45c8
--- /dev/null
+++ b/src/Infrastructure/Mail/src/Exception/SendMailFailedException.php
@@ -0,0 +1,32 @@
+getMessage(),
+ ),
+ ErrorDomain::Mail,
+ ErrorCode::Failed
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Infrastructure/Mail/src/Service/MailService.php b/src/Infrastructure/Mail/src/Service/MailService.php
new file mode 100644
index 0000000..a2073f9
--- /dev/null
+++ b/src/Infrastructure/Mail/src/Service/MailService.php
@@ -0,0 +1,120 @@
+engine = new Engine();
+ }
+
+ /**
+ * @throws SendMailFailedException
+ */
+ public function send(
+ string $template,
+ array $templateData,
+ string $recipient,
+ ?string $sender = null,
+ ?string $senderName = null
+ ): void {
+ try {
+ $mail = $this->getMail(
+ template: $template,
+ templateData: $templateData,
+ recipient: $recipient,
+ sender: $sender,
+ senderName: $senderName
+ );
+
+ $mailer = $this->getMailer();
+
+ $mailer->send($mail);
+ } catch (Throwable $e) {
+ $this->logger->exception($e);
+
+ throw new SendMailFailedException(
+ $template,
+ $templateData,
+ $recipient,
+ $e
+ );
+ }
+ }
+
+ private function getMailer(): Mailer
+ {
+ $smtpConfig = $this->configService->resolve('mail.smtp-server');
+
+ return new SmtpMailer(
+ host: $smtpConfig['host'],
+ username: $smtpConfig['username'],
+ password: $smtpConfig['password'],
+ encryption: $smtpConfig['encryption'],
+ port: $smtpConfig['port'],
+ );
+ }
+
+ private function getMail(
+ string $template,
+ array $templateData,
+ string $recipient,
+ ?string $sender = null,
+ ?string $senderName = null
+ ): Message {
+ $templatePath = self::TEMPLATE_PATH . $template . '/template.latte';
+ $assetsPath = self::TEMPLATE_PATH . $template . "/";
+
+ if (!file_exists($templatePath)) {
+ throw new Exception("Template File does not exist");
+ }
+
+ if (!is_dir($assetsPath)) {
+ $assetsPath = null;
+ }
+
+ $mail = new Message();
+ $mail->setFrom($this->getSenderName($sender, $senderName));
+ $mail->addTo($recipient);
+ $mail->setHtmlBody(
+ $this->engine->renderToString(
+ $templatePath,
+ $templateData,
+ ),
+ $assetsPath
+ );
+
+ return $mail;
+ }
+
+ private function getSenderName(
+ ?string $sender,
+ ?string $senderName
+ ): string {
+ $defaultConfig = $this->configService->resolve('mail.default');
+
+ $from = $sender ?? $defaultConfig['sender'] ?? throw new Exception('Could not determine Sender');
+ if ($senderName !== null) {
+ $from = sprintf('%s <%s>', $senderName, $from);
+ }
+ return $from;
+ }
+}
\ No newline at end of file