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 / Cron / Workers / Bounce.php
mailpoet / lib / Cron / Workers Last commit date
Automations 1 year ago KeyCheck 2 years ago SendingQueue 3 months ago StatsNotifications 3 months ago AuthorizedSendingEmailsCheck.php 3 years ago BackfillEngagementData.php 2 years ago Bounce.php 2 months ago BounceTaskSubscribersCleanup.php 2 months ago BulkConfirmationEmailResend.php 4 months ago ExportFilesCleanup.php 3 months ago InactiveSubscribersMaintenance.php 2 months ago LogCleanup.php 11 months ago Mixpanel.php 11 months ago NewsletterTemplateThumbnails.php 2 years ago ReEngagementEmailsScheduler.php 2 years ago Scheduler.php 3 months ago SendingQueueBodyCleanup.php 4 months ago SendingTaskSubscribersCleanup.php 4 months ago SimpleWorker.php 2 years ago StatisticsExport.php 4 months ago SubscriberLimitNotificationWorker.php 3 months ago SubscriberLinkTokens.php 2 years ago SubscribersCountCacheRecalculation.php 2 months ago SubscribersEngagementScore.php 2 years ago SubscribersLastEngagement.php 4 months ago SubscribersSegmentsCountSync.php 2 months ago SubscribersStatsReport.php 2 years ago Tracks.php 11 months ago UnconfirmedSubscribersCleanup.php 4 months ago UnsubscribeTokens.php 2 months ago WooCommercePastOrders.php 2 years ago WooCommerceSync.php 3 years ago WorkersFactory.php 2 months ago index.php 3 years ago
Bounce.php
244 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Cron\Workers;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\NewsletterEntity;
9 use MailPoet\Entities\ScheduledTaskEntity;
10 use MailPoet\Entities\StatisticsBounceEntity;
11 use MailPoet\Entities\SubscriberEntity;
12 use MailPoet\Mailer\Mailer;
13 use MailPoet\Newsletter\Sending\SendingQueuesRepository;
14 use MailPoet\Services\Bridge;
15 use MailPoet\Services\Bridge\API;
16 use MailPoet\Settings\SettingsController;
17 use MailPoet\Statistics\StatisticsBouncesRepository;
18 use MailPoet\Subscribers\SubscribersRepository;
19 use MailPoetVendor\Carbon\Carbon;
20
21 class Bounce extends SimpleWorker {
22 const TASK_TYPE = 'bounce';
23 const SUPPORT_MULTIPLE_INSTANCES = false;
24
25 // The sending service never reports bounces older than this. Requests with a
26 // `from` further back are rejected, so the range is clamped to stay within it.
27 const MAX_LOOKBACK_DAYS = 14;
28
29 // Stores the `to` of the last fully-processed report so the next daily run
30 // starts its range exactly there, giving gap-free coverage without querying
31 // previous tasks.
32 const LAST_REPORT_TO_SETTING_KEY = 'bounce.last_report_to';
33
34 // Keys under which the in-progress report range and pagination cursor are
35 // persisted on the task meta, so a run that hits the execution limit resumes
36 // the same range from the next page instead of restarting at page 1.
37 const META_FROM = 'report_from';
38 const META_TO = 'report_to';
39 const META_PAGE = 'report_page';
40
41 public $api;
42
43 /** @var SettingsController */
44 private $settings;
45
46 /** @var Bridge */
47 private $bridge;
48
49 /** @var SubscribersRepository */
50 private $subscribersRepository;
51
52 /** @var SendingQueuesRepository */
53 private $sendingQueuesRepository;
54
55 /** @var StatisticsBouncesRepository */
56 private $statisticsBouncesRepository;
57
58 public function __construct(
59 SettingsController $settings,
60 SubscribersRepository $subscribersRepository,
61 SendingQueuesRepository $sendingQueuesRepository,
62 StatisticsBouncesRepository $statisticsBouncesRepository,
63 Bridge $bridge
64 ) {
65 $this->settings = $settings;
66 $this->bridge = $bridge;
67 parent::__construct();
68 $this->subscribersRepository = $subscribersRepository;
69 $this->sendingQueuesRepository = $sendingQueuesRepository;
70 $this->statisticsBouncesRepository = $statisticsBouncesRepository;
71 }
72
73 public function init() {
74 if (!$this->api) {
75 $this->api = new API($this->settings->get(Mailer::MAILER_CONFIG_SETTING_NAME)['mailpoet_api_key']);
76 }
77 }
78
79 public function checkProcessingRequirements() {
80 return $this->bridge->isMailpoetSendingServiceEnabled();
81 }
82
83 public function processTaskStrategy(ScheduledTaskEntity $task, $timer) {
84 [$from, $to] = $this->getReportRange($task);
85 $page = $this->getReportPage($task);
86
87 do {
88 // abort if execution limit is reached
89 $this->cronHelper->enforceExecutionLimit($timer);
90
91 $report = $this->api->getBouncesReport($from, $to, $page);
92 if (!is_array($report)) {
93 // Transient failure: leave the task running so it retries with the
94 // same range and page on the next cron tick.
95 return false;
96 }
97
98 $recipients = isset($report['recipients']) && is_array($report['recipients']) ? $report['recipients'] : [];
99 $this->processRecipients($task, $recipients);
100
101 $hasMore = !empty($report['has_more']);
102 $page++;
103 // Persist the cursor so a subsequent execution-limit timeout resumes from
104 // the next unprocessed page instead of replaying the whole range.
105 $this->saveReportPage($task, $page);
106 } while ($hasMore);
107
108 // The whole range is consumed; record its `to` as the basis for the next
109 // daily run's `from` so coverage stays continuous.
110 $existing = $this->settings->get(self::LAST_REPORT_TO_SETTING_KEY);
111 $existingTo = null;
112 if (is_string($existing) && $existing !== '') {
113 try {
114 $existingTo = Carbon::parse($existing);
115 } catch (\Exception $e) {
116 $existingTo = null;
117 }
118 }
119 if (!$existingTo || $to->greaterThan($existingTo)) {
120 $this->settings->set(self::LAST_REPORT_TO_SETTING_KEY, $to->format(\DateTimeInterface::ATOM));
121 }
122 return true;
123 }
124
125 /**
126 * @return array{0: Carbon, 1: Carbon}
127 */
128 private function getReportRange(ScheduledTaskEntity $task): array {
129 $meta = $task->getMeta();
130 if (is_array($meta) && isset($meta[self::META_FROM], $meta[self::META_TO])) {
131 return [Carbon::parse($meta[self::META_FROM]), Carbon::parse($meta[self::META_TO])];
132 }
133
134 $to = Carbon::now()->millisecond(0);
135 $from = $this->getReportFromDate($to);
136
137 $meta = is_array($meta) ? $meta : [];
138 $meta[self::META_FROM] = $from->format(\DateTimeInterface::ATOM);
139 $meta[self::META_TO] = $to->format(\DateTimeInterface::ATOM);
140 $meta[self::META_PAGE] = $meta[self::META_PAGE] ?? 1;
141 $task->setMeta($meta);
142 $this->scheduledTasksRepository->persist($task);
143 $this->scheduledTasksRepository->flush();
144
145 return [$from, $to];
146 }
147
148 private function getReportPage(ScheduledTaskEntity $task): int {
149 $meta = $task->getMeta();
150 $page = is_array($meta) && isset($meta[self::META_PAGE]) ? (int)$meta[self::META_PAGE] : 1;
151 return $page > 0 ? $page : 1;
152 }
153
154 private function saveReportPage(ScheduledTaskEntity $task, int $page): void {
155 $meta = $task->getMeta();
156 $meta = is_array($meta) ? $meta : [];
157 $meta[self::META_PAGE] = $page;
158 $task->setMeta($meta);
159 $this->scheduledTasksRepository->persist($task);
160 $this->scheduledTasksRepository->flush();
161 }
162
163 public function processRecipients(ScheduledTaskEntity $task, array $recipients): void {
164 $emails = array_values(array_unique(array_filter(
165 $recipients,
166 function ($email): bool {
167 return is_string($email) && $email !== '';
168 }
169 )));
170 if (empty($emails)) {
171 return;
172 }
173
174 // Only subscribers currently subscribed/unconfirmed transition to bounced,
175 // preserving prior behavior. Loading them in one query (instead of one
176 // lookup per recipient) is the batching this task needed; the status change
177 // itself stays on the managed entities so the Doctrine lifecycle listeners
178 // (status-change notifications, subscriber counts) still fire.
179 $subscribers = $this->subscribersRepository->findBy([
180 'email' => $emails,
181 'status' => [SubscriberEntity::STATUS_SUBSCRIBED, SubscriberEntity::STATUS_UNCONFIRMED],
182 'deletedAt' => null,
183 ]);
184 if (empty($subscribers)) {
185 return;
186 }
187
188 $previousTask = $this->scheduledTasksRepository->findPreviousTask($task);
189 foreach ($subscribers as $subscriber) {
190 $subscriber->setStatus(SubscriberEntity::STATUS_BOUNCED);
191 $this->saveBouncedStatistics($subscriber, $task, $previousTask);
192 }
193 // A single flush commits the status changes and the new statistics together
194 // in one transaction, so a failure cannot record statistics without the
195 // matching status change. A replayed page is then a no-op: the subscribers
196 // are already bounced and fall outside the status filter above.
197 $this->subscribersRepository->flush();
198 }
199
200 public function getNextRunDate() {
201 $date = Carbon::now()->millisecond(0);
202 return $date->startOfDay()
203 ->addDay()
204 ->addHours(rand(0, 5))
205 ->addMinutes(rand(0, 59))
206 ->addSeconds(rand(0, 59));
207 }
208
209 private function getReportFromDate(Carbon $now): Carbon {
210 $lastReportTo = $this->settings->get(self::LAST_REPORT_TO_SETTING_KEY);
211 // A malformed stored value (corruption, manual edit, older code) would make
212 // Carbon::parse throw and crash the worker on every run, so guard it the
213 // same way the persist path above does and fall back to the default `from`.
214 $from = $now->copy()->subDay();
215 if (is_string($lastReportTo) && $lastReportTo !== '') {
216 try {
217 $from = Carbon::parse($lastReportTo);
218 } catch (\Exception $e) {
219 $from = $now->copy()->subDay();
220 }
221 }
222
223 // Keep an hour of margin inside MAX_LOOKBACK_DAYS so clock skew and request
224 // latency can't push the `from` past the limit the service enforces.
225 $earliestAllowed = $now->copy()->subDays(self::MAX_LOOKBACK_DAYS)->addHour();
226 return $from->lessThan($earliestAllowed) ? $earliestAllowed : $from;
227 }
228
229 private function saveBouncedStatistics(SubscriberEntity $subscriber, ScheduledTaskEntity $task, ?ScheduledTaskEntity $previousTask): void {
230 $dateFrom = null;
231 if ($previousTask instanceof ScheduledTaskEntity) {
232 $dateFrom = $previousTask->getScheduledAt();
233 }
234 $queues = $this->sendingQueuesRepository->findAllForSubscriberSentBetween($subscriber, $task->getScheduledAt(), $dateFrom);
235 foreach ($queues as $queue) {
236 $newsletter = $queue->getNewsletter();
237 if ($newsletter instanceof NewsletterEntity) {
238 $statistics = new StatisticsBounceEntity($newsletter, $queue, $subscriber);
239 $this->statisticsBouncesRepository->persist($statistics);
240 }
241 }
242 }
243 }
244