Merge pull request #17736 from civicrm/5.27
[civicrm-core.git] / ext / flexmailer / src / Listener / DefaultSender.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11 namespace Civi\FlexMailer\Listener;
12
13 use Civi\FlexMailer\Event\SendBatchEvent;
14
15 class DefaultSender extends BaseListener {
16 const BULK_MAIL_INSERT_COUNT = 10;
17
18 public function onSend(SendBatchEvent $e) {
19 static $smtpConnectionErrors = 0;
20
21 if (!$this->isActive()) {
22 return;
23 }
24
25 $e->stopPropagation();
26
27 $job = $e->getJob();
28 $mailing = $e->getMailing();
29 $job_date = \CRM_Utils_Date::isoToMysql($job->scheduled_date);
30 $mailer = \Civi::service('pear_mail');
31
32 $targetParams = $deliveredParams = array();
33 $count = 0;
34 $retryBatch = FALSE;
35
36 foreach ($e->getTasks() as $key => $task) {
37 /** @var \Civi\FlexMailer\FlexMailerTask $task */
38 /** @var \Mail_mime $message */
39 if (!$task->hasContent()) {
40 continue;
41 }
42
43 $message = \Civi\FlexMailer\MailParams::convertMailParamsToMime($task->getMailParams());
44
45 if (empty($message)) {
46 // lets keep the message in the queue
47 // most likely a permissions related issue with smarty templates
48 // or a bad contact id? CRM-9833
49 continue;
50 }
51
52 // disable error reporting on real mailings (but leave error reporting for tests), CRM-5744
53 if ($job_date) {
54 $errorScope = \CRM_Core_TemporaryErrorScope::ignoreException();
55 }
56
57 $headers = $message->headers();
58 $result = $mailer->send($headers['To'], $message->headers(), $message->get());
59
60 if ($job_date) {
61 unset($errorScope);
62 }
63
64 if (is_a($result, 'PEAR_Error')) {
65 /** @var \PEAR_Error $result */
66 // CRM-9191
67 $message = $result->getMessage();
68 if ($this->isTemporaryError($result->getMessage())) {
69 // lets log this message and code
70 $code = $result->getCode();
71 \CRM_Core_Error::debug_log_message("SMTP Socket Error or failed to set sender error. Message: $message, Code: $code");
72
73 // these are socket write errors which most likely means smtp connection errors
74 // lets skip them and reconnect.
75 $smtpConnectionErrors++;
76 if ($smtpConnectionErrors <= 5) {
77 $mailer->disconnect();
78 $retryBatch = TRUE;
79 continue;
80 }
81
82 // seems like we have too many of them in a row, we should
83 // write stuff to disk and abort the cron job
84 $job->writeToDB($deliveredParams, $targetParams, $mailing, $job_date);
85
86 \CRM_Core_Error::debug_log_message("Too many SMTP Socket Errors. Exiting");
87 \CRM_Utils_System::civiExit();
88 }
89 else {
90 $this->recordBounce($job, $task, $result->getMessage());
91 }
92 }
93 else {
94 // Register the delivery event.
95 $deliveredParams[] = $task->getEventQueueId();
96 $targetParams[] = $task->getContactId();
97
98 $count++;
99 if ($count % self::BULK_MAIL_INSERT_COUNT == 0) {
100 $job->writeToDB($deliveredParams, $targetParams, $mailing, $job_date);
101 $count = 0;
102
103 // hack to stop mailing job at run time, CRM-4246.
104 // to avoid making too many DB calls for this rare case
105 // lets do it when we snapshot
106 $status = \CRM_Core_DAO::getFieldValue(
107 'CRM_Mailing_DAO_MailingJob',
108 $job->id,
109 'status',
110 'id',
111 TRUE
112 );
113
114 if ($status != 'Running') {
115 $e->setCompleted(FALSE);
116 return;
117 }
118 }
119 }
120
121 unset($result);
122
123 // seems like a successful delivery or bounce, lets decrement error count
124 // only if we have smtp connection errors
125 if ($smtpConnectionErrors > 0) {
126 $smtpConnectionErrors--;
127 }
128
129 // If we have enabled the Throttle option, this is the time to enforce it.
130 $mailThrottleTime = \CRM_Core_Config::singleton()->mailThrottleTime;
131 if (!empty($mailThrottleTime)) {
132 usleep((int) $mailThrottleTime);
133 }
134 }
135
136 $completed = $job->writeToDB(
137 $deliveredParams,
138 $targetParams,
139 $mailing,
140 $job_date
141 );
142 if ($retryBatch) {
143 $completed = FALSE;
144 }
145 $e->setCompleted($completed);
146 }
147
148 /**
149 * Determine if an SMTP error is temporary or permanent.
150 *
151 * @param string $message
152 * PEAR error message.
153 * @return bool
154 * TRUE - Temporary/retriable error
155 * FALSE - Permanent/non-retriable error
156 */
157 protected function isTemporaryError($message) {
158 // SMTP response code is buried in the message.
159 $code = preg_match('/ \(code: (.+), response: /', $message, $matches) ? $matches[1] : '';
160
161 if (strpos($message, 'Failed to write to socket') !== FALSE) {
162 return TRUE;
163 }
164
165 // Register 5xx SMTP response code (permanent failure) as bounce.
166 if (isset($code{0}) && $code{0} === '5') {
167 return FALSE;
168 }
169
170 if (strpos($message, 'Failed to set sender') !== FALSE) {
171 return TRUE;
172 }
173
174 if (strpos($message, 'Failed to add recipient') !== FALSE) {
175 return TRUE;
176 }
177
178 if (strpos($message, 'Failed to send data') !== FALSE) {
179 return TRUE;
180 }
181
182 return FALSE;
183 }
184
185 /**
186 * @param \CRM_Mailing_BAO_MailingJob $job
187 * @param \Civi\FlexMailer\FlexMailerTask $task
188 * @param string $errorMessage
189 */
190 protected function recordBounce($job, $task, $errorMessage) {
191 $params = array(
192 'event_queue_id' => $task->getEventQueueId(),
193 'job_id' => $job->id,
194 'hash' => $task->getHash(),
195 );
196 $params = array_merge($params,
197 \CRM_Mailing_BAO_BouncePattern::match($errorMessage)
198 );
199 \CRM_Mailing_Event_BAO_Bounce::create($params);
200 }
201
202 }