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