diff --git a/config/app.php b/config/app.php index 5bb3885a..2386dbbc 100644 --- a/config/app.php +++ b/config/app.php @@ -69,6 +69,9 @@ return [ // callable like [$instance, 'method'] or 'function' // or $function // ] + + 'message.created' => \Engelsystem\Events\Listener\Messages::class . '@created', + 'news.created' => \Engelsystem\Events\Listener\News::class . '@created', 'oauth2.login' => \Engelsystem\Events\Listener\OAuth2::class . '@login', diff --git a/db/factories/User/SettingsFactory.php b/db/factories/User/SettingsFactory.php index d61bd234..f399ed31 100644 --- a/db/factories/User/SettingsFactory.php +++ b/db/factories/User/SettingsFactory.php @@ -18,6 +18,7 @@ class SettingsFactory extends Factory 'language' => $this->faker->locale(), 'theme' => $this->faker->numberBetween(1, 20), 'email_human' => $this->faker->boolean(), + 'email_messages' => $this->faker->boolean(), 'email_goody' => $this->faker->boolean(), 'email_shiftinfo' => $this->faker->boolean(), 'email_news' => $this->faker->boolean(), diff --git a/db/migrations/2023_02_26_000000_AddEmailMessagesToUsersSettings.php b/db/migrations/2023_02_26_000000_AddEmailMessagesToUsersSettings.php new file mode 100644 index 00000000..12a7450b --- /dev/null +++ b/db/migrations/2023_02_26_000000_AddEmailMessagesToUsersSettings.php @@ -0,0 +1,33 @@ +schema->table('users_settings', function (Blueprint $table): void { + $table->boolean('email_messages')->default(false)->after('email_human'); + }); + } + + /** + * Reverse the migration + */ + public function down(): void + { + $this->schema->table('users_settings', function (Blueprint $table): void { + $table->dropColumn('email_messages'); + }); + } +} diff --git a/includes/pages/guest_login.php b/includes/pages/guest_login.php index 4498c17f..5fe470ea 100644 --- a/includes/pages/guest_login.php +++ b/includes/pages/guest_login.php @@ -57,6 +57,7 @@ function guest_register() $pronoun = ''; $email_shiftinfo = false; $email_by_human_allowed = false; + $email_messages = false; $email_news = false; $email_goody = false; $tshirt_size = ''; @@ -155,6 +156,10 @@ function guest_register() $email_by_human_allowed = true; } + if ($request->has('email_messages')) { + $email_messages = true; + } + if ($request->has('email_news')) { $email_news = true; } @@ -263,6 +268,7 @@ function guest_register() 'language' => $session->get('locale'), 'theme' => config('theme'), 'email_human' => $email_by_human_allowed, + 'email_messages' => $email_messages, 'email_goody' => $email_goody, 'email_shiftinfo' => $email_shiftinfo, 'email_news' => $email_news, @@ -434,6 +440,11 @@ function guest_register() __('Notify me of new news'), $email_news ), + form_checkbox( + 'email_messages', + __('settings.profile.email_messages'), + $email_messages + ), form_checkbox( 'email_by_human_allowed', __('Allow heaven angels to contact you by e-mail.'), diff --git a/resources/lang/de_DE/additional.po b/resources/lang/de_DE/additional.po index aad375fb..0e8cd7ce 100644 --- a/resources/lang/de_DE/additional.po +++ b/resources/lang/de_DE/additional.po @@ -166,6 +166,12 @@ msgstr "Es gibt eine neue News: %1$s" msgid "notification.news.new.text" msgstr "Du kannst sie dir unter %3$s anschauen." +msgid "notification.messages.new" +msgstr "Neue private Nachricht von %s" + +msgid "notification.messages.new.text" +msgstr "Du hast eine neue private Nachricht von %s bekommen. Du kannst sie dir unter %s anschauen." + msgid "notification.angeltype.confirmed" msgstr "Du wurdest als %s bestätigt" diff --git a/resources/lang/de_DE/default.po b/resources/lang/de_DE/default.po index 2499bf97..d5e96a22 100644 --- a/resources/lang/de_DE/default.po +++ b/resources/lang/de_DE/default.po @@ -2999,6 +2999,9 @@ msgstr "Das %s darf mir E-Mails senden (z.B. wenn sich meine Schichten ändern). msgid "settings.profile.email_news" msgstr "Benachrichtige mich bei neuen News." +msgid "settings.profile.email_messages" +msgstr "Benachrichtige mich bei neuen privaten Nachrichten." + msgid "settings.profile.email_by_human_allowed" msgstr "Erlaube Himmel-Engeln dich per Mail zu kontaktieren." diff --git a/resources/lang/en_US/additional.po b/resources/lang/en_US/additional.po index 57f5f2dc..6d3f1864 100644 --- a/resources/lang/en_US/additional.po +++ b/resources/lang/en_US/additional.po @@ -165,6 +165,12 @@ msgstr "A new news is available: %1$s" msgid "notification.news.new.text" msgstr "You can watch it at %3$s" +msgid "notification.messages.new" +msgstr "New private message from %s" + +msgid "notification.messages.new.text" +msgstr "you've got a new private message from %s. You can view it under %s" + msgid "notification.angeltype.confirmed" msgstr "You have been confirmed as an %s" diff --git a/resources/lang/en_US/default.po b/resources/lang/en_US/default.po index 5ab33542..1109ff2c 100644 --- a/resources/lang/en_US/default.po +++ b/resources/lang/en_US/default.po @@ -268,6 +268,9 @@ msgstr "The %s is allowed to send me an e-mail (e.g. when my shifts change)." msgid "settings.profile.email_news" msgstr "Notify me of new news." +msgid "settings.profile.email_messages" +msgstr "Notify me on new private messages." + msgid "settings.profile.email_by_human_allowed" msgstr "Allow heaven angels to contact you by e-mail." diff --git a/resources/views/emails/messages-new.twig b/resources/views/emails/messages-new.twig new file mode 100644 index 00000000..eea4e2c2 --- /dev/null +++ b/resources/views/emails/messages-new.twig @@ -0,0 +1,8 @@ +{% extends "emails/mail.twig" %} + +{% block introduction %} +{% endblock %} + +{% block message %} +{{ __('notification.messages.new.text', [sender, url('/messages/' ~ send_message.sender.id)]) }} +{% endblock %} diff --git a/resources/views/pages/settings/profile.twig b/resources/views/pages/settings/profile.twig index 8fa87421..4f63cac2 100644 --- a/resources/views/pages/settings/profile.twig +++ b/resources/views/pages/settings/profile.twig @@ -117,6 +117,11 @@ __('settings.profile.email_news'), user.settings.email_news ) }} + {{ f.checkbox( + 'email_messages', + __('settings.profile.email_messages'), + user.settings.email_messages + ) }} {{ f.checkbox( 'email_human', __('settings.profile.email_by_human_allowed'), diff --git a/src/Controllers/MessagesController.php b/src/Controllers/MessagesController.php index 94569f47..6122409e 100644 --- a/src/Controllers/MessagesController.php +++ b/src/Controllers/MessagesController.php @@ -149,6 +149,8 @@ class MessagesController extends BaseController $newMessage->read = $otherUser->id == $currentUser->id; // if its to myself, I obviously read it. $newMessage->save(); + event('message.created', ['message' => $newMessage]); + return $this->redirect->to('/messages/' . $otherUser->id . '#newest'); } diff --git a/src/Controllers/SettingsController.php b/src/Controllers/SettingsController.php index 1fc2a4de..6e605591 100644 --- a/src/Controllers/SettingsController.php +++ b/src/Controllers/SettingsController.php @@ -85,6 +85,7 @@ class SettingsController extends BaseController $user->settings->email_shiftinfo = $data['email_shiftinfo'] ?: false; $user->settings->email_news = $data['email_news'] ?: false; $user->settings->email_human = $data['email_human'] ?: false; + $user->settings->email_messages = $data['email_messages'] ?: false; if (config('enable_goody')) { $user->settings->email_goody = $data['email_goody'] ?: false; @@ -275,6 +276,7 @@ class SettingsController extends BaseController 'email_shiftinfo' => 'optional|checked', 'email_news' => 'optional|checked', 'email_human' => 'optional|checked', + 'email_messages' => 'optional|checked', 'email_goody' => 'optional|checked', ]; if (config('enable_planned_arrival')) { diff --git a/src/Events/Listener/Messages.php b/src/Events/Listener/Messages.php new file mode 100644 index 00000000..5494299e --- /dev/null +++ b/src/Events/Listener/Messages.php @@ -0,0 +1,46 @@ +receiver->settings->email_messages) { + return; + } + + $this->sendMail($message, $message->receiver, 'notification.messages.new', 'emails/messages-new'); + } + + private function sendMail(Message $message, User $user, string $subject, string $template): void + { + try { + $this->mailer->sendViewTranslated( + $user, + $subject, + $template, + ['sender' => $message->sender->name, 'send_message' => $message, 'username' => $user->name] + ); + } catch (TransportException $e) { + $this->log->error( + 'Unable to send email "{title}" to user {user} with {exception}', + ['title' => $subject, 'user' => $user->name, 'exception' => $e] + ); + } + } +} diff --git a/src/Models/User/Settings.php b/src/Models/User/Settings.php index 2501e8d9..9b936f8c 100644 --- a/src/Models/User/Settings.php +++ b/src/Models/User/Settings.php @@ -11,6 +11,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder; * @property string $language * @property int $theme * @property bool $email_human + * @property bool $email_messages * @property bool $email_goody * @property bool $email_shiftinfo * @property bool $email_news @@ -19,6 +20,7 @@ use Illuminate\Database\Query\Builder as QueryBuilder; * @method static QueryBuilder|Settings[] whereLanguage($value) * @method static QueryBuilder|Settings[] whereTheme($value) * @method static QueryBuilder|Settings[] whereEmailHuman($value) + * @method static QueryBuilder|Settings[] whereEmailMessages($value) * @method static QueryBuilder|Settings[] whereEmailGoody($value) * @method static QueryBuilder|Settings[] whereEmailShiftinfo($value) * @method static QueryBuilder|Settings[] whereEmailNews($value) @@ -34,6 +36,7 @@ class Settings extends HasUserModel /** @var array Default attributes */ protected $attributes = [ // phpcs:ignore 'email_human' => false, + 'email_messages' => false, 'email_goody' => false, 'email_shiftinfo' => false, 'email_news' => false, @@ -50,6 +53,7 @@ class Settings extends HasUserModel 'language', 'theme', 'email_human', + 'email_messages', 'email_goody', 'email_shiftinfo', 'email_news', @@ -61,6 +65,7 @@ class Settings extends HasUserModel 'user_id' => 'integer', 'theme' => 'integer', 'email_human' => 'boolean', + 'email_messages' => 'boolean', 'email_goody' => 'boolean', 'email_shiftinfo' => 'boolean', 'email_news' => 'boolean', diff --git a/tests/Unit/Controllers/MessagesControllerTest.php b/tests/Unit/Controllers/MessagesControllerTest.php index 69495675..e71d17ae 100644 --- a/tests/Unit/Controllers/MessagesControllerTest.php +++ b/tests/Unit/Controllers/MessagesControllerTest.php @@ -6,6 +6,7 @@ namespace Engelsystem\Test\Unit\Controllers; use Carbon\Carbon; use Engelsystem\Controllers\MessagesController; +use Engelsystem\Events\EventDispatcher; use Engelsystem\Helpers\Authenticator; use Engelsystem\Http\Exceptions\HttpForbidden; use Engelsystem\Http\Exceptions\ValidationException; @@ -34,6 +35,8 @@ class MessagesControllerTest extends ControllerTest protected Carbon $oneMinuteAgo; protected Carbon $twoMinutesAgo; + protected EventDispatcher $events; + /** * @testdox index: underNormalConditions -> returnsCorrectViewAndData * @covers \Engelsystem\Controllers\MessagesController::__construct @@ -229,7 +232,7 @@ class MessagesControllerTest extends ControllerTest */ public function testRedirectToConversationWithUserIdGivenRedirect(): void { - $this->request = $this->request->withParsedBody(['user_id' => '1']); + $this->request = $this->request->withParsedBody(['user_id' => '1']); $this->response->expects($this->once()) ->method('redirectTo') ->with('http://localhost/messages/1#newest') @@ -441,6 +444,8 @@ class MessagesControllerTest extends ControllerTest ->with('http://localhost/messages/' . $this->userB->id . '#newest') ->willReturn($this->response); + $this->setExpects($this->events, 'dispatch', ['message.created'], []); + $this->controller->send($this->request); $msg = Message::whereText('a')->first(); @@ -463,6 +468,8 @@ class MessagesControllerTest extends ControllerTest ->with('http://localhost/messages/' . $this->userA->id . '#newest') ->willReturn($this->response); + $this->setExpects($this->events, 'dispatch', ['message.created'], []); + $this->controller->send($this->request); $msg = Message::whereText('a')->first(); @@ -536,6 +543,9 @@ class MessagesControllerTest extends ControllerTest $this->controller = $this->app->get(MessagesController::class); $this->controller->setValidator(new Validator()); + + $this->events = $this->createMock(EventDispatcher::class); + $this->app->instance('events.dispatcher', $this->events); } protected function assertArrayOrCollection(mixed $obj): void @@ -547,11 +557,11 @@ class MessagesControllerTest extends ControllerTest { Message::unguard(); // unguard temporarily to save custom creation dates. $msg = new Message([ - 'user_id' => $from->id, + 'user_id' => $from->id, 'receiver_id' => $to->id, - 'text' => $text, - 'created_at' => $at, - 'updated_at' => $at, + 'text' => $text, + 'created_at' => $at, + 'updated_at' => $at, ]); $msg->save(); Message::reguard(); diff --git a/tests/Unit/Controllers/SettingsControllerTest.php b/tests/Unit/Controllers/SettingsControllerTest.php index c16b2b12..8d835da6 100644 --- a/tests/Unit/Controllers/SettingsControllerTest.php +++ b/tests/Unit/Controllers/SettingsControllerTest.php @@ -45,6 +45,7 @@ class SettingsControllerTest extends ControllerTest 'email_shiftinfo' => true, 'email_news' => true, 'email_human' => true, + 'email_messages' => true, 'email_goody' => true, 'shirt_size' => 'S', ]; @@ -122,6 +123,7 @@ class SettingsControllerTest extends ControllerTest $this->assertEquals($body['email_shiftinfo'], $this->user->settings->email_shiftinfo); $this->assertEquals($body['email_news'], $this->user->settings->email_news); $this->assertEquals($body['email_human'], $this->user->settings->email_human); + $this->assertEquals($body['email_messages'], $this->user->settings->email_messages); $this->assertEquals($body['email_goody'], $this->user->settings->email_goody); $this->assertEquals($body['shirt_size'], $this->user->personalData->shirt_size); } diff --git a/tests/Unit/Events/Listener/MessagesTest.php b/tests/Unit/Events/Listener/MessagesTest.php new file mode 100644 index 00000000..79a17051 --- /dev/null +++ b/tests/Unit/Events/Listener/MessagesTest.php @@ -0,0 +1,114 @@ +createMock(EngelsystemMailer::class); + /** @var User $user */ + $user = User::factory() + ->has(Settings::factory([ + 'email_messages' => true, + ])) + ->create(); + $message = Message::factory()->create(['receiver_id' => $user->id]); + + $mailer->expects($this->once()) + ->method('sendViewTranslated') + ->willReturnCallback(function ( + User $receiver, + string $subject, + string $template, + array $data + ) use ($user): void { + $this->assertEquals($user->id, $receiver->id); + $this->assertEquals('notification.messages.new', $subject); + $this->assertEquals('emails/messages-new', $template); + $this->assertArrayHasKey('username', $data); + $this->assertArrayHasKey('sender', $data); + $this->assertArrayHasKey('send_message', $data); + }); + + $handler = new Messages($this->log, $mailer); + $handler->created($message); + } + + /** + * @covers \Engelsystem\Events\Listener\Messages::created + */ + public function testCreatedNoEmail(): void + { + /** @var EngelsystemMailer|MockObject $mailer */ + $mailer = $this->createMock(EngelsystemMailer::class); + /** @var User $user */ + $user = User::factory() + ->has(Settings::factory([ + 'email_messages' => false, + ])) + ->create(); + $message = Message::factory()->create(['receiver_id' => $user->id]); + $mailer->expects($this->never())->method('sendViewTranslated'); + + $handler = new Messages($this->log, $mailer); + $handler->created($message); + } + + /** + * @covers \Engelsystem\Events\Listener\Messages::sendMail + */ + public function testSendMailExceptionHandling(): void + { + /** @var EngelsystemMailer|MockObject $mailer */ + $mailer = $this->createMock(EngelsystemMailer::class); + /** @var User $user */ + $user = User::factory() + ->has(Settings::factory([ + 'email_messages' => true, + ])) + ->create(); + $message = Message::factory()->create(['receiver_id' => $user->id]); + $mailer->expects($this->once()) + ->method('sendViewTranslated') + ->willReturnCallback(function (): void { + throw new TransportException(); + }); + + $handler = new Messages($this->log, $mailer); + + $handler->created($message); + $this->assertTrue($this->log->hasErrorThatContains('Unable to send email')); + } + + protected function setUp(): void + { + $this->log = new TestLogger(); + + parent::setUp(); + $this->initDatabase(); + } +}