<?php
namespace App\EventSubscriber;
use App\DBAL\Types\NotificationCommandType;
use App\DBAL\Types\NotificationType;
use App\Entity\Notification;
use App\Entity\Source;
use App\Entity\UserInterface;
use App\Entity\Workspace\Member;
use App\Entity\Workspace\Stream;
use App\Entity\Workspace\Stream\Collaborator;
use App\Entity\Workspace\Stream\Document;
use App\Entity\Workspace\Stream\Event;
use App\Entity\Workspace\Stream\Task;
use App\Event\ObjectEvent;
use App\Events;
use App\Service\Mailer;
use App\Util\InflectorFactory;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Prepare and process emails.
*/
class EmailSubscriber implements EventSubscriberInterface
{
/** @var Mailer */
private $mailer;
/** @var UrlGeneratorInterface */
private $urlGenerator;
/** @var string */
private $replyName;
/** @var string */
private $replyEmail;
/**
* @param Mailer $mailer
* @param UrlGeneratorInterface $urlGenerator
* @param string $replyName
* @param string $replyEmail
*/
public function __construct(
Mailer $mailer,
UrlGeneratorInterface $urlGenerator,
string $replyName,
string $replyEmail
) {
$this->mailer = $mailer;
$this->urlGenerator = $urlGenerator;
$this->replyName = $replyName;
$this->replyEmail = $replyEmail;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array
{
return [
Events::EMAIL_ACCOUNT_EXPIRED => 'onAccountExpired',
Events::EMAIL_NOTIFICATION => 'onNotification',
Events::EMAIL_REGISTRATION_TOKEN_SENT => 'onRegistrationTokenSent',
Events::EMAIL_RESETTING_PASSWORD_SENT => 'onResettingPasswordSent',
Events::EMAIL_COLLABORATOR_ADDED => 'onCollaboratorAdded',
Events::EMAIL_TARGET_USER_ADDED => 'onTargetUserAdded',
Events::EMAIL_INVITATION_SENT => 'onInvitationSent',
Events::EMAIL_INVITATION_REMINDER => 'onInvitationReminder',
Events::EMAIL_INVITATION_CANCELED => 'onInvitationCanceled',
Events::EMAIL_INVITATION_EXPIRED => 'onInvitationExpired',
Events::EMAIL_WEEKLY_PROGRESS_REPORT => 'onWeeklyProgressReport',
Events::EMAIL_DAILY_DIGEST_REPORT => 'onDailyDigestReport',
Events::EMAIL_DAILY_TODO_REPORT => 'onDailyTodoReport',
Events::EMAIL_SOURCE_INVITATION_SENT => 'onSourceInvitationSent',
Events::EMAIL_MFA_ENABLED => 'onMultiFactorEnabled',
Events::EMAIL_MFA_DISABLED => 'onMultiFactorDisabled',
Events::EMAIL_MFA_CODE_SENT => 'onMultiFactorCodeSent',
];
}
/**
* @param ObjectEvent $event
*/
public function onAccountExpired(ObjectEvent $event): void
{
/** @var UserInterface $user */
$user = $event->getObject();
$this->mailer->sendEmail('emails/account_expired.html.twig', [
'user' => $user,
], $user->getEmail());
}
/**
* @param ObjectEvent $event
*/
public function onNotification(ObjectEvent $event): void
{
/** @var Notification $notification */
$notification = $event->getObject();
switch ($notification->getCommand()) {
case NotificationCommandType::COMMENT_ADDED:
case NotificationCommandType::COLLABORATOR_MENTIONED:
$sender = new Address($this->replyEmail, $notification->getCreator() ?: $this->replyName);
if ($replyCode = $notification->getParams()['commentId'] ?? null) {
$comment = $notification->getObject()->getCommentById($replyCode);
}
break;
case NotificationCommandType::OBJECT_CREATED:
case NotificationCommandType::OBJECT_COMPLETED:
case NotificationCommandType::OBJECT_DUE:
case NotificationCommandType::OBJECT_REMINDER:
break;
// Ignore in-app only and other commands not included (catch all)
case NotificationCommandType::OBJECT_UPDATED:
case NotificationCommandType::OBJECT_DELETED:
case NotificationCommandType::MEMBER_ADDED:
case NotificationCommandType::COLLABORATOR_ADDED:
case NotificationCommandType::REACTION_ADDED:
default:
return;
}
/** @var Stream|Document|Event|Task $object */
$object = $notification->getObject();
switch ($notification->getType()) {
case NotificationType::EVENT:
$subPath = '/calendar';
// no break
case NotificationType::TASK:
$subPath = $subPath ?? '/board';
// no break
case NotificationType::DOCUMENT:
if ($comment ?? null) {
$objectUrl = $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/s/%s/%s%s/%s/%s',
$object->getWorkspace()->getId(),
$object->getStream()->getId(),
InflectorFactory::create()->pluralize($notification->getType()),
$subPath ?? '',
$object->getId(),
$comment->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL);
} else {
$objectUrl = $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/s/%s/%s%s/%s',
$object->getWorkspace()->getId(),
$object->getStream()->getId(),
InflectorFactory::create()->pluralize($notification->getType()),
$subPath ?? '',
$object->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL);
}
break;
case NotificationType::STREAM:
if ($object->isDirectMessage()) {
if ($comment ?? null) {
$objectUrl = $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/messages/%s/%s',
$object->getWorkspace()->getId(),
$object->getId(),
$comment->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL);
} else {
$objectUrl = $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/messages/%s',
$object->getWorkspace()->getId(),
$object->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL);
}
} else {
if ($comment ?? null) {
$objectUrl = $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/s/%s/chat/%s',
$object->getWorkspace()->getId(),
$object->getId(),
$comment->getParent()
? sprintf('%s/%s', $comment->getParent()->getId(), $comment->getId())
: $comment->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL);
} else {
$objectUrl = $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/s/%s',
$object->getWorkspace()->getId(),
$object->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL);
}
}
break;
}
$context = [
'notificationId' => (string) $notification->getId(),
'type' => $notification->getType(),
'sender' => $notification->getCreator(),
'receiver' => $notification->getOwner(),
'params' => $notification->getParams(),
'object' => $notification->getObject(),
'objectUrl' => $objectUrl ?? null,
];
if ($notification->getObject() instanceof Stream && $notification->getObject()->isDirectMessage()) {
$context['type'] = NotificationType::MESSAGE;
}
if ($replyCode ?? null) {
$context['replyCode'] = $replyCode;
}
if ($notification->getObject() instanceof Event) {
$icsFileContent = $notification->getObject()->generateICalendarFileContent();
$attachments = [
[
'name' => sprintf('event-%s.ics', time()),
'content' => $icsFileContent,
],
];
}
$this->mailer->sendEmail(
sprintf('emails/notification_%s.html.twig', $notification->getCommand()),
$context,
$notification->getOwner()->getEmail(),
$sender ?? null,
$attachments ?? []
);
}
/**
* @param ObjectEvent $event
*/
public function onRegistrationTokenSent(ObjectEvent $event): void
{
/** @var UserInterface $user */
$user = $event->getObject();
$this->mailer->sendEmail(
'emails/registration_token.html.twig',
[
'user' => $user,
'confirmTokenUrl' => $this->urlGenerator
->generate('loader', [
'reactRouting' => 'confirm-token',
'email' => $user->getEmail(),
], UrlGeneratorInterface::ABSOLUTE_URL),
],
$user->getEmail()
);
}
/**
* @param ObjectEvent $event
*/
public function onResettingPasswordSent(ObjectEvent $event): void
{
/** @var UserInterface $user */
$user = $event->getObject();
$this->mailer->sendEmail(
'emails/registration_forgot.html.twig',
[
'user' => $user,
'confirmTokenUrl' => $this->urlGenerator
->generate('loader', [
'reactRouting' => 'confirm-token',
'email' => $user->getEmail(),
'reset-password' => true,
], UrlGeneratorInterface::ABSOLUTE_URL),
],
$user->getEmail()
);
}
/**
* @param ObjectEvent $event
*/
public function onCollaboratorAdded(ObjectEvent $event): void
{
/** @var Collaborator $object */
$object = $event->getObject();
/** @var Stream $stream */
$stream = $object->getStream();
/** @var string|null $message */
$message = $event->getArguments()['message'] ?? null;
$this->mailer->sendEmail(
'emails/notification_collaborator_added.html.twig',
[
'type' => $stream->isDirectMessage()
? NotificationType::MESSAGE
: NotificationType::STREAM,
'sender' => $stream->getCreator(),
'receiver' => $object->getUser(),
'object' => $stream,
'message' => $message,
'objectUrl' => $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/%s/%s',
$stream->getWorkspace()->getId(),
$stream->isDirectMessage() ? 'messages' : 's',
$stream->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL),
],
$object->getUser()->getEmail(),
);
}
/**
* @param ObjectEvent $event
*/
public function onTargetUserAdded(ObjectEvent $event): void
{
/** @var Document|Event|Task $object */
$object = $event->getObject();
/** @var UserInterface|null $user */
if (!$user = $event->getArguments()['user'] ?? null) {
return;
}
if ($object instanceof Document) {
$type = NotificationType::DOCUMENT;
$objectUrl = $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/s/%s/documents/%s',
$object->getWorkspace()->getId(),
$object->getStream()->getId(),
$object->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL);
} elseif ($object instanceof Event) {
$type = NotificationType::EVENT;
$objectUrl = $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/s/%s/events/%s',
$object->getWorkspace()->getId(),
$object->getStream()->getId(),
$object->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL);
$icsFileContent = $object->generateICalendarFileContent();
$attachments = [
[
'name' => sprintf('event-%s.ics', time()),
'content' => $icsFileContent,
],
];
} elseif ($object instanceof Task) {
$type = NotificationType::TASK;
$objectUrl = $this->urlGenerator->generate('loader', [
'reactRouting' => sprintf(
'w/%s/s/%s/tasks/board/%s',
$object->getWorkspace()->getId(),
$object->getStream()->getId(),
$object->getId()
),
], UrlGeneratorInterface::ABSOLUTE_URL);
} else {
return;
}
$this->mailer->sendEmail(
'emails/notification_target_user_added.html.twig',
[
'type' => $type,
'sender' => $object->getCreator(),
'receiver' => $user,
'object' => $object,
'objectUrl' => str_replace('%23', '#', $objectUrl),
],
$user->getEmail(),
null,
$attachments ?? []
);
}
/**
* @param ObjectEvent $event
*/
public function onInvitationSent(ObjectEvent $event): void
{
/** @var Member|Collaborator $object */
$object = $event->getObject();
/** @var string|null $message */
$message = $event->getArguments()['message'] ?? null;
if ($object instanceof Member) {
$workspace = $object->getWorkspace();
} elseif ($object instanceof Collaborator) {
$workspace = $object->getWorkspace();
} else {
return;
}
$this->mailer->sendEmail(
'emails/invitation_invite.html.twig',
[
'workspace' => $workspace,
'inviter' => $workspace->getOwner(),
'invitee' => $object,
'message' => $message,
'invitationUrl' => $this->urlGenerator->generate('invitation', [
'type' => $object instanceof Member
? NotificationType::WORKSPACE
: NotificationType::STREAM,
'token' => $object->getInvitationToken(),
], UrlGeneratorInterface::ABSOLUTE_URL),
],
$object->getEmail() ?: $object->getUser()->getEmail()
);
}
/**
* @param ObjectEvent $event
*/
public function onInvitationReminder(ObjectEvent $event): void
{
/** @var Member|Collaborator $object */
$object = $event->getObject();
if ($object instanceof Member) {
$workspace = $object->getWorkspace();
} elseif ($object instanceof Collaborator) {
$workspace = $object->getWorkspace();
} else {
return;
}
$this->mailer->sendEmail(
'emails/invitation_reminder.html.twig',
[
'workspace' => $workspace,
'inviter' => $workspace->getOwner(),
'invitee' => $object,
'invitationUrl' => $this->urlGenerator->generate('invitation', [
'type' => $object instanceof Member
? NotificationType::WORKSPACE
: NotificationType::STREAM,
'token' => $object->getInvitationToken(),
], UrlGeneratorInterface::ABSOLUTE_URL),
],
$object->getEmail() ?: $object->getUser()->getEmail()
);
}
/**
* @param ObjectEvent $event
*/
public function onInvitationCanceled(ObjectEvent $event): void
{
/** @var Member|Collaborator $object */
$object = $event->getObject();
if (!$object instanceof Member && !$object instanceof Collaborator) {
return;
}
$this->mailer->sendEmail(
'emails/invitation_canceled.html.twig',
[],
$object->getEmail() ?: $object->getUser()->getEmail()
);
}
/**
* @param ObjectEvent $event
*/
public function onInvitationExpired(ObjectEvent $event): void
{
/** @var Member|Collaborator $object */
$object = $event->getObject();
if ($object instanceof Member) {
$workspace = $object->getWorkspace();
} elseif ($object instanceof Collaborator) {
$workspace = $object->getWorkspace();
} else {
return;
}
$this->mailer->sendEmail(
'emails/invitation_expired.html.twig',
[
'workspace' => $workspace,
'inviter' => $workspace->getOwner(),
'invitationUrl' => $this->urlGenerator->generate('invitation', [
'type' => $object instanceof Member
? NotificationType::WORKSPACE
: NotificationType::STREAM,
'token' => $object->getInvitationToken(),
], UrlGeneratorInterface::ABSOLUTE_URL),
],
$object->getEmail() ?: $object->getUser()->getEmail()
);
}
/**
* @param GenericEvent $event
*/
public function onWeeklyProgressReport(GenericEvent $event)
{
/** @var UserInterface $user */
$user = $event->getSubject();
/** @var array<string, number> $actions */
$actions = $event->getArguments();
$this->mailer->sendEmail(
'emails/weekly_progress_report.html.twig',
[
'user' => $user,
'actions' => $actions,
'homepageUrl' => $this->urlGenerator->generate('loader', [],
UrlGeneratorInterface::ABSOLUTE_URL),
],
$user->getEmail()
);
}
/**
* @param GenericEvent $event
*/
public function onDailyDigestReport(GenericEvent $event)
{
/** @var UserInterface $user */
$user = $event->getSubject();
/** @var Notification[] notifications */
$notifications = $event->getArguments();
$this->mailer->sendEmail(
'emails/daily_digest_report.html.twig',
[
'user' => $user,
'notifications' => $notifications,
'homepageUrl' => $this->urlGenerator->generate('loader', [],
UrlGeneratorInterface::ABSOLUTE_URL),
],
$user->getEmail()
);
}
/**
* @param GenericEvent $event
*/
public function onDailyTodoReport(GenericEvent $event)
{
/** @var UserInterface $user */
$user = $event->getSubject();
/** @var (Document|Task|Event)[] $items */
$items = $event->getArguments();
$this->mailer->sendEmail(
'emails/daily_todo_report.html.twig',
[
'user' => $user,
'items' => $items,
'homepageUrl' => $this->urlGenerator->generate('loader', [],
UrlGeneratorInterface::ABSOLUTE_URL),
],
$user->getEmail()
);
}
/**
* @param GenericEvent $event
*/
public function onSourceInvitationSent(GenericEvent $event)
{
/** @var Source $source */
$source = $event->getSubject();
/** @var string[] $emails */
$emails = $event->getArguments();
foreach ($emails as $email) {
$this->mailer->sendEmail(
'emails/source_invitation.html.twig',
[
'email' => $email,
'invitationUrl' => $this->urlGenerator
->generate('loader', [
'reactRouting' => 'registration',
'source' => (string) $source->getId(),
'inviteEmail' => $email,
], UrlGeneratorInterface::ABSOLUTE_URL),
],
$email
);
}
}
/**
* @param ObjectEvent $event
*/
public function onMultiFactorEnabled(ObjectEvent $event): void
{
/** @var User $user */
$user = $event->getObject();
$this->mailer->sendEmail('emails/mfa_enabled.html.twig', [], $user->getEmail());
}
/**
* @param ObjectEvent $event
*/
public function onMultiFactorDisabled(ObjectEvent $event): void
{
/** @var User $user */
$user = $event->getObject();
$this->mailer->sendEmail('emails/mfa_disabled.html.twig', [], $user->getEmail());
}
/**
* @param ObjectEvent $event
*/
public function onMultiFactorCodeSent(ObjectEvent $event): void
{
/** @var User $user */
$user = $event->getObject();
$this->mailer->sendEmail(
'emails/mfa_code.html.twig',
[
'user' => $user,
],
$user->getEmail()
);
}
}