Merge pull request #15958 from civicrm/5.20
[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 *
326 * @return array
327 */
328 public static function parseMailingObject(&$mail) {
329
330 $config = CRM_Core_Config::singleton();
331
332 // get ready for collecting data about this email
333 // and put it in a standardized format
334 $params = ['is_error' => 0];
335
336 // Sometimes $mail->from is unset because ezcMail didn't handle format
337 // of From header. CRM-19215.
338 if (!isset($mail->from)) {
339 if (preg_match('/^([^ ]*)( (.*))?$/', $mail->getHeader('from'), $matches)) {
340 $mail->from = new ezcMailAddress($matches[1], trim($matches[2]));
341 }
342 }
343
344 $params['from'] = [];
345 self::parseAddress($mail->from, $field, $params['from'], $mail);
346
347 // we definitely need a contact id for the from address
348 // if we dont have one, skip this email
349 if (empty($params['from']['id'])) {
350 return NULL;
351 }
352
353 $emailFields = ['to', 'cc', 'bcc'];
354 foreach ($emailFields as $field) {
355 $value = $mail->$field;
356 self::parseAddresses($value, $field, $params, $mail);
357 }
358
359 // define other parameters
360 $params['subject'] = $mail->subject;
361 $params['date'] = date("YmdHi00",
362 strtotime($mail->getHeader("Date"))
363 );
364 $attachments = [];
365 $params['body'] = self::formatMailPart($mail->body, $attachments);
366
367 // format and move attachments to the civicrm area
368 if (!empty($attachments)) {
369 $date = date('YmdHis');
370 $config = CRM_Core_Config::singleton();
371 for ($i = 0; $i < count($attachments); $i++) {
372 $attachNum = $i + 1;
373 $fileName = basename($attachments[$i]['fullName']);
374 $newName = CRM_Utils_File::makeFileName($fileName);
375 $location = $config->uploadDir . $newName;
376
377 // move file to the civicrm upload directory
378 rename($attachments[$i]['fullName'], $location);
379
380 $mimeType = "{$attachments[$i]['contentType']}/{$attachments[$i]['mimeType']}";
381
382 $params["attachFile_$attachNum"] = [
383 'uri' => $fileName,
384 'type' => $mimeType,
385 'upload_date' => $date,
386 'location' => $location,
387 ];
388 }
389 }
390
391 return $params;
392 }
393
394 /**
395 * @param $address
396 * @param array $params
397 * @param $subParam
398 * @param $mail
399 */
400 public static function parseAddress(&$address, &$params, &$subParam, &$mail) {
401 // CRM-9484
402 if (empty($address->email)) {
403 return;
404 }
405
406 $subParam['email'] = $address->email;
407 $subParam['name'] = $address->name;
408
409 $contactID = self::getContactID($subParam['email'],
410 $subParam['name'],
411 TRUE,
412 $mail
413 );
414 $subParam['id'] = $contactID ? $contactID : NULL;
415 }
416
417 /**
418 * @param $addresses
419 * @param $token
420 * @param array $params
421 * @param $mail
422 */
423 public static function parseAddresses(&$addresses, $token, &$params, &$mail) {
424 $params[$token] = [];
425
426 foreach ($addresses as $address) {
427 $subParam = [];
428 self::parseAddress($address, $params, $subParam, $mail);
429 $params[$token][] = $subParam;
430 }
431 }
432
433 /**
434 * Retrieve a contact ID and if not present.
435 *
436 * Create one with this email
437 *
438 * @param string $email
439 * @param string $name
440 * @param bool $create
441 * @param string $mail
442 *
443 * @return int|null
444 */
445 public static function getContactID($email, $name = NULL, $create = TRUE, &$mail) {
446 $dao = CRM_Contact_BAO_Contact::matchContactOnEmail($email, 'Individual');
447
448 $contactID = NULL;
449 if ($dao) {
450 $contactID = $dao->contact_id;
451 }
452
453 $result = NULL;
454 CRM_Utils_Hook::emailProcessorContact($email, $contactID, $result);
455
456 if (!empty($result)) {
457 if ($result['action'] == self::EMAILPROCESSOR_IGNORE) {
458 return NULL;
459 }
460 if ($result['action'] == self::EMAILPROCESSOR_OVERRIDE) {
461 return $result['contactID'];
462 }
463
464 // else this is now create individual
465 // so we just fall out and do what we normally do
466 }
467
468 if ($contactID) {
469 return $contactID;
470 }
471
472 if (!$create) {
473 return NULL;
474 }
475
476 // contact does not exist, lets create it
477 $params = [
478 'contact_type' => 'Individual',
479 'email-Primary' => $email,
480 ];
481
482 CRM_Utils_String::extractName($name, $params);
483
484 return CRM_Contact_BAO_Contact::createProfileContact($params);
485 }
486
487 }