PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Newsletter / NewsletterResendController.php
mailpoet / lib / Newsletter Last commit date
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