8889841cSubscribersCountCacheRecalculation.php000064400000006117150514627470014226 0ustar00transientCache = $transientCache; $this->segmentsRepository = $segmentsRepository; $this->subscribersCountsController = $subscribersCountsController; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $segments = $this->segmentsRepository->findAll(); foreach ($segments as $segment) { $this->recalculateSegmentCache($timer, (int)$segment->getId(), $segment); } // update cache for subscribers without segment $this->recalculateSegmentCache($timer, 0); // remove redundancies from cache $this->subscribersCountsController->removeRedundancyFromStatisticsCache(); return true; } private function recalculateSegmentCache($timer, int $segmentId, ?SegmentEntity $segment = null): void { $this->cronHelper->enforceExecutionLimit($timer); $now = Carbon::now(); $item = $this->transientCache->getItem(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY, $segmentId); if ($item === null || !isset($item['created_at']) || $now->diffInMinutes($item['created_at']) > self::EXPIRATION_IN_MINUTES) { if ($segment) { $this->subscribersCountsController->recalculateSegmentStatisticsCache($segment); if ($segment->isStatic()) { $this->subscribersCountsController->recalculateSegmentGlobalStatusStatisticsCache($segment); } } else { $this->subscribersCountsController->recalculateSubscribersWithoutSegmentStatisticsCache(); } } } public function getNextRunDate() { return Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); } public function shouldBeScheduled(): bool { $scheduledOrRunningTask = $this->scheduledTasksRepository->findScheduledOrRunningTask(self::TASK_TYPE); if ($scheduledOrRunningTask) { return false; } $now = Carbon::now(); $oldestCreatedAt = $this->transientCache->getOldestCreatedAt(TransientCache::SUBSCRIBERS_STATISTICS_COUNT_KEY); return $oldestCreatedAt === null || $now->diffInMinutes($oldestCreatedAt) > self::EXPIRATION_IN_MINUTES; } } SendingQueue/SendingThrottlingHandler.php000064400000005635150514627470014641 0ustar00logger = $loggerFactory->getLogger(LoggerFactory::TOPIC_SENDING); $this->settings = $settings; $this->wp = $wp; } public function getBatchSize(): int { $throttlingSettings = $this->loadSettings(); if (isset($throttlingSettings['batch_size'])) { return $throttlingSettings['batch_size']; } return $this->getMaxBatchSize(); } private function getMaxBatchSize(): int { return $this->wp->applyFilters('mailpoet_cron_worker_sending_queue_batch_size', self::BATCH_SIZE); } public function throttleBatchSize(): int { $batchSize = $this->getBatchSize(); if ($batchSize > 1) { $batchSize = (int)ceil($this->getBatchSize() / 2); $throttlingSettings = $this->loadSettings(); $throttlingSettings['batch_size'] = $batchSize; unset($throttlingSettings['success_count']); $this->logger->error("MailPoet throttling: decrease batch_size to: {$batchSize}"); $this->saveSettings($throttlingSettings); } return $batchSize; } public function processSuccess(): void { $throttlingSettings = $this->loadSettings(); if (!isset($throttlingSettings['batch_size'])) { return; } $throttlingSettings['success_count'] = isset($throttlingSettings['success_count']) ? ++$throttlingSettings['success_count'] : 1; $this->logger->info("MailPoet throttling: increase success_count to: {$throttlingSettings['success_count']}"); if ($throttlingSettings['success_count'] >= self::SUCCESS_THRESHOLD_TO_INCREASE) { unset($throttlingSettings['success_count']); $throttlingSettings['batch_size'] = min($this->getMaxBatchSize(), $throttlingSettings['batch_size'] * 2); $this->logger->info("MailPoet throttling: increase batch_size to: {$throttlingSettings['batch_size']}"); if ($this->getMaxBatchSize() === $throttlingSettings['batch_size']) { unset($throttlingSettings['batch_size']); } } $this->saveSettings($throttlingSettings); } private function loadSettings(): ?array { return $this->settings->get(self::SETTINGS_KEY); } private function saveSettings(array $settings): void { $this->settings->set(self::SETTINGS_KEY, $settings); } } SendingQueue/SendingErrorHandler.php000064400000003646150514627470013574 0ustar00throttlingHandler = $throttlingHandler; } public function processError( MailerError $error, SendingTask $sendingTask, array $preparedSubscribersIds, array $preparedSubscribers ) { if ($error->getLevel() === MailerError::LEVEL_HARD) { return $this->processHardError($error); } $this->processSoftError($error, $sendingTask, $preparedSubscribersIds, $preparedSubscribers); } private function processHardError(MailerError $error) { if ($error->getRetryInterval() !== null) { MailerLog::processNonBlockingError($error->getOperation(), $error->getMessageWithFailedSubscribers(), $error->getRetryInterval()); } else { $throttledBatchSize = null; if ($error->getOperation() === MailerError::OPERATION_CONNECT) { $throttledBatchSize = $this->throttlingHandler->throttleBatchSize(); } MailerLog::processError($error->getOperation(), $error->getMessageWithFailedSubscribers(), null, false, $throttledBatchSize); } } private function processSoftError(MailerError $error, SendingTask $sendingTask, $preparedSubscribersIds, $preparedSubscribers) { foreach ($error->getSubscriberErrors() as $subscriberError) { $subscriberIdIndex = array_search($subscriberError->getEmail(), $preparedSubscribers); $message = $subscriberError->getMessage() ?: $error->getMessage(); $sendingTask->saveSubscriberError($preparedSubscribersIds[$subscriberIdIndex], $message); } } } SendingQueue/Tasks/Posts.php000064400000004643150514627470012070 0ustar00loggerFactory = LoggerFactory::getInstance(); $this->newsletterPostRepository = ContainerWrapper::getInstance()->get(NewsletterPostsRepository::class); } public function extractAndSave($renderedNewsletter, NewsletterEntity $newsletter): bool { if ($newsletter->getType() !== NewsletterEntity::TYPE_NOTIFICATION_HISTORY) { return false; } $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info( 'extract and save posts - before', ['newsletter_id' => $newsletter->getId()] ); preg_match_all( '/data-post-id="(\d+)"/ism', $renderedNewsletter['html'], $matchedPostsIds); $matchedPostsIds = $matchedPostsIds[1]; if (!count($matchedPostsIds)) { return false; } $parent = $newsletter->getParent(); // parent post notification if (!$parent instanceof NewsletterEntity) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info( 'parent post has not been found', ['newsletter_id' => $newsletter->getId()] ); return false; } foreach ($matchedPostsIds as $postId) { $newsletterPost = new NewsletterPostEntity($parent, $postId); $this->newsletterPostRepository->persist($newsletterPost); } $this->newsletterPostRepository->flush(); $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info( 'extract and save posts - after', ['newsletter_id' => $newsletter->getId(), 'matched_posts_ids' => $matchedPostsIds] ); return true; } public function getAlcPostsCount($renderedNewsletter, NewsletterEntity $newsletter) { $templatePostsCount = substr_count($newsletter->getContent(), 'data-post-id'); $newsletterPostsCount = substr_count($renderedNewsletter['html'], 'data-post-id'); return $newsletterPostsCount - $templatePostsCount; } } SendingQueue/Tasks/Newsletter.php000064400000027403150514627470013113 0ustar00get(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->linksTask = ContainerWrapper::getInstance()->get(LinksTask::class); $this->newsletterLinks = ContainerWrapper::getInstance()->get(NewsletterLinks::class); $this->sendingQueuesRepository = ContainerWrapper::getInstance()->get(SendingQueuesRepository::class); } public function getNewsletterFromQueue(Sending $sendingTask): ?NewsletterEntity { // get existing active or sending newsletter $sendingQueue = $sendingTask->getSendingQueueEntity(); $newsletter = $sendingQueue->getNewsletter(); if ( is_null($newsletter) || $newsletter->getDeletedAt() !== null || !in_array($newsletter->getStatus(), [NewsletterEntity::STATUS_ACTIVE, NewsletterEntity::STATUS_SENDING]) || $newsletter->getStatus() === NewsletterEntity::STATUS_CORRUPT ) { 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; } public function preProcessNewsletter(NewsletterEntity $newsletter, Sending $sendingTask) { // return the newsletter if it was previously rendered /** @phpstan-ignore-next-line - SendingQueue::getNewsletterRenderedBody() is called inside Sending using __call(). Sending will be refactored soon to stop using Paris models. */ if (!is_null($sendingTask->getNewsletterRenderedBody())) { return (!$sendingTask->validate()) ? $this->stopNewsletterPreProcessing(sprintf('QUEUE-%d-RENDER', $sendingTask->id)) : $newsletter; } $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'pre-processing newsletter', ['newsletter_id' => $newsletter->getId(), 'task_id' => $sendingTask->taskId] ); $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, $sendingTask); $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, $sendingTask); } else { // render newsletter $renderedNewsletter = $this->renderer->render($newsletter, $sendingTask); $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' => $sendingTask->taskId] ); $this->newslettersRepository->bulkDelete([(int)$newsletter->getId()]); return false; } // extract and save newsletter posts $this->postsTask->extractAndSave($renderedNewsletter, $newsletter); $sendingQueueEntity = $sendingTask->getSendingQueueEntity(); if ($campaignId !== null) { $this->sendingQueuesRepository->saveCampaignId($sendingQueueEntity, $campaignId); } // update queue with the rendered and pre-processed newsletter $sendingTask->newsletterRenderedSubject = ShortcodesTask::process( $newsletter->getSubject(), $renderedNewsletter['html'], $newsletter, null, $sendingQueueEntity ); // if the rendered subject is empty, use a default subject, // having no subject in a newsletter is considered spammy if (empty(trim((string)$sendingTask->newsletterRenderedSubject))) { $sendingTask->newsletterRenderedSubject = __('No subject', 'mailpoet'); } $renderedNewsletter = $this->emoji->encodeEmojisInBody($renderedNewsletter); $sendingTask->newsletterRenderedBody = $renderedNewsletter; $sendingTask->save(); // catch DB errors $queueErrors = $sendingTask->getErrors(); if (!$queueErrors) { // verify that the rendered body was successfully saved $sendingQueue = SendingQueueModel::findOne($sendingTask->id); if ($sendingQueue instanceof SendingQueueModel) { $queueErrors = ($sendingQueue->validate() !== true); } } if ($queueErrors) { $this->stopNewsletterPreProcessing(sprintf('QUEUE-%d-SAVE', $sendingTask->id)); } 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, Sending $sendingTask): array { $sendingQueue = $sendingTask->queue(); $renderedNewsletter = $sendingQueue->getNewsletterRenderedBody(); $renderedNewsletter = $this->emoji->decodeEmojisInBody($renderedNewsletter); $preparedNewsletter = Helpers::joinObject( [ $sendingTask->newsletterRenderedSubject, $renderedNewsletter['html'], $renderedNewsletter['text'], ] ); $sendingQueueEntity = $sendingTask->getSendingQueueEntity(); $preparedNewsletter = ShortcodesTask::process( $preparedNewsletter, null, $newsletter, $subscriber, $sendingQueueEntity ); if ($this->trackingEnabled) { $preparedNewsletter = $this->newsletterLinks->replaceSubscriberData( $subscriber->getId(), $sendingTask->id, $preparedNewsletter ); } $preparedNewsletter = Helpers::splitObject($preparedNewsletter); return [ 'id' => $newsletter->getId(), 'subject' => $preparedNewsletter[0], 'body' => [ 'html' => $preparedNewsletter[1], 'text' => $preparedNewsletter[2], ], ]; } public function markNewsletterAsSent(NewsletterEntity $newsletter, Sending $sendingTask) { // if it's a standard or notification history newsletter, update its status if ( $newsletter->getType() === NewsletterEntity::TYPE_STANDARD || $newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION_HISTORY ) { $scheduledTask = $sendingTask->task(); $newsletter->setStatus(NewsletterEntity::STATUS_SENT); $newsletter->setSentAt(new Carbon($scheduledTask->processedAt)); $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); } } SendingQueue/Tasks/Links.php000064400000006776150514627470012051 0ustar00linkTokens = $linkTokens; $this->newsletterLinks = $newsletterLinks; $this->newsletterLinkRepository = $newsletterLinkRepository; $this->trackingConfig = $trackingConfig; } public function process($renderedNewsletter, NewsletterEntity $newsletter, $queue) { [$renderedNewsletter, $links] = $this->hashAndReplaceLinks($renderedNewsletter, $newsletter->getId(), $queue->id); $this->saveLinks($links, $newsletter, $queue); return $renderedNewsletter; } public function hashAndReplaceLinks($renderedNewsletter, $newsletterId, $queueId) { // join HTML and TEXT rendered body into a text string $content = Helpers::joinObject($renderedNewsletter); [$content, $links] = $this->newsletterLinks->process($content, $newsletterId, $queueId); $links = $this->newsletterLinks->ensureInstantUnsubscribeLink($links); // split the processed body with hashed links back to HTML and TEXT list($renderedNewsletter['html'], $renderedNewsletter['text']) = Helpers::splitObject($content); return [ $renderedNewsletter, $links, ]; } public function saveLinks($links, NewsletterEntity $newsletter, $queue) { return $this->newsletterLinks->save($links, $newsletter->getId(), $queue->id); } public function getUnsubscribeUrl($queueId, SubscriberEntity $subscriber = null) { if ($this->trackingConfig->isEmailTrackingEnabled() && $subscriber) { $linkHash = $this->newsletterLinkRepository->findOneBy( [ 'queue' => $queueId, 'url' => NewsletterLinkEntity::INSTANT_UNSUBSCRIBE_LINK_SHORT_CODE, ] ); if (!$linkHash instanceof NewsletterLinkEntity) { return ''; } $data = $this->newsletterLinks->createUrlDataObject( $subscriber->getId(), $this->linkTokens->getToken($subscriber), $queueId, $linkHash->getHash(), false ); $url = Router::buildRequest( Track::ENDPOINT, Track::ACTION_CLICK, $data ); } else { $subscriptionUrlFactory = SubscriptionUrlFactory::getInstance(); $url = $subscriptionUrlFactory->getUnsubscribeUrl($subscriber, $queueId); } return $url; } public function getOneClickUnsubscribeUrl($queueId, SubscriberEntity $subscriber): string { $subscriptionUrlFactory = SubscriptionUrlFactory::getInstance(); return $subscriptionUrlFactory->getUnsubscribeUrl($subscriber, $queueId); } } SendingQueue/Tasks/Shortcodes.php000064400000002707150514627470013074 0ustar00get(NewsletterShortcodes::class); if ($queue instanceof SendingQueueEntity) { $shortcodes->setQueue($queue); } else { $shortcodes->setQueue(null); } if ($newsletter instanceof NewsletterEntity) { $shortcodes->setNewsletter($newsletter); } else { $shortcodes->setNewsletter(null); } if ($subscriber instanceof SubscriberEntity) { $shortcodes->setSubscriber($subscriber); } else { $shortcodes->setSubscriber(null); } return $shortcodes->replace($content, $contentSource); } } SendingQueue/Tasks/index.php000064400000000006150514627470012054 0ustar00mailerFactory = $mailerFactory; $this->mailer = $this->configureMailer(); } public function configureMailer($newsletter = null) { $sender['address'] = (!empty($newsletter->senderAddress)) ? $newsletter->senderAddress : null; $sender['name'] = (!empty($newsletter->senderName)) ? $newsletter->senderName : null; $replyTo['address'] = (!empty($newsletter->replyToAddress)) ? $newsletter->replyToAddress : null; $replyTo['name'] = (!empty($newsletter->replyToName)) ? $newsletter->replyToName : null; if (!$sender['address']) { $sender = null; } if (!$replyTo['address']) { $replyTo = null; } $this->mailer = $this->mailerFactory->buildMailer(null, $sender, $replyTo); return $this->mailer; } public function getMailerLog() { return MailerLog::getMailerLog(); } public function updateSentCount() { return MailerLog::incrementSentCount(); } public function getProcessingMethod() { return ($this->mailer->mailerMethod instanceof MailPoet) ? 'bulk' : 'individual'; } public function prepareSubscriberForSending($subscriber) { return $this->mailer->formatSubscriberNameAndEmailAddress($subscriber); } public function sendBulk($preparedNewsletters, $preparedSubscribers, $extraParams = []) { if ($this->getProcessingMethod() === 'individual') { throw new \LogicException('Trying to send a batch with individual processing method'); } return $this->mailer->mailerMethod->send( $preparedNewsletters, $preparedSubscribers, $extraParams ); } public function send($preparedNewsletter, $preparedSubscriber, $extraParams = []) { if ($this->getProcessingMethod() === 'bulk') { throw new \LogicException('Trying to send an individual email with a bulk processing method'); } return $this->mailer->mailerMethod->send( $preparedNewsletter, $preparedSubscriber, $extraParams ); } } SendingQueue/SendingQueue.php000064400000046352150514627470012272 0ustar00errorHandler = $errorHandler; $this->throttlingHandler = $throttlingHandler; $this->statsNotificationsScheduler = $statsNotificationsScheduler; $this->subscribersFinder = $subscriberFinder; $this->mailerTask = $mailerTask; $this->newsletterTask = ($newsletterTask) ? $newsletterTask : new NewsletterTask(); $this->segmentsRepository = $segmentsRepository; $this->mailerMetaInfo = new MetaInfo; $this->wp = $wp; $this->loggerFactory = $loggerFactory; $this->newslettersRepository = $newslettersRepository; $this->cronHelper = $cronHelper; $this->links = $links; $this->scheduledTasksRepository = $scheduledTasksRepository; $this->subscribersRepository = $subscribersRepository; $this->sendingQueuesRepository = $sendingQueuesRepository; } public function process($timer = false) { $timer = $timer ?: microtime(true); $this->enforceSendingAndExecutionLimits($timer); foreach ($this->scheduledTasksRepository->findRunningSendingTasks(self::TASK_BATCH_SIZE) as $taskEntity) { $task = ScheduledTask::findOne($taskEntity->getId()); if (!$task instanceof ScheduledTask) continue; $queue = SendingTask::createFromScheduledTask($task); if (!$queue instanceof SendingTask) continue; $task = $queue->task(); if (!$task instanceof ScheduledTask) continue; if ($this->isInProgress($task)) { if ($this->isTimeout($task)) { $this->stopProgress($task); } else { continue; } } $this->startProgress($task); try { $this->scheduledTasksRepository->touchAllByIds([$queue->taskId]); $this->processSending($queue, (int)$timer); } catch (\Exception $e) { $this->stopProgress($task); throw $e; } $this->stopProgress($task); } } private function processSending(SendingTask $queue, int $timer): void { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'sending queue processing', ['task_id' => $queue->taskId] ); $this->deleteTaskIfNewsletterDoesNotExist($queue); $newsletterEntity = $this->newsletterTask->getNewsletterFromQueue($queue); if (!$newsletterEntity) { return; } // pre-process newsletter (render, replace shortcodes/links, etc.) $newsletterEntity = $this->newsletterTask->preProcessNewsletter($newsletterEntity, $queue); if (!$newsletterEntity) { $this->deleteTask($queue); return; } $newsletter = Newsletter::findOne($newsletterEntity->getId()); if (!$newsletter) { return; } // clone the original object to be used for processing $_newsletter = (object)$newsletter->asArray(); $_newsletter->options = $newsletterEntity->getOptionsAsArray(); // configure mailer $this->mailerTask->configureMailer($newsletter); // get newsletter segments $newsletterSegmentsIds = $newsletterEntity->getSegmentIds(); // Pause task in case some of related segments was deleted or trashed if ($newsletterSegmentsIds && !$this->checkDeletedSegments($newsletterSegmentsIds)) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'pause task in sending queue due deleted or trashed segment', ['task_id' => $queue->taskId] ); $queue->status = ScheduledTaskEntity::STATUS_PAUSED; $queue->save(); $this->wp->setTransient(self::EMAIL_WITH_INVALID_SEGMENT_OPTION, $newsletter->subject); return; } // get subscribers $subscriberBatches = new BatchIterator($queue->taskId, $this->getBatchSize()); if ($subscriberBatches->count() === 0) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'no subscribers to process', ['task_id' => $queue->taskId] ); $task = $queue->getSendingQueueEntity()->getTask(); if ($task) { $this->scheduledTasksRepository->invalidateTask($task); } return; } /** @var int[] $subscribersToProcessIds - it's required for PHPStan */ foreach ($subscriberBatches as $subscribersToProcessIds) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'subscriber batch processing', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId, 'subscriber_batch_count' => count($subscribersToProcessIds)] ); if (!empty($newsletterSegmentsIds[0])) { // Check that subscribers are in segments $foundSubscribersIds = $this->subscribersFinder->findSubscribersInSegments($subscribersToProcessIds, $newsletterSegmentsIds); $foundSubscribers = empty($foundSubscribersIds) ? [] : SubscriberModel::whereIn('id', $foundSubscribersIds) ->whereNull('deleted_at') ->findMany(); } else { // No segments = Welcome emails or some Automatic emails. // Welcome emails or some Automatic emails use segments only for scheduling and store them as a newsletter option $foundSubscribers = SubscriberModel::whereIn('id', $subscribersToProcessIds); $foundSubscribers = $newsletter->type === NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL ? $foundSubscribers->whereNotEqual('status', SubscriberModel::STATUS_BOUNCED) : $foundSubscribers->where('status', SubscriberModel::STATUS_SUBSCRIBED); $foundSubscribers = $foundSubscribers ->whereNull('deleted_at') ->findMany(); $foundSubscribersIds = SubscriberModel::extractSubscribersIds($foundSubscribers); } // if some subscribers weren't found, remove them from the processing list if (count($foundSubscribersIds) !== count($subscribersToProcessIds)) { $subscribersToRemove = array_diff( $subscribersToProcessIds, $foundSubscribersIds ); $queue->removeSubscribers($subscribersToRemove); if (!$queue->countToProcess) { $this->newsletterTask->markNewsletterAsSent($newsletterEntity, $queue); continue; } // if there aren't any subscribers to process in batch (e.g. all unsubscribed or were deleted) continue with next batch if (count($foundSubscribersIds) === 0) { continue; } } $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'before queue chunk processing', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId, 'found_subscribers_count' => count($foundSubscribers)] ); // reschedule bounce task to run sooner, if needed $this->reScheduleBounceTask(); if ($newsletterEntity->getStatus() !== NewsletterEntity::STATUS_CORRUPT) { $queue = $this->processQueue( $queue, $_newsletter, $foundSubscribers, $timer ); $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'after queue chunk processing', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId] ); if ($queue->status === ScheduledTaskEntity::STATUS_COMPLETED) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'completed newsletter sending', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId] ); $this->newsletterTask->markNewsletterAsSent($newsletterEntity, $queue); $this->statsNotificationsScheduler->schedule($newsletterEntity); } $this->enforceSendingAndExecutionLimits($timer); } else { $this->sendingQueuesRepository->pause($queue->getSendingQueueEntity()); $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->error( 'Can\'t send corrupt newsletter', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId] ); } } } public function getBatchSize(): int { return $this->throttlingHandler->getBatchSize(); } public function processQueue($queue, $newsletter, $subscribers, $timer) { // determine if processing is done in bulk or individually $processingMethod = $this->mailerTask->getProcessingMethod(); $preparedNewsletters = []; $preparedSubscribers = []; $preparedSubscribersIds = []; $unsubscribeUrls = []; $statistics = []; $metas = []; $oneClickUnsubscribeUrls = []; $sendingQueueEntity = $queue->getSendingQueueEntity(); $sendingQueueMeta = $sendingQueueEntity->getMeta() ?? []; $campaignId = $sendingQueueMeta['campaignId'] ?? null; $newsletterEntity = $this->newslettersRepository->findOneById($newsletter->id); foreach ($subscribers as $subscriber) { $subscriberEntity = $this->subscribersRepository->findOneById($subscriber->id); if (!$subscriberEntity instanceof SubscriberEntity) { continue; } if (!$newsletterEntity instanceof NewsletterEntity) { continue; } // render shortcodes and replace subscriber data in tracked links $preparedNewsletters[] = $this->newsletterTask->prepareNewsletterForSending( $newsletterEntity, $subscriberEntity, $queue ); // format subscriber name/address according to mailer settings $preparedSubscribers[] = $this->mailerTask->prepareSubscriberForSending( $subscriber ); $preparedSubscribersIds[] = $subscriber->id; // create personalized instant unsubsribe link $unsubscribeUrls[] = $this->links->getUnsubscribeUrl($queue->id, $subscriberEntity); $oneClickUnsubscribeUrls[] = $this->links->getOneClickUnsubscribeUrl($queue->id, $subscriberEntity); $metasForSubscriber = $this->mailerMetaInfo->getNewsletterMetaInfo($newsletter, $subscriberEntity); if ($campaignId) { $metasForSubscriber['campaign_id'] = $campaignId; } $metas[] = $metasForSubscriber; // keep track of values for statistics purposes $statistics[] = [ 'newsletter_id' => $newsletter->id, 'subscriber_id' => $subscriber->id, 'queue_id' => $queue->id, ]; if ($processingMethod === 'individual') { $queue = $this->sendNewsletter( $queue, $preparedSubscribersIds[0], $preparedNewsletters[0], $preparedSubscribers[0], $statistics[0], $timer, [ 'unsubscribe_url' => $unsubscribeUrls[0], 'meta' => $metas[0], 'one_click_unsubscribe' => $oneClickUnsubscribeUrls, ] ); $preparedNewsletters = []; $preparedSubscribers = []; $preparedSubscribersIds = []; $unsubscribeUrls = []; $oneClickUnsubscribeUrls = []; $statistics = []; $metas = []; } } if ($processingMethod === 'bulk') { $queue = $this->sendNewsletters( $queue, $preparedSubscribersIds, $preparedNewsletters, $preparedSubscribers, $statistics, $timer, [ 'unsubscribe_url' => $unsubscribeUrls, 'meta' => $metas, 'one_click_unsubscribe' => $oneClickUnsubscribeUrls, ] ); } return $queue; } public function sendNewsletter( SendingTask $sendingTask, $preparedSubscriberId, $preparedNewsletter, $preparedSubscriber, $statistics, $timer, $extraParams = [] ) { // send newsletter $sendResult = $this->mailerTask->send( $preparedNewsletter, $preparedSubscriber, $extraParams ); return $this->processSendResult( $sendingTask, $sendResult, [$preparedSubscriber], [$preparedSubscriberId], [$statistics], $timer ); } public function sendNewsletters( SendingTask $sendingTask, $preparedSubscribersIds, $preparedNewsletters, $preparedSubscribers, $statistics, $timer, $extraParams = [] ) { // send newsletters $sendResult = $this->mailerTask->sendBulk( $preparedNewsletters, $preparedSubscribers, $extraParams ); return $this->processSendResult( $sendingTask, $sendResult, $preparedSubscribers, $preparedSubscribersIds, $statistics, $timer ); } /** * Checks whether some of segments was deleted or trashed * @param int[] $segmentIds */ private function checkDeletedSegments(array $segmentIds): bool { if (count($segmentIds) === 0) { return true; } $segmentIds = array_unique($segmentIds); $segments = $this->segmentsRepository->findBy(['id' => $segmentIds]); // Some segment was deleted from DB if (count($segmentIds) > count($segments)) { return false; } foreach ($segments as $segment) { if ($segment->getDeletedAt() !== null) { return false; } } return true; } private function processSendResult( SendingTask $sendingTask, $sendResult, array $preparedSubscribers, array $preparedSubscribersIds, array $statistics, $timer ) { // log error message and schedule retry/pause sending if ($sendResult['response'] === false) { $error = $sendResult['error']; $this->errorHandler->processError($error, $sendingTask, $preparedSubscribersIds, $preparedSubscribers); } elseif (!$sendingTask->updateProcessedSubscribers($preparedSubscribersIds)) { // update processed/to process list MailerLog::processError( 'processed_list_update', sprintf('QUEUE-%d-PROCESSED-LIST-UPDATE', $sendingTask->id), null, true ); } // log statistics StatisticsNewslettersModel::createMultiple($statistics); // update the sent count $this->mailerTask->updateSentCount(); // enforce execution limits if queue is still being processed if ($sendingTask->status !== ScheduledTaskEntity::STATUS_COMPLETED) { $this->enforceSendingAndExecutionLimits($timer); } $this->throttlingHandler->processSuccess(); return $sendingTask; } public function enforceSendingAndExecutionLimits($timer) { // abort if execution limit is reached $this->cronHelper->enforceExecutionLimit($timer); // abort if sending limit has been reached MailerLog::enforceExecutionRequirements(); } private function reScheduleBounceTask() { $bounceTasks = $this->scheduledTasksRepository->findFutureScheduledByType(Bounce::TASK_TYPE); if (count($bounceTasks)) { $bounceTask = reset($bounceTasks); if (Carbon::createFromTimestamp((int)current_time('timestamp'))->addHours(42)->lessThan($bounceTask->getScheduledAt())) { $randomOffset = rand(-6 * 60 * 60, 6 * 60 * 60); $bounceTask->setScheduledAt(Carbon::createFromTimestamp((int)current_time('timestamp'))->addSeconds((36 * 60 * 60) + $randomOffset)); $this->scheduledTasksRepository->persist($bounceTask); $this->scheduledTasksRepository->flush(); } } } private function isInProgress(ScheduledTask $task): bool { if (!empty($task->inProgress)) { // Do not run multiple instances of the task return true; } return false; } private function startProgress(ScheduledTask $task): void { $task->inProgress = true; $task->save(); } private function stopProgress(ScheduledTask $task): void { $task->inProgress = false; $task->save(); } private function isTimeout(ScheduledTask $task): bool { $currentTime = Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); $updated = strtotime((string)$task->updatedAt); if ($updated !== false) { $updatedAt = Carbon::createFromTimestamp($updated); } if (isset($updatedAt) && $updatedAt->diffInSeconds($currentTime, false) > $this->getExecutionLimit()) { return true; } return false; } private function getExecutionLimit(): int { return $this->cronHelper->getDaemonExecutionLimit() * 3; } private function deleteTaskIfNewsletterDoesNotExist(SendingTask $sendingTask) { $sendingQueue = $sendingTask->getSendingQueueEntity(); $newsletter = $sendingQueue->getNewsletter(); if ($newsletter !== null) { return; } $this->deleteTask($sendingTask); } private function deleteTask(SendingTask $queue) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->info( 'delete task in sending queue', ['task_id' => $queue->taskId] ); $queue->delete(); } } SendingQueue/index.php000064400000000006150514627470010767 0ustar00subscribersEmailCountsController = $subscribersEmailCountsController; $this->entityManager = $entityManager; $this->settings = $settings; $this->trackingConfig = $trackingConfig; parent::__construct(); } public function checkProcessingRequirements() { if (!$this->trackingConfig->isEmailTrackingEnabled()) { return false; } $daysToInactive = (int)$this->settings->get('deactivate_subscriber_after_inactive_days'); if ($daysToInactive === 0) { return false; } return true; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $previousTask = $this->findPreviousTask($task); $dateFromLastRun = null; if ($previousTask instanceof ScheduledTaskEntity) { $dateFromLastRun = $previousTask->getScheduledAt(); } $meta = $task->getMeta(); $lastSubscriberId = isset($meta['last_subscriber_id']) ? (int)$meta['last_subscriber_id'] : 0; $highestSubscriberId = isset($meta['highest_subscriber_id']) ? (int)$meta['highest_subscriber_id'] : $this->getHighestSubscriberId(); $meta['highest_subscriber_id'] = $highestSubscriberId; $task->setMeta($meta); while ($lastSubscriberId <= $highestSubscriberId) { [$count, $lastSubscriberId] = $this->subscribersEmailCountsController->updateSubscribersEmailCounts($dateFromLastRun, self::BATCH_SIZE, $lastSubscriberId); if ($count === 0) { break; } $meta['last_subscriber_id'] = $lastSubscriberId++; $task->setMeta($meta); $this->scheduledTasksRepository->persist($task); $this->scheduledTasksRepository->flush(); $this->cronHelper->enforceExecutionLimit($timer); }; $this->schedule(); return true; } private function findPreviousTask(ScheduledTaskEntity $task): ?ScheduledTaskEntity { return $this->scheduledTasksRepository->findPreviousTask($task); } private function getHighestSubscriberId(): int { $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $result = $this->entityManager->getConnection()->executeQuery("SELECT MAX(id) FROM $subscribersTable LIMIT 1;")->fetchNumeric(); /** @var int[] $result - it's required for PHPStan */ return is_array($result) && isset($result[0]) ? (int)$result[0] : 0; } } SubscribersStatsReport.php000064400000003626150514627470012000 0ustar00subscribersCountReporter = $subscribersCountReporter; $this->serviceChecker = $servicesChecker; $this->workerScheduler = $workerScheduler; } public function checkProcessingRequirements() { return (bool)$this->serviceChecker->getValidAccountKey(); } public function processTaskStrategy(ScheduledTaskEntity $task, $timer): bool { $key = $this->serviceChecker->getValidAccountKey(); if ($key === null) { return false; } $result = $this->subscribersCountReporter->report($key); // We have a valid key, but request failed if ($result === false) { $this->workerScheduler->rescheduleProgressively($task); } return $result; } public function getNextRunDate() { $date = Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); // Spread the check within 6 hours after midnight so that all plugins don't ping the service at the same time return $date->startOfDay() ->addDay() ->addHours(rand(0, 5)) ->addMinutes(rand(0, 59)) ->addSeconds(rand(0, 59)); } } SimpleWorker.php000064400000005661150514627470007723 0ustar00get(WPFunctions::class); $this->wp = $wp; $this->cronHelper = ContainerWrapper::getInstance()->get(CronHelper::class); $this->cronWorkerScheduler = ContainerWrapper::getInstance()->get(CronWorkerScheduler::class); $this->scheduledTasksRepository = ContainerWrapper::getInstance()->get(ScheduledTasksRepository::class); } public function getTaskType() { return static::TASK_TYPE; } public function supportsMultipleInstances() { return static::SUPPORT_MULTIPLE_INSTANCES; } public function schedule() { $this->cronWorkerScheduler->schedule(static::TASK_TYPE, $this->getNextRunDate()); } protected function scheduleImmediately(): void { $this->cronWorkerScheduler->schedule(static::TASK_TYPE, $this->getNextRunDateImmediately()); } public function checkProcessingRequirements() { return true; } public function init() { } public function prepareTaskStrategy(ScheduledTaskEntity $task, $timer) { return true; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { return true; } public function getNextRunDate() { // random day of the next week $date = Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); $date->setISODate((int)$date->format('o'), ((int)$date->format('W')) + 1, mt_rand(1, 7)); $date->startOfDay(); return $date; } protected function getNextRunDateImmediately(): Carbon { return Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); } public function scheduleAutomatically() { return static::AUTOMATIC_SCHEDULING; } protected function getCompletedTasks() { return $this->scheduledTasksRepository->findCompletedByType(static::TASK_TYPE, CronWorkerRunner::TASK_BATCH_SIZE); } } ReEngagementEmailsScheduler.php000064400000001604150514627470012624 0ustar00reEngagementEmailsScheduler = $reEngagementEmailsScheduler; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $this->reEngagementEmailsScheduler->scheduleAll(); return true; } public function getNextRunDate() { return Carbon::createFromTimestamp($this->wp->currentTime('timestamp'))->addDay(); } } WorkersFactory.php000064400000012562150514627470010262 0ustar00container = $container; } /** @return SchedulerWorker */ public function createScheduleWorker() { return $this->container->get(SchedulerWorker::class); } /** @return SendingQueueWorker */ public function createQueueWorker() { return $this->container->get(SendingQueueWorker::class); } /** @return StatsNotificationsWorker */ public function createStatsNotificationsWorker() { return $this->container->get(StatsNotificationsWorker::class); } /** @return StatsNotificationsWorkerForAutomatedEmails */ public function createStatsNotificationsWorkerForAutomatedEmails() { return $this->container->get(StatsNotificationsWorkerForAutomatedEmails::class); } /** @return SendingServiceKeyCheckWorker */ public function createSendingServiceKeyCheckWorker() { return $this->container->get(SendingServiceKeyCheckWorker::class); } /** @return PremiumKeyCheckWorker */ public function createPremiumKeyCheckWorker() { return $this->container->get(PremiumKeyCheckWorker::class); } /** @return BounceWorker */ public function createBounceWorker() { return $this->container->get(BounceWorker::class); } /** @return WooCommerceSyncWorker */ public function createWooCommerceSyncWorker() { return $this->container->get(WooCommerceSyncWorker::class); } /** @return ExportFilesCleanup */ public function createExportFilesCleanupWorker() { return $this->container->get(ExportFilesCleanup::class); } /** @return Beamer */ public function createBeamerkWorker() { return $this->container->get(Beamer::class); } /** @return InactiveSubscribers */ public function createInactiveSubscribersWorker() { return $this->container->get(InactiveSubscribers::class); } /** @return UnsubscribeTokens */ public function createUnsubscribeTokensWorker() { return $this->container->get(UnsubscribeTokens::class); } /** @return SubscriberLinkTokens */ public function createSubscriberLinkTokensWorker() { return $this->container->get(SubscriberLinkTokens::class); } /** @return SubscribersEngagementScore */ public function createSubscribersEngagementScoreWorker() { return $this->container->get(SubscribersEngagementScore::class); } /** @return SubscribersLastEngagement */ public function createSubscribersLastEngagementWorker() { return $this->container->get(SubscribersLastEngagement::class); } /** @return AuthorizedSendingEmailsCheck */ public function createAuthorizedSendingEmailsCheckWorker() { return $this->container->get(AuthorizedSendingEmailsCheck::class); } /** @return WooCommercePastOrders */ public function createWooCommercePastOrdersWorker() { return $this->container->get(WooCommercePastOrders::class); } /** @return SubscribersCountCacheRecalculation */ public function createSubscribersCountCacheRecalculationWorker() { return $this->container->get(SubscribersCountCacheRecalculation::class); } /** @return ReEngagementEmailsScheduler */ public function createReEngagementEmailsSchedulerWorker() { return $this->container->get(ReEngagementEmailsScheduler::class); } /** @return SubscribersStatsReport */ public function createSubscribersStatsReportWorker() { return $this->container->get(SubscribersStatsReport::class); } /** @return NewsletterTemplateThumbnails */ public function createNewsletterTemplateThumbnailsWorker() { return $this->container->get(NewsletterTemplateThumbnails::class); } /** @return SubscribersEmailCount */ public function createSubscribersEmailCountsWorker() { return $this->container->get(SubscribersEmailCount::class); } /** @return AbandonedCartWorker */ public function createAbandonedCartWorker() { return $this->container->get(AbandonedCartWorker::class); } } WooCommerceSync.php000064400000003740150514627470010350 0ustar00woocommerceSegment = $woocommerceSegment; $this->woocommerceHelper = $woocommerceHelper; parent::__construct(); } public function checkProcessingRequirements() { return $this->woocommerceHelper->isWooCommerceActive(); } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $meta = $task->getMeta(); $highestOrderId = $this->getHighestOrderId(); if (!isset($meta['last_checked_order_id'])) { $meta['last_checked_order_id'] = 0; } do { $this->cronHelper->enforceExecutionLimit($timer); $meta['last_checked_order_id'] = $this->woocommerceSegment->synchronizeCustomers( $meta['last_checked_order_id'], $highestOrderId, self::BATCH_SIZE ); $task->setMeta($meta); $this->scheduledTasksRepository->persist($task); $this->scheduledTasksRepository->flush(); } while ($meta['last_checked_order_id'] < $highestOrderId); return true; } private function getHighestOrderId(): int { $orders = $this->woocommerceHelper->wcGetOrders( [ 'status' => 'all', 'type' => 'shop_order', 'limit' => 1, 'orderby' => 'ID', 'order' => 'DESC', 'return' => 'ids', ] ); return (!empty($orders)) ? $orders[0] : 0; } } WooCommercePastOrders.php000064400000005264150514627470011525 0ustar00woocommerceHelper = $woocommerceHelper; $this->woocommercePurchases = $woocommercePurchases; $this->statisticsClicksRepository = $statisticsClicksRepository; parent::__construct(); } public function checkProcessingRequirements() { return $this->woocommerceHelper->isWooCommerceActive() && empty($this->getCompletedTasks()); // run only once } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $oldestClick = $this->statisticsClicksRepository->findOneBy([], ['createdAt' => 'asc']); if (!$oldestClick instanceof StatisticsClickEntity) { return true; } // continue from 'last_processed_id' from previous run $meta = $task->getMeta(); $lastId = isset($meta['last_processed_id']) ? $meta['last_processed_id'] : 0; add_filter('posts_where', function ($where = '') use ($lastId) { global $wpdb; return $where . " AND {$wpdb->prefix}posts.ID > " . $lastId; }, 10, 2); $orderIds = $this->woocommerceHelper->wcGetOrders([ 'status' => 'completed', 'date_completed' => '>=' . (($createdAt = $oldestClick->getCreatedAt()) ? $createdAt->format('Y-m-d H:i:s') : null), 'orderby' => 'ID', 'order' => 'ASC', 'limit' => self::BATCH_SIZE, 'return' => 'ids', ]); if (empty($orderIds)) { return true; } foreach ($orderIds as $orderId) { $this->woocommercePurchases->trackPurchase($orderId, false); } $task->setMeta(['last_processed_id' => end($orderIds)]); $this->scheduledTasksRepository->persist($task); $this->scheduledTasksRepository->flush(); return false; } public function getNextRunDate() { return Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); // schedule immediately } } Scheduler.php000064400000041654150514627470007220 0ustar00cronHelper = $cronHelper; $this->subscribersFinder = $subscribersFinder; $this->loggerFactory = $loggerFactory; $this->cronWorkerScheduler = $cronWorkerScheduler; $this->scheduledTasksRepository = $scheduledTasksRepository; $this->newslettersRepository = $newslettersRepository; $this->segmentsRepository = $segmentsRepository; $this->newsletterSegmentRepository = $newsletterSegmentRepository; $this->wp = $wp; $this->security = $security; $this->scheduler = $scheduler; } public function process($timer = false) { $timer = $timer ?: microtime(true); // abort if execution limit is reached $this->cronHelper->enforceExecutionLimit($timer); $scheduledTasks = $this->getScheduledSendingTasks(); if (!count($scheduledTasks)) return false; // To prevent big changes we convert ScheduledTaskEntity to old model $scheduledQueues = []; foreach ($scheduledTasks as $scheduledTask) { $task = ScheduledTask::findOne($scheduledTask->getId()); if (!$task) continue; $scheduledQueue = SendingTask::createFromScheduledTask($task); if (!$scheduledQueue) continue; $scheduledQueues[] = $scheduledQueue; } $this->updateTasks($scheduledTasks); foreach ($scheduledQueues as $i => $queue) { $newsletter = Newsletter::findOneWithOptions($queue->newsletterId); if (!$newsletter || $newsletter->deletedAt !== null) { $queue->delete(); } elseif ($newsletter->status !== NewsletterEntity::STATUS_ACTIVE && $newsletter->status !== NewsletterEntity::STATUS_SCHEDULED) { continue; } elseif ($newsletter->type === NewsletterEntity::TYPE_WELCOME) { $this->processWelcomeNewsletter($newsletter, $queue); } elseif ($newsletter->type === NewsletterEntity::TYPE_NOTIFICATION) { $this->processPostNotificationNewsletter($newsletter, $queue); } elseif ($newsletter->type === NewsletterEntity::TYPE_STANDARD) { $this->processScheduledStandardNewsletter($newsletter, $queue); } elseif ($newsletter->type === NewsletterEntity::TYPE_AUTOMATIC) { $this->processScheduledAutomaticEmail($newsletter, $queue); } elseif ($newsletter->type === NewsletterEntity::TYPE_RE_ENGAGEMENT) { $this->processReEngagementEmail($queue); } elseif ($newsletter->type === NewsletterEntity::TYPE_AUTOMATION) { $this->processScheduledAutomationEmail($queue); } elseif ($newsletter->type === NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL) { $this->processScheduledTransactionalEmail($queue); } $this->cronHelper->enforceExecutionLimit($timer); } } public function processWelcomeNewsletter($newsletter, $queue) { $subscribers = $queue->getSubscribers(); if (empty($subscribers[0])) { $queue->delete(); $this->updateScheduledTaskEntity($queue, true); return false; } $subscriberId = (int)$subscribers[0]; if ($newsletter->event === 'segment') { if ($this->verifyMailpoetSubscriber($subscriberId, $newsletter, $queue) === false) { return false; } } else { if ($newsletter->event === 'user') { if ($this->verifyWPSubscriber($subscriberId, $newsletter, $queue) === false) { return false; } } } $queue->status = null; $queue->save(); $this->updateScheduledTaskEntity($queue); return true; } public function processPostNotificationNewsletter($newsletter, SendingTask $queue) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info( 'process post notification in scheduler', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId] ); $newsletterEntity = $this->newslettersRepository->findOneById($newsletter->id); if (!$newsletterEntity instanceof NewsletterEntity) { throw new InvalidStateException(); } // ensure that segments exist $segments = $newsletterEntity->getSegmentIds(); if (empty($segments)) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info( 'post notification no segments', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId] ); return $this->deleteQueueOrUpdateNextRunDate($queue, $newsletter); } // ensure that subscribers are in segments $taskModel = $queue->task(); $taskEntity = $this->scheduledTasksRepository->findOneById($taskModel->id); if ($taskEntity instanceof ScheduledTaskEntity) { $subscribersCount = $this->subscribersFinder->addSubscribersToTaskFromSegments($taskEntity, $segments); } if (empty($subscribersCount)) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info( 'post notification no subscribers', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId, 'segment_ids' => $segments] ); return $this->deleteQueueOrUpdateNextRunDate($queue, $newsletter); } // create a duplicate newsletter that acts as a history record try { $notificationHistory = $this->createPostNotificationHistory($newsletterEntity); } catch (\Exception $exception) { $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->error( 'creating post notification history failed', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId, 'error' => $exception->getMessage()] ); return false; } // queue newsletter for delivery $queue->newsletterId = (int)$notificationHistory->getId(); $queue->updateCount(); $queue->status = null; $queue->save(); $this->updateScheduledTaskEntity($queue); // Because there is mixed usage of the old and new model, we want to be sure about the correct state $this->newslettersRepository->refresh($notificationHistory); $queue->getSendingQueueEntity(); // This call refreshes sending queue entity $this->loggerFactory->getLogger(LoggerFactory::TOPIC_POST_NOTIFICATIONS)->info( 'post notification set status to sending', ['newsletter_id' => $newsletter->id, 'task_id' => $queue->taskId] ); return true; } public function processScheduledAutomaticEmail($newsletter, $queue) { if ($newsletter->sendTo === 'segment') { $segment = $this->segmentsRepository->findOneById($newsletter->segment); if ($segment instanceof SegmentEntity) { $taskModel = $queue->task(); $taskEntity = $this->scheduledTasksRepository->findOneById($taskModel->id); if ($taskEntity instanceof ScheduledTaskEntity) { $result = $this->subscribersFinder->addSubscribersToTaskFromSegments($taskEntity, [(int)$segment->getId()]); } if (empty($result)) { $queue->delete(); $this->updateScheduledTaskEntity($queue, true); return false; } } } else { $subscribers = $queue->getSubscribers(); $subscriber = (!empty($subscribers) && is_array($subscribers)) ? Subscriber::findOne($subscribers[0]) : false; if (!$subscriber) { $queue->delete(); $this->updateScheduledTaskEntity($queue, true); return false; } if ($this->verifySubscriber($subscriber, $queue) === false) { return false; } } $queue->status = null; $queue->save(); $this->updateScheduledTaskEntity($queue); return true; } public function processScheduledAutomationEmail($queue): bool { $subscribers = $queue->getSubscribers(); $subscriber = (!empty($subscribers) && is_array($subscribers)) ? Subscriber::findOne($subscribers[0]) : null; if (!$subscriber) { $queue->delete(); $this->updateScheduledTaskEntity($queue, true); return false; } if (!$this->verifySubscriber($subscriber, $queue)) { return false; } $queue->status = null; $queue->save(); $this->updateScheduledTaskEntity($queue); return true; } public function processScheduledTransactionalEmail($queue): bool { $subscribers = $queue->getSubscribers(); $subscriber = (!empty($subscribers) && is_array($subscribers)) ? Subscriber::findOne($subscribers[0]) : null; if (!$subscriber) { $queue->delete(); $this->updateScheduledTaskEntity($queue, true); return false; } if (!$this->verifySubscriber($subscriber, $queue)) { $queue->delete(); $this->updateScheduledTaskEntity($queue, true); return false; } $queue->status = null; $queue->save(); $this->updateScheduledTaskEntity($queue); return true; } public function processScheduledStandardNewsletter($newsletter, SendingTask $task) { $newsletterEntity = $this->newslettersRepository->findOneById($newsletter->id); $taskEntity = null; if ($newsletterEntity instanceof NewsletterEntity) { $segments = $newsletterEntity->getSegmentIds(); $taskModel = $task->task(); $taskEntity = $this->scheduledTasksRepository->findOneById($taskModel->id); if ($taskEntity instanceof ScheduledTaskEntity) { $this->subscribersFinder->addSubscribersToTaskFromSegments($taskEntity, $segments); } } // update current queue $task->updateCount(); $task->status = null; $task->save(); // update newsletter status $newsletter->setStatus(Newsletter::STATUS_SENDING); $newsletterEntity && $this->newslettersRepository->refresh($newsletterEntity); $this->updateScheduledTaskEntity($task); return true; } private function processReEngagementEmail($queue) { $queue->status = null; $queue->save(); $this->updateScheduledTaskEntity($queue); return true; } public function verifyMailpoetSubscriber($subscriberId, $newsletter, $queue) { $subscriber = Subscriber::findOne($subscriberId); // check if subscriber is in proper segment $subscriberInSegment = SubscriberSegment::where('subscriber_id', $subscriberId) ->where('segment_id', $newsletter->segment) ->where('status', 'subscribed') ->findOne(); if (!$subscriber || !$subscriberInSegment) { $queue->delete(); return false; } return $this->verifySubscriber($subscriber, $queue); } public function verifyWPSubscriber($subscriberId, $newsletter, $queue) { // check if user has the proper role $subscriber = Subscriber::findOne($subscriberId); if (!$subscriber || $subscriber->isWPUser() === false) { $queue->delete(); return false; } $wpUser = get_userdata($subscriber->wpUserId); if ($wpUser === false) { $queue->delete(); return false; } if ( $newsletter->role !== WelcomeScheduler::WORDPRESS_ALL_ROLES && !in_array($newsletter->role, ((array)$wpUser)['roles']) ) { $queue->delete(); return false; } return $this->verifySubscriber($subscriber, $queue); } public function verifySubscriber($subscriber, $queue) { $newsletter = $queue->newsletterId ? $this->newslettersRepository->findOneById($queue->newsletterId) : null; if ($newsletter && $newsletter->getType() === NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL) { return $subscriber->status !== Subscriber::STATUS_BOUNCED; } if ($subscriber->status === Subscriber::STATUS_UNCONFIRMED) { // reschedule delivery $task = $this->scheduledTasksRepository->findOneById($queue->task()->id); if ($task instanceof ScheduledTaskEntity) { $this->cronWorkerScheduler->rescheduleProgressively($task); } return false; } else if ($subscriber->status === Subscriber::STATUS_UNSUBSCRIBED) { $queue->delete(); return false; } return true; } public function deleteQueueOrUpdateNextRunDate($queue, $newsletter) { if ($newsletter->intervalType === PostNotificationScheduler::INTERVAL_IMMEDIATELY) { $queue->delete(); $this->updateScheduledTaskEntity($queue, true); return; } else { $nextRunDate = $this->scheduler->getNextRunDate($newsletter->schedule); if (!$nextRunDate) { $queue->delete(); $this->updateScheduledTaskEntity($queue, true); return; } $queue->scheduledAt = $nextRunDate; $queue->save(); $this->updateScheduledTaskEntity($queue); } } private function updateScheduledTaskEntity(SendingTask $queue, bool $hasBeenDeleted = false) { $taskModel = $queue->task(); if (!$taskModel instanceof ScheduledTask) { return; } $taskEntity = $this->scheduledTasksRepository->findOneById($taskModel->id); if (!$taskEntity instanceof ScheduledTaskEntity) { return; } $hasBeenDeleted ? $this->scheduledTasksRepository->detach($taskEntity) : $this->scheduledTasksRepository->refresh($taskEntity); } public function createPostNotificationHistory(NewsletterEntity $newsletter): NewsletterEntity { // clone newsletter $notificationHistory = clone $newsletter; $notificationHistory->setParent($newsletter); $notificationHistory->setType(NewsletterEntity::TYPE_NOTIFICATION_HISTORY); $notificationHistory->setStatus(NewsletterEntity::STATUS_SENDING); $notificationHistory->setUnsubscribeToken($this->security->generateUnsubscribeTokenByEntity($notificationHistory)); // reset timestamps $createdAt = Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); $notificationHistory->setCreatedAt($createdAt); $notificationHistory->setUpdatedAt($createdAt); $notificationHistory->setDeletedAt(null); // reset hash $notificationHistory->setHash(Security::generateHash()); $this->newslettersRepository->persist($notificationHistory); $this->newslettersRepository->flush(); // create relationships between notification history and segments foreach ($newsletter->getNewsletterSegments() as $newsletterSegment) { $segment = $newsletterSegment->getSegment(); if (!$segment) { continue; } $duplicateSegment = new NewsletterSegmentEntity($notificationHistory, $segment); $notificationHistory->getNewsletterSegments()->add($duplicateSegment); $this->newsletterSegmentRepository->persist($duplicateSegment); } $this->newslettersRepository->flush(); return $notificationHistory; } /** * @param ScheduledTaskEntity[] $scheduledTasks */ private function updateTasks(array $scheduledTasks): void { $ids = array_map(function (ScheduledTaskEntity $scheduledTask): ?int { return $scheduledTask->getId(); }, $scheduledTasks); $ids = array_filter($ids); $this->scheduledTasksRepository->touchAllByIds($ids); } /** * @return ScheduledTaskEntity[] */ public function getScheduledSendingTasks(): array { return $this->scheduledTasksRepository->findScheduledSendingTasks(self::TASK_BATCH_SIZE); } } SubscribersLastEngagement.php000064400000005513150514627470012401 0ustar00entityManager = $entityManager; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer): bool { $meta = $task->getMeta(); $minId = $meta['nextId'] ?? 1; $highestId = $this->getHighestSubscriberId(); while ($minId <= $highestId) { $maxId = $minId + self::BATCH_SIZE; $this->processBatch($minId, $maxId); $task->setMeta(['nextId' => $maxId]); $this->scheduledTasksRepository->persist($task); $this->scheduledTasksRepository->flush(); $this->cronHelper->enforceExecutionLimit($timer); // Throws exception and interrupts process if over execution limit $minId = $maxId; } return true; } private function processBatch(int $minSubscriberId, int $maxSubscriberId): void { $statisticsClicksTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName(); $statisticsOpensTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName(); $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $query = " UPDATE $subscribersTable as mps LEFT JOIN (SELECT max(created_at) as created_at, subscriber_id FROM $statisticsOpensTable as mpsoinner GROUP BY mpsoinner.subscriber_id) as mpso ON mpso.subscriber_id = mps.id LEFT JOIN (SELECT max(created_at) as created_at, subscriber_id FROM $statisticsClicksTable as mpscinner GROUP BY mpscinner.subscriber_id) as mpsc ON mpsc.subscriber_id = mps.id SET mps.last_engagement_at = NULLIF(GREATEST(COALESCE(mpso.created_at, '0'), COALESCE(mpsc.created_at, '0')), '0') WHERE mps.last_engagement_at IS NULL AND mps.id >= $minSubscriberId AND mps.id < $maxSubscriberId; "; $this->entityManager->getConnection()->executeStatement($query); } private function getHighestSubscriberId(): int { $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $result = $this->entityManager->getConnection()->executeQuery("SELECT MAX(id) FROM $subscribersTable LIMIT 1;")->fetchNumeric(); return is_array($result) && isset($result[0]) ? (int)$result[0] : 0; } } NewsletterTemplateThumbnails.php000064400000001465150514627470013155 0ustar00thumbnailSaver = $thumbnailSaver; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $this->thumbnailSaver->ensureTemplateThumbnailsForAll(); return true; } } ExportFilesCleanup.php000064400000001713150514627470011046 0ustar00getPathname(); $created = $file->getMTime(); $now = new Carbon(); if (Carbon::createFromTimestamp((int)$created)->lessThan($now->subDays(self::DELETE_FILES_AFTER_X_DAYS))) { unlink($name); }; } return true; } } KeyCheck/SendingServiceKeyCheck.php000064400000004210150514627470013272 0ustar00settings = $settings; $this->servicesChecker = $servicesChecker; parent::__construct($cronWorkerScheduler); } public function checkProcessingRequirements() { return Bridge::isMPSendingServiceEnabled(); } /** * @return \DateTimeInterface|Carbon */ public function getNextRunDate() { // when key pending approval, check key sate every hour if ($this->servicesChecker->isMailPoetAPIKeyPendingApproval()) { $date = Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); return $date->addHour(); } return parent::getNextRunDate(); } public function checkKey() { // for phpstan because we set bridge property in the init function if (!$this->bridge) { throw new InvalidStateException('The class was not initialized properly. Please call the Init method before.'); }; $wasPendingApproval = $this->servicesChecker->isMailPoetAPIKeyPendingApproval(); $mssKey = $this->settings->get(Mailer::MAILER_CONFIG_SETTING_NAME)['mailpoet_api_key']; $result = $this->bridge->checkMSSKey($mssKey); $this->bridge->storeMSSKeyAndState($mssKey, $result); $isPendingApproval = $this->servicesChecker->isMailPoetAPIKeyPendingApproval(); if ($wasPendingApproval && !$isPendingApproval) { MailerLog::resumeSending(); } return $result; } } KeyCheck/KeyCheckWorker.php000064400000003027150514627470011640 0ustar00cronWorkerScheduler = $cronWorkerScheduler; } public function init() { if (!$this->bridge) { $this->bridge = new Bridge(); } } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { try { $result = $this->checkKey(); } catch (\Exception $e) { $result = false; } if (empty($result['code']) || $result['code'] == Bridge::CHECK_ERROR_UNAVAILABLE) { $this->cronWorkerScheduler->rescheduleProgressively($task); return false; } return true; } public function getNextRunDate() { $date = Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); return $date->startOfDay() ->addDay() ->addHours(rand(0, 5)) ->addMinutes(rand(0, 59)) ->addSeconds(rand(0, 59)); } public abstract function checkKey(); } KeyCheck/index.php000064400000000006150514627470010061 0ustar00settings = $settings; parent::__construct($cronWorkerScheduler); } public function checkProcessingRequirements() { return Bridge::isPremiumKeySpecified(); } public function checkKey() { // for phpstan because we set bridge property in the init function if (!$this->bridge) { throw new InvalidStateException('The class was not initialized properly. Please call the Init method before.'); }; $premiumKey = $this->settings->get(Bridge::PREMIUM_KEY_SETTING_NAME); $result = $this->bridge->checkPremiumKey($premiumKey); $this->bridge->storePremiumKeyAndState($premiumKey, $result); return $result; } } UnsubscribeTokens.php000064400000004035150514627470010742 0ustar00getMeta(); if (!isset($meta['last_subscriber_id'])) { $meta['last_subscriber_id'] = 0; } do { $this->cronHelper->enforceExecutionLimit($timer); $subscribersCount = $this->addTokens(Subscriber::class, $meta['last_subscriber_id']); $task->setMeta($meta); $this->scheduledTasksRepository->persist($task); $this->scheduledTasksRepository->flush(); } while ($subscribersCount === self::BATCH_SIZE); do { $this->cronHelper->enforceExecutionLimit($timer); $newslettersCount = $this->addTokens(Newsletter::class, $meta['last_newsletter_id']); $task->setMeta($meta); $this->scheduledTasksRepository->persist($task); $this->scheduledTasksRepository->flush(); } while ($newslettersCount === self::BATCH_SIZE); if ($subscribersCount > 0 || $newslettersCount > 0) { return false; } return true; } private function addTokens($model, &$lastProcessedId = 0) { $instances = $model::whereNull('unsubscribe_token') ->whereGt('id', (int)$lastProcessedId) ->orderByAsc('id') ->limit(self::BATCH_SIZE) ->findMany(); foreach ($instances as $instance) { $lastProcessedId = $instance->id; $instance->set('unsubscribe_token', Security::generateUnsubscribeToken($model)); $instance->save(); } return count($instances); } public function getNextRunDate() { return Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); } } Bounce.php000064400000013325150514627470006507 0ustar00settings = $settings; $this->bridge = $bridge; parent::__construct(); $this->subscribersRepository = $subscribersRepository; $this->sendingQueuesRepository = $sendingQueuesRepository; $this->statisticsBouncesRepository = $statisticsBouncesRepository; $this->scheduledTaskSubscribersRepository = $scheduledTaskSubscribersRepository; } public function init() { if (!$this->api) { $this->api = new API($this->settings->get(Mailer::MAILER_CONFIG_SETTING_NAME)['mailpoet_api_key']); } } public function checkProcessingRequirements() { return $this->bridge->isMailpoetSendingServiceEnabled(); } public function prepareTaskStrategy(ScheduledTaskEntity $task, $timer) { $this->scheduledTaskSubscribersRepository->createSubscribersForBounceWorker($task); if (!$this->scheduledTaskSubscribersRepository->countBy(['task' => $task, 'processed' => ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED])) { $this->scheduledTaskSubscribersRepository->deleteByScheduledTask($task); return false; } return true; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $subscriberBatches = new BatchIterator($task->getId(), self::BATCH_SIZE); if (count($subscriberBatches) === 0) { $this->scheduledTaskSubscribersRepository->deleteByScheduledTask($task); return true; // mark completed } /** @var int[] $subscribersToProcessIds - it's required for PHPStan */ foreach ($subscriberBatches as $subscribersToProcessIds) { // abort if execution limit is reached $this->cronHelper->enforceExecutionLimit($timer); $subscriberEmails = $this->subscribersRepository->getUndeletedSubscribersEmailsByIds($subscribersToProcessIds); $subscriberEmails = array_column($subscriberEmails, 'email'); $this->processEmails($task, $subscriberEmails); $this->scheduledTaskSubscribersRepository->updateProcessedSubscribers($task, $subscribersToProcessIds); } return true; } public function processEmails(ScheduledTaskEntity $task, array $subscriberEmails) { $checkedEmails = $this->api->checkBounces($subscriberEmails); $this->processApiResponse($task, (array)$checkedEmails); } public function processApiResponse(ScheduledTaskEntity $task, array $checkedEmails) { $previousTask = $this->findPreviousTask($task); foreach ($checkedEmails as $email) { if (!isset($email['address'], $email['bounce'])) { continue; } if ($email['bounce'] === self::BOUNCED_HARD) { $subscriber = $this->subscribersRepository->findOneBy(['email' => $email['address']]); if (!$subscriber instanceof SubscriberEntity) continue; $subscriber->setStatus(SubscriberEntity::STATUS_BOUNCED); $this->saveBouncedStatistics($subscriber, $task, $previousTask); } } $this->subscribersRepository->flush(); } public function getNextRunDate() { $date = Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); return $date->startOfDay() ->addDay() ->addHours(rand(0, 5)) ->addMinutes(rand(0, 59)) ->addSeconds(rand(0, 59)); } private function findPreviousTask(ScheduledTaskEntity $task): ?ScheduledTaskEntity { return $this->scheduledTasksRepository->findPreviousTask($task); } private function saveBouncedStatistics(SubscriberEntity $subscriber, ScheduledTaskEntity $task, ?ScheduledTaskEntity $previousTask): void { $dateFrom = null; if ($previousTask instanceof ScheduledTaskEntity) { $dateFrom = $previousTask->getScheduledAt(); } $queues = $this->sendingQueuesRepository->findAllForSubscriberSentBetween($subscriber, $task->getScheduledAt(), $dateFrom); foreach ($queues as $queue) { $newsletter = $queue->getNewsletter(); if ($newsletter instanceof NewsletterEntity) { $statistics = new StatisticsBounceEntity($newsletter, $queue, $subscriber); $this->statisticsBouncesRepository->persist($statistics); } } } } InactiveSubscribers.php000064400000005573150514627470011253 0ustar00inactiveSubscribersController = $inactiveSubscribersController; $this->settings = $settings; $this->trackingConfig = $trackingConfig; $this->subscribersRepository = $subscribersRepository; parent::__construct(); } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { if (!$this->trackingConfig->isEmailTrackingEnabled()) { $this->schedule(); return true; } $daysToInactive = (int)$this->settings->get('deactivate_subscriber_after_inactive_days'); // Activate all inactive subscribers in case the feature is turned off if ($daysToInactive === 0) { $this->inactiveSubscribersController->reactivateInactiveSubscribers(); $this->schedule(); return true; } // Handle activation/deactivation within interval $meta = $task->getMeta(); $lastSubscriberId = isset($meta['last_subscriber_id']) ? $meta['last_subscriber_id'] : 0; if (isset($meta['max_subscriber_id'])) { $maxSubscriberId = $meta['max_subscriber_id']; } else { $maxSubscriberId = $this->subscribersRepository->getMaxSubscriberId(); } while ($lastSubscriberId <= $maxSubscriberId) { $count = $this->inactiveSubscribersController->markInactiveSubscribers($daysToInactive, self::BATCH_SIZE, $lastSubscriberId); if ($count === false) { break; } $lastSubscriberId += self::BATCH_SIZE; $task->setMeta(['last_subscriber_id' => $lastSubscriberId]); $this->scheduledTasksRepository->persist($task); $this->scheduledTasksRepository->flush(); $this->cronHelper->enforceExecutionLimit($timer); }; while ($this->inactiveSubscribersController->markActiveSubscribers($daysToInactive, self::BATCH_SIZE) === self::BATCH_SIZE) { $this->cronHelper->enforceExecutionLimit($timer); }; $this->schedule(); return true; } } index.php000064400000000006150514627470006373 0ustar00settings = $settings; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { return $this->setLastAnnouncementDate(); } public function setLastAnnouncementDate() { $response = $this->wp->wpRemoteGet(self::API_URL . '/posts?published=true&maxResults=1', [ 'headers' => [ 'Beamer-Api-Key' => self::API_KEY, ], ]); $posts = $this->wp->wpRemoteRetrieveBody($response); if (empty($posts)) return false; $posts = json_decode($posts); if (empty($posts) || empty($posts[0]->date)) return false; $this->settings->set('last_announcement_date', Carbon::createFromTimeString($posts[0]->date)->getTimestamp()); return true; } public function getNextRunDate() { // once every two weeks on a random day of the week, random time of the day $date = Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); return $date ->next(Carbon::MONDAY) ->startOfDay() ->addDays(rand(7, 13)) ->addHours(rand(0, 23)) ->addMinutes(rand(0, 59)); } } AuthorizedSendingEmailsCheck.php000064400000001744150514627470013015 0ustar00authorizedEmailsController = $authorizedEmailsController; parent::__construct(); } public function checkProcessingRequirements() { return Bridge::isMPSendingServiceEnabled(); } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $this->authorizedEmailsController->checkAuthorizedEmailAddresses(); return true; } } Automations/index.php000064400000000006150514627470010676 0ustar00automationStorage = $automationStorage; } public function checkProcessingRequirements() { return true; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $productIds = $task->getMeta()['product_ids'] ?? []; $automationId = $task->getMeta()['automation_id'] ?? 0; $automationVersion = $task->getMeta()['automation_version'] ?? 0; if (!$productIds || !$automationId || !$automationVersion) { return true; } $lastActivityAt = $task->getCreatedAt(); $subscribers = $task->getSubscribers(); if ($subscribers->count() !== 1) { return false; } $subscriber = isset($subscribers[0]) ? $subscribers[0]->getSubscriber() : null; if (!$subscriber) { return false; } $automation = $this->automationStorage->getAutomation((int)$automationId, (int)$automationVersion); if (!$automation || $automation->getStatus() !== Automation::STATUS_ACTIVE) { return false; } $this->wp->doAction( self::ACTION, $subscriber, $productIds, $lastActivityAt ); return true; } } StatsNotifications/Worker.php000064400000020144150514627470012372 0ustar00renderer = $renderer; $this->mailerFactory = $mailerFactory; $this->settings = $settings; $this->cronHelper = $cronHelper; $this->mailerMetaInfo = $mailerMetaInfo; $this->repository = $repository; $this->entityManager = $entityManager; $this->newsletterLinkRepository = $newsletterLinkRepository; $this->newsletterStatisticsRepository = $newsletterStatisticsRepository; $this->subscribersFeature = $subscribersFeature; $this->subscribersRepository = $subscribersRepository; $this->servicesChecker = $servicesChecker; } /** @throws \Exception */ public function process($timer = false) { $timer = $timer ?: microtime(true); $settings = $this->settings->get(self::SETTINGS_KEY); // Cleanup potential orphaned task created due bug MAILPOET-3015 $this->repository->deleteOrphanedScheduledTasks(); foreach ($this->repository->findScheduled(Sending::RESULT_BATCH_SIZE) as $statsNotificationEntity) { try { $extraParams = [ 'meta' => $this->mailerMetaInfo->getStatsNotificationMetaInfo(), ]; $this->mailerFactory->getDefaultMailer()->send($this->constructNewsletter($statsNotificationEntity), $settings['address'], $extraParams); } catch (\Exception $e) { if (WP_DEBUG) { throw $e; } } finally { $task = $statsNotificationEntity->getTask(); if ($task instanceof ScheduledTaskEntity) { $this->markTaskAsFinished($task); } } $this->cronHelper->enforceExecutionLimit($timer); } } private function constructNewsletter(StatsNotificationEntity $statsNotificationEntity) { $newsletter = $statsNotificationEntity->getNewsletter(); if (!$newsletter instanceof NewsletterEntity) { throw new \RuntimeException('Missing newsletter entity for statistic notification.'); } $link = $this->newsletterLinkRepository->findTopLinkForNewsletter((int)$newsletter->getId()); $sendingQueue = $newsletter->getLatestQueue(); if (!$sendingQueue instanceof SendingQueueEntity) { throw new \RuntimeException('Missing sending queue entity for statistic notification.'); } $context = $this->prepareContext($newsletter, $sendingQueue, $link); $subject = $sendingQueue->getNewsletterRenderedSubject(); return [ // translators: %s is the subject of the email. 'subject' => sprintf(_x('Stats for email %s', 'title of an automatic email containing statistics (newsletter open rate, click rate, etc)', 'mailpoet'), $subject), 'body' => [ 'html' => $this->renderer->render('emails/statsNotification.html', $context), 'text' => $this->renderer->render('emails/statsNotification.txt', $context), ], ]; } private function prepareContext(NewsletterEntity $newsletter, SendingQueueEntity $sendingQueue, NewsletterLinkEntity $link = null) { $statistics = $this->newsletterStatisticsRepository->getStatistics($newsletter); $clicked = ($statistics->getClickCount() * 100) / $statistics->getTotalSentCount(); $opened = ($statistics->getOpenCount() * 100) / $statistics->getTotalSentCount(); $machineOpened = ($statistics->getMachineOpenCount() * 100) / $statistics->getTotalSentCount(); $unsubscribed = ($statistics->getUnsubscribeCount() * 100) / $statistics->getTotalSentCount(); $bounced = ($statistics->getBounceCount() * 100) / $statistics->getTotalSentCount(); $subject = $sendingQueue->getNewsletterRenderedSubject(); $subscribersCount = $this->subscribersRepository->getTotalSubscribers(); $hasValidApiKey = $this->subscribersFeature->hasValidApiKey(); $context = [ 'subject' => $subject, // translators: %1$s is the percentage of clicks, %2$s the percentage of opens and %3$s the number of unsubscribes. 'preheader' => sprintf(_x( '%1$s%% clicks, %2$s%% opens, %3$s%% unsubscribes in a nutshell.', 'newsletter open rate, click rate and unsubscribe rate', 'mailpoet'), number_format($clicked, 2), number_format($opened, 2), number_format($unsubscribed, 2) ), 'topLinkClicks' => 0, 'linkSettings' => WPFunctions::get()->getSiteUrl(null, '/wp-admin/admin.php?page=mailpoet-settings#basics'), 'linkStats' => WPFunctions::get()->getSiteUrl(null, '/wp-admin/admin.php?page=mailpoet-newsletters&stats=' . $newsletter->getId()), 'clicked' => $clicked, 'opened' => $opened, 'machineOpened' => $machineOpened, 'unsubscribed' => $unsubscribed, 'bounced' => $bounced, 'subscribersLimitReached' => $this->subscribersFeature->check(), 'hasValidApiKey' => $hasValidApiKey, 'subscribersLimit' => $this->subscribersFeature->getSubscribersLimit(), 'upgradeNowLink' => $hasValidApiKey ? 'https://account.mailpoet.com/orders/upgrade/' . $this->servicesChecker->generatePartialApiKey() : 'https://account.mailpoet.com/?s=' . ($subscribersCount + 1), ]; if ($link) { $context['topLinkClicks'] = $link->getTotalClicksCount(); $mappings = self::getShortcodeLinksMapping(); $context['topLink'] = isset($mappings[$link->getUrl()]) ? $mappings[$link->getUrl()] : $link->getUrl(); } return $context; } private function markTaskAsFinished(ScheduledTaskEntity $task) { $task->setStatus(ScheduledTaskEntity::STATUS_COMPLETED); $task->setProcessedAt(new Carbon); $task->setScheduledAt(null); $this->entityManager->flush(); } public static function getShortcodeLinksMapping() { return [ NewsletterLinkEntity::UNSUBSCRIBE_LINK_SHORT_CODE => __('Unsubscribe link', 'mailpoet'), NewsletterLinkEntity::INSTANT_UNSUBSCRIBE_LINK_SHORT_CODE => __('Unsubscribe link (without confirmation)', 'mailpoet'), '[link:subscription_manage_url]' => __('Manage subscription link', 'mailpoet'), '[link:newsletter_view_in_browser_url]' => __('View in browser link', 'mailpoet'), ]; } } StatsNotifications/StatsNotificationsRepository.php000064400000004504150514627470017053 0ustar00 */ class StatsNotificationsRepository extends Repository { protected function getEntityClassName() { return StatsNotificationEntity::class; } /** * @param int $newsletterId * @return StatsNotificationEntity|null */ public function findOneByNewsletterId($newsletterId) { return $this->doctrineRepository ->createQueryBuilder('sn') ->andWhere('sn.newsletter = :newsletterId') ->setParameter('newsletterId', $newsletterId) ->setMaxResults(1) ->getQuery() ->getOneOrNullResult(); } /** * @param int|null $limit * @return StatsNotificationEntity[] */ public function findScheduled($limit = null) { $date = new Carbon(); $query = $this->doctrineRepository ->createQueryBuilder('sn') ->join('sn.task', 'tasks') ->join('sn.newsletter', 'n') ->addSelect('tasks') ->addSelect('n') ->addOrderBy('tasks.priority') ->addOrderBy('tasks.updatedAt') ->where('tasks.deletedAt IS NULL') ->andWhere('tasks.status = :status') ->setParameter('status', ScheduledTaskEntity::STATUS_SCHEDULED) ->andWhere('tasks.scheduledAt < :date') ->setParameter('date', $date) ->andWhere('tasks.type = :workerType') ->setParameter('workerType', Worker::TASK_TYPE); if (is_int($limit)) { $query->setMaxResults($limit); } return $query->getQuery()->getResult(); } public function deleteOrphanedScheduledTasks() { $scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName(); $statsNotificationsTable = $this->entityManager->getClassMetadata(StatsNotificationEntity::class)->getTableName(); $this->entityManager->getConnection()->executeStatement(" DELETE st FROM $scheduledTasksTable st LEFT JOIN $statsNotificationsTable sn ON sn.task_id = st.id WHERE sn.id IS NULL AND st.type = :taskType; ", ['taskType' => Worker::TASK_TYPE]); } } StatsNotifications/NewsletterLinkRepository.php000064400000002664150514627470016202 0ustar00 */ class NewsletterLinkRepository extends Repository { protected function getEntityClassName() { return NewsletterLinkEntity::class; } /** * @param int $newsletterId * @return NewsletterLinkEntity|null */ public function findTopLinkForNewsletter($newsletterId) { $statisticsClicksTable = $this->entityManager->getClassMetadata(StatisticsClickEntity::class)->getTableName(); $topIdQuery = $this->entityManager->getConnection()->createQueryBuilder() ->select('c.link_id') ->addSelect('count(c.id) AS counter') ->from($statisticsClicksTable, 'c') ->where('c.newsletter_id = :newsletterId') ->setParameter('newsletterId', $newsletterId) ->groupBy('c.link_id') ->orderBy('counter', 'desc') ->setMaxResults(1) ->execute(); if (!$topIdQuery instanceof Statement) { return null; } $topId = $topIdQuery->fetch(); if (is_array($topId) && isset($topId['link_id'])) { return $this->findOneById((int)$topId['link_id']); } return null; } } StatsNotifications/Scheduler.php000064400000005777150514627470013056 0ustar00settings = $settings; $this->entityManager = $entityManager; $this->repository = $repository; $this->trackingConfig = $trackingConfig; } public function schedule(NewsletterEntity $newsletter) { if (!$this->shouldSchedule($newsletter)) { return false; } $task = new ScheduledTaskEntity(); $task->setType(Worker::TASK_TYPE); $task->setStatus(ScheduledTaskEntity::STATUS_SCHEDULED); $task->setScheduledAt($this->getNextRunDate()); $this->entityManager->persist($task); $this->entityManager->flush(); $statsNotifications = new StatsNotificationEntity($newsletter, $task); $this->entityManager->persist($statsNotifications); $this->entityManager->flush(); } private function shouldSchedule(NewsletterEntity $newsletter) { if ($this->isDisabled()) { return false; } if (!in_array($newsletter->getType(), $this->supportedTypes)) { return false; } if ($this->hasTaskBeenScheduled($newsletter->getId())) { return false; } return true; } private function isDisabled() { $settings = $this->settings->get(Worker::SETTINGS_KEY); if (!is_array($settings)) { return true; } if (!isset($settings['enabled'])) { return true; } if (!isset($settings['address'])) { return true; } if (empty(trim($settings['address']))) { return true; } if (!$this->trackingConfig->isEmailTrackingEnabled()) { return true; } return !(bool)$settings['enabled']; } private function hasTaskBeenScheduled($newsletterId) { $existing = $this->repository->findOneByNewsletterId($newsletterId); return $existing instanceof StatsNotificationEntity; } private function getNextRunDate() { $date = new Carbon(); $date->addHours(self::HOURS_TO_SEND_AFTER_NEWSLETTER); return $date; } } StatsNotifications/index.php000064400000000006150514627470012223 0ustar00mailerFactory = $mailerFactory; $this->settings = $settings; $this->renderer = $renderer; $this->mailerMetaInfo = $mailerMetaInfo; $this->repository = $repository; $this->newsletterStatisticsRepository = $newsletterStatisticsRepository; $this->trackingConfig = $trackingConfig; } public function checkProcessingRequirements() { $settings = $this->settings->get(Worker::SETTINGS_KEY); if (!is_array($settings)) { return false; } if (!isset($settings['automated'])) { return false; } if (!isset($settings['address'])) { return false; } if (empty(trim($settings['address']))) { return false; } if (!$this->trackingConfig->isEmailTrackingEnabled()) { return false; } return (bool)$settings['automated']; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { try { $settings = $this->settings->get(Worker::SETTINGS_KEY); $newsletters = $this->getNewsletters(); if ($newsletters) { $extraParams = [ 'meta' => $this->mailerMetaInfo->getStatsNotificationMetaInfo(), ]; $this->mailerFactory->getDefaultMailer()->send($this->constructNewsletter($newsletters), $settings['address'], $extraParams); } } catch (\Exception $e) { if (WP_DEBUG) { throw $e; } } return true; } /** * @param array $newsletters */ private function constructNewsletter(array $newsletters): array { $context = $this->prepareContext($newsletters); return [ 'subject' => __('Your monthly stats are in!', 'mailpoet'), 'body' => [ 'html' => $this->renderer->render('emails/statsNotificationAutomatedEmails.html', $context), 'text' => $this->renderer->render('emails/statsNotificationAutomatedEmails.txt', $context), ], ]; } /** * @return array */ protected function getNewsletters(): array { $result = []; $newsletters = $this->repository->findActiveByTypes( [NewsletterEntity::TYPE_AUTOMATIC, NewsletterEntity::TYPE_WELCOME] ); foreach ($newsletters as $newsletter) { $statistics = $this->newsletterStatisticsRepository->getStatistics($newsletter); if ($statistics->getTotalSentCount()) { $result[] = [ 'statistics' => $statistics, 'newsletter' => $newsletter, ]; } } return $result; } /** * @param array $newsletters * @return array */ private function prepareContext(array $newsletters): array { $context = [ 'linkSettings' => WPFunctions::get()->getSiteUrl(null, '/wp-admin/admin.php?page=mailpoet-settings#basics'), 'newsletters' => [], ]; foreach ($newsletters as $row) { $statistics = $row['statistics']; $newsletter = $row['newsletter']; $clicked = ($statistics->getClickCount() * 100) / $statistics->getTotalSentCount(); $opened = ($statistics->getOpenCount() * 100) / $statistics->getTotalSentCount(); $machineOpened = ($statistics->getMachineOpenCount() * 100) / $statistics->getTotalSentCount(); $unsubscribed = ($statistics->getUnsubscribeCount() * 100) / $statistics->getTotalSentCount(); $bounced = ($statistics->getBounceCount() * 100) / $statistics->getTotalSentCount(); $context['newsletters'][] = [ 'linkStats' => WPFunctions::get()->getSiteUrl(null, '/wp-admin/admin.php?page=mailpoet-newsletters#/stats/' . $newsletter->getId()), 'clicked' => $clicked, 'opened' => $opened, 'machineOpened' => $machineOpened, 'unsubscribed' => $unsubscribed, 'bounced' => $bounced, 'subject' => $newsletter->getSubject(), ]; } return $context; } public function getNextRunDate() { $wp = new WPFunctions; $date = Carbon::createFromTimestamp($wp->currentTime('timestamp')); return $date->endOfMonth()->next(Carbon::MONDAY)->midDay(); } } SubscriberLinkTokens.php000064400000003432150514627470011377 0ustar00get(EntityManager::class); $subscribersRepository = ContainerWrapper::getInstance()->get(SubscribersRepository::class); $subscribersTable = $entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $connection = $entityManager->getConnection(); $count = $subscribersRepository->countBy(['linkToken' => null]); if ($count) { $authKey = defined('AUTH_KEY') ? AUTH_KEY : ''; $connection->executeStatement( "UPDATE {$subscribersTable} SET link_token = SUBSTRING(MD5(CONCAT(:authKey, email)), 1, :tokenLength) WHERE link_token IS NULL LIMIT :limit", ['authKey' => $authKey, 'tokenLength' => SubscriberEntity::OBSOLETE_LINK_TOKEN_LENGTH, 'limit' => self::BATCH_SIZE], ['authKey' => \PDO::PARAM_STR, 'tokenLength' => \PDO::PARAM_INT, 'limit' => \PDO::PARAM_INT] ); $this->schedule(); } return true; } public function getNextRunDate() { $wp = new WPFunctions(); return Carbon::createFromTimestamp($wp->currentTime('timestamp')); } } SubscribersEngagementScore.php000064400000004575150514627470012560 0ustar00segmentsRepository = $segmentsRepository; $this->statisticsOpensRepository = $statisticsOpensRepository; $this->subscribersRepository = $subscribersRepository; } public function processTaskStrategy(ScheduledTaskEntity $task, $timer) { $recalculatedSubscribersCount = $this->recalculateSubscribers(); if ($recalculatedSubscribersCount > 0) { $this->scheduleImmediately(); return true; } $recalculatedSegmentsCount = $this->recalculateSegments(); if ($recalculatedSegmentsCount > 0) { $this->scheduleImmediately(); return true; } $this->schedule(); return true; } private function recalculateSubscribers(): int { $subscribers = $this->subscribersRepository->findByUpdatedScoreNotInLastMonth(self::BATCH_SIZE); foreach ($subscribers as $subscriber) { $this->statisticsOpensRepository->recalculateSubscriberScore($subscriber); } return count($subscribers); } private function recalculateSegments(): int { $segments = $this->segmentsRepository->findByUpdatedScoreNotInLastDay(self::BATCH_SIZE); foreach ($segments as $segment) { $this->statisticsOpensRepository->recalculateSegmentScore($segment); } return count($segments); } public function getNextRunDate() { // random day of the next week $date = Carbon::createFromTimestamp($this->wp->currentTime('timestamp')); $date->addDay(); $date->setTime(mt_rand(0, 23), mt_rand(0, 59)); return $date; } }