Merge pull request #14955 from eileenmcnaughton/rel_alok_ref
[civicrm-core.git] / CRM / Utils / Mail / Incoming.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
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
31 * @copyright CiviCRM LLC (c) 2004-2019
32 */
33 class CRM_Utils_Mail_Incoming {
34 const
35 EMAILPROCESSOR_CREATE_INDIVIDUAL = 1,
36 EMAILPROCESSOR_OVERRIDE = 2,
37 EMAILPROCESSOR_IGNORE = 3;
38
39 /**
40 * @param $mail
41 * @param $attachments
42 *
43 * @return string
44 */
45 public static function formatMail($mail, &$attachments) {
46 $t = '';
47 $t .= "From: " . self::formatAddress($mail->from) . "\n";
48 $t .= "To: " . self::formatAddresses($mail->to) . "\n";
49 $t .= "Cc: " . self::formatAddresses($mail->cc) . "\n";
50 $t .= "Bcc: " . self::formatAddresses($mail->bcc) . "\n";
51 $t .= 'Date: ' . date(DATE_RFC822, $mail->timestamp) . "\n";
52 $t .= 'Subject: ' . $mail->subject . "\n";
53 $t .= "MessageId: " . $mail->messageId . "\n";
54 $t .= "\n";
55 $t .= self::formatMailPart($mail->body, $attachments);
56 return $t;
57 }
58
59 /**
60 * @param $part
61 * @param $attachments
62 *
63 * @throws Exception
64 */
65 public static function formatMailPart($part, &$attachments) {
66 if ($part instanceof ezcMail) {
67 return self::formatMail($part, $attachments);
68 }
69
70 if ($part instanceof ezcMailText) {
71 return self::formatMailText($part, $attachments);
72 }
73
74 if ($part instanceof ezcMailFile) {
75 return self::formatMailFile($part, $attachments);
76 }
77
78 if ($part instanceof ezcMailRfc822Digest) {
79 return self::formatMailRfc822Digest($part, $attachments);
80 }
81
82 if ($part instanceof ezcMailMultiPart) {
83 return self::formatMailMultipart($part, $attachments);
84 }
85
86 if ($part instanceof ezcMailDeliveryStatus) {
87 return self::formatMailDeliveryStatus($part);
88 }
89
90 // CRM-19111 - Handle blank emails with a subject.
91 if (!$part) {
92 return NULL;
93 }
94
95 return self::formatMailUnrecognisedPart($part);
96 }
97
98 /**
99 * @param $part
100 * @param $attachments
101 *
102 * @throws Exception
103 */
104 public static function formatMailMultipart($part, &$attachments) {
105 if ($part instanceof ezcMailMultiPartAlternative) {
106 return self::formatMailMultipartAlternative($part, $attachments);
107 }
108
109 if ($part instanceof ezcMailMultiPartDigest) {
110 return self::formatMailMultipartDigest($part, $attachments);
111 }
112
113 if ($part instanceof ezcMailMultiPartRelated) {
114 return self::formatMailMultipartRelated($part, $attachments);
115 }
116
117 if ($part instanceof ezcMailMultiPartMixed) {
118 return self::formatMailMultipartMixed($part, $attachments);
119 }
120
121 if ($part instanceof ezcMailMultipartReport) {
122 return self::formatMailMultipartReport($part, $attachments);
123 }
124
125 if ($part instanceof ezcMailDeliveryStatus) {
126 return self::formatMailDeliveryStatus($part);
127 }
128
129 return self::formatMailUnrecognisedPart($part);
130 }
131
132 /**
133 * @param $part
134 * @param $attachments
135 *
136 * @return string
137 */
138 public static function formatMailMultipartMixed($part, &$attachments) {
139 $t = '';
140 foreach ($part->getParts() as $key => $alternativePart) {
141 $t .= self::formatMailPart($alternativePart, $attachments);
142 }
143 return $t;
144 }
145
146 /**
147 * @param $part
148 * @param $attachments
149 *
150 * @return string
151 */
152 public static function formatMailMultipartRelated($part, &$attachments) {
153 $t = '';
154 $t .= "-RELATED MAIN PART-\n";
155 $t .= self::formatMailPart($part->getMainPart(), $attachments);
156 foreach ($part->getRelatedParts() as $key => $alternativePart) {
157 $t .= "-RELATED PART $key-\n";
158 $t .= self::formatMailPart($alternativePart, $attachments);
159 }
160 $t .= "-RELATED END-\n";
161 return $t;
162 }
163
164 /**
165 * @param $part
166 * @param $attachments
167 *
168 * @return string
169 */
170 public static function formatMailMultipartDigest($part, &$attachments) {
171 $t = '';
172 foreach ($part->getParts() as $key => $alternativePart) {
173 $t .= "-DIGEST-$key-\n";
174 $t .= self::formatMailPart($alternativePart, $attachments);
175 }
176 $t .= "-DIGEST END---\n";
177 return $t;
178 }
179
180 /**
181 * @param $part
182 * @param $attachments
183 *
184 * @return string
185 */
186 public static function formatMailRfc822Digest($part, &$attachments) {
187 $t = '';
188 $t .= "-DIGEST-ITEM-\n";
189 $t .= "Item:\n\n";
190 $t .= self::formatMailpart($part->mail, $attachments);
191 $t .= "-DIGEST ITEM END-\n";
192 return $t;
193 }
194
195 /**
196 * @param $part
197 * @param $attachments
198 *
199 * @return string
200 */
201 public static function formatMailMultipartAlternative($part, &$attachments) {
202 $t = '';
203 foreach ($part->getParts() as $key => $alternativePart) {
204 $t .= "-ALTERNATIVE ITEM $key-\n";
205 $t .= self::formatMailPart($alternativePart, $attachments);
206 }
207 $t .= "-ALTERNATIVE END-\n";
208 return $t;
209 }
210
211 /**
212 * @param $part
213 * @param $attachments
214 *
215 * @return string
216 */
217 public static function formatMailText($part, &$attachments) {
218 $t = "\n{$part->text}\n";
219 return $t;
220 }
221
222 /**
223 * @param $part
224 * @param $attachments
225 *
226 * @return string
227 */
228 public static function formatMailMultipartReport($part, &$attachments) {
229 $t = '';
230 foreach ($part->getParts() as $key => $reportPart) {
231 $t .= "-REPORT-$key-\n";
232 $t .= self::formatMailPart($reportPart, $attachments);
233 }
234 $t .= "-REPORT END---\n";
235 return $t;
236 }
237
238 /**
239 * @param $part
240 *
241 * @return string
242 */
243 public static function formatMailDeliveryStatus($part) {
244 $t = '';
245 $t .= "-DELIVERY STATUS BEGIN-\n";
246 $t .= $part->generateBody();
247 $t .= "-DELIVERY STATUS END-\n";
248 return $t;
249 }
250
251 /**
252 * @param $part
253 *
254 * @return string
255 */
256 public function formatUnrecognisedPart($part) {
257 CRM_Core_Error::debug_log_message(ts('CRM_Utils_Mail_Incoming: Unable to handle message part of type "%1".', [1 => get_class($part)]));
258 return ts('Unrecognised message part of type "%1".', [1 => get_class($part)]);
259 }
260
261 /**
262 * @param $part
263 * @param $attachments
264 *
265 * @return null
266 */
267 public static function formatMailFile($part, &$attachments) {
268 $attachments[] = [
269 'dispositionType' => $part->dispositionType,
270 'contentType' => $part->contentType,
271 'mimeType' => $part->mimeType,
272 'contentID' => $part->contentId,
273 'fullName' => $part->fileName,
274 ];
275 return NULL;
276 }
277
278 /**
279 * @param $addresses
280 *
281 * @return string
282 */
283 public static function formatAddresses($addresses) {
284 $fa = [];
285 foreach ($addresses as $address) {
286 $fa[] = self::formatAddress($address);
287 }
288 return implode(', ', $fa);
289 }
290
291 /**
292 * @param $address
293 *
294 * @return string
295 */
296 public static function formatAddress($address) {
297 $name = '';
298 if (!empty($address->name)) {
299 $name = "{$address->name} ";
300 }
301 return $name . "<{$address->email}>";
302 }
303
304 /**
305 * @param $file
306 *
307 * @return array
308 * @throws Exception
309 */
310 public function &parse(&$file) {
311
312 // check that the file exists and has some content
313 if (!file_exists($file) ||
314 !trim(file_get_contents($file))
315 ) {
316 return CRM_Core_Error::createAPIError(ts('%1 does not exists or is empty',
317 [1 => $file]
318 ));
319 }
320
321 // explode email to digestable format
322 $set = new ezcMailFileSet([$file]);
323 $parser = new ezcMailParser();
324 $mail = $parser->parseMail($set);
325
326 if (!$mail) {
327 return CRM_Core_Error::createAPIError(ts('%1 could not be parsed',
328 [1 => $file]
329 ));
330 }
331
332 // since we only have one fileset
333 $mail = $mail[0];
334
335 $mailParams = self::parseMailingObject($mail);
336 return $mailParams;
337 }
338
339 /**
340 * @param $mail
341 *
342 * @return array
343 */
344 public static function parseMailingObject(&$mail) {
345
346 $config = CRM_Core_Config::singleton();
347
348 // get ready for collecting data about this email
349 // and put it in a standardized format
350 $params = ['is_error' => 0];
351
352 // Sometimes $mail->from is unset because ezcMail didn't handle format
353 // of From header. CRM-19215.
354 if (!isset($mail->from)) {
355 if (preg_match('/^([^ ]*)( (.*))?$/', $mail->getHeader('from'), $matches)) {
356 $mail->from = new ezcMailAddress($matches[1], trim($matches[2]));
357 }
358 }
359
360 $params['from'] = [];
361 self::parseAddress($mail->from, $field, $params['from'], $mail);
362
363 // we definitely need a contact id for the from address
364 // if we dont have one, skip this email
365 if (empty($params['from']['id'])) {
366 return NULL;
367 }
368
369 $emailFields = ['to', 'cc', 'bcc'];
370 foreach ($emailFields as $field) {
371 $value = $mail->$field;
372 self::parseAddresses($value, $field, $params, $mail);
373 }
374
375 // define other parameters
376 $params['subject'] = $mail->subject;
377 $params['date'] = date("YmdHi00",
378 strtotime($mail->getHeader("Date"))
379 );
380 $attachments = [];
381 $params['body'] = self::formatMailPart($mail->body, $attachments);
382
383 // format and move attachments to the civicrm area
384 if (!empty($attachments)) {
385 $date = date('YmdHis');
386 $config = CRM_Core_Config::singleton();
387 for ($i = 0; $i < count($attachments); $i++) {
388 $attachNum = $i + 1;
389 $fileName = basename($attachments[$i]['fullName']);
390 $newName = CRM_Utils_File::makeFileName($fileName);
391 $location = $config->uploadDir . $newName;
392
393 // move file to the civicrm upload directory
394 rename($attachments[$i]['fullName'], $location);
395
396 $mimeType = "{$attachments[$i]['contentType']}/{$attachments[$i]['mimeType']}";
397
398 $params["attachFile_$attachNum"] = [
399 'uri' => $fileName,
400 'type' => $mimeType,
401 'upload_date' => $date,
402 'location' => $location,
403 ];
404 }
405 }
406
407 return $params;
408 }
409
410 /**
411 * @param $address
412 * @param array $params
413 * @param $subParam
414 * @param $mail
415 */
416 public static function parseAddress(&$address, &$params, &$subParam, &$mail) {
417 // CRM-9484
418 if (empty($address->email)) {
419 return;
420 }
421
422 $subParam['email'] = $address->email;
423 $subParam['name'] = $address->name;
424
425 $contactID = self::getContactID($subParam['email'],
426 $subParam['name'],
427 TRUE,
428 $mail
429 );
430 $subParam['id'] = $contactID ? $contactID : NULL;
431 }
432
433 /**
434 * @param $addresses
435 * @param $token
436 * @param array $params
437 * @param $mail
438 */
439 public static function parseAddresses(&$addresses, $token, &$params, &$mail) {
440 $params[$token] = [];
441
442 foreach ($addresses as $address) {
443 $subParam = [];
444 self::parseAddress($address, $params, $subParam, $mail);
445 $params[$token][] = $subParam;
446 }
447 }
448
449 /**
450 * Retrieve a contact ID and if not present.
451 *
452 * Create one with this email
453 *
454 * @param string $email
455 * @param string $name
456 * @param bool $create
457 * @param string $mail
458 *
459 * @return int|null
460 */
461 public static function getContactID($email, $name = NULL, $create = TRUE, &$mail) {
462 $dao = CRM_Contact_BAO_Contact::matchContactOnEmail($email, 'Individual');
463
464 $contactID = NULL;
465 if ($dao) {
466 $contactID = $dao->contact_id;
467 }
468
469 $result = NULL;
470 CRM_Utils_Hook::emailProcessorContact($email, $contactID, $result);
471
472 if (!empty($result)) {
473 if ($result['action'] == self::EMAILPROCESSOR_IGNORE) {
474 return NULL;
475 }
476 if ($result['action'] == self::EMAILPROCESSOR_OVERRIDE) {
477 return $result['contactID'];
478 }
479
480 // else this is now create individual
481 // so we just fall out and do what we normally do
482 }
483
484 if ($contactID) {
485 return $contactID;
486 }
487
488 if (!$create) {
489 return NULL;
490 }
491
492 // contact does not exist, lets create it
493 $params = [
494 'contact_type' => 'Individual',
495 'email-Primary' => $email,
496 ];
497
498 CRM_Utils_String::extractName($name, $params);
499
500 return CRM_Contact_BAO_Contact::createProfileContact($params);
501 }
502
503 }