src/EventSubscriber/EmailSubscriber.php line 342

Open in your IDE?
  1. <?php
  2. namespace App\EventSubscriber;
  3. use App\DBAL\Types\NotificationCommandType;
  4. use App\DBAL\Types\NotificationType;
  5. use App\Entity\Notification;
  6. use App\Entity\Source;
  7. use App\Entity\UserInterface;
  8. use App\Entity\Workspace\Member;
  9. use App\Entity\Workspace\Stream;
  10. use App\Entity\Workspace\Stream\Collaborator;
  11. use App\Entity\Workspace\Stream\Document;
  12. use App\Entity\Workspace\Stream\Event;
  13. use App\Entity\Workspace\Stream\Task;
  14. use App\Event\ObjectEvent;
  15. use App\Events;
  16. use App\Service\Mailer;
  17. use App\Util\InflectorFactory;
  18. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  19. use Symfony\Component\EventDispatcher\GenericEvent;
  20. use Symfony\Component\Mime\Address;
  21. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  22. /**
  23.  * Prepare and process emails.
  24.  */
  25. class EmailSubscriber implements EventSubscriberInterface
  26. {
  27.     /** @var Mailer */
  28.     private $mailer;
  29.     /** @var UrlGeneratorInterface */
  30.     private $urlGenerator;
  31.     /** @var string */
  32.     private $replyName;
  33.     /** @var string */
  34.     private $replyEmail;
  35.     /**
  36.      * @param Mailer                $mailer
  37.      * @param UrlGeneratorInterface $urlGenerator
  38.      * @param string                $replyName
  39.      * @param string                $replyEmail
  40.      */
  41.     public function __construct(
  42.         Mailer $mailer,
  43.         UrlGeneratorInterface $urlGenerator,
  44.         string $replyName,
  45.         string $replyEmail
  46.     ) {
  47.         $this->mailer $mailer;
  48.         $this->urlGenerator $urlGenerator;
  49.         $this->replyName $replyName;
  50.         $this->replyEmail $replyEmail;
  51.     }
  52.     /**
  53.      * {@inheritdoc}
  54.      */
  55.     public static function getSubscribedEvents(): array
  56.     {
  57.         return [
  58.             Events::EMAIL_ACCOUNT_EXPIRED => 'onAccountExpired',
  59.             Events::EMAIL_NOTIFICATION => 'onNotification',
  60.             Events::EMAIL_REGISTRATION_TOKEN_SENT => 'onRegistrationTokenSent',
  61.             Events::EMAIL_RESETTING_PASSWORD_SENT => 'onResettingPasswordSent',
  62.             Events::EMAIL_COLLABORATOR_ADDED => 'onCollaboratorAdded',
  63.             Events::EMAIL_TARGET_USER_ADDED => 'onTargetUserAdded',
  64.             Events::EMAIL_INVITATION_SENT => 'onInvitationSent',
  65.             Events::EMAIL_INVITATION_REMINDER => 'onInvitationReminder',
  66.             Events::EMAIL_INVITATION_CANCELED => 'onInvitationCanceled',
  67.             Events::EMAIL_INVITATION_EXPIRED => 'onInvitationExpired',
  68.             Events::EMAIL_WEEKLY_PROGRESS_REPORT => 'onWeeklyProgressReport',
  69.             Events::EMAIL_DAILY_DIGEST_REPORT => 'onDailyDigestReport',
  70.             Events::EMAIL_DAILY_TODO_REPORT => 'onDailyTodoReport',
  71.             Events::EMAIL_SOURCE_INVITATION_SENT => 'onSourceInvitationSent',
  72.             Events::EMAIL_MFA_ENABLED => 'onMultiFactorEnabled',
  73.             Events::EMAIL_MFA_DISABLED => 'onMultiFactorDisabled',
  74.             Events::EMAIL_MFA_CODE_SENT => 'onMultiFactorCodeSent',
  75.         ];
  76.     }
  77.     /**
  78.      * @param ObjectEvent $event
  79.      */
  80.     public function onAccountExpired(ObjectEvent $event): void
  81.     {
  82.         /** @var UserInterface $user */
  83.         $user $event->getObject();
  84.         $this->mailer->sendEmail('emails/account_expired.html.twig', [
  85.             'user' => $user,
  86.         ], $user->getEmail());
  87.     }
  88.     /**
  89.      * @param ObjectEvent $event
  90.      */
  91.     public function onNotification(ObjectEvent $event): void
  92.     {
  93.         /** @var Notification $notification */
  94.         $notification $event->getObject();
  95.         switch ($notification->getCommand()) {
  96.             case NotificationCommandType::COMMENT_ADDED:
  97.             case NotificationCommandType::COLLABORATOR_MENTIONED:
  98.                 $sender = new Address($this->replyEmail$notification->getCreator() ?: $this->replyName);
  99.                 if ($replyCode $notification->getParams()['commentId'] ?? null) {
  100.                     $comment $notification->getObject()->getCommentById($replyCode);
  101.                 }
  102.                 break;
  103.             case NotificationCommandType::OBJECT_CREATED:
  104.             case NotificationCommandType::OBJECT_COMPLETED:
  105.             case NotificationCommandType::OBJECT_DUE:
  106.             case NotificationCommandType::OBJECT_REMINDER:
  107.                 break;
  108.             // Ignore in-app only and other commands not included (catch all)
  109.             case NotificationCommandType::OBJECT_UPDATED:
  110.             case NotificationCommandType::OBJECT_DELETED:
  111.             case NotificationCommandType::MEMBER_ADDED:
  112.             case NotificationCommandType::COLLABORATOR_ADDED:
  113.             case NotificationCommandType::REACTION_ADDED:
  114.             default:
  115.                 return;
  116.         }
  117.         /** @var Stream|Document|Event|Task $object */
  118.         $object $notification->getObject();
  119.         switch ($notification->getType()) {
  120.             case NotificationType::EVENT:
  121.                 $subPath '/calendar';
  122.                 // no break
  123.             case NotificationType::TASK:
  124.                 $subPath $subPath ?? '/board';
  125.                 // no break
  126.             case NotificationType::DOCUMENT:
  127.                 if ($comment ?? null) {
  128.                     $objectUrl $this->urlGenerator->generate('loader', [
  129.                         'reactRouting' => sprintf(
  130.                             'w/%s/s/%s/%s%s/%s/%s',
  131.                             $object->getWorkspace()->getId(),
  132.                             $object->getStream()->getId(),
  133.                             InflectorFactory::create()->pluralize($notification->getType()),
  134.                             $subPath ?? '',
  135.                             $object->getId(),
  136.                             $comment->getId()
  137.                         ),
  138.                     ], UrlGeneratorInterface::ABSOLUTE_URL);
  139.                 } else {
  140.                     $objectUrl $this->urlGenerator->generate('loader', [
  141.                         'reactRouting' => sprintf(
  142.                             'w/%s/s/%s/%s%s/%s',
  143.                             $object->getWorkspace()->getId(),
  144.                             $object->getStream()->getId(),
  145.                             InflectorFactory::create()->pluralize($notification->getType()),
  146.                             $subPath ?? '',
  147.                             $object->getId()
  148.                         ),
  149.                     ], UrlGeneratorInterface::ABSOLUTE_URL);
  150.                 }
  151.                 break;
  152.             case NotificationType::STREAM:
  153.                 if ($object->isDirectMessage()) {
  154.                     if ($comment ?? null) {
  155.                         $objectUrl $this->urlGenerator->generate('loader', [
  156.                             'reactRouting' => sprintf(
  157.                                 'w/%s/messages/%s/%s',
  158.                                 $object->getWorkspace()->getId(),
  159.                                 $object->getId(),
  160.                                 $comment->getId()
  161.                             ),
  162.                         ], UrlGeneratorInterface::ABSOLUTE_URL);
  163.                     } else {
  164.                         $objectUrl $this->urlGenerator->generate('loader', [
  165.                             'reactRouting' => sprintf(
  166.                                 'w/%s/messages/%s',
  167.                                 $object->getWorkspace()->getId(),
  168.                                 $object->getId()
  169.                             ),
  170.                         ], UrlGeneratorInterface::ABSOLUTE_URL);
  171.                     }
  172.                 } else {
  173.                     if ($comment ?? null) {
  174.                         $objectUrl $this->urlGenerator->generate('loader', [
  175.                             'reactRouting' => sprintf(
  176.                                 'w/%s/s/%s/chat/%s',
  177.                                 $object->getWorkspace()->getId(),
  178.                                 $object->getId(),
  179.                                 $comment->getParent()
  180.                                     ? sprintf('%s/%s'$comment->getParent()->getId(), $comment->getId())
  181.                                     : $comment->getId()
  182.                             ),
  183.                         ], UrlGeneratorInterface::ABSOLUTE_URL);
  184.                     } else {
  185.                         $objectUrl $this->urlGenerator->generate('loader', [
  186.                             'reactRouting' => sprintf(
  187.                                 'w/%s/s/%s',
  188.                                 $object->getWorkspace()->getId(),
  189.                                 $object->getId()
  190.                             ),
  191.                         ], UrlGeneratorInterface::ABSOLUTE_URL);
  192.                     }
  193.                 }
  194.                 break;
  195.         }
  196.         $context = [
  197.             'notificationId' => (string) $notification->getId(),
  198.             'type' => $notification->getType(),
  199.             'sender' => $notification->getCreator(),
  200.             'receiver' => $notification->getOwner(),
  201.             'params' => $notification->getParams(),
  202.             'object' => $notification->getObject(),
  203.             'objectUrl' => $objectUrl ?? null,
  204.         ];
  205.         if ($notification->getObject() instanceof Stream && $notification->getObject()->isDirectMessage()) {
  206.             $context['type'] = NotificationType::MESSAGE;
  207.         }
  208.         if ($replyCode ?? null) {
  209.             $context['replyCode'] = $replyCode;
  210.         }
  211.         if ($notification->getObject() instanceof Event) {
  212.             $icsFileContent $notification->getObject()->generateICalendarFileContent();
  213.             $attachments = [
  214.                 [
  215.                     'name' => sprintf('event-%s.ics'time()),
  216.                     'content' => $icsFileContent,
  217.                 ],
  218.             ];
  219.         }
  220.         $this->mailer->sendEmail(
  221.             sprintf('emails/notification_%s.html.twig'$notification->getCommand()),
  222.             $context,
  223.             $notification->getOwner()->getEmail(),
  224.             $sender ?? null,
  225.             $attachments ?? []
  226.         );
  227.     }
  228.     /**
  229.      * @param ObjectEvent $event
  230.      */
  231.     public function onRegistrationTokenSent(ObjectEvent $event): void
  232.     {
  233.         /** @var UserInterface $user */
  234.         $user $event->getObject();
  235.         $this->mailer->sendEmail(
  236.             'emails/registration_token.html.twig',
  237.             [
  238.                 'user' => $user,
  239.                 'confirmTokenUrl' => $this->urlGenerator
  240.                     ->generate('loader', [
  241.                         'reactRouting' => 'confirm-token',
  242.                         'email' => $user->getEmail(),
  243.                     ], UrlGeneratorInterface::ABSOLUTE_URL),
  244.             ],
  245.             $user->getEmail()
  246.         );
  247.     }
  248.     /**
  249.      * @param ObjectEvent $event
  250.      */
  251.     public function onResettingPasswordSent(ObjectEvent $event): void
  252.     {
  253.         /** @var UserInterface $user */
  254.         $user $event->getObject();
  255.         $this->mailer->sendEmail(
  256.             'emails/registration_forgot.html.twig',
  257.             [
  258.                 'user' => $user,
  259.                 'confirmTokenUrl' => $this->urlGenerator
  260.                     ->generate('loader', [
  261.                         'reactRouting' => 'confirm-token',
  262.                         'email' => $user->getEmail(),
  263.                         'reset-password' => true,
  264.                     ], UrlGeneratorInterface::ABSOLUTE_URL),
  265.             ],
  266.             $user->getEmail()
  267.         );
  268.     }
  269.     /**
  270.      * @param ObjectEvent $event
  271.      */
  272.     public function onCollaboratorAdded(ObjectEvent $event): void
  273.     {
  274.         /** @var Collaborator $object */
  275.         $object $event->getObject();
  276.         /** @var Stream $stream */
  277.         $stream $object->getStream();
  278.         /** @var string|null $message */
  279.         $message $event->getArguments()['message'] ?? null;
  280.         $this->mailer->sendEmail(
  281.             'emails/notification_collaborator_added.html.twig',
  282.             [
  283.                 'type' => $stream->isDirectMessage()
  284.                     ? NotificationType::MESSAGE
  285.                     NotificationType::STREAM,
  286.                 'sender' => $stream->getCreator(),
  287.                 'receiver' => $object->getUser(),
  288.                 'object' => $stream,
  289.                 'message' => $message,
  290.                 'objectUrl' => $this->urlGenerator->generate('loader', [
  291.                     'reactRouting' => sprintf(
  292.                         'w/%s/%s/%s',
  293.                         $stream->getWorkspace()->getId(),
  294.                         $stream->isDirectMessage() ? 'messages' 's',
  295.                         $stream->getId()
  296.                     ),
  297.                 ], UrlGeneratorInterface::ABSOLUTE_URL),
  298.             ],
  299.             $object->getUser()->getEmail(),
  300.         );
  301.     }
  302.     /**
  303.      * @param ObjectEvent $event
  304.      */
  305.     public function onTargetUserAdded(ObjectEvent $event): void
  306.     {
  307.         /** @var Document|Event|Task $object */
  308.         $object $event->getObject();
  309.         /** @var UserInterface|null $user */
  310.         if (!$user $event->getArguments()['user'] ?? null) {
  311.             return;
  312.         }
  313.         if ($object instanceof Document) {
  314.             $type NotificationType::DOCUMENT;
  315.             $objectUrl $this->urlGenerator->generate('loader', [
  316.                 'reactRouting' => sprintf(
  317.                     'w/%s/s/%s/documents/%s',
  318.                     $object->getWorkspace()->getId(),
  319.                     $object->getStream()->getId(),
  320.                     $object->getId()
  321.                 ),
  322.             ], UrlGeneratorInterface::ABSOLUTE_URL);
  323.         } elseif ($object instanceof Event) {
  324.             $type NotificationType::EVENT;
  325.             $objectUrl $this->urlGenerator->generate('loader', [
  326.                 'reactRouting' => sprintf(
  327.                     'w/%s/s/%s/events/%s',
  328.                     $object->getWorkspace()->getId(),
  329.                     $object->getStream()->getId(),
  330.                     $object->getId()
  331.                 ),
  332.             ], UrlGeneratorInterface::ABSOLUTE_URL);
  333.             $icsFileContent $object->generateICalendarFileContent();
  334.             $attachments = [
  335.                 [
  336.                     'name' => sprintf('event-%s.ics'time()),
  337.                     'content' => $icsFileContent,
  338.                 ],
  339.             ];
  340.         } elseif ($object instanceof Task) {
  341.             $type NotificationType::TASK;
  342.             $objectUrl $this->urlGenerator->generate('loader', [
  343.                 'reactRouting' => sprintf(
  344.                     'w/%s/s/%s/tasks/board/%s',
  345.                     $object->getWorkspace()->getId(),
  346.                     $object->getStream()->getId(),
  347.                     $object->getId()
  348.                 ),
  349.             ], UrlGeneratorInterface::ABSOLUTE_URL);
  350.         } else {
  351.             return;
  352.         }
  353.         $this->mailer->sendEmail(
  354.             'emails/notification_target_user_added.html.twig',
  355.             [
  356.                 'type' => $type,
  357.                 'sender' => $object->getCreator(),
  358.                 'receiver' => $user,
  359.                 'object' => $object,
  360.                 'objectUrl' => str_replace('%23''#'$objectUrl),
  361.             ],
  362.             $user->getEmail(),
  363.             null,
  364.             $attachments ?? []
  365.         );
  366.     }
  367.     /**
  368.      * @param ObjectEvent $event
  369.      */
  370.     public function onInvitationSent(ObjectEvent $event): void
  371.     {
  372.         /** @var Member|Collaborator $object */
  373.         $object $event->getObject();
  374.         /** @var string|null $message */
  375.         $message $event->getArguments()['message'] ?? null;
  376.         if ($object instanceof Member) {
  377.             $workspace $object->getWorkspace();
  378.         } elseif ($object instanceof Collaborator) {
  379.             $workspace $object->getWorkspace();
  380.         } else {
  381.             return;
  382.         }
  383.         $this->mailer->sendEmail(
  384.             'emails/invitation_invite.html.twig',
  385.             [
  386.                 'workspace' => $workspace,
  387.                 'inviter' => $workspace->getOwner(),
  388.                 'invitee' => $object,
  389.                 'message' => $message,
  390.                 'invitationUrl' => $this->urlGenerator->generate('invitation', [
  391.                     'type' => $object instanceof Member
  392.                         NotificationType::WORKSPACE
  393.                         NotificationType::STREAM,
  394.                     'token' => $object->getInvitationToken(),
  395.                 ], UrlGeneratorInterface::ABSOLUTE_URL),
  396.             ],
  397.             $object->getEmail() ?: $object->getUser()->getEmail()
  398.         );
  399.     }
  400.     /**
  401.      * @param ObjectEvent $event
  402.      */
  403.     public function onInvitationReminder(ObjectEvent $event): void
  404.     {
  405.         /** @var Member|Collaborator $object */
  406.         $object $event->getObject();
  407.         if ($object instanceof Member) {
  408.             $workspace $object->getWorkspace();
  409.         } elseif ($object instanceof Collaborator) {
  410.             $workspace $object->getWorkspace();
  411.         } else {
  412.             return;
  413.         }
  414.         $this->mailer->sendEmail(
  415.             'emails/invitation_reminder.html.twig',
  416.             [
  417.                 'workspace' => $workspace,
  418.                 'inviter' => $workspace->getOwner(),
  419.                 'invitee' => $object,
  420.                 'invitationUrl' => $this->urlGenerator->generate('invitation', [
  421.                     'type' => $object instanceof Member
  422.                         NotificationType::WORKSPACE
  423.                         NotificationType::STREAM,
  424.                     'token' => $object->getInvitationToken(),
  425.                 ], UrlGeneratorInterface::ABSOLUTE_URL),
  426.             ],
  427.             $object->getEmail() ?: $object->getUser()->getEmail()
  428.         );
  429.     }
  430.     /**
  431.      * @param ObjectEvent $event
  432.      */
  433.     public function onInvitationCanceled(ObjectEvent $event): void
  434.     {
  435.         /** @var Member|Collaborator $object */
  436.         $object $event->getObject();
  437.         if (!$object instanceof Member && !$object instanceof Collaborator) {
  438.             return;
  439.         }
  440.         $this->mailer->sendEmail(
  441.             'emails/invitation_canceled.html.twig',
  442.             [],
  443.             $object->getEmail() ?: $object->getUser()->getEmail()
  444.         );
  445.     }
  446.     /**
  447.      * @param ObjectEvent $event
  448.      */
  449.     public function onInvitationExpired(ObjectEvent $event): void
  450.     {
  451.         /** @var Member|Collaborator $object */
  452.         $object $event->getObject();
  453.         if ($object instanceof Member) {
  454.             $workspace $object->getWorkspace();
  455.         } elseif ($object instanceof Collaborator) {
  456.             $workspace $object->getWorkspace();
  457.         } else {
  458.             return;
  459.         }
  460.         $this->mailer->sendEmail(
  461.             'emails/invitation_expired.html.twig',
  462.             [
  463.                 'workspace' => $workspace,
  464.                 'inviter' => $workspace->getOwner(),
  465.                 'invitationUrl' => $this->urlGenerator->generate('invitation', [
  466.                     'type' => $object instanceof Member
  467.                         NotificationType::WORKSPACE
  468.                         NotificationType::STREAM,
  469.                     'token' => $object->getInvitationToken(),
  470.                 ], UrlGeneratorInterface::ABSOLUTE_URL),
  471.             ],
  472.             $object->getEmail() ?: $object->getUser()->getEmail()
  473.         );
  474.     }
  475.     /**
  476.      * @param GenericEvent $event
  477.      */
  478.     public function onWeeklyProgressReport(GenericEvent $event)
  479.     {
  480.         /** @var UserInterface $user */
  481.         $user $event->getSubject();
  482.         /** @var array<string, number> $actions */
  483.         $actions $event->getArguments();
  484.         $this->mailer->sendEmail(
  485.             'emails/weekly_progress_report.html.twig',
  486.             [
  487.                 'user' => $user,
  488.                 'actions' => $actions,
  489.                 'homepageUrl' => $this->urlGenerator->generate('loader', [],
  490.                     UrlGeneratorInterface::ABSOLUTE_URL),
  491.             ],
  492.             $user->getEmail()
  493.         );
  494.     }
  495.     /**
  496.      * @param GenericEvent $event
  497.      */
  498.     public function onDailyDigestReport(GenericEvent $event)
  499.     {
  500.         /** @var UserInterface $user */
  501.         $user $event->getSubject();
  502.         /** @var Notification[] notifications */
  503.         $notifications $event->getArguments();
  504.         $this->mailer->sendEmail(
  505.             'emails/daily_digest_report.html.twig',
  506.             [
  507.                 'user' => $user,
  508.                 'notifications' => $notifications,
  509.                 'homepageUrl' => $this->urlGenerator->generate('loader', [],
  510.                     UrlGeneratorInterface::ABSOLUTE_URL),
  511.             ],
  512.             $user->getEmail()
  513.         );
  514.     }
  515.     /**
  516.      * @param GenericEvent $event
  517.      */
  518.     public function onDailyTodoReport(GenericEvent $event)
  519.     {
  520.         /** @var UserInterface $user */
  521.         $user $event->getSubject();
  522.         /** @var (Document|Task|Event)[] $items */
  523.         $items $event->getArguments();
  524.         $this->mailer->sendEmail(
  525.             'emails/daily_todo_report.html.twig',
  526.             [
  527.                 'user' => $user,
  528.                 'items' => $items,
  529.                 'homepageUrl' => $this->urlGenerator->generate('loader', [],
  530.                     UrlGeneratorInterface::ABSOLUTE_URL),
  531.             ],
  532.             $user->getEmail()
  533.         );
  534.     }
  535.     /**
  536.      * @param GenericEvent $event
  537.      */
  538.     public function onSourceInvitationSent(GenericEvent $event)
  539.     {
  540.         /** @var Source $source */
  541.         $source $event->getSubject();
  542.         /** @var string[] $emails */
  543.         $emails $event->getArguments();
  544.         foreach ($emails as $email) {
  545.             $this->mailer->sendEmail(
  546.                 'emails/source_invitation.html.twig',
  547.                 [
  548.                     'email' => $email,
  549.                     'invitationUrl' => $this->urlGenerator
  550.                         ->generate('loader', [
  551.                             'reactRouting' => 'registration',
  552.                             'source' => (string) $source->getId(),
  553.                             'inviteEmail' => $email,
  554.                         ], UrlGeneratorInterface::ABSOLUTE_URL),
  555.                 ],
  556.                 $email
  557.             );
  558.         }
  559.     }
  560.     /**
  561.      * @param ObjectEvent $event
  562.      */
  563.     public function onMultiFactorEnabled(ObjectEvent $event): void
  564.     {
  565.         /** @var User $user */
  566.         $user $event->getObject();
  567.         $this->mailer->sendEmail('emails/mfa_enabled.html.twig', [], $user->getEmail());
  568.     }
  569.     /**
  570.      * @param ObjectEvent $event
  571.      */
  572.     public function onMultiFactorDisabled(ObjectEvent $event): void
  573.     {
  574.         /** @var User $user */
  575.         $user $event->getObject();
  576.         $this->mailer->sendEmail('emails/mfa_disabled.html.twig', [], $user->getEmail());
  577.     }
  578.     /**
  579.      * @param ObjectEvent $event
  580.      */
  581.     public function onMultiFactorCodeSent(ObjectEvent $event): void
  582.     {
  583.         /** @var User $user */
  584.         $user $event->getObject();
  585.         $this->mailer->sendEmail(
  586.             'emails/mfa_code.html.twig',
  587.             [
  588.                 'user' => $user,
  589.             ],
  590.             $user->getEmail()
  591.         );
  592.     }
  593. }