config = $config; $this->entityManager = $entityManager; $this->defaultLanguage = $defaultLanguage; $this->emailSender = $emailSender; $this->log = $log; $this->accountFactory = $accountFactory; $this->campaignService = $campaignService; $this->headersPreparator = $headersPreparator; $this->templateProcessor = $templateProcessor; } /** * @throws Error * @throws NoSmtp */ public function process(MassEmail $massEmail, bool $isTest = false): void { $maxBatchSize = $this->config->get('massEmailMaxPerHourCount', self::MAX_PER_HOUR_COUNT); if (!$isTest) { $threshold = new DateTime(); $threshold->modify('-1 hour'); $sentLastHourCount = $this->entityManager ->getRDBRepositoryByClass(EmailQueueItem::class) ->where([ 'status' => EmailQueueItem::STATUS_SENT, 'sentAt>' => $threshold->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT), ]) ->count(); if ($sentLastHourCount >= $maxBatchSize) { return; } $maxBatchSize = $maxBatchSize - $sentLastHourCount; } $queueItemList = $this->entityManager ->getRDBRepositoryByClass(EmailQueueItem::class) ->where([ 'status' => EmailQueueItem::STATUS_PENDING, 'massEmailId' => $massEmail->getId(), 'isTest' => $isTest, ]) ->limit(0, $maxBatchSize) ->find(); $templateId = $massEmail->getEmailTemplateId(); if (!$templateId) { $this->setFailed($massEmail); return; } $campaign = null; $campaignId = $massEmail->getCampaignId(); if ($campaignId) { $campaign = $this->entityManager->getEntityById(Campaign::ENTITY_TYPE, $campaignId); } $emailTemplate = $this->entityManager ->getRDBRepositoryByClass(EmailTemplate::class) ->getById($templateId); if (!$emailTemplate) { $this->setFailed($massEmail); return; } /** @var Collection $attachmentList */ $attachmentList = $this->entityManager ->getRDBRepositoryByClass(EmailTemplate::class) ->getRelation($emailTemplate, 'attachments') ->find(); $smtpParams = null; $senderParams = SenderParams::create(); $inboundEmailId = $massEmail->getInboundEmailId(); if ($inboundEmailId) { $account = $this->accountFactory->create($inboundEmailId); $smtpParams = $account->getSmtpParams(); if ( !$account->isAvailableForSending() || !$account->getEntity()->smtpIsForMassEmail() || !$smtpParams ) { throw new Error("Mass Email: Group email account {$inboundEmailId} can't be used for mass email."); } if ($account->getEntity()->getReplyToAddress()) { $senderParams = $senderParams ->withReplyToAddress( $account->getEntity()->getReplyToAddress() ); } } foreach ($queueItemList as $queueItem) { $this->sendQueueItem( $queueItem, $massEmail, $emailTemplate, $attachmentList, $campaign, $isTest, $smtpParams, $senderParams ); } if ($isTest) { return; } $countLeft = $this->entityManager ->getRDBRepositoryByClass(EmailQueueItem::class) ->where([ 'status' => EmailQueueItem::STATUS_PENDING, 'massEmailId' => $massEmail->getId(), 'isTest' => false, ]) ->count(); if ($countLeft === 0) { $massEmail->set('status', MassEmail::STATUS_COMPLETE); $this->entityManager->saveEntity($massEmail); } } /** * @param iterable $trackingUrlList */ private function getPreparedEmail( EmailQueueItem $queueItem, MassEmail $massEmail, EmailTemplate $emailTemplate, Entity $target, iterable $trackingUrlList = [] ): ?Email { $emailAddress = $target->get('emailAddress'); if (!$emailAddress) { return null; } $emailData = $this->templateProcessor->process( $emailTemplate, TemplateParams::create() ->withApplyAcl(false) // @todo Revise. ->withCopyAttachments(false), // @todo Revise. TemplateData::create() ->withParent($target) ); $body = $emailData->getBody(); $optOutUrl = $this->getSiteUrl() . '?entryPoint=unsubscribe&id=' . $queueItem->getId(); $optOutLink = '' . $this->defaultLanguage->translateLabel('Unsubscribe', 'labels', Campaign::ENTITY_TYPE) . ''; $body = str_replace('{optOutUrl}', $optOutUrl, $body); $body = str_replace('{optOutLink}', $optOutLink, $body); foreach ($trackingUrlList as $trackingUrl) { $url = $this->getSiteUrl() . '?entryPoint=campaignUrl&id=' . $trackingUrl->getId() . '&queueItemId=' . $queueItem->getId(); $body = str_replace($trackingUrl->get('urlToUse'), $url, $body); } if ( !$this->config->get('massEmailDisableMandatoryOptOutLink') && stripos($body, '?entryPoint=unsubscribe&id') === false ) { if ($emailData->isHtml()) { $body .= "

" . $optOutLink; } else { $body .= "\n\n" . $optOutUrl; } } $trackImageAlt = $this->defaultLanguage->translateLabel('Campaign', 'scopeNames'); $trackOpenedUrl = $this->getSiteUrl() . '?entryPoint=campaignTrackOpened&id=' . $queueItem->getId(); $trackOpenedHtml = '' . $trackImageAlt . ''; if ( $massEmail->getCampaignId() && $this->config->get('massEmailOpenTracking') ) { if ($emailData->isHtml()) { $body .= '
' . $trackOpenedHtml; } } /** @var Email $email */ $email = $this->entityManager ->getRDBRepositoryByClass(Email::class) ->getNew(); $email ->addToAddress($emailAddress) ->setSubject($emailData->getSubject()) ->setBody($body) ->setIsHtml($emailData->isHtml()) ->setAttachmentIdList($emailData->getAttachmentIdList()); if ($massEmail->getFromAddress()) { $email->setFromAddress($massEmail->getFromAddress()); } $replyToAddress = $massEmail->getReplyToAddress(); if ($replyToAddress) { $email->addReplyToAddress($replyToAddress); } return $email; } private function prepareQueueItemMessage( EmailQueueItem $queueItem, Sender $sender, Message $message, SenderParams $senderParams ): void { $id = $queueItem->getId(); $this->headersPreparator->prepare($message->getHeaders(), new Data($id, $senderParams)); $fromAddress = $senderParams->getFromAddress(); if ( $this->config->get('massEmailVerp') && $fromAddress && strpos($fromAddress, '@') ) { $bounceAddress = explode('@', $fromAddress)[0] . '+bounce-qid-' . $id . '@' . explode('@', $fromAddress)[1]; $sender->withEnvelopeOptions(['from' => $bounceAddress]); } } private function setFailed(MassEmail $massEmail): void { $massEmail->set('status', MassEmail::STATUS_FAILED); $this->entityManager->saveEntity($massEmail); $queueItemList = $this->entityManager ->getRDBRepositoryByClass(EmailQueueItem::class) ->where([ 'status' => EmailQueueItem::STATUS_PENDING, 'massEmailId' => $massEmail->getId(), ]) ->find(); foreach ($queueItemList as $queueItem) { $queueItem->set('status', EmailQueueItem::STATUS_FAILED); $this->entityManager->saveEntity($queueItem); } } /** * @param Collection $attachmentList */ private function sendQueueItem( EmailQueueItem $queueItem, MassEmail $massEmail, EmailTemplate $emailTemplate, $attachmentList, ?Campaign $campaign, bool $isTest, ?SmtpParams $smtpParams, SenderParams $senderParams ): void { /** @var ?EmailQueueItem $queueItemFetched */ $queueItemFetched = $this->entityManager->getEntityById($queueItem->getEntityType(), $queueItem->getId()); if ( !$queueItemFetched || $queueItemFetched->getStatus() !== EmailQueueItem::STATUS_PENDING ) { return; } $queueItem->set('status', EmailQueueItem::STATUS_SENDING); $this->entityManager->saveEntity($queueItem); $target = $this->entityManager->getEntityById($queueItem->getTargetType(), $queueItem->getTargetId()); $emailAddress = $target ? $target->get('emailAddress') : null; if ( !$target || !$target->hasId() || !$emailAddress ) { $queueItem->set('status', EmailQueueItem::STATUS_FAILED); $this->entityManager->saveEntity($queueItem); return; } /** @var EmailAddressRepository $emailAddressRepository */ $emailAddressRepository = $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE); $emailAddressRecord = $emailAddressRepository->getByAddress($emailAddress); if ( $emailAddressRecord && ( $emailAddressRecord->isInvalid() || $emailAddressRecord->isOptedOut() ) ) { $queueItem->set('status', EmailQueueItem::STATUS_FAILED); $this->entityManager->saveEntity($queueItem); return; } /** @var CampaignTrackingUrl[] $trackingUrlList */ $trackingUrlList = []; if ($campaign) { /** @var Collection $trackingUrlList */ $trackingUrlList = $this->entityManager ->getRDBRepositoryByClass(Campaign::class) ->getRelation($campaign, 'trackingUrls') ->find(); } $email = $this->getPreparedEmail($queueItem, $massEmail, $emailTemplate, $target, $trackingUrlList); if (!$email) { return; } if ($email->get('replyToAddress')) { // @todo Revise. $senderParams = $senderParams->withReplyToAddress(null); } if ($campaign) { $email->setLinkMultipleIdList('teams', $campaign->getLinkMultipleIdList('teams')); } $senderParams = $senderParams->withFromAddress( $massEmail->getFromAddress() ?? $this->config->get('outboundEmailFromAddress') ); if ($massEmail->getFromName()) { $senderParams = $senderParams->withFromName($massEmail->getFromName()); } if ($massEmail->getReplyToName()) { $senderParams = $senderParams->withReplyToName($massEmail->getReplyToName()); } try { $attemptCount = $queueItem->getAttemptCount(); $attemptCount++; $queueItem->set('attemptCount', $attemptCount); $sender = $this->emailSender->create(); if ($smtpParams) { $sender->withSmtpParams($smtpParams); } $message = new Message(); $this->prepareQueueItemMessage($queueItem, $sender, $message, $senderParams); $sender ->withParams($senderParams) ->withMessage($message) ->withAttachments($attachmentList) ->send($email); } catch (Exception $e) { $maxAttemptCount = $this->config->get('massEmailMaxAttemptCount', self::MAX_ATTEMPT_COUNT); $queueItem->getAttemptCount() >= $maxAttemptCount ? $queueItem->set('status', EmailQueueItem::STATUS_FAILED) : $queueItem->set('status', EmailQueueItem::STATUS_PENDING); $this->entityManager->saveEntity($queueItem); $this->log->error("Mass Email, send item: {$e->getCode()}, " . $e->getMessage()); return; } $emailObject = $emailTemplate; if ($massEmail->storeSentEmails() && !$isTest) { $this->entityManager->saveEntity($email); $emailObject = $email; } $queueItem->set('emailAddress', $target->get('emailAddress')); $queueItem->set('status', EmailQueueItem::STATUS_SENT); $queueItem->set('sentAt', date(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT)); $this->entityManager->saveEntity($queueItem); if ($campaign) { $this->campaignService->logSent($campaign->getId(), $queueItem, $emailObject); } } private function getSiteUrl(): string { return $this->config->get('massEmailSiteUrl') ?? $this->config->get('siteUrl'); } }