workflowId; if (!$this->validateSendEmailData($data)) { throw new Error("Workflow[$workflowId][sendEmail]: Email data is invalid."); } $data->doNotStore ??= false; $data->returnEmailId ??= false; $data->from ??= (object) []; $data->to ??= (object) []; $data->cc ??= null; $data->replyTo ??= null; $data->attachmentIds ??= []; /** * @var object{ * variables?: stdClass, * optOutLink?: bool, * attachmentIds: string[], * entityType?: string|null, * entityId?: string|null, * from: stdClass, * to: stdClass, * cc: stdClass|null, * replyTo: stdClass|null, * doNotStore: bool, * returnEmailId: bool, * } & stdClass $data */ if ($workflowId) { $workflow = $this->entityManager->getRDBRepositoryByClass(WorkflowEntity::class)->getById($workflowId); if (!$workflow || !$workflow->isActive()) { return false; } } $entity = null; if (!empty($data->entityType) && !empty($data->entityId)) { $entity = $this->entityManager->getEntityById($data->entityType, $data->entityId); } if (!$entity) { throw new Error("Workflow[$workflowId][sendEmail]: Target Entity is not found."); } $this->recordServiceContainer->get($entity->getEntityType()) ->loadAdditionalFields($entity); $fromAddress = $this->getEmailAddress($data->from); $toAddress = $this->getEmailAddress($data->to); $replyToAddress = !empty($data->replyTo) ? $this->getEmailAddress($data->replyTo) : null; $ccAddress = !empty($data->cc) ? $this->getEmailAddress($data->cc) : null; if (!$fromAddress) { throw new Error("Workflow[$workflowId][sendEmail]: From email address is empty or could not be obtained."); } if (!$toAddress) { throw new Error("Workflow[$workflowId][sendEmail]: To email address is empty."); } /** @var array $entityHash */ $entityHash = [$data->entityType => $entity]; if ( isset($data->to->entityType) && isset($data->to->entityId) && $data->to->entityType !== $data->entityType ) { /** @var string $toEntityType */ $toEntityType = $data->to->entityType; $toEntity = $this->entityManager->getEntityById($toEntityType, $data->to->entityId); if ($toEntity) { $entityHash[$toEntityType] = $toEntity; } } $fromName = null; if ( isset($data->from->entityType) && isset($data->from->entityId) && $data->from->entityType === User::ENTITY_TYPE ) { $user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($data->from->entityId); if ($user) { $entityHash[User::ENTITY_TYPE] = $user; $fromName = $user->getName(); } } $sender = $this->emailSender->create(); $templateResult = $this->getTemplateResult( data: $data, entityHash: $entityHash, toEmailAddress: $toAddress, entity: $entity, ); [$subject, $body] = $this->prepareSubjectBody( templateResult: $templateResult, data: $data, toEmailAddress: $toAddress, sender: $sender, ); $emailData = [ 'from' => $fromAddress, 'to' => $toAddress, 'cc' => $ccAddress, 'replyTo' => $replyToAddress, 'subject' => $subject, 'body' => $body, 'isHtml' => $templateResult->isHtml(), 'parentId' => $entity->getId(), 'parentType' => $entity->getEntityType(), ]; if ($fromName !== null) { $emailData['fromName'] = $fromName; } $email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew(); $email->setMultiple($emailData); $attachmentList = $this->getAttachmentList($templateResult, $data->attachmentIds); if (!$data->doNotStore) { // Additional attachments not added intentionally? $email->set('attachmentsIds', $templateResult->getAttachmentIdList()); } $smtpParams = $this->prepareSmtpParams($data, $fromAddress); if ($smtpParams) { $sender->withSmtpParams($smtpParams); } $sender->withAttachments($attachmentList); if ($replyToAddress) { $senderParams = SenderParams::create()->withReplyToAddress($replyToAddress); $sender->withParams($senderParams); } try { $sender->send($email); } catch (Exception $e) { $sendExceptionMessage = $e->getMessage(); throw new Error("Workflow[$workflowId][sendEmail]: $sendExceptionMessage.", 0, $e); } if ($data->doNotStore) { return true; } $this->storeEmail($email, $data); if ($data->returnEmailId) { return $email->getId(); } return true; } private function validateSendEmailData(stdClass $data): bool { if ( !isset($data->entityId) || !(isset($data->entityType)) || !isset($data->emailTemplateId) || !isset($data->from) || !isset($data->to) ) { return false; } return true; } private function getEmailAddress(stdClass $data): ?string { if (isset($data->email)) { return $data->email; } $entityType = $data->entityType ?? $data->entityName ?? null; $entity = null; if (isset($entityType) && isset($data->entityId)) { $entity = $this->entityManager->getEntityById($entityType, $data->entityId); } $workflowHelper = $this->workflowHelper; if (isset($data->type)) { switch ($data->type) { case 'specifiedTeams': $userIds = $workflowHelper->getUserIdsByTeamIds($data->entityIds); return implode('; ', $workflowHelper->getUsersEmailAddress($userIds)); case 'teamUsers': if (!$entity instanceof CoreEntity) { return null; } $entity->loadLinkMultipleField('teams'); $userIds = $workflowHelper->getUserIdsByTeamIds($entity->get('teamsIds')); return implode('; ', $workflowHelper->getUsersEmailAddress($userIds)); case 'followers': if (!$entity) { return null; } $userIds = $workflowHelper->getFollowerUserIds($entity); return implode('; ', $workflowHelper->getUsersEmailAddress($userIds)); case 'followersExcludingAssignedUser': if (!$entity) { return null; } $userIds = $workflowHelper->getFollowerUserIdsExcludingAssignedUser($entity); return implode('; ', $workflowHelper->getUsersEmailAddress($userIds)); case 'system': return $this->config->get('outboundEmailFromAddress'); case 'specifiedUsers': return implode('; ', $workflowHelper->getUsersEmailAddress($data->entityIds)); case 'specifiedContacts': return implode('; ', $workflowHelper->getEmailAddressesForEntity('Contact', $data->entityIds)); } } if ($entity instanceof Entity && $entity->hasAttribute('emailAddress')) { return $entity->get('emailAddress'); } if ( isset($data->type) && isset($entityType) && isset($data->entityIds) && is_array($data->entityIds) ) { return implode('; ', $workflowHelper->getEmailAddressesForEntity($entityType, $data->entityIds)); } return null; } private function applyTrackingUrlsToEmailBody(string $body, string $toEmailAddress): string { $siteUrl = $this->getSiteUrl(); if (!str_contains($body, '{trackingUrl:')) { return $body; } $hash = $this->hasher->hash($toEmailAddress); preg_match_all('/\{trackingUrl:(.*?)}/', $body, $matches); /** @phpstan-ignore-next-line */ if (!$matches || !count($matches)) { return $body; } foreach ($matches[0] as $item) { $id = explode(':', trim($item, '{}'), 2)[1] ?? null; if (!$id) { continue; } if (strpos($id, '.')) { [$id, $uid] = explode('.', $id); $uidHash = $this->hasher->hash($uid); $url = "$siteUrl?entryPoint=campaignUrl&id=$id&uid=$uid&hash=$uidHash"; } else { $url = "$siteUrl?entryPoint=campaignUrl&id=$id&emailAddress=$toEmailAddress&hash=$hash"; } $body = str_replace($item, $url, $body); } return $body; } /** * @throws Error * @throws NoSmtp */ private function getUserSmtpParams(string $emailAddress, string $userId): ?SmtpParams { $user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId); if (!$user || !$user->isActive()) { return null; } $emailAccount = $this->entityManager ->getRDBRepositoryByClass(EmailAccount::class) ->where([ 'emailAddress' => $emailAddress, 'assignedUserId' => $userId, 'useSmtp' => true, 'status' => EmailAccount::STATUS_ACTIVE, ]) ->findOne(); if (!$emailAccount) { return null; } $factory = $this->injectableFactory->create(PersonalAccountFactory::class); $params = $factory->create($emailAccount->getId()) ->getSmtpParams(); if (!$params) { return null; } return $params->withFromName($user->getName()); } /** * @throws Error * @throws NoSmtp */ private function getGroupSmtpParams(string $emailAddress): ?SmtpParams { $inboundEmail = $this->entityManager ->getRDBRepositoryByClass(InboundEmail::class) ->where([ 'status' => InboundEmail::STATUS_ACTIVE, 'useSmtp' => true, 'smtpHost!=' => null, 'emailAddress' => $emailAddress, ]) ->findOne(); if (!$inboundEmail) { return null; } return $this->injectableFactory ->create(GroupAccountFactory::class) ->create($inboundEmail->getId()) ->getSmtpParams(); } /** * @param Result $templateResult * @param string[] $attachmentIds * @return Attachment[] */ private function getAttachmentList(Result $templateResult, array $attachmentIds): array { $attachmentList = []; foreach (array_merge($templateResult->getAttachmentIdList(), $attachmentIds) as $attachmentId) { $attachment = $this->entityManager ->getRDBRepositoryByClass(Attachment::class) ->getById($attachmentId); if ($attachment) { $attachmentList[] = $attachment; } } return $attachmentList; } private function storeEmail(Email $email, stdClass $data): void { $processId = $data->processId ?? null; $emailTemplateId = $data->emailTemplateId ?? null; $teamsIds = []; if ($processId) { $process = $this->entityManager ->getRDBRepositoryByClass(BpmnProcessEntity::class) ->getById($processId); if ($process) { $teamsIds = $process->getLinkMultipleIdList('teams'); } } else if ($emailTemplateId) { $emailTemplate = $this->entityManager ->getRDBRepositoryByClass(EmailTemplate::class) ->getById($emailTemplateId); if ($emailTemplate) { $teamsIds = $emailTemplate->getLinkMultipleIdList('teams'); } } if (count($teamsIds)) { $email->set('teamsIds', $teamsIds); } $this->entityManager->saveEntity($email, ['createdById' => 'system']); } /** * @throws Error * @throws NoSmtp */ private function prepareSmtpParams(stdClass $data, string $fromEmailAddress): ?SmtpParams { if ( isset($data->from->entityType) && $data->from->entityType === User::ENTITY_TYPE && isset($data->from->entityId) ) { return $this->getUserSmtpParams($fromEmailAddress, $data->from->entityId); } if (isset($data->from->email)) { return $this->getGroupSmtpParams($fromEmailAddress); } return null; } private function getEmailTemplate(stdClass $data): EmailTemplate { $emailTemplateId = $data->emailTemplateId ?? null; if (!$emailTemplateId) { throw new RuntimeException("No email template."); } $emailTemplate = $this->entityManager ->getRDBRepositoryByClass(EmailTemplate::class) ->getById($emailTemplateId); if (!$emailTemplate) { throw new RuntimeException("Email template $emailTemplateId not found."); } return $emailTemplate; } /** * @param array $entityHash * @return Result */ private function getTemplateResult( stdClass $data, array $entityHash, string $toEmailAddress, Entity $entity ): Result { $emailTemplate = $this->getEmailTemplate($data); $emailTemplateData = EmailTemplateData::create() ->withEntityHash($entityHash) ->withEmailAddress($toEmailAddress) ->withParentId($entity->getId()) ->withParentType($entity->getEntityType()); if ( $entity->hasAttribute('parentId') && $entity->hasAttribute('parentType') ) { $emailTemplateData = $emailTemplateData ->withRelatedId($entity->get('parentId')) ->withRelatedType($entity->get('parentType')); } return $this->emailTemplateProcessor->process( $emailTemplate, EmailTemplateParams::create()->withCopyAttachments(), $emailTemplateData ); } private function applyOptOutLink( string $toEmailAddress, string $body, Result $templateResult, Sender $sender, ): string { $siteUrl = $this->getSiteUrl(); $hash = $this->hasher->hash($toEmailAddress); $optOutUrl = "$siteUrl?entryPoint=unsubscribe&emailAddress=$toEmailAddress&hash=$hash"; $optOutLink = "" . "{$this->defaultLanguage->translateLabel('Unsubscribe', 'labels', 'Campaign')}"; $body = str_replace('{optOutUrl}', $optOutUrl, $body); $body = str_replace('{optOutLink}', $optOutLink, $body); if (stripos($body, '?entryPoint=unsubscribe') === false) { if ($templateResult->isHtml()) { $body .= "

" . $optOutLink; } else { $body .= "\n\n" . $optOutUrl; } } if (method_exists($sender, 'withAddedHeader')) { /** @phpstan-ignore-line */ $sender->withAddedHeader('List-Unsubscribe', '<' . $optOutUrl . '>'); } else { $message = new Message(); $message->getHeaders()->addHeaderLine('List-Unsubscribe', '<' . $optOutUrl . '>'); $sender->withMessage($message); } return $body; } /** * @param Result $templateResult * @param object{variables?: stdClass, optOutLink?: bool}&stdClass $data * @return array{?string, ?string} */ private function prepareSubjectBody( Result $templateResult, stdClass $data, string $toEmailAddress, Sender $sender ): array { $subject = $templateResult->getSubject(); $body = $templateResult->getBody(); if (isset($data->variables)) { foreach (get_object_vars($data->variables) as $key => $value) { if (!is_string($value) && !is_int($value) && !is_float($value)) { continue; } if (is_int($value) || is_float($value)) { $value = strval($value); } else if (!$value) { continue; } $subject = str_replace('{$$' . $key . '}', $value, $subject); $body = str_replace('{$$' . $key . '}', $value, $body); } } $body = $this->applyTrackingUrlsToEmailBody($body, $toEmailAddress); if ($data->optOutLink ?? false) { $body = $this->applyOptOutLink($toEmailAddress, $body, $templateResult, $sender); } return [$subject, $body]; } private function getSiteUrl(): ?string { return $this->config->get('workflowEmailSiteUrl') ?? $this->config->get('siteUrl'); } }