get(TrackingConfig::class); $this->trackingEnabled = $trackingConfig->isEmailTrackingEnabled(); if ($wp === null) { $wp = new WPFunctions; } $this->wp = $wp; if ($postsTask === null) { $postsTask = new PostsTask; } $this->postsTask = $postsTask; if ($gaTracking === null) { $gaTracking = ContainerWrapper::getInstance()->get(GATracking::class); } $this->gaTracking = $gaTracking; $this->loggerFactory = LoggerFactory::getInstance(); if ($emoji === null) { $emoji = new Emoji(); } $this->emoji = $emoji; $this->renderer = ContainerWrapper::getInstance()->get(Renderer::class); $this->newslettersRepository = ContainerWrapper::getInstance()->get(NewslettersRepository::class); $this->newsletterDeleteController = ContainerWrapper::getInstance()->get(NewsletterDeleteController::class); $this->linksTask = ContainerWrapper::getInstance()->get(LinksTask::class); $this->newsletterLinks = ContainerWrapper::getInstance()->get(NewsletterLinks::class); $this->sendingQueuesRepository = ContainerWrapper::getInstance()->get(SendingQueuesRepository::class); $this->segmentsRepository = ContainerWrapper::getInstance()->get(SegmentsRepository::class); $this->scheduledTasksRepository = ContainerWrapper::getInstance()->get(ScheduledTasksRepository::class); $this->personalizer = ContainerWrapper::getInstance()->get(Personalizer::class); } public function getNewsletterFromQueue(ScheduledTaskEntity $task): ?NewsletterEntity { // get existing active or sending newsletter $queue = $task->getSendingQueue(); $newsletter = $queue ? $queue->getNewsletter() : null; if ( is_null($newsletter) || $newsletter->getDeletedAt() !== null || !in_array($newsletter->getStatus(), [NewsletterEntity::STATUS_ACTIVE, NewsletterEntity::STATUS_SENDING]) || $newsletter->getStatus() === NewsletterEntity::STATUS_CORRUPT ) { $this->recoverFromInvalidState($task); return null; } // if this is a notification history, get existing active or sending parent newsletter if ($newsletter->getType() == NewsletterEntity::TYPE_NOTIFICATION_HISTORY) { $parentNewsletter = $newsletter->getParent(); if ( is_null($parentNewsletter) || $parentNewsletter->getDeletedAt() !== null || !in_array($parentNewsletter->getStatus(), [NewsletterEntity::STATUS_ACTIVE, NewsletterEntity::STATUS_SENDING]) ) { return null; } } return $newsletter; } /** * Pre-processes the newsletter before sending. * - Renders the newsletter * - Adds tracking * - Extracts links * - Checks if the newsletter is a post notification and if it contains at least 1 ALC post. * If not it deletes the notification history record and all associate entities. * * @return NewsletterEntity|false - Returns false only if the newsletter is a post notification history and was deleted. * */ public function preProcessNewsletter(NewsletterEntity $newsletter, ScheduledTaskEntity $task) { // return the newsletter if it was previously rendered $queue = $task->getSendingQueue(); if (!$queue) { throw new RuntimeException('Can‘t pre-process newsletter without queue.'); } if ($queue->getNewsletterRenderedBody() !== null) { return $newsletter; } $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'pre-processing newsletter', ['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()] ); $campaignId = null; // if tracking is enabled, do additional processing if ($this->trackingEnabled) { // hook to the newsletter post-processing filter and add tracking image $this->trackingImageInserted = OpenTracking::addTrackingImage(); // render newsletter $renderedNewsletter = $this->renderer->render($newsletter, $queue); $renderedNewsletter = $this->wp->applyFilters( 'mailpoet_sending_newsletter_render_after_pre_process', $renderedNewsletter, $newsletter ); if (is_array($renderedNewsletter)) { $campaignId = $this->calculateCampaignId($newsletter, $renderedNewsletter); } $renderedNewsletter = $this->gaTracking->applyGATracking($renderedNewsletter, $newsletter); // hash and save all links $renderedNewsletter = $this->linksTask->process($renderedNewsletter, $newsletter, $queue); } else { // render newsletter $renderedNewsletter = $this->renderer->render($newsletter, $queue); $renderedNewsletter = $this->wp->applyFilters( 'mailpoet_sending_newsletter_render_after_pre_process', $renderedNewsletter, $newsletter ); if (is_array($renderedNewsletter)) { $campaignId = $this->calculateCampaignId($newsletter, $renderedNewsletter); } $renderedNewsletter = $this->gaTracking->applyGATracking($renderedNewsletter, $newsletter); } // check if this is a post notification and if it contains at least 1 ALC post if ( $newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION_HISTORY && $this->postsTask->getAlcPostsCount($renderedNewsletter, $newsletter) === 0 ) { // delete notification history record since it will never be sent $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info( 'no posts in post notification, deleting it', ['newsletter_id' => $newsletter->getId(), 'task_id' => $task->getId()] ); $this->newsletterDeleteController->bulkDelete([(int)$newsletter->getId()]); return false; } // extract and save newsletter posts $this->postsTask->extractAndSave($renderedNewsletter, $newsletter); if ($campaignId !== null) { $this->sendingQueuesRepository->saveCampaignId($queue, $campaignId); } $filterSegmentId = $newsletter->getFilterSegmentId(); if ($filterSegmentId) { $filterSegment = $this->segmentsRepository->findOneById($filterSegmentId); if ($filterSegment instanceof SegmentEntity && $filterSegment->getType() === SegmentEntity::TYPE_DYNAMIC) { $this->sendingQueuesRepository->saveFilterSegmentMeta($queue, $filterSegment); } } // update queue with the rendered and pre-processed newsletter $queue->setNewsletterRenderedSubject( ShortcodesTask::process( $newsletter->getSubject(), $renderedNewsletter['html'], $newsletter, null, $queue ) ); // if the rendered subject is empty, use a default subject, // having no subject in a newsletter is considered spammy if (empty(trim((string)$queue->getNewsletterRenderedSubject()))) { $queue->setNewsletterRenderedSubject(__('No subject', 'mailpoet')); } $renderedNewsletter = $this->emoji->encodeEmojisInBody($renderedNewsletter); $queue->setNewsletterRenderedBody($renderedNewsletter); try { $this->sendingQueuesRepository->flush(); } catch (\Throwable $e) { $this->stopNewsletterPreProcessing(sprintf('QUEUE-%d-SAVE', $queue->getId())); } return $newsletter; } /** * Shortcodes and links will be replaced in the subject, html and text body * to speed the processing, join content into a continuous string. */ public function prepareNewsletterForSending(NewsletterEntity $newsletter, SubscriberEntity $subscriber, SendingQueueEntity $queue): array { $renderedNewsletter = $queue->getNewsletterRenderedBody(); $renderedNewsletter = $this->emoji->decodeEmojisInBody($renderedNewsletter); $preparedNewsletter = Helpers::joinObject( [ $queue->getNewsletterRenderedSubject(), $renderedNewsletter['html'], $renderedNewsletter['text'], ] ); $preparedNewsletter = ShortcodesTask::process( $preparedNewsletter, null, $newsletter, $subscriber, $queue ); if ($this->trackingEnabled) { $preparedNewsletter = $this->newsletterLinks->replaceSubscriberData( $subscriber->getId(), $queue->getId(), $preparedNewsletter ); } $preparedNewsletter = Helpers::splitObject($preparedNewsletter); if ($newsletter->getWpPostId() !== null) { $this->personalizer->set_context([ 'recipient_email' => $subscriber->getEmail(), 'newsletter_id' => $newsletter->getId(), 'queue_id' => $queue->getId(), ]); foreach ($preparedNewsletter as $key => $content) { $preparedNewsletter[$key] = $this->personalizer->personalize_content($content); } } return [ 'id' => $newsletter->getId(), 'subject' => $preparedNewsletter[0], 'body' => [ 'html' => $preparedNewsletter[1], 'text' => $preparedNewsletter[2], ], ]; } public function markNewsletterAsSent(NewsletterEntity $newsletter) { // if it's a standard or notification history newsletter, update its status if ( $newsletter->getType() === NewsletterEntity::TYPE_STANDARD || $newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION_HISTORY ) { $newsletter->setStatus(NewsletterEntity::STATUS_SENT); $newsletter->setSentAt(Carbon::now()->millisecond(0)); $this->newslettersRepository->persist($newsletter); $this->newslettersRepository->flush(); } } public function stopNewsletterPreProcessing($errorCode = null) { MailerLog::processError( 'queue_save', __('There was an error processing your newsletter during sending. If possible, please contact us and report this issue.', 'mailpoet'), $errorCode ); } /** * @param NewsletterEntity $newsletter * @param array $renderedNewsletters - The pre-processed renderered newsletters, before link tracking has been added or shortcodes have been processed. * * @return string */ public function calculateCampaignId(NewsletterEntity $newsletter, array $renderedNewsletters): string { $relevantContent = [ $newsletter->getId(), $newsletter->getSubject(), ]; if (isset($renderedNewsletters['text'])) { $relevantContent[] = $renderedNewsletters['text']; } // The text version of emails contains just the alt text of images, which could be the same for multiple images. In order to ensure // campaign IDs change when images change, we should consider all image URLs. if (isset($renderedNewsletters['html'])) { $html = pQuery::parseStr($renderedNewsletters['html']); if ($html instanceof DomNode) { foreach ($html->query('img') as $imageNode) { $src = $imageNode->getAttribute('src'); if (is_string($src)) { $relevantContent[] = $src; } } } } return substr(md5(implode('|', $relevantContent)), 0, 16); } /** * This method recovers the scheduled task and newsletter from a state when sending cannot proceed. */ private function recoverFromInvalidState(ScheduledTaskEntity $task): void { // When newsletter does not exist, we need to remove the scheduled task and sending queue. $queue = $task->getSendingQueue(); $newsletter = $queue ? $queue->getNewsletter() : null; if (!$newsletter) { $this->scheduledTasksRepository->remove($task); if ($queue) { $this->sendingQueuesRepository->remove($queue); } $this->sendingQueuesRepository->flush(); return; } // Only deleted newsletter or newsletter with unexpected state should pass here. // Because this state cannot proceed with sending, we need to pause the scheduled task. $task->setStatus(ScheduledTaskEntity::STATUS_PAUSED); $this->scheduledTasksRepository->flush(); } }