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 / Automation / Integrations / MailPoet / Actions / SendLatestNewsletterAction.php
mailpoet / lib / Automation / Integrations / MailPoet / Actions Last commit date
AutomationSendEmailSubjectResolver.php 2 months ago SendEmailAction.php 1 month ago SendLatestNewsletterAction.php 2 months ago index.php 3 years ago
SendLatestNewsletterAction.php
343 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Automation\Integrations\MailPoet\Actions;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Automation\Engine\Control\StepRunController;
9 use MailPoet\Automation\Engine\Data\Step;
10 use MailPoet\Automation\Engine\Data\StepRunArgs;
11 use MailPoet\Automation\Engine\Data\StepValidationArgs;
12 use MailPoet\Automation\Engine\Exceptions\NotFoundException;
13 use MailPoet\Automation\Engine\Integration\Action;
14 use MailPoet\Automation\Engine\Integration\ValidationException;
15 use MailPoet\Automation\Integrations\MailPoet\Payloads\SegmentPayload;
16 use MailPoet\Automation\Integrations\MailPoet\Payloads\SubscriberPayload;
17 use MailPoet\Automation\Integrations\MailPoet\Subjects\SegmentSubject;
18 use MailPoet\Automation\Integrations\MailPoet\Subjects\SubscriberSubject;
19 use MailPoet\Entities\NewsletterEntity;
20 use MailPoet\Entities\ScheduledTaskSubscriberEntity;
21 use MailPoet\Entities\SubscriberEntity;
22 use MailPoet\InvalidStateException;
23 use MailPoet\Newsletter\NewslettersRepository;
24 use MailPoet\Newsletter\Scheduler\LatestNewsletterScheduler;
25 use MailPoet\Segments\SegmentsRepository;
26 use MailPoet\Subscribers\SubscriberSegmentRepository;
27 use MailPoet\Subscribers\SubscribersRepository;
28 use MailPoet\Validator\Builder;
29 use MailPoet\Validator\Schema\ObjectSchema;
30 use Throwable;
31
32 class SendLatestNewsletterAction implements Action {
33 public const KEY = 'mailpoet:send-latest-newsletter';
34
35 private const POLL_INTERVALS = [
36 5 * MINUTE_IN_SECONDS,
37 10 * MINUTE_IN_SECONDS,
38 45 * MINUTE_IN_SECONDS,
39 4 * HOUR_IN_SECONDS,
40 19 * HOUR_IN_SECONDS,
41 4 * DAY_IN_SECONDS,
42 25 * DAY_IN_SECONDS,
43 ];
44
45 private const OPTIN_RETRY_INTERVALS = [
46 1 * MINUTE_IN_SECONDS,
47 5 * MINUTE_IN_SECONDS,
48 20 * MINUTE_IN_SECONDS,
49 1 * HOUR_IN_SECONDS,
50 12 * HOUR_IN_SECONDS,
51 1 * DAY_IN_SECONDS,
52 ];
53
54 private const WAIT_OPTIN = 'wait_optin';
55 private const OPTIN_RETRIES = 'optin_retries';
56 private const OUTCOME = 'outcome';
57 private const NEWSLETTER_ID = 'newsletter_id';
58 private const OUTCOME_POLLING = 'polling';
59 private const OUTCOME_SENT = 'sent';
60 private const OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER = 'skipped-ineligible-subscriber';
61
62 private SubscribersRepository $subscribersRepository;
63
64 private SubscriberSegmentRepository $subscriberSegmentRepository;
65
66 private NewslettersRepository $newslettersRepository;
67
68 private SegmentsRepository $segmentsRepository;
69
70 private LatestNewsletterScheduler $latestNewsletterScheduler;
71
72 public function __construct(
73 SubscribersRepository $subscribersRepository,
74 SubscriberSegmentRepository $subscriberSegmentRepository,
75 NewslettersRepository $newslettersRepository,
76 SegmentsRepository $segmentsRepository,
77 LatestNewsletterScheduler $latestNewsletterScheduler
78 ) {
79 $this->subscribersRepository = $subscribersRepository;
80 $this->subscriberSegmentRepository = $subscriberSegmentRepository;
81 $this->newslettersRepository = $newslettersRepository;
82 $this->segmentsRepository = $segmentsRepository;
83 $this->latestNewsletterScheduler = $latestNewsletterScheduler;
84 }
85
86 public function getKey(): string {
87 return self::KEY;
88 }
89
90 public function getName(): string {
91 // translators: automation action title
92 return __('Send latest newsletter', 'mailpoet');
93 }
94
95 public function getArgsSchema(): ObjectSchema {
96 return Builder::object([])
97 ->disableAdditionalProperties()
98 ->maxProperties(0);
99 }
100
101 public function getSubjectKeys(): array {
102 return [
103 SubscriberSubject::KEY,
104 SegmentSubject::KEY,
105 ];
106 }
107
108 public function validate(StepValidationArgs $args): void {
109 if ($args->getStep()->getArgs() !== []) {
110 throw ValidationException::create()
111 ->withMessage(__('This action does not accept configuration.', 'mailpoet'))
112 ->withError('general', __('Send latest newsletter does not accept any configuration fields.', 'mailpoet'));
113 }
114
115 try {
116 $args->getSingleSubject(SubscriberSubject::KEY);
117 $args->getSingleSubject(SegmentSubject::KEY);
118 } catch (NotFoundException $e) {
119 throw ValidationException::create()
120 ->withMessage(__('This action needs a trigger list.', 'mailpoet'))
121 ->withError('general', __('This action needs a trigger list, such as "Someone subscribes".', 'mailpoet'));
122 }
123 }
124
125 public function run(StepRunArgs $args, StepRunController $controller): void {
126 [$subscriber, $segmentId] = $this->getSubscriberAndSegment($args);
127 $state = null;
128
129 if ($args->isFirstRun()) {
130 if ($this->isSubscriberIneligible($subscriber, $segmentId)) {
131 $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER);
132 return;
133 }
134
135 if ($subscriber->getStatus() === SubscriberEntity::STATUS_UNCONFIRMED) {
136 $controller->getRunLog()->saveLogData([self::WAIT_OPTIN => 1]);
137 $this->rerunLater($args->getRunNumber(), $controller, true);
138 return;
139 }
140
141 if (!$this->scheduleLatestNewsletter($args, $controller, $subscriber, $segmentId)) {
142 return;
143 }
144 } else {
145 $state = $this->getRunLogData($controller);
146 if (($state[self::WAIT_OPTIN] ?? 0) === 1) {
147 if ($subscriber->getStatus() === SubscriberEntity::STATUS_UNCONFIRMED) {
148 $this->rerunLater($args->getRunNumber(), $controller, true);
149 return;
150 }
151
152 if ($this->isSubscriberIneligible($subscriber, $segmentId)) {
153 $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER);
154 return;
155 }
156
157 $optinRetryCount = max(0, $args->getRunNumber() - 1);
158 $controller->getRunLog()->saveLogData([
159 self::WAIT_OPTIN => 0,
160 self::OPTIN_RETRIES => $optinRetryCount,
161 ]);
162 $state[self::OPTIN_RETRIES] = $optinRetryCount;
163 if (!$this->scheduleLatestNewsletter($args, $controller, $subscriber, $segmentId)) {
164 return;
165 }
166 }
167
168 $success = $this->checkSendingStatus($args, $controller, $subscriber, $segmentId);
169 if ($success) {
170 return;
171 }
172 }
173
174 $runNumber = $args->getRunNumber();
175 $state = $state ?? $this->getRunLogData($controller);
176 $optinRetryCount = $state[self::OPTIN_RETRIES] ?? 0;
177 $runNumber -= $optinRetryCount;
178 $this->rerunLater($runNumber, $controller, false);
179 }
180
181 private function scheduleLatestNewsletter(StepRunArgs $args, StepRunController $controller, SubscriberEntity $subscriber, int $segmentId): bool {
182 try {
183 $result = $this->latestNewsletterScheduler->schedule($subscriber, $segmentId, $this->getAutomationMeta($args));
184 } catch (Throwable $e) {
185 throw InvalidStateException::create()->withMessage(__('Could not create sending task.', 'mailpoet'));
186 }
187
188 $newsletter = $result['newsletter'];
189 $newsletterId = $newsletter instanceof NewsletterEntity ? $newsletter->getId() : null;
190 $this->saveOutcome($controller, $result['outcome'], $newsletterId);
191
192 if ($result['outcome'] === LatestNewsletterScheduler::OUTCOME_SCHEDULED) {
193 return true;
194 }
195 return false;
196 }
197
198 private function checkSendingStatus(StepRunArgs $args, StepRunController $controller, SubscriberEntity $subscriber, int $segmentId): bool {
199 $state = $this->getRunLogData($controller);
200 $newsletterId = (int)($state[self::NEWSLETTER_ID] ?? 0);
201 if (!$newsletterId) {
202 return true;
203 }
204
205 $newsletter = $this->latestNewsletterScheduler->getScheduledTaskSubscriber(
206 $this->getNewsletter($newsletterId),
207 $subscriber,
208 $args->getAutomationRun()
209 );
210 if (!$newsletter) {
211 if ($this->isSubscriberIneligible($subscriber, $segmentId)) {
212 $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER, $newsletterId);
213 return true;
214 }
215 throw InvalidStateException::create()->withMessage(__('Email failed to schedule.', 'mailpoet'));
216 }
217
218 if ($newsletter->getFailed() === ScheduledTaskSubscriberEntity::FAIL_STATUS_FAILED) {
219 throw InvalidStateException::create()->withMessage(
220 // translators: %s is the error message.
221 sprintf(__('Email failed to send. Error: %s', 'mailpoet'), $newsletter->getError() ?: 'Unknown error')
222 );
223 }
224
225 $wasSent = $newsletter->getProcessed() === ScheduledTaskSubscriberEntity::STATUS_PROCESSED;
226 if ($wasSent) {
227 $this->saveOutcome($controller, self::OUTCOME_SENT, $newsletterId);
228 return true;
229 }
230
231 if ($this->isSubscriberIneligible($subscriber, $segmentId)) {
232 $this->latestNewsletterScheduler->saveErrorAndPause(
233 $newsletter,
234 __('Subscriber is no longer eligible for this email.', 'mailpoet')
235 );
236 $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER, $newsletterId);
237 return true;
238 }
239
240 $optinRetryCount = (int)($state[self::OPTIN_RETRIES] ?? 0);
241 $pollRunNumber = max(1, $args->getRunNumber() - $optinRetryCount);
242 $isLastRun = $pollRunNumber >= 1 + count(self::POLL_INTERVALS);
243 if (!$isLastRun) {
244 $this->saveOutcome($controller, self::OUTCOME_POLLING, $newsletterId);
245 return false;
246 }
247
248 $error = __('Email sending process timed out.', 'mailpoet');
249 $this->latestNewsletterScheduler->saveErrorAndPause($newsletter, $error);
250 throw InvalidStateException::create()->withMessage($error);
251 }
252
253 private function getNewsletter(int $newsletterId): NewsletterEntity {
254 $newsletter = $this->newslettersRepository->findOneById($newsletterId);
255 if (!$newsletter instanceof NewsletterEntity) {
256 throw InvalidStateException::create()->withMessage(__('Email failed to schedule.', 'mailpoet'));
257 }
258 return $newsletter;
259 }
260
261 /**
262 * @return array{0: SubscriberEntity, 1: int}
263 */
264 private function getSubscriberAndSegment(StepRunArgs $args): array {
265 try {
266 $subscriberId = $args->getSinglePayloadByClass(SubscriberPayload::class)->getId();
267 $segmentId = $args->getSinglePayloadByClass(SegmentPayload::class)->getId();
268 } catch (Throwable $e) {
269 throw InvalidStateException::create()->withMessage(__('This action needs a trigger list, such as "Someone subscribes".', 'mailpoet'));
270 }
271
272 $subscriber = $this->subscribersRepository->findOneById($subscriberId);
273 if (!$subscriber) {
274 throw InvalidStateException::create()->withMessage(__('Subscriber was not found.', 'mailpoet'));
275 }
276 if (!$this->segmentsRepository->findOneById($segmentId)) {
277 throw InvalidStateException::create()->withMessage(__('Cannot send the email because the list was not found.', 'mailpoet'));
278 }
279
280 return [$subscriber, $segmentId];
281 }
282
283 private function isSubscriberIneligible(SubscriberEntity $subscriber, int $segmentId): bool {
284 if (
285 $subscriber->getDeletedAt() !== null
286 || in_array($subscriber->getStatus(), [SubscriberEntity::STATUS_BOUNCED, SubscriberEntity::STATUS_UNSUBSCRIBED], true)
287 ) {
288 return true;
289 }
290
291 if ($subscriber->getStatus() !== SubscriberEntity::STATUS_SUBSCRIBED) {
292 return false;
293 }
294
295 return !$this->subscriberSegmentRepository->findOneBy([
296 'subscriber' => $subscriber->getId(),
297 'segment' => $segmentId,
298 'status' => SubscriberEntity::STATUS_SUBSCRIBED,
299 ]);
300 }
301
302 private function rerunLater(int $runNumber, StepRunController $controller, bool $waitingForOptIn): void {
303 if ($waitingForOptIn) {
304 if ($runNumber > count(self::OPTIN_RETRY_INTERVALS)) {
305 $this->saveOutcome($controller, self::OUTCOME_SKIPPED_INELIGIBLE_SUBSCRIBER);
306 return;
307 }
308 $controller->scheduleProgress(time() + self::OPTIN_RETRY_INTERVALS[$runNumber - 1]);
309 return;
310 }
311
312 $nextInterval = self::POLL_INTERVALS[$runNumber - 1] ?? 0;
313 $controller->scheduleProgress(time() + $nextInterval);
314 }
315
316 private function getRunLogData(StepRunController $controller): array {
317 $runLog = $controller->getRunLog()->getLog();
318 return $runLog->getData();
319 }
320
321 /** @return array{id: mixed, run_id: mixed, step_id: mixed, run_number: mixed} */
322 private function getAutomationMeta(StepRunArgs $args): array {
323 return [
324 'id' => $args->getAutomation()->getId(),
325 'run_id' => $args->getAutomationRun()->getId(),
326 'step_id' => $args->getStep()->getId(),
327 'run_number' => $args->getRunNumber(),
328 ];
329 }
330
331 private function saveOutcome(StepRunController $controller, string $outcome, ?int $newsletterId = null): void {
332 $data = [self::OUTCOME => $outcome];
333 if ($newsletterId) {
334 $data[self::NEWSLETTER_ID] = $newsletterId;
335 }
336 $controller->getRunLog()->saveLogData($data);
337 }
338
339 public function onDuplicate(Step $step): Step {
340 return $step;
341 }
342 }
343