diff --git a/src/Configuration/MailConfiguration.php b/src/Configuration/MailConfiguration.php
new file mode 100644
index 00000000..c9512179
--- /dev/null
+++ b/src/Configuration/MailConfiguration.php
@@ -0,0 +1,24 @@
+ getenv('APP_ENV'),
- 'MAILER_FROM' => getenv('MAILER_FROM'),
'CORS_ALLOW_ORIGIN' => getenv('CORS_ALLOW_ORIGIN'),
];
}
diff --git a/src/EventSubscriber/RegistrationSubscriber.php b/src/EventSubscriber/RegistrationSubscriber.php
index db441e6b..636825dc 100644
--- a/src/EventSubscriber/RegistrationSubscriber.php
+++ b/src/EventSubscriber/RegistrationSubscriber.php
@@ -16,8 +16,10 @@ use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
- * This class intercepts the registration to make sure the first-ever
- * registered user will get the SUPER_ADMIN role.
+ * This class intercepts the registration to make sure:
+ *
+ * - the first-ever registered user will get the SUPER_ADMIN role
+ * - the user uses the current request locale as initial language setting
*/
class RegistrationSubscriber implements EventSubscriberInterface
{
@@ -49,7 +51,7 @@ class RegistrationSubscriber implements EventSubscriberInterface
*/
public function onRegistrationSuccess(FormEvent $event)
{
- /** @var \FOS\UserBundle\Model\UserInterface $user */
+ /** @var User $user */
$user = $event->getForm()->getData();
$roles = [User::ROLE_USER];
@@ -57,6 +59,7 @@ class RegistrationSubscriber implements EventSubscriberInterface
$roles = [User::ROLE_SUPER_ADMIN];
}
+ $user->setLanguage($event->getRequest()->getLocale());
$user->setRoles($roles);
}
}
diff --git a/src/Mail/KimaiMailer.php b/src/Mail/KimaiMailer.php
new file mode 100644
index 00000000..74acdde1
--- /dev/null
+++ b/src/Mail/KimaiMailer.php
@@ -0,0 +1,43 @@
+configuration = $configuration;
+ $this->mailer = $mailer;
+ }
+
+ public function send(RawMessage $message, Envelope $envelope = null): void
+ {
+ if ($message instanceof Email) {
+ $message->from($this->configuration->getFromAddress());
+ }
+
+ $this->mailer->send($message);
+ }
+}
diff --git a/src/Mail/UserMails.php b/src/Mail/UserMails.php
new file mode 100644
index 00000000..0ec38d2e
--- /dev/null
+++ b/src/Mail/UserMails.php
@@ -0,0 +1,97 @@
+mailer = $mailer;
+ $this->router = $router;
+ $this->translator = $translator;
+ }
+
+ public function sendConfirmationEmailMessage(UserInterface $user)
+ {
+ $username = $user->getUsername();
+ $language = 'en';
+
+ if ($user instanceof User) {
+ $username = $user->getDisplayName();
+ $language = $user->getLanguage();
+ }
+
+ $url = $this->router->generate('fos_user_registration_confirm', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
+
+ $email = (new TemplatedEmail())
+ ->to(new Address($user->getEmail()))
+ ->subject(
+ $this->translator->trans('registration.subject', ['%username%' => $username], 'email', $language)
+ )
+ ->htmlTemplate('emails/confirmation.html.twig')
+ ->context([
+ 'user' => $user,
+ 'username' => $username,
+ 'confirmationUrl' => $url,
+ ])
+ ;
+
+ $this->mailer->send($email);
+ }
+
+ public function sendResettingEmailMessage(UserInterface $user)
+ {
+ $username = $user->getUsername();
+ $language = 'en';
+
+ if ($user instanceof User) {
+ $username = $user->getDisplayName();
+ $language = $user->getLanguage();
+ }
+
+ $url = $this->router->generate('fos_user_resetting_reset', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
+
+ $email = (new TemplatedEmail())
+ ->to(new Address($user->getEmail()))
+ ->subject(
+ $this->translator->trans('reset.subject', ['%username%' => $username], 'email', $language)
+ )
+ ->htmlTemplate('emails/password-reset.html.twig')
+ ->context([
+ 'user' => $user,
+ 'username' => $username,
+ 'confirmationUrl' => $url,
+ ])
+ ;
+
+ $this->mailer->send($email);
+ }
+}
diff --git a/symfony.lock b/symfony.lock
index e103f948..2d1194f0 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -222,6 +222,12 @@
"league/csv": {
"version": "9.4.1"
},
+ "league/html-to-markdown": {
+ "version": "4.9.1"
+ },
+ "lorenzo/pinky": {
+ "version": "1.0.5"
+ },
"markbaker/complex": {
"version": "1.4.6"
},
@@ -417,8 +423,14 @@
"setasign/fpdi": {
"version": "1.6.2"
},
- "swiftmailer/swiftmailer": {
- "version": "v6.0.2"
+ "symfony/amazon-mailer": {
+ "version": "4.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "master",
+ "version": "4.4",
+ "ref": "9648db3ecae5c8a6b1a5f74715d3907124348815"
+ }
},
"symfony/asset": {
"version": "v4.0.3"
@@ -510,6 +522,15 @@
"ref": "137a14eeb6b3f5370e7147af8aff6518504f50c7"
}
},
+ "symfony/google-mailer": {
+ "version": "4.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "master",
+ "version": "4.4",
+ "ref": "f8fd4ddb9b477510f8f4bce2b9c054ab428c0120"
+ }
+ },
"symfony/http-foundation": {
"version": "v4.0.3"
},
@@ -525,6 +546,36 @@
"symfony/ldap": {
"version": "v4.2.8"
},
+ "symfony/mailchimp-mailer": {
+ "version": "4.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "master",
+ "version": "4.4",
+ "ref": "1be490e068472fc87e226cf54bc95fb6f2e49117"
+ }
+ },
+ "symfony/mailer": {
+ "version": "4.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "master",
+ "version": "4.3",
+ "ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2"
+ },
+ "files": [
+ "config/packages/mailer.yaml"
+ ]
+ },
+ "symfony/mailgun-mailer": {
+ "version": "4.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "master",
+ "version": "4.4",
+ "ref": "91bc9c127983c37808481733bcf83b7651b1f941"
+ }
+ },
"symfony/maker-bundle": {
"version": "1.0",
"recipe": {
@@ -552,9 +603,6 @@
"symfony/options-resolver": {
"version": "v4.0.3"
},
- "symfony/orm-pack": {
- "version": "v1.0.6"
- },
"symfony/phpunit-bridge": {
"version": "3.3",
"recipe": {
@@ -588,6 +636,15 @@
"symfony/polyfill-php73": {
"version": "v1.11.0"
},
+ "symfony/postmark-mailer": {
+ "version": "4.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "master",
+ "version": "4.4",
+ "ref": "84842f6791eaef80618dda244d4e3bbcbbcdf543"
+ }
+ },
"symfony/process": {
"version": "v4.0.3"
},
@@ -630,6 +687,15 @@
"symfony/security-http": {
"version": "v4.3.3"
},
+ "symfony/sendgrid-mailer": {
+ "version": "4.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "master",
+ "version": "4.4",
+ "ref": "b46e4f8f1adb2864f0aa9c0ff57aea3324a07199"
+ }
+ },
"symfony/serializer": {
"version": "v4.1.2"
},
@@ -642,15 +708,6 @@
"symfony/string": {
"version": "v5.0.4"
},
- "symfony/swiftmailer-bundle": {
- "version": "2.5",
- "recipe": {
- "repo": "github.com/symfony/recipes",
- "branch": "master",
- "version": "2.5",
- "ref": "8daf8ede72274989fb0dc66b83cb4bc1693afbb9"
- }
- },
"symfony/templating": {
"version": "v4.1.1"
},
@@ -720,9 +777,18 @@
"theseer/tokenizer": {
"version": "1.1.0"
},
+ "tijsverkoyen/css-to-inline-styles": {
+ "version": "2.2.2"
+ },
+ "twig/cssinliner-extra": {
+ "version": "v3.0.1"
+ },
"twig/extra-bundle": {
"version": "v3.0.1"
},
+ "twig/inky-extra": {
+ "version": "v3.0.1"
+ },
"twig/intl-extra": {
"version": "v3.0.1"
},
diff --git a/templates/emails/confirmation.html.twig b/templates/emails/confirmation.html.twig
new file mode 100644
index 00000000..1bccf2bc
--- /dev/null
+++ b/templates/emails/confirmation.html.twig
@@ -0,0 +1,21 @@
+{% extends 'emails/layout.html.twig' %}
+
+{% block title %}
+ {{ 'registration.title'|trans({'%username%': user.displayName}, 'email') }}
+{% endblock %}
+
+{% block body %}
+ {{ 'registration.intro'|trans({'%email%': user.email}, 'email') }}
+
+
+
+
+
+
+
+
{{ 'button_copy_url_to_browser'|trans({'%url%': confirmationUrl}, 'email') }}
+
+
+
+ {{ 'automated_email_dont_answer'|trans({}, 'email') }}
+{% endblock %}
diff --git a/templates/emails/layout.html.twig b/templates/emails/layout.html.twig
new file mode 100644
index 00000000..045f3576
--- /dev/null
+++ b/templates/emails/layout.html.twig
@@ -0,0 +1,29 @@
+{% apply inky_to_html|inline_css(source('@css/emails.css')) %}
+
+
+
+
+
+
+
+
+
+
+ {% block body %}{% endblock %}
+
+
+
+{% endapply %}
diff --git a/templates/emails/password-reset.html.twig b/templates/emails/password-reset.html.twig
new file mode 100644
index 00000000..745c20ee
--- /dev/null
+++ b/templates/emails/password-reset.html.twig
@@ -0,0 +1,21 @@
+{% extends 'emails/layout.html.twig' %}
+
+{% block title %}
+ {{ 'reset.title'|trans({'%username%': user.displayName}, 'email') }}
+{% endblock %}
+
+{% block body %}
+ {{ 'reset.intro'|trans({'%username%': user.displayName, '%email%': user.email}, 'email') }}
+
+
+
+
+
+
+
+
{{ 'button_copy_url_to_browser'|trans({'%url%': confirmationUrl}, 'email') }}
+
+
+
+ {{ 'automated_email_dont_answer'|trans({}, 'email') }}
+{% endblock %}
diff --git a/tests/Configuration/MailConfigurationTest.php b/tests/Configuration/MailConfigurationTest.php
new file mode 100644
index 00000000..603bbf9e
--- /dev/null
+++ b/tests/Configuration/MailConfigurationTest.php
@@ -0,0 +1,33 @@
+getFromAddress());
+
+ putenv('MAILER_FROM=');
+ self::assertNull($sut->getFromAddress());
+
+ putenv('MAILER_FROM=' . $previous);
+ }
+}
diff --git a/tests/EventSubscriber/RegistrationSubscriberTest.php b/tests/EventSubscriber/RegistrationSubscriberTest.php
index 5f9ca429..984f5693 100644
--- a/tests/EventSubscriber/RegistrationSubscriberTest.php
+++ b/tests/EventSubscriber/RegistrationSubscriberTest.php
@@ -50,6 +50,7 @@ class RegistrationSubscriberTest extends TestCase
$form->method('getData')->willReturn($user);
$request = $this->createMock(Request::class);
+ $request->expects($this->any())->method('getLocale')->willReturn('ru');
$event = new FormEvent($form, $request);
@@ -57,6 +58,7 @@ class RegistrationSubscriberTest extends TestCase
$sut->onRegistrationSuccess($event);
$this->assertEquals($expectedRoles, $user->getRoles());
+ $this->assertEquals('ru', $user->getLanguage());
}
public function getTestData()
diff --git a/tests/Mail/KimaiMailerTest.php b/tests/Mail/KimaiMailerTest.php
new file mode 100644
index 00000000..53541601
--- /dev/null
+++ b/tests/Mail/KimaiMailerTest.php
@@ -0,0 +1,51 @@
+createMock(MailConfiguration::class);
+ $config->expects($this->any())->method('getFromAddress')->willReturn('zippel@example.com');
+
+ $mailer = $this->createMock(MailerInterface::class);
+
+ return new KimaiMailer($config, $mailer);
+ }
+
+ public function testSendSetsFrom()
+ {
+ $user = new User();
+ $user->setUsername('Testing');
+ $user->setEmail('foo@example.com');
+ $user->setAlias('Super User');
+
+ $mailer = $this->getSut();
+ $message = new Email();
+
+ self::assertEquals([], $message->getFrom());
+
+ $mailer->send($message);
+
+ self::assertEquals([new Address('zippel@example.com')], $message->getFrom());
+ }
+}
diff --git a/tests/Mail/UserMailsTest.php b/tests/Mail/UserMailsTest.php
new file mode 100644
index 00000000..dfca2fdc
--- /dev/null
+++ b/tests/Mail/UserMailsTest.php
@@ -0,0 +1,67 @@
+createMock(MailConfiguration::class);
+ $config->expects($this->any())->method('getFromAddress')->willReturn('zippel@example.com');
+ $mailer = $this->createMock(MailerInterface::class);
+ $mailer->expects($this->once())->method('send')->willReturnCallback(function (Email $message) {
+ self::assertEquals([new Address('zippel@example.com')], $message->getFrom());
+ self::assertEquals([new Address('foo@example.com')], $message->getTo());
+ self::assertEquals('foo', $message->getSubject());
+ });
+ $kimaiMailer = new KimaiMailer($config, $mailer);
+ $router = $this->createMock(UrlGeneratorInterface::class);
+ $translator = $this->createMock(TranslatorInterface::class);
+ $translator->expects($this->any())->method('trans')->willReturn('foo');
+
+ return new UserMails($kimaiMailer, $router, $translator);
+ }
+
+ public function testSendConfirmationEmailMessage()
+ {
+ $user = new User();
+ $user->setUsername('Testing');
+ $user->setEmail('foo@example.com');
+ $user->setAlias('Super User');
+
+ $mailer = $this->getSut();
+ $mailer->sendConfirmationEmailMessage($user);
+ }
+
+ public function testSendResettingEmailMessage()
+ {
+ $user = new User();
+ $user->setUsername('Testing');
+ $user->setEmail('foo@example.com');
+ $user->setAlias('Super User');
+
+ $mailer = $this->getSut();
+ $mailer->sendResettingEmailMessage($user);
+ }
+}
diff --git a/translations/email.de.xlf b/translations/email.de.xlf
new file mode 100644
index 00000000..187ddbea
--- /dev/null
+++ b/translations/email.de.xlf
@@ -0,0 +1,47 @@
+
+
+
+
+
+ button_copy_url_to_browser
+ Falls der Button nicht funktioniert, kopieren Sie bitte die folgende URL in ihren Browser: %url%
+
+
+ automated_email_dont_answer
+ Dies ist eine automatisch generierte E-Mail. Bitte antworten Sie nicht, die Adresse wird nicht gelesen.
+
+
+ registration.subject
+ Benutzerkonto aktivieren
+
+
+ registration.title
+ Willkommen %username%
+
+
+ registration.button
+ Aktivieren Sie Ihren Zugang
+
+
+ registration.intro
+ Sie haben sich für die Kimai Zeiterfassung mit der Email Adresse %email% registriert. Jetzt müssen Sie den Link in den nächsten Stunden anklicken, bevor der Link keine Gültigkeit mehr hat.
+
+
+ reset.subject
+ Passwort zurücksetzen
+
+
+ reset.title
+ Das kann passieren ...
+
+
+ reset.button
+ Passwort zurücksetzen
+
+
+ reset.intro
+ ... Du hast also Dein Passwort vergessen? Keine Sorge: Kimai wird Dir dabei helfen ein neues zu erstellen:
+
+
+
+
diff --git a/translations/email.en.xlf b/translations/email.en.xlf
new file mode 100644
index 00000000..e891f09d
--- /dev/null
+++ b/translations/email.en.xlf
@@ -0,0 +1,47 @@
+
+
+
+
+
+ button_copy_url_to_browser
+ If the button doesn't work, please copy & paste the following URL to your browser: %url%
+
+
+ automated_email_dont_answer
+ This is an auto generated e-mail. Please do not reply, the address will not be read.
+
+
+ registration.subject
+ Activate your account
+
+
+ registration.title
+ Welcome %username%
+
+
+ registration.button
+ Activate your account
+
+
+ registration.intro
+ You signed up at the Kimai time-tracker with the email %email%. Now you have to activate your account in the next few hours, before the link expires.
+
+
+ reset.subject
+ Reset your password
+
+
+ reset.title
+ That can happen ...
+
+
+ reset.button
+ Reset your password
+
+
+ reset.intro
+ ... so you forgot your password? Don't worry: Kimai will help you to create a new one:
+
+
+
+