Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
39de6fd5 | 4 | | CiviCRM version 4.6 | |
6a488035 | 5 | +--------------------------------------------------------------------+ |
06b69b18 | 6 | | Copyright CiviCRM LLC (c) 2004-2014 | |
6a488035 TO |
7 | +--------------------------------------------------------------------+ |
8 | | This file is a part of CiviCRM. | | |
9 | | | | |
10 | | CiviCRM is free software; you can copy, modify, and distribute it | | |
11 | | under the terms of the GNU Affero General Public License | | |
12 | | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | | |
13 | | | | |
14 | | CiviCRM is distributed in the hope that it will be useful, but | | |
15 | | WITHOUT ANY WARRANTY; without even the implied warranty of | | |
16 | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | | |
17 | | See the GNU Affero General Public License for more details. | | |
18 | | | | |
19 | | You should have received a copy of the GNU Affero General Public | | |
20 | | License and the CiviCRM Licensing Exception along | | |
21 | | with this program; if not, contact CiviCRM LLC | | |
22 | | at info[AT]civicrm[DOT]org. If you have questions about the | | |
23 | | GNU Affero General Public License or the licensing of CiviCRM, | | |
24 | | see the CiviCRM license FAQ at http://civicrm.org/licensing | | |
25 | +--------------------------------------------------------------------+ | |
26 | */ | |
27 | ||
28 | /** | |
29 | * | |
30 | * @package CRM | |
06b69b18 | 31 | * @copyright CiviCRM LLC (c) 2004-2014 |
6a488035 TO |
32 | * $Id$ |
33 | * | |
34 | */ | |
35 | ||
36 | // we should consider moving these to the settings table | |
37 | // before the 4.1 release | |
38 | define('EMAIL_ACTIVITY_TYPE_ID', NULL); | |
39 | define('MAIL_BATCH_SIZE', 50); | |
5bc392e6 EM |
40 | |
41 | /** | |
42 | * Class CRM_Utils_Mail_EmailProcessor | |
43 | */ | |
6a488035 TO |
44 | class CRM_Utils_Mail_EmailProcessor { |
45 | ||
46 | /** | |
47 | * Process the default mailbox (ie. that is used by civiMail for the bounce) | |
48 | * | |
ae5ffbb7 TO |
49 | * @return bool |
50 | * Always returns true (for the api). at a later stage we should | |
51 | * fix this to return true on success / false on failure etc. | |
6a488035 | 52 | */ |
00be9182 | 53 | public static function processBounces() { |
ae5ffbb7 | 54 | $dao = new CRM_Core_DAO_MailSettings(); |
353ffa53 | 55 | $dao->domain_id = CRM_Core_Config::domainID(); |
6a488035 TO |
56 | $dao->is_default = TRUE; |
57 | $dao->find(); | |
58 | ||
59 | while ($dao->fetch()) { | |
60 | self::_process(TRUE, $dao); | |
61 | } | |
62 | ||
63 | // always returns true, i.e. never fails :) | |
64 | return TRUE; | |
65 | } | |
66 | ||
67 | /** | |
68 | * Delete old files from a given directory (recursively) | |
69 | * | |
77855840 TO |
70 | * @param string $dir |
71 | * Directory to cleanup. | |
72 | * @param int $age | |
73 | * Files older than this many seconds will be deleted (default: 60 days). | |
6a488035 TO |
74 | * |
75 | * @return void | |
76 | */ | |
00be9182 | 77 | public static function cleanupDir($dir, $age = 5184000) { |
6a488035 TO |
78 | // return early if we can’t read/write the dir |
79 | if (!is_writable($dir) or !is_readable($dir) or !is_dir($dir)) { | |
80 | return; | |
81 | } | |
82 | ||
83 | foreach (scandir($dir) as $file) { | |
84 | ||
85 | // don’t go up the directory stack and skip new files/dirs | |
86 | if ($file == '.' or $file == '..') { | |
87 | continue; | |
88 | } | |
89 | if (filemtime("$dir/$file") > time() - $age) { | |
90 | continue; | |
91 | } | |
92 | ||
93 | // it’s an old file/dir, so delete/recurse | |
94 | is_dir("$dir/$file") ? self::cleanupDir("$dir/$file", $age) : unlink("$dir/$file"); | |
95 | } | |
96 | } | |
97 | ||
98 | /** | |
99 | * Process the mailboxes that aren't default (ie. that aren't used by civiMail for the bounce) | |
100 | * | |
101 | * @return void | |
102 | */ | |
00be9182 | 103 | public static function processActivities() { |
ae5ffbb7 | 104 | $dao = new CRM_Core_DAO_MailSettings(); |
353ffa53 | 105 | $dao->domain_id = CRM_Core_Config::domainID(); |
6a488035 TO |
106 | $dao->is_default = FALSE; |
107 | $dao->find(); | |
108 | $found = FALSE; | |
109 | while ($dao->fetch()) { | |
110 | $found = TRUE; | |
111 | self::_process(FALSE, $dao); | |
112 | } | |
113 | if (!$found) { | |
114 | CRM_Core_Error::fatal(ts('No mailboxes have been configured for Email to Activity Processing')); | |
115 | } | |
116 | return $found; | |
117 | } | |
118 | ||
119 | /** | |
120 | * Process the mailbox for all the settings from civicrm_mail_settings | |
121 | * | |
fd31fa4c | 122 | * @param bool|string $civiMail if true, processing is done in CiviMail context, or Activities otherwise. |
6a488035 TO |
123 | * |
124 | * @return void | |
125 | */ | |
00be9182 | 126 | public static function process($civiMail = TRUE) { |
ae5ffbb7 | 127 | $dao = new CRM_Core_DAO_MailSettings(); |
6a488035 TO |
128 | $dao->domain_id = CRM_Core_Config::domainID(); |
129 | $dao->find(); | |
130 | ||
131 | while ($dao->fetch()) { | |
132 | self::_process($civiMail, $dao); | |
133 | } | |
134 | } | |
135 | ||
5bc392e6 EM |
136 | /** |
137 | * @param $civiMail | |
c490a46a | 138 | * @param CRM_Core_DAO $dao |
5bc392e6 EM |
139 | * |
140 | * @throws Exception | |
141 | */ | |
00be9182 | 142 | public static function _process($civiMail, $dao) { |
6a488035 TO |
143 | // 0 = activities; 1 = bounce; |
144 | $usedfor = $dao->is_default; | |
145 | ||
ae5ffbb7 TO |
146 | $emailActivityTypeId |
147 | = (defined('EMAIL_ACTIVITY_TYPE_ID') && EMAIL_ACTIVITY_TYPE_ID) ? EMAIL_ACTIVITY_TYPE_ID : CRM_Core_OptionGroup::getValue( | |
6a488035 | 148 | 'activity_type', |
353ffa53 TO |
149 | 'Inbound Email', |
150 | 'name' | |
151 | ); | |
6a488035 TO |
152 | |
153 | if (!$emailActivityTypeId) { | |
154 | CRM_Core_Error::fatal(ts('Could not find a valid Activity Type ID for Inbound Email')); | |
155 | } | |
156 | ||
353ffa53 TO |
157 | $config = CRM_Core_Config::singleton(); |
158 | $verpSeperator = preg_quote($config->verpSeparator); | |
6a488035 | 159 | $twoDigitStringMin = $verpSeperator . '(\d+)' . $verpSeperator . '(\d+)'; |
353ffa53 TO |
160 | $twoDigitString = $twoDigitStringMin . $verpSeperator; |
161 | $threeDigitString = $twoDigitString . '(\d+)' . $verpSeperator; | |
6a488035 TO |
162 | |
163 | // FIXME: legacy regexen to handle CiviCRM 2.1 address patterns, with domain id and possible VERP part | |
164 | $commonRegex = '/^' . preg_quote($dao->localpart) . '(b|bounce|c|confirm|o|optOut|r|reply|re|e|resubscribe|u|unsubscribe)' . $threeDigitString . '([0-9a-f]{16})(-.*)?@' . preg_quote($dao->domain) . '$/'; | |
165 | $subscrRegex = '/^' . preg_quote($dao->localpart) . '(s|subscribe)' . $twoDigitStringMin . '@' . preg_quote($dao->domain) . '$/'; | |
166 | ||
167 | // a common-for-all-actions regex to handle CiviCRM 2.2 address patterns | |
168 | $regex = '/^' . preg_quote($dao->localpart) . '(b|c|e|o|r|u)' . $twoDigitString . '([0-9a-f]{16})@' . preg_quote($dao->domain) . '$/'; | |
169 | ||
170 | // a tighter regex for finding bounce info in soft bounces’ mail bodies | |
171 | $rpRegex = '/Return-Path: ' . preg_quote($dao->localpart) . '(b)' . $twoDigitString . '([0-9a-f]{16})@' . preg_quote($dao->domain) . '/'; | |
172 | ||
1a4d92b6 DL |
173 | // a regex for finding bound info X-Header |
174 | $rpXheaderRegex = '/X-CiviMail-Bounce: ' . preg_quote($dao->localpart) . '(b)' . $twoDigitString . '([0-9a-f]{16})@' . preg_quote($dao->domain) . '/'; | |
175 | ||
6a488035 TO |
176 | // retrieve the emails |
177 | try { | |
178 | $store = CRM_Mailing_MailStore::getStore($dao->name); | |
179 | } | |
353ffa53 | 180 | catch (Exception$e) { |
86bfa4f6 | 181 | $message = ts('Could not connect to MailStore for ') . $dao->username . '@' . $dao->server . '<p>'; |
6a488035 TO |
182 | $message .= ts('Error message: '); |
183 | $message .= '<pre>' . $e->getMessage() . '</pre><p>'; | |
184 | CRM_Core_Error::fatal($message); | |
185 | } | |
186 | ||
6a488035 TO |
187 | // process fifty at a time, CRM-4002 |
188 | while ($mails = $store->fetchNext(MAIL_BATCH_SIZE)) { | |
189 | foreach ($mails as $key => $mail) { | |
190 | ||
191 | // for every addressee: match address elements if it's to CiviMail | |
192 | $matches = array(); | |
193 | $action = NULL; | |
194 | ||
195 | if ($usedfor == 1) { | |
196 | foreach ($mail->to as $address) { | |
197 | if (preg_match($regex, $address->email, $matches)) { | |
198 | list($match, $action, $job, $queue, $hash) = $matches; | |
199 | break; | |
200 | // FIXME: the below elseifs should be dropped when we drop legacy support | |
201 | } | |
202 | elseif (preg_match($commonRegex, $address->email, $matches)) { | |
203 | list($match, $action, $_, $job, $queue, $hash) = $matches; | |
204 | break; | |
205 | } | |
206 | elseif (preg_match($subscrRegex, $address->email, $matches)) { | |
207 | list($match, $action, $_, $job) = $matches; | |
208 | break; | |
209 | } | |
210 | } | |
211 | ||
212 | // CRM-5471: if $matches is empty, it still might be a soft bounce sent | |
213 | // to another address, so scan the body for ‘Return-Path: …bounce-pattern…’ | |
214 | if (!$matches and preg_match($rpRegex, $mail->generateBody(), $matches)) { | |
215 | list($match, $action, $job, $queue, $hash) = $matches; | |
216 | } | |
217 | ||
1a4d92b6 DL |
218 | // if $matches is still empty, look for the X-CiviMail-Bounce header |
219 | // CRM-9855 | |
220 | if (!$matches and preg_match($rpXheaderRegex, $mail->generateBody(), $matches)) { | |
221 | list($match, $action, $job, $queue, $hash) = $matches; | |
222 | } | |
223 | ||
6a488035 TO |
224 | // if all else fails, check Delivered-To for possible pattern |
225 | if (!$matches and preg_match($regex, $mail->getHeader('Delivered-To'), $matches)) { | |
226 | list($match, $action, $job, $queue, $hash) = $matches; | |
227 | } | |
228 | } | |
229 | ||
230 | // preseve backward compatibility | |
231 | if ($usedfor == 0 || !$civiMail) { | |
232 | // if its the activities that needs to be processed .. | |
233 | $mailParams = CRM_Utils_Mail_Incoming::parseMailingObject($mail); | |
234 | ||
69c9ffb3 | 235 | require_once 'CRM/Utils/DeprecatedUtils.php'; |
6a488035 TO |
236 | $params = _civicrm_api3_deprecated_activity_buildmailparams($mailParams, $emailActivityTypeId); |
237 | ||
238 | $params['version'] = 3; | |
239 | $result = civicrm_api('activity', 'create', $params); | |
240 | ||
241 | if ($result['is_error']) { | |
242 | $matches = FALSE; | |
243 | echo "Failed Processing: {$mail->subject}. Reason: {$result['error_message']}\n"; | |
244 | } | |
245 | else { | |
246 | $matches = TRUE; | |
247 | echo "Processed as Activity: {$mail->subject}\n"; | |
248 | } | |
249 | ||
250 | CRM_Utils_Hook::emailProcessor('activity', $params, $mail, $result); | |
251 | } | |
252 | ||
253 | // if $matches is empty, this email is not CiviMail-bound | |
254 | if (!$matches) { | |
255 | $store->markIgnored($key); | |
256 | continue; | |
257 | } | |
258 | ||
259 | // get $replyTo from either the Reply-To header or from From | |
260 | // FIXME: make sure it works with Reply-Tos containing non-email stuff | |
261 | $replyTo = $mail->getHeader('Reply-To') ? $mail->getHeader('Reply-To') : $mail->from->email; | |
262 | ||
263 | // handle the action by passing it to the proper API call | |
264 | // FIXME: leave only one-letter cases when dropping legacy support | |
265 | if (!empty($action)) { | |
266 | $result = NULL; | |
267 | ||
268 | switch ($action) { | |
269 | case 'b': | |
270 | case 'bounce': | |
271 | $text = ''; | |
272 | if ($mail->body instanceof ezcMailText) { | |
273 | $text = $mail->body->text; | |
274 | } | |
275 | elseif ($mail->body instanceof ezcMailMultipart) { | |
276 | if ($mail->body instanceof ezcMailMultipartRelated) { | |
277 | foreach ($mail->body->getRelatedParts() as $part) { | |
278 | if (isset($part->subType) and $part->subType == 'plain') { | |
279 | $text = $part->text; | |
280 | break; | |
281 | } | |
282 | } | |
283 | } | |
284 | else { | |
285 | foreach ($mail->body->getParts() as $part) { | |
286 | if (isset($part->subType) and $part->subType == 'plain') { | |
287 | $text = $part->text; | |
288 | break; | |
289 | } | |
290 | } | |
291 | } | |
292 | } | |
293 | ||
294 | if ( | |
295 | $text == NULL && | |
296 | $mail->subject == "Delivery Status Notification (Failure)" | |
297 | ) { | |
298 | // Exchange error - CRM-9361 | |
299 | foreach ($mail->body->getParts() as $part) { | |
300 | if ($part instanceof ezcMailDeliveryStatus) { | |
301 | foreach ($part->recipients as $rec) { | |
302 | if ($rec["Status"] == "5.1.1") { | |
303 | $text = "Delivery to the following recipients failed"; | |
304 | break; | |
305 | } | |
306 | } | |
307 | } | |
308 | } | |
309 | } | |
310 | ||
311 | if (empty($text)) { | |
312 | // If bounce processing fails, just take the raw body. Cf. CRM-11046 | |
313 | $text = $mail->generateBody(); | |
314 | ||
315 | // if text is still empty, lets fudge a blank text so the api call below will succeed | |
6ac9d864 DL |
316 | if (empty($text)) { |
317 | $text = ts('We could not extract the mail body from this bounce message.'); | |
318 | } | |
6a488035 TO |
319 | } |
320 | ||
321 | $params = array( | |
322 | 'job_id' => $job, | |
323 | 'event_queue_id' => $queue, | |
324 | 'hash' => $hash, | |
325 | 'body' => $text, | |
326 | 'version' => 3, | |
327 | ); | |
328 | $result = civicrm_api('Mailing', 'event_bounce', $params); | |
329 | break; | |
330 | ||
331 | case 'c': | |
332 | case 'confirm': | |
333 | // CRM-7921 | |
334 | $params = array( | |
335 | 'contact_id' => $job, | |
336 | 'subscribe_id' => $queue, | |
337 | 'hash' => $hash, | |
338 | 'version' => 3, | |
339 | ); | |
340 | $result = civicrm_api('Mailing', 'event_confirm', $params); | |
341 | break; | |
342 | ||
343 | case 'o': | |
344 | case 'optOut': | |
345 | $params = array( | |
346 | 'job_id' => $job, | |
347 | 'event_queue_id' => $queue, | |
348 | 'hash' => $hash, | |
349 | 'version' => 3, | |
350 | ); | |
351 | $result = civicrm_api('MailingGroup', 'event_domain_unsubscribe', $params); | |
352 | break; | |
353 | ||
354 | case 'r': | |
355 | case 'reply': | |
356 | // instead of text and HTML parts (4th and 6th params) send the whole email as the last param | |
357 | $params = array( | |
358 | 'job_id' => $job, | |
359 | 'event_queue_id' => $queue, | |
360 | 'hash' => $hash, | |
361 | 'bodyTxt' => NULL, | |
362 | 'replyTo' => $replyTo, | |
363 | 'bodyHTML' => NULL, | |
364 | 'fullEmail' => $mail->generate(), | |
365 | 'version' => 3, | |
366 | ); | |
367 | $result = civicrm_api('Mailing', 'event_reply', $params); | |
368 | break; | |
369 | ||
370 | case 'e': | |
371 | case 're': | |
372 | case 'resubscribe': | |
373 | $params = array( | |
374 | 'job_id' => $job, | |
375 | 'event_queue_id' => $queue, | |
376 | 'hash' => $hash, | |
377 | 'version' => 3, | |
378 | ); | |
379 | $result = civicrm_api('MailingGroup', 'event_resubscribe', $params); | |
380 | break; | |
381 | ||
382 | case 's': | |
383 | case 'subscribe': | |
384 | $params = array( | |
385 | 'email' => $mail->from->email, | |
386 | 'group_id' => $job, | |
387 | 'version' => 3, | |
388 | ); | |
389 | $result = civicrm_api('MailingGroup', 'event_subscribe', $params); | |
390 | break; | |
391 | ||
392 | case 'u': | |
393 | case 'unsubscribe': | |
394 | $params = array( | |
395 | 'job_id' => $job, | |
396 | 'event_queue_id' => $queue, | |
397 | 'hash' => $hash, | |
398 | 'version' => 3, | |
399 | ); | |
400 | $result = civicrm_api('MailingGroup', 'event_unsubscribe', $params); | |
401 | break; | |
402 | } | |
403 | ||
404 | if ($result['is_error']) { | |
405 | echo "Failed Processing: {$mail->subject}, Action: $action, Job ID: $job, Queue ID: $queue, Hash: $hash. Reason: {$result['error_message']}\n"; | |
406 | } | |
407 | else { | |
408 | CRM_Utils_Hook::emailProcessor('mailing', $params, $mail, $result, $action); | |
409 | } | |
410 | } | |
411 | ||
412 | $store->markProcessed($key); | |
413 | } | |
414 | // CRM-7356 – used by IMAP only | |
415 | $store->expunge(); | |
416 | } | |
417 | } | |
418 | } |