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