Editor
2 months ago
Embed
1 month ago
Links
2 months ago
Listing
9 months ago
Options
2 years ago
Preview
1 month ago
Renderer
4 days ago
RestApi
1 week ago
Scheduler
4 days ago
Segment
1 year ago
Sending
1 week ago
Sharing
1 month ago
Shortcodes
2 days ago
Statistics
1 week ago
ViewInBrowser
1 month ago
ApiDataSanitizer.php
3 years ago
AutomatedLatestContent.php
2 months ago
AutomaticEmailsRepository.php
3 years ago
BlockPostQuery.php
2 months ago
BulkActionController.php
1 month ago
BulkActionException.php
1 month ago
DynamicProducts.php
11 months ago
NewsletterCoupon.php
2 months ago
NewsletterDeleteController.php
2 years ago
NewsletterHtmlSanitizer.php
2 years ago
NewsletterPostsRepository.php
2 years ago
NewsletterResendController.php
4 days ago
NewsletterSaveController.php
1 month ago
NewsletterValidator.php
1 year ago
NewslettersRepository.php
2 weeks ago
StatusController.php
1 month ago
Url.php
2 months ago
index.php
3 years ago
NewsletterResendController.php
329 lines
| 1 | <?php declare(strict_types = 1); |
| 2 | |
| 3 | namespace MailPoet\Newsletter; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use MailPoet\Cron\ActionScheduler\Actions\DaemonTrigger; |
| 9 | use MailPoet\Cron\CronTrigger; |
| 10 | use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker; |
| 11 | use MailPoet\Entities\NewsletterEntity; |
| 12 | use MailPoet\Entities\ScheduledTaskEntity; |
| 13 | use MailPoet\Entities\ScheduledTaskSubscriberEntity; |
| 14 | use MailPoet\Entities\SendingQueueEntity; |
| 15 | use MailPoet\Entities\StatisticsNewsletterEntity; |
| 16 | use MailPoet\Entities\StatisticsOpenEntity; |
| 17 | use MailPoet\Entities\SubscriberEntity; |
| 18 | use MailPoet\Mailer\MailerFactory; |
| 19 | use MailPoet\Newsletter\Sending\ScheduledTasksRepository; |
| 20 | use MailPoet\Newsletter\Sending\SendingQueuesRepository; |
| 21 | use MailPoet\Settings\SettingsController; |
| 22 | use MailPoet\Settings\TrackingConfig; |
| 23 | use MailPoet\Subscribers\TrackingConsentController; |
| 24 | use MailPoet\UnexpectedValueException; |
| 25 | use MailPoet\Util\License\Features\Subscribers as SubscribersFeature; |
| 26 | use MailPoet\WP\Functions as WPFunctions; |
| 27 | use MailPoetVendor\Carbon\Carbon; |
| 28 | use MailPoetVendor\Doctrine\DBAL\ArrayParameterType; |
| 29 | use MailPoetVendor\Doctrine\DBAL\ParameterType; |
| 30 | use MailPoetVendor\Doctrine\ORM\EntityManager; |
| 31 | |
| 32 | class NewsletterResendController { |
| 33 | const MIN_RESEND_DELAY_HOURS = 24; |
| 34 | const MAX_RESEND_DELAY_HOURS = 72; |
| 35 | |
| 36 | /** @var NewsletterSaveController */ |
| 37 | private $newsletterSaveController; |
| 38 | |
| 39 | /** @var NewsletterDeleteController */ |
| 40 | private $newsletterDeleteController; |
| 41 | |
| 42 | /** @var NewslettersRepository */ |
| 43 | private $newslettersRepository; |
| 44 | |
| 45 | /** @var EntityManager */ |
| 46 | private $entityManager; |
| 47 | |
| 48 | /** @var ScheduledTasksRepository */ |
| 49 | private $scheduledTasksRepository; |
| 50 | |
| 51 | /** @var SendingQueuesRepository */ |
| 52 | private $sendingQueuesRepository; |
| 53 | |
| 54 | /** @var WPFunctions */ |
| 55 | private $wp; |
| 56 | |
| 57 | /** @var DaemonTrigger */ |
| 58 | private $daemonTrigger; |
| 59 | |
| 60 | /** @var SettingsController */ |
| 61 | private $settings; |
| 62 | |
| 63 | /** @var SubscribersFeature */ |
| 64 | private $subscribersFeature; |
| 65 | |
| 66 | /** @var MailerFactory */ |
| 67 | private $mailerFactory; |
| 68 | |
| 69 | /** @var TrackingConfig */ |
| 70 | private $trackingConfig; |
| 71 | |
| 72 | /** @var TrackingConsentController */ |
| 73 | private $trackingConsentController; |
| 74 | |
| 75 | public function __construct( |
| 76 | NewsletterSaveController $newsletterSaveController, |
| 77 | NewsletterDeleteController $newsletterDeleteController, |
| 78 | NewslettersRepository $newslettersRepository, |
| 79 | EntityManager $entityManager, |
| 80 | ScheduledTasksRepository $scheduledTasksRepository, |
| 81 | SendingQueuesRepository $sendingQueuesRepository, |
| 82 | WPFunctions $wp, |
| 83 | DaemonTrigger $daemonTrigger, |
| 84 | SettingsController $settings, |
| 85 | SubscribersFeature $subscribersFeature, |
| 86 | MailerFactory $mailerFactory, |
| 87 | TrackingConfig $trackingConfig, |
| 88 | TrackingConsentController $trackingConsentController |
| 89 | ) { |
| 90 | $this->newsletterSaveController = $newsletterSaveController; |
| 91 | $this->newsletterDeleteController = $newsletterDeleteController; |
| 92 | $this->newslettersRepository = $newslettersRepository; |
| 93 | $this->entityManager = $entityManager; |
| 94 | $this->scheduledTasksRepository = $scheduledTasksRepository; |
| 95 | $this->sendingQueuesRepository = $sendingQueuesRepository; |
| 96 | $this->wp = $wp; |
| 97 | $this->daemonTrigger = $daemonTrigger; |
| 98 | $this->settings = $settings; |
| 99 | $this->subscribersFeature = $subscribersFeature; |
| 100 | $this->mailerFactory = $mailerFactory; |
| 101 | $this->trackingConfig = $trackingConfig; |
| 102 | $this->trackingConsentController = $trackingConsentController; |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Extensibility hook for premium/plan restrictions. |
| 107 | * Override or decorate this method to add per-site daily caps, |
| 108 | * plan-based limits, or other restrictions. |
| 109 | */ |
| 110 | public function canResend(NewsletterEntity $newsletter): void { |
| 111 | if (!$this->trackingConfig->isEmailTrackingEnabled()) { |
| 112 | throw UnexpectedValueException::create() |
| 113 | ->withMessage(__('Resend to non-openers requires email tracking to be enabled.', 'mailpoet')); |
| 114 | } |
| 115 | |
| 116 | if ($newsletter->getType() !== NewsletterEntity::TYPE_STANDARD) { |
| 117 | throw UnexpectedValueException::create() |
| 118 | ->withMessage(__('Only standard newsletters can be resent.', 'mailpoet')); |
| 119 | } |
| 120 | |
| 121 | if ($newsletter->getStatus() !== NewsletterEntity::STATUS_SENT) { |
| 122 | throw UnexpectedValueException::create() |
| 123 | ->withMessage(__('Only sent newsletters can be resent.', 'mailpoet')); |
| 124 | } |
| 125 | |
| 126 | $queue = $newsletter->getLatestQueue(); |
| 127 | if ($queue && $queue->getCountToProcess() > 0) { |
| 128 | throw UnexpectedValueException::create() |
| 129 | ->withMessage(__('This email is still being sent. Wait until sending is complete.', 'mailpoet')); |
| 130 | } |
| 131 | |
| 132 | if ($newsletter->getParent() !== null) { |
| 133 | throw UnexpectedValueException::create() |
| 134 | ->withMessage(__('A resent email cannot be resent again.', 'mailpoet')); |
| 135 | } |
| 136 | |
| 137 | if (!$newsletter->getChildren()->isEmpty()) { |
| 138 | throw UnexpectedValueException::create() |
| 139 | ->withMessage(__('This email has already been resent.', 'mailpoet')); |
| 140 | } |
| 141 | |
| 142 | $sentAt = $newsletter->getSentAt(); |
| 143 | if (!$sentAt) { |
| 144 | throw UnexpectedValueException::create() |
| 145 | ->withMessage(__('This email has no sent date and cannot be resent.', 'mailpoet')); |
| 146 | } |
| 147 | |
| 148 | $hoursSinceSent = Carbon::now()->diffInHours(Carbon::createFromTimestamp($sentAt->getTimestamp()), true); |
| 149 | if ($hoursSinceSent < self::MIN_RESEND_DELAY_HOURS) { |
| 150 | throw UnexpectedValueException::create() |
| 151 | ->withMessage(__('You can resend this email at least 1 day after it was sent.', 'mailpoet')); |
| 152 | } |
| 153 | if ($hoursSinceSent > self::MAX_RESEND_DELAY_HOURS) { |
| 154 | throw UnexpectedValueException::create() |
| 155 | ->withMessage(__('You can only resend this email within 3 days of sending it.', 'mailpoet')); |
| 156 | } |
| 157 | |
| 158 | if ($this->subscribersFeature->check()) { |
| 159 | throw UnexpectedValueException::create() |
| 160 | ->withMessage(__('Subscribers limit reached.', 'mailpoet')); |
| 161 | } |
| 162 | |
| 163 | try { |
| 164 | $this->mailerFactory->getDefaultMailer(); |
| 165 | } catch (\Exception $e) { |
| 166 | throw UnexpectedValueException::create() |
| 167 | ->withMessage($e->getMessage()); |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | public function resendToNonOpeners(NewsletterEntity $newsletter, string $subject): NewsletterEntity { |
| 172 | $this->canResend($newsletter); |
| 173 | |
| 174 | $subject = trim($subject); |
| 175 | if ($subject === '') { |
| 176 | throw UnexpectedValueException::create() |
| 177 | ->withMessage(__('Subject line is required.', 'mailpoet')); |
| 178 | } |
| 179 | |
| 180 | $originalSubject = trim($newsletter->getSubject()); |
| 181 | if (strcasecmp($subject, $originalSubject) === 0) { |
| 182 | throw UnexpectedValueException::create() |
| 183 | ->withMessage(__('The subject line must be different from the original email.', 'mailpoet')); |
| 184 | } |
| 185 | |
| 186 | $nonOpenerIds = $this->getNonOpenerIds($newsletter); |
| 187 | if (empty($nonOpenerIds)) { |
| 188 | throw UnexpectedValueException::create() |
| 189 | ->withMessage(__('All recipients have already opened this email.', 'mailpoet')); |
| 190 | } |
| 191 | |
| 192 | $duplicate = $this->newsletterSaveController->duplicate($newsletter); |
| 193 | $duplicate->setParent($newsletter); |
| 194 | $duplicate->setSubject($subject); |
| 195 | |
| 196 | $wpPostId = $duplicate->getWpPostId(); |
| 197 | if ($wpPostId) { |
| 198 | $this->wp->wpUpdatePost([ |
| 199 | 'ID' => $wpPostId, |
| 200 | 'post_title' => $subject, |
| 201 | ]); |
| 202 | } |
| 203 | |
| 204 | $this->newslettersRepository->flush(); |
| 205 | |
| 206 | try { |
| 207 | $scheduledTask = new ScheduledTaskEntity(); |
| 208 | $scheduledTask->setType(SendingQueueWorker::TASK_TYPE); |
| 209 | $scheduledTask->setPriority(ScheduledTaskEntity::PRIORITY_MEDIUM); |
| 210 | $scheduledTask->setStatus(null); |
| 211 | $this->scheduledTasksRepository->persist($scheduledTask); |
| 212 | $this->scheduledTasksRepository->flush(); |
| 213 | |
| 214 | $sendingQueue = new SendingQueueEntity(); |
| 215 | $sendingQueue->setNewsletter($duplicate); |
| 216 | $sendingQueue->setTask($scheduledTask); |
| 217 | $this->sendingQueuesRepository->persist($sendingQueue); |
| 218 | $this->sendingQueuesRepository->flush(); |
| 219 | $this->newslettersRepository->refresh($duplicate); |
| 220 | |
| 221 | $insertedCount = $this->addSubscribersToTask($scheduledTask, $nonOpenerIds); |
| 222 | |
| 223 | if ($insertedCount === 0) { |
| 224 | throw UnexpectedValueException::create() |
| 225 | ->withMessage(__('No eligible recipients found. All non-openers have since unsubscribed, bounced, or left the original segments.', 'mailpoet')); |
| 226 | } |
| 227 | |
| 228 | $this->sendingQueuesRepository->updateCounts($sendingQueue); |
| 229 | $duplicate->setStatus(NewsletterEntity::STATUS_SENDING); |
| 230 | $this->newslettersRepository->flush(); |
| 231 | } catch (\Throwable $e) { |
| 232 | $this->cleanupDuplicate($duplicate, $scheduledTask ?? null, $sendingQueue ?? null); |
| 233 | throw $e; |
| 234 | } |
| 235 | |
| 236 | if ($this->settings->get('cron_trigger.method') === CronTrigger::METHOD_ACTION_SCHEDULER) { |
| 237 | $this->daemonTrigger->process(); |
| 238 | } |
| 239 | |
| 240 | return $duplicate; |
| 241 | } |
| 242 | |
| 243 | private function cleanupDuplicate( |
| 244 | NewsletterEntity $duplicate, |
| 245 | ?ScheduledTaskEntity $task, |
| 246 | ?SendingQueueEntity $queue |
| 247 | ): void { |
| 248 | $duplicateId = $duplicate->getId(); |
| 249 | if ($duplicateId) { |
| 250 | $this->newsletterDeleteController->bulkDelete([$duplicateId]); |
| 251 | return; |
| 252 | } |
| 253 | if ($queue) { |
| 254 | $this->entityManager->remove($queue); |
| 255 | } |
| 256 | if ($task) { |
| 257 | $this->entityManager->remove($task); |
| 258 | } |
| 259 | $this->entityManager->flush(); |
| 260 | } |
| 261 | |
| 262 | /** @return int[] */ |
| 263 | private function getNonOpenerIds(NewsletterEntity $newsletter): array { |
| 264 | $statisticsNewsletterTable = $this->entityManager->getClassMetadata(StatisticsNewsletterEntity::class)->getTableName(); |
| 265 | $statisticsOpenTable = $this->entityManager->getClassMetadata(StatisticsOpenEntity::class)->getTableName(); |
| 266 | $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); |
| 267 | |
| 268 | $connection = $this->entityManager->getConnection(); |
| 269 | |
| 270 | $result = $connection->executeQuery( |
| 271 | "SELECT DISTINCT sn.subscriber_id |
| 272 | FROM $statisticsNewsletterTable sn |
| 273 | INNER JOIN $subscribersTable s |
| 274 | ON s.id = sn.subscriber_id |
| 275 | LEFT JOIN $statisticsOpenTable so |
| 276 | ON so.newsletter_id = sn.newsletter_id |
| 277 | AND so.subscriber_id = sn.subscriber_id |
| 278 | WHERE sn.newsletter_id = ? |
| 279 | AND so.id IS NULL |
| 280 | AND s.tracking_consent != 'denied' |
| 281 | AND (? = 1 OR s.tracking_consent != 'unknown')", |
| 282 | [ |
| 283 | $newsletter->getId(), |
| 284 | $this->trackingConsentController->shouldTrackUnknownConsent() ? 1 : 0, |
| 285 | ], |
| 286 | [ |
| 287 | ParameterType::INTEGER, |
| 288 | ParameterType::INTEGER, |
| 289 | ] |
| 290 | ); |
| 291 | |
| 292 | /** @var list<string|int> $column */ |
| 293 | $column = $result->fetchFirstColumn(); |
| 294 | return array_map('intval', $column); |
| 295 | } |
| 296 | |
| 297 | /** @param int[] $subscriberIds */ |
| 298 | private function addSubscribersToTask(ScheduledTaskEntity $task, array $subscriberIds): int { |
| 299 | $scheduledTaskSubscriberTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName(); |
| 300 | $subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); |
| 301 | |
| 302 | $connection = $this->entityManager->getConnection(); |
| 303 | |
| 304 | $result = $connection->executeQuery( |
| 305 | "INSERT IGNORE INTO $scheduledTaskSubscriberTable |
| 306 | (task_id, subscriber_id, processed) |
| 307 | SELECT DISTINCT ? as task_id, subscribers.`id` as subscriber_id, ? as processed |
| 308 | FROM $subscriberTable subscribers |
| 309 | WHERE subscribers.`deleted_at` IS NULL |
| 310 | AND subscribers.`status` = ? |
| 311 | AND subscribers.`id` IN (?)", |
| 312 | [ |
| 313 | $task->getId(), |
| 314 | ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED, |
| 315 | SubscriberEntity::STATUS_SUBSCRIBED, |
| 316 | $subscriberIds, |
| 317 | ], |
| 318 | [ |
| 319 | ParameterType::INTEGER, |
| 320 | ParameterType::INTEGER, |
| 321 | ParameterType::STRING, |
| 322 | ArrayParameterType::INTEGER, |
| 323 | ] |
| 324 | ); |
| 325 | |
| 326 | return $result->rowCount(); |
| 327 | } |
| 328 | } |
| 329 |