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 / Subscribers / InactiveSubscribersController.php
mailpoet / lib / Subscribers Last commit date
ConfirmationEmailTemplate 1 month ago ImportExport 2 weeks ago RestApi 4 days ago Statistics 1 month ago BulkActionController.php 4 days ago BulkActionException.php 2 months ago BulkConfirmationEmailResender.php 2 months ago ConfirmationEmailCustomizer.php 2 months ago ConfirmationEmailMailer.php 2 months ago ConfirmationEmailResolver.php 2 months ago EngagementDataBackfiller.php 2 months ago InactiveSubscribersController.php 5 days ago LinkTokens.php 2 months ago NewSubscriberNotificationMailer.php 2 months ago RequiredCustomFieldValidator.php 2 months ago SegmentsCountRecalculator.php 2 weeks ago Source.php 2 months ago SubscriberActions.php 2 months ago SubscriberCustomFieldRepository.php 3 years ago SubscriberIPsRepository.php 2 years ago SubscriberLimitNotificationEvaluator.php 2 months ago SubscriberLimitNotificationMailer.php 2 months ago SubscriberLimitNotificationScheduler.php 2 months ago SubscriberListingRepository.php 2 weeks ago SubscriberPersonalDataEraser.php 2 months ago SubscriberSaveController.php 4 days ago SubscriberSegmentRepository.php 2 weeks ago SubscriberSubscribeController.php 2 weeks ago SubscriberTagRepository.php 4 years ago SubscribersCountsController.php 2 weeks ago SubscribersEmailCountsController.php 2 weeks ago SubscribersRepository.php 4 days ago TrackingConsentController.php 5 days ago index.php 3 years ago
InactiveSubscribersController.php
200 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Subscribers;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\ScheduledTaskEntity;
9 use MailPoet\Entities\ScheduledTaskSubscriberEntity;
10 use MailPoet\Entities\SendingQueueEntity;
11 use MailPoet\Entities\SubscriberEntity;
12 use MailPoetVendor\Carbon\Carbon;
13 use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
14 use MailPoetVendor\Doctrine\DBAL\ParameterType;
15 use MailPoetVendor\Doctrine\ORM\EntityManager;
16
17 class InactiveSubscribersController {
18
19 const UNOPENED_EMAILS_THRESHOLD = 3;
20 const LIFETIME_EMAILS_THRESHOLD = 10;
21
22 private $processedTaskIdsTableCreated = false;
23
24 /** @var EntityManager */
25 private $entityManager;
26
27 /** @var TrackingConsentController */
28 private $trackingConsentController;
29
30 public function __construct(
31 EntityManager $entityManager,
32 TrackingConsentController $trackingConsentController
33 ) {
34 $this->entityManager = $entityManager;
35 $this->trackingConsentController = $trackingConsentController;
36 }
37
38 public function markInactiveSubscribers(int $daysToInactive, int $startId, int $endId, ?int $unopenedEmails = self::UNOPENED_EMAILS_THRESHOLD) {
39 $thresholdDate = $this->getThresholdDate($daysToInactive);
40 return $this->deactivateSubscribers($thresholdDate, $startId, $endId, $unopenedEmails);
41 }
42
43 public function markActiveSubscribers(int $daysToInactive, int $batchSize): int {
44 $thresholdDate = $this->getThresholdDate($daysToInactive);
45 return $this->activateSubscribers($thresholdDate, $batchSize);
46 }
47
48 public function reactivateInactiveSubscribers(): void {
49 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
50 $reactivateAllInactiveQuery = "
51 UPDATE {$subscribersTable} SET status = :statusSubscribed WHERE status = :statusInactive
52 ";
53 $this->entityManager->getConnection()->executeQuery($reactivateAllInactiveQuery, [
54 'statusSubscribed' => SubscriberEntity::STATUS_SUBSCRIBED,
55 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
56 ]);
57 }
58
59 private function getThresholdDate(int $daysToInactive): Carbon {
60 $now = new Carbon();
61 return $now->subDays($daysToInactive);
62 }
63
64 /**
65 * @return int
66 */
67 private function deactivateSubscribers(Carbon $thresholdDate, int $startId, int $endId, ?int $unopenedEmails = self::UNOPENED_EMAILS_THRESHOLD) {
68 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
69 $scheduledTasksTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName();
70 $scheduledTaskSubscribersTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName();
71 $sendingQueuesTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName();
72 $connection = $this->entityManager->getConnection();
73
74 $thresholdDateIso = $thresholdDate->toDateTimeString();
75 $dayAgo = new Carbon();
76 $dayAgoIso = $dayAgo->subDay()->toDateTimeString();
77
78 // Temporary table with processed tasks from threshold date up to yesterday
79 $processedTaskIdsTable = 'inactive_task_ids';
80 if (!$this->processedTaskIdsTableCreated) {
81 $processedTaskIdsTableSql = "
82 CREATE TEMPORARY TABLE IF NOT EXISTS {$processedTaskIdsTable}
83 (INDEX task_id_ids (id), PRIMARY KEY (`id`))
84 SELECT DISTINCT task_id as id FROM {$sendingQueuesTable} as sq
85 JOIN {$scheduledTasksTable} as st ON sq.task_id = st.id
86 WHERE st.processed_at > :thresholdDate
87 AND st.processed_at < :dayAgo
88 ";
89 $connection->executeQuery($processedTaskIdsTableSql, [
90 'thresholdDate' => $thresholdDateIso,
91 'dayAgo' => $dayAgoIso,
92 ]);
93 $this->processedTaskIdsTableCreated = true;
94 }
95
96 // Select subscribers who received at least a number of emails after threshold date and subscribed before that
97 $lifetimeEmailsThreshold = self::LIFETIME_EMAILS_THRESHOLD;
98 $inactiveSubscriberIdsTmpTable = 'inactive_subscriber_ids';
99 $connection->executeQuery(
100 "
101 CREATE TEMPORARY TABLE IF NOT EXISTS {$inactiveSubscriberIdsTmpTable}
102 (UNIQUE subscriber_id (id), PRIMARY KEY (`id`))
103 SELECT s.id FROM {$subscribersTable} as s
104 JOIN {$scheduledTaskSubscribersTable} as sts USE INDEX (subscriber_id) ON s.id = sts.subscriber_id
105 JOIN {$processedTaskIdsTable} task_ids ON task_ids.id = sts.task_id
106 WHERE s.last_subscribed_at < :thresholdDate
107 AND s.status = :status
108 AND s.tracking_consent != 'denied'
109 AND (:trackUnknown = 1 OR s.tracking_consent != 'unknown')
110 AND s.id >= :startId
111 AND s.id <= :endId
112 AND s.email_count >= {$lifetimeEmailsThreshold}
113 GROUP BY s.id
114 HAVING count(s.id) >= :unopenedEmailsThreshold
115 ",
116 [
117 'thresholdDate' => $thresholdDateIso,
118 'status' => SubscriberEntity::STATUS_SUBSCRIBED,
119 'trackUnknown' => $this->trackingConsentController->shouldTrackUnknownConsent() ? 1 : 0,
120 'startId' => $startId,
121 'endId' => $endId,
122 'unopenedEmailsThreshold' => $unopenedEmails,
123 ]
124 );
125
126 $result = $connection->executeQuery("
127 SELECT isi.id FROM {$inactiveSubscriberIdsTmpTable} isi
128 LEFT OUTER JOIN {$subscribersTable} as s ON isi.id = s.id AND GREATEST(
129 COALESCE(s.last_engagement_at, '0'),
130 COALESCE(s.last_subscribed_at, '0'),
131 COALESCE(s.created_at, '0')
132 ) > :thresholdDate
133 WHERE s.id IS NULL
134 ", [
135 'thresholdDate' => $thresholdDateIso,
136 ]);
137 $idsToDeactivate = $result->fetchAllAssociative();
138
139 $connection->executeQuery("DROP TABLE {$inactiveSubscriberIdsTmpTable}");
140
141 $idsToDeactivate = $this->extractIds($idsToDeactivate);
142 if (!count($idsToDeactivate)) {
143 return 0;
144 }
145 $connection->executeQuery("UPDATE {$subscribersTable} SET status = :statusInactive WHERE id IN (:idsToDeactivate)", [
146 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
147 'idsToDeactivate' => $idsToDeactivate,
148 ], ['idsToDeactivate' => ArrayParameterType::INTEGER]);
149 return count($idsToDeactivate);
150 }
151
152 private function activateSubscribers(Carbon $thresholdDate, int $batchSize): int {
153 $subscribersTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName();
154 $connection = $this->entityManager->getConnection();
155
156 $idsToActivate = $connection->executeQuery("
157 SELECT s.id
158 FROM {$subscribersTable} s
159 LEFT OUTER JOIN {$subscribersTable} s2 ON s.id = s2.id AND GREATEST(
160 COALESCE(s2.last_engagement_at, '0'),
161 COALESCE(s2.last_subscribed_at, '0'),
162 COALESCE(s2.created_at, '0')
163 ) > :thresholdDate
164 WHERE s.last_subscribed_at < :thresholdDate
165 AND s.status = :statusInactive
166 AND s2.id IS NOT NULL
167 GROUP BY s.id
168 LIMIT :batchSize
169 ", [
170 'thresholdDate' => $thresholdDate,
171 'statusInactive' => SubscriberEntity::STATUS_INACTIVE,
172 'batchSize' => $batchSize,
173 ], ['batchSize' => ParameterType::INTEGER])->fetchAllAssociative();
174
175 $idsToActivate = $this->extractIds($idsToActivate);
176 if (!count($idsToActivate)) {
177 return 0;
178 }
179 $connection->executeQuery("UPDATE {$subscribersTable} SET status = :statusSubscribed WHERE id IN (:idsToActivate)", [
180 'statusSubscribed' => SubscriberEntity::STATUS_SUBSCRIBED,
181 'idsToActivate' => $idsToActivate,
182 ], ['idsToActivate' => ArrayParameterType::INTEGER]);
183 return count($idsToActivate);
184 }
185
186 /**
187 * @param array<array<string, mixed>> $rows
188 * @return int[]
189 */
190 private function extractIds(array $rows): array {
191 $ids = [];
192 foreach ($rows as $row) {
193 if (isset($row['id']) && is_numeric($row['id'])) {
194 $ids[] = (int)$row['id'];
195 }
196 }
197 return $ids;
198 }
199 }
200