*/ private $emailNotificationEntityHandlerHash = []; /** @var array */ private $userIdPortalCacheMap = []; public function __construct( private EntityManager $entityManager, private HtmlizerFactory $htmlizerFactory, private EmailSender $emailSender, private Config $config, private InjectableFactory $injectableFactory, private TemplateFileManager $templateFileManager, private Metadata $metadata, private Language $language, private Log $log, private NoteAccessControl $noteAccessControl ) {} public function process(): void { $mentionEmailNotifications = $this->config->get('mentionEmailNotifications'); $streamEmailNotifications = $this->config->get('streamEmailNotifications'); $portalStreamEmailNotifications = $this->config->get('portalStreamEmailNotifications'); $typeList = []; if ($mentionEmailNotifications) { $typeList[] = Notification::TYPE_MENTION_IN_POST; } if ($streamEmailNotifications || $portalStreamEmailNotifications) { $typeList[] = Notification::TYPE_NOTE; } if (empty($typeList)) { return; } $fromDt = new DateTime(); $fromDt->modify('-' . self::HOURS_THRESHOLD . ' hours'); $where = [ 'createdAt>' => $fromDt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT), 'read' => false, 'emailIsProcessed' => false, ]; $delay = $this->config->get('emailNotificationsDelay'); if ($delay) { $delayDt = new DateTime(); $delayDt->modify('-' . $delay . ' seconds'); $where[] = ['createdAt<' => $delayDt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)]; } $queryList = []; foreach ($typeList as $type) { $itemBuilder = null; if ($type === Notification::TYPE_MENTION_IN_POST) { $itemBuilder = $this->getNotificationQueryBuilderMentionInPost(); } if ($type === Notification::TYPE_NOTE) { $itemBuilder = $this->getNotificationQueryBuilderNote(); } if (!$itemBuilder) { continue; } $itemBuilder->where($where); $queryList[] = $itemBuilder->build(); } $builder = $this->entityManager ->getQueryBuilder() ->union() ->order('number') ->limit(0, self::PROCESS_MAX_COUNT); foreach ($queryList as $query) { $builder->query($query); } $unionQuery = $builder->build(); $sql = $this->entityManager ->getQueryComposer() ->compose($unionQuery); /** @var Collection $notificationList */ $notificationList = $this->entityManager ->getRDBRepository(Notification::ENTITY_TYPE) ->findBySql($sql); foreach ($notificationList as $notification) { $notification->set('emailIsProcessed', true); $type = $notification->getType(); try { if ($type === Notification::TYPE_NOTE) { $this->processNotificationNote($notification); } else if ($type === Notification::TYPE_MENTION_IN_POST) { $this->processNotificationMentionInPost($notification); } else { // For bc. $methodName = 'processNotification' . ucfirst($type ?? 'Dummy'); if (method_exists($this, $methodName)) { $this->$methodName($notification); } } } catch (Throwable $e) { $this->log->error("Email Notification: " . $e->getMessage()); } $this->entityManager->saveEntity($notification); } } protected function getNotificationQueryBuilderMentionInPost(): SelectBuilder { return $this->entityManager ->getQueryBuilder() ->select() ->from(Notification::ENTITY_TYPE) ->where([ 'type' => Notification::TYPE_MENTION_IN_POST, ]); } protected function getNotificationQueryBuilderNote(): SelectBuilder { $noteNotificationTypeList = $this->config->get('streamEmailNotificationsTypeList', []); $builder = $this->entityManager ->getQueryBuilder() ->select() ->from(Notification::ENTITY_TYPE) ->join(Note::ENTITY_TYPE, 'note', ['note.id:' => 'relatedId']) ->where([ 'type' => Notification::TYPE_NOTE, 'relatedType' => Note::ENTITY_TYPE, 'note.type' => $noteNotificationTypeList, ]); $entityList = $this->config->get('streamEmailNotificationsEntityList'); if (empty($entityList)) { $builder->where([ 'relatedParentType' => null, ]); } else { $builder->where([ 'OR' => [ [ 'relatedParentType' => $entityList, ], [ 'relatedParentType' => null, ], ], ]); } $forInternal = $this->config->get('streamEmailNotifications'); $forPortal = $this->config->get('portalStreamEmailNotifications'); if ($forInternal && !$forPortal) { $builder->where([ 'user.type!=' => User::TYPE_PORTAL, ]); } else if (!$forInternal && $forPortal) { $builder->where([ 'user.type' => User::TYPE_PORTAL, ]); } return $builder; } protected function processNotificationMentionInPost(Entity $notification): void { if (!$notification->get('userId')) { return; } $userId = $notification->get('userId'); $user = $this->entityManager->getEntity(User::ENTITY_TYPE, $userId); if (!$user) { return; } $emailAddress = $user->get('emailAddress'); if (!$emailAddress) { return; } $preferences = $this->entityManager->getEntity(Preferences::ENTITY_TYPE, $userId); if (!$preferences) { return; } if (!$preferences->get('receiveMentionEmailNotifications')) { return; } if ($notification->get('relatedType') !== Note::ENTITY_TYPE || !$notification->get('relatedId')) { return; } $note = $this->entityManager->getEntity(Note::ENTITY_TYPE, $notification->get('relatedId')); if (!$note) { return; } $parent = null; $parentId = $note->get('parentId'); $parentType = $note->get('parentType'); $data = []; if ($parentId && $parentType) { $parent = $this->entityManager->getEntity($parentType, $parentId); if (!$parent) { return; } $data['url'] = $this->getSiteUrl($user) . '/#' . $parentType . '/view/' . $parentId; $data['parentName'] = $parent->get('name'); $data['parentType'] = $parentType; $data['parentId'] = $parentId; } else { $data['url'] = $this->getSiteUrl($user) . '/#Notification'; } $data['userName'] = $note->get('createdByName'); $post = Markdown::defaultTransform( $note->get('post') ?? '' ); $data['post'] = $post; $subjectTpl = $this->templateFileManager->getTemplate('mention', 'subject'); $bodyTpl = $this->templateFileManager->getTemplate('mention', 'body'); $subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl); $subject = $this->getHtmlizer()->render($note, $subjectTpl, 'mention-email-subject', $data, true); $body = $this->getHtmlizer()->render($note, $bodyTpl, 'mention-email-body', $data, true); $email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE); $email->set([ 'subject' => $subject, 'body' => $body, 'isHtml' => true, 'to' => $emailAddress, 'isSystem' => true, ]); if ($parentId && $parentType) { $email->set([ 'parentId' => $parentId, 'parentType' => $parentType, ]); } $senderParams = SenderParams::create(); if ($parent) { $handler = $this->getHandler('mention', $parentType); if ($handler) { $handler->prepareEmail($email, $parent, $user); $senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams; } } try { $this->emailSender ->withParams($senderParams) ->send($email); } catch (Exception $e) { $this->log->error('EmailNotification: [' . $e->getCode() . '] ' .$e->getMessage()); } } protected function processNotificationNote(Entity $notification): void { if ($notification->get('relatedType') !== Note::ENTITY_TYPE) { return; } if (!$notification->get('relatedId')) { return; } /** @var ?Note $note */ $note = $this->entityManager->getEntity(Note::ENTITY_TYPE, $notification->get('relatedId')); if (!$note) { return; } $noteNotificationTypeList = $this->config->get('streamEmailNotificationsTypeList', []); if (!in_array($note->get('type'), $noteNotificationTypeList)) { return; } if (!$notification->get('userId')) { return; } $userId = $notification->get('userId'); /** @var ?User $user */ $user = $this->entityManager->getEntity(User::ENTITY_TYPE, $userId); if (!$user) { return; } $emailAddress = $user->get('emailAddress'); if (!$emailAddress) { return; } $preferences = $this->entityManager->getEntity(Preferences::ENTITY_TYPE, $userId); if (!$preferences) { return; } if (!$preferences->get('receiveStreamEmailNotifications')) { return; } $type = $note->getType(); if ($type === Note::TYPE_POST) { $this->processNotificationNotePost($note, $user); return; } if ($type === Note::TYPE_STATUS) { $this->processNotificationNoteStatus($note, $user); return; } if ($type === Note::TYPE_EMAIL_RECEIVED) { $this->processNotificationNoteEmailReceived($note, $user); return; } /** For bc. */ $methodName = 'processNotificationNote' . $type; if (method_exists($this, $methodName)) { $this->$methodName($note, $user); } } protected function getHandler(string $type, string $entityType): ?EmailNotificationHandler { $key = $type . '-' . $entityType; if (!array_key_exists($key, $this->emailNotificationEntityHandlerHash)) { $this->emailNotificationEntityHandlerHash[$key] = null; /** @var ?class-string $className */ $className = $this->metadata ->get(['notificationDefs', $entityType, 'emailNotificationHandlerClassNameMap', $type]); if ($className && class_exists($className)) { $handler = $this->injectableFactory->create($className); $this->emailNotificationEntityHandlerHash[$key] = $handler; } } return $this->emailNotificationEntityHandlerHash[$key]; } protected function processNotificationNotePost(Note $note, User $user): void { $parentId = $note->get('parentId'); $parentType = $note->getParentType(); $emailAddress = $user->get('emailAddress'); if (!$emailAddress) { return; } $data = []; $data['userName'] = $note->get('createdByName'); $post = Markdown::defaultTransform( $note->get('post') ?? '' ); $data['post'] = $post; $parent = null; if ($parentId && $parentType) { $parent = $this->entityManager->getEntityById($parentType, $parentId); if (!$parent) { return; } $data['url'] = $this->getSiteUrl($user) . '/#' . $parentType . '/view/' . $parentId; $data['parentName'] = $parent->get('name'); $data['parentType'] = $parentType; $data['parentId'] = $parentId; $data['name'] = $data['parentName']; $data['entityType'] = $this->language->translateLabel($parentType, 'scopeNames'); $data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']); $subjectTpl = $this->templateFileManager->getTemplate('notePost', 'subject', $parentType); $bodyTpl = $this->templateFileManager->getTemplate('notePost', 'body', $parentType); $subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl); $subject = $this->getHtmlizer()->render( $note, $subjectTpl, 'note-post-email-subject-' . $parentType, $data, true ); $body = $this->getHtmlizer()->render( $note, $bodyTpl, 'note-post-email-body-' . $parentType, $data, true ); } else { $data['url'] = $this->getSiteUrl($user) . '/#Notification'; $subjectTpl = $this->templateFileManager->getTemplate('notePostNoParent', 'subject'); $bodyTpl = $this->templateFileManager->getTemplate('notePostNoParent', 'body'); $subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl); $subject = $this->getHtmlizer()->render($note, $subjectTpl, 'note-post-email-subject', $data, true); $body = $this->getHtmlizer()->render($note, $bodyTpl, 'note-post-email-body', $data, true); } $email = $this->entityManager->getNewEntity('Email'); $email->set([ 'subject' => $subject, 'body' => $body, 'isHtml' => true, 'to' => $emailAddress, 'isSystem' => true, ]); if ($parentId && $parentType) { $email->set([ 'parentId' => $parentId, 'parentType' => $parentType, ]); } $senderParams = SenderParams::create(); if ($parent) { $handler = $this->getHandler('notePost', $parent->getEntityType()); if ($handler) { $handler->prepareEmail($email, $parent, $user); $senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams; } } try { $this->emailSender ->withParams($senderParams) ->send($email); } catch (Exception $e) { $this->log->error('EmailNotification: [' . $e->getCode() . '] ' .$e->getMessage()); } } private function getSiteUrl(User $user): string { $portal = null; if (!$user->isPortal()) { return $this->config->getSiteUrl(); } if (!array_key_exists($user->getId(), $this->userIdPortalCacheMap)) { $this->userIdPortalCacheMap[$user->getId()] = null; /** @var string[] $portalIdList */ $portalIdList = $user->getLinkMultipleIdList('portals'); $defaultPortalId = $this->config->get('defaultPortalId'); $portalId = null; if (in_array($defaultPortalId, $portalIdList)) { $portalId = $defaultPortalId; } else if (count($portalIdList)) { $portalId = $portalIdList[0]; } if ($portalId) { $portal = $this->entityManager->getEntityById(Portal::ENTITY_TYPE, $portalId); } if ($portal) { $this->getPortalRepository()->loadUrlField($portal); $this->userIdPortalCacheMap[$user->getId()] = $portal; } } else { $portal = $this->userIdPortalCacheMap[$user->getId()]; } if ($portal) { return rtrim($portal->get('url'), '/'); } return $this->config->getSiteUrl(); } protected function processNotificationNoteStatus(Note $note, User $user): void { $this->noteAccessControl->apply($note, $user); $parentId = $note->get('parentId'); $parentType = $note->getParentType(); $emailAddress = $user->get('emailAddress'); if (!$emailAddress) { return; } $data = []; if (!$parentId || !$parentType) { return; } $parent = $this->entityManager->getEntity($parentType, $parentId); if (!$parent) { return; } $data['url'] = $this->getSiteUrl($user) . '/#' . $parentType . '/view/' . $parentId; $data['parentName'] = $parent->get('name'); $data['parentType'] = $parentType; $data['parentId'] = $parentId; $data['name'] = $data['parentName']; $data['entityType'] = $this->language->translateLabel($parentType, 'scopeNames'); $data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']); $noteData = $note->get('data'); if (empty($noteData)) { return; } if ($noteData->value === null) { return; } $data['value'] = $noteData->value; $data['field'] = $field = $noteData->field; if (!is_string($field)) { return; } $data['valueTranslated'] = $this->language->translateOption($data['value'], $data['field'], $parentType); $data['fieldTranslated'] = $this->language->translateLabel($field, 'fields', $parentType); $data['fieldTranslatedLowerCase'] = Util::mbLowerCaseFirst($data['fieldTranslated']); $data['userName'] = $note->get('createdByName'); $subjectTpl = $this->templateFileManager->getTemplate('noteStatus', 'subject', $parentType); $bodyTpl = $this->templateFileManager->getTemplate('noteStatus', 'body', $parentType); $subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl); $subject = $this->getHtmlizer()->render( $note, $subjectTpl, 'note-status-email-subject', $data, true ); $body = $this->getHtmlizer()->render( $note, $bodyTpl, 'note-status-email-body', $data, true ); $email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE); $email->set([ 'subject' => $subject, 'body' => $body, 'isHtml' => true, 'to' => $emailAddress, 'isSystem' => true, 'parentId' => $parentId, 'parentType' => $parentType, ]); $senderParams = SenderParams::create(); $handler = $this->getHandler('status', $parentType); if ($handler) { $handler->prepareEmail($email, $parent, $user); $senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams; } try { $this->emailSender ->withParams($senderParams) ->send($email); } catch (Exception $e) { $this->log->error('EmailNotification: [' . $e->getCode() . '] ' .$e->getMessage()); } } protected function processNotificationNoteEmailReceived(Note $note, User $user): void { $parentId = $note->get('parentId'); $parentType = $note->getParentType(); $allowedEntityTypeList = $this->config->get('streamEmailNotificationsEmailReceivedEntityTypeList'); if ( is_array($allowedEntityTypeList) && !in_array($parentType, $allowedEntityTypeList) ) { return; } $emailAddress = $user->get('emailAddress'); if (!$emailAddress) { return; } $noteData = $note->get('data'); if (!($noteData instanceof stdClass)) { return; } if (!isset($noteData->emailId)) { return; } $emailSubject = $this->entityManager->getEntityById(Email::ENTITY_TYPE, $noteData->emailId); if (!$emailSubject) { return; } $emailRepository = $this->entityManager->getRDBRepository(Email::ENTITY_TYPE); $eaList = $user->get('emailAddresses'); foreach ($eaList as $ea) { if ( $emailRepository->getRelation($emailSubject, 'toEmailAddresses')->isRelated($ea) || $emailRepository->getRelation($emailSubject, 'ccEmailAddresses')->isRelated($ea) ) { return; } } $data = []; $data['fromName'] = ''; if (isset($noteData->personEntityName)) { $data['fromName'] = $noteData->personEntityName; } else if (isset($noteData->fromString)) { $data['fromName'] = $noteData->fromString; } $data['subject'] = ''; if (isset($noteData->emailName)) { $data['subject'] = $noteData->emailName; } $data['post'] = nl2br($note->get('post')); if (!$parentId || !$parentType) { return; } $parent = $this->entityManager->getEntity($parentType, $parentId); if (!$parent) { return; } $data['url'] = $this->getSiteUrl($user) . '/#' . $parentType . '/view/' . $parentId; $data['parentName'] = $parent->get('name'); $data['parentType'] = $parentType; $data['parentId'] = $parentId; $data['name'] = $data['parentName']; $data['entityType'] = $this->language->translateLabel($parentType, 'scopeNames'); $data['entityTypeLowerFirst'] = Util::mbLowerCaseFirst($data['entityType']); $subjectTpl = $this->templateFileManager->getTemplate('noteEmailReceived', 'subject', $parentType); $bodyTpl = $this->templateFileManager->getTemplate('noteEmailReceived', 'body', $parentType); $subjectTpl = str_replace(["\n", "\r"], '', $subjectTpl); $subject = $this->getHtmlizer()->render( $note, $subjectTpl, 'note-email-received-email-subject-' . $parentType, $data, true ); $body = $this->getHtmlizer()->render( $note, $bodyTpl, 'note-email-received-email-body-' . $parentType, $data, true ); $email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE); $email->set([ 'subject' => $subject, 'body' => $body, 'isHtml' => true, 'to' => $emailAddress, 'isSystem' => true, 'parentId' => $parentId, 'parentType' => $parentType, ]); $senderParams = SenderParams::create(); $handler = $this->getHandler('emailReceived', $parentType); if ($handler) { $handler->prepareEmail($email, $parent, $user); $senderParams = $handler->getSenderParams($parent, $user) ?? $senderParams; } try { $this->emailSender ->withParams($senderParams) ->send($email); } catch (Exception $e) { $this->log->error('EmailNotification: [' . $e->getCode() . '] ' .$e->getMessage()); } } private function getHtmlizer(): Htmlizer { if (!$this->htmlizer) { $this->htmlizer = $this->htmlizerFactory->create(true); } return $this->htmlizer; } private function getPortalRepository(): PortalRepository { /** @var PortalRepository */ return $this->entityManager->getRepository(Portal::ENTITY_TYPE); } }