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