Passed
Push — developer ( 4e3135...f5c82a )
by Radosław
30:25 queued 12:59
created

Imap::getEmail()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
dl 0
loc 8
rs 10
c 1
b 0
f 0
cc 3
nc 2
nop 1
1
<?php
2
/**
3
 * Mail outlook message file.
4
 *
5
 * @package App
6
 *
7
 * @copyright YetiForce S.A.
8
 * @license   YetiForce Public License 5.0 (licenses/LicenseEN.txt or yetiforce.com)
9
 * @author    Radosław Skrzypczak <[email protected]>
10
 */
11
12
namespace App\Mail\Message;
13
14
/**
15
 * Mail outlook message class.
16
 */
17
class Imap extends Base
18
{
19
	/**
20
	 * Scanner engine name.
21
	 *
22
	 * @var string
23
	 */
24
	public $name = 'Imap';
25
	public $processData = [];
26
	protected $actions = [];
27
	protected $body;
28
	protected $mailCrmId;
29
	protected $documents = [];
30
	protected $attachments = [];
31
	/**
32
	 * @var int Mail type
33
	 *
34
	 * @see self::MAIL_TYPES,
35
	 */
36
	protected $mailType;
37
38
	/**
39
	 * Get instance by crm mail ID.
40
	 *
41
	 * @param int $crmId
42
	 *
43
	 * @return \self
0 ignored issues
show
Bug introduced by
The type self was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
44
	 */
45
	public static function getInstanceById(int $crmId)
46
	{
47
		$recordModel = \Vtiger_Record_Model::getInstanceById($crmId);
48
		$instance = new static();
49
		$instance->set('uid', $recordModel->get('uid'))
50
			->set('date', $recordModel->get('date'))
51
			->set('from', explode(',', $recordModel->get('from_email')))
52
			->set('to', explode(',', $recordModel->get('to_email')))
53
			->set('cc', array_filter(explode(',', $recordModel->get('cc_email'))))
54
			->set('bcc', array_filter(explode(',', $recordModel->get('bcc_email'))))
55
			->set('reply_to', array_filter(explode(',', $recordModel->get('reply_to_email'))))
56
			->set('cid', $recordModel->get('cid'));
57
		$instance->mailType = $recordModel->get('type');
58
		$instance->mailCrmId = $crmId;
59
		$instance->body = $recordModel->get('content');
60
61
		return $instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $instance returns the type App\Mail\Message\Imap which is incompatible with the documented return type self.
Loading history...
62
	}
63
64
	/**
65
	 * Set third-party message object.
66
	 *
67
	 * @param object $message
68
	 *
69
	 * @return $this
70
	 */
71
	public function setMessage($message)
72
	{
73
		$this->message = $message;
0 ignored issues
show
Bug Best Practice introduced by
The property message does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
74
		return $this;
75
	}
76
77
	/** {@inheritdoc} */
78
	public function getMailCrmId(int $mailAccountId)
79
	{
80
		if (!$this->mailCrmId) {
81
			if (empty($this->getMsgId()) || \Config\Modules\OSSMailScanner::$ONE_MAIL_FOR_MULTIPLE_RECIPIENTS) {
0 ignored issues
show
Bug introduced by
The type Config\Modules\OSSMailScanner was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
82
				$query = (new \App\Db\Query())->select(['ossmailviewid'])->from('vtiger_ossmailview')->where(['cid' => $this->getUniqueId()])->limit(1);
83
			} else {
84
				$queryGenerator = new \App\QueryGenerator('OSSMailView');
85
				$queryGenerator->permissions = false;
86
				$query = $queryGenerator->setFields(['id'])->addNativeCondition(['vtiger_ossmailview.cid' => $this->getUniqueId()])
87
					->addCondition('rc_user', $mailAccountId, 'e')->setLimit(1)->createQuery();
88
			}
89
			$this->mailCrmId = $query->scalar() ?: null;
90
		}
91
92
		return $this->mailCrmId;
93
	}
94
95
	/**
96
	 * Find crm ID by cid.
97
	 *
98
	 * @return int|null
99
	 */
100
	public function getMailCrmIdByCid()
101
	{
102
		return (new \App\Db\Query())->select(['ossmailviewid'])->from('vtiger_ossmailview')->where(['cid' => $this->getUniqueId()])->limit(1)->scalar() ?: null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return new App\Db\Query(...it(1)->scalar() ?: null also could return the type string which is incompatible with the documented return type integer|null.
Loading history...
103
	}
104
105
	/**
106
	 * Set mail record ID.
107
	 *
108
	 * @param int $mailCrmId
109
	 *
110
	 * @return $this
111
	 */
112
	public function setMailCrmId(int $mailCrmId)
113
	{
114
		$this->mailCrmId = $mailCrmId;
115
		return $this;
116
	}
117
118
	/**
119
	 * Set process data.
120
	 *
121
	 * @param string $action
122
	 * @param mixed  $value
123
	 *
124
	 * @return $this
125
	 */
126
	public function setProcessData(string $action, $value)
127
	{
128
		$this->processData[$action] = $value;
129
		return $this;
130
	}
131
132
	/**
133
	 * Get process data.
134
	 *
135
	 * @param string $action
136
	 *
137
	 * @return mixed
138
	 */
139
	public function getProcessData(string $action = '')
140
	{
141
		return $action ? $this->processData[$action] ?? [] : [];
142
	}
143
144
	/**
145
	 * Generation crm unique id.
146
	 *
147
	 * @return string
148
	 */
149
	public function getUniqueId()
150
	{
151
		if (!$this->has('cid')) {
152
			$uid = hash('sha256', implode(',', $this->getEmail('from')) . '|' . $this->getDate() . '|' . $this->getSubject() . '|' . $this->getMsgId());
153
			$this->set('cid', $uid);
154
		}
155
156
		return $this->get('cid');
157
	}
158
159
	/**
160
	 * Get message_id from header.
161
	 *
162
	 * @return string
163
	 */
164
	public function getMsgId(): string
165
	{
166
		if (!$this->has('message_id')) {
167
			$attr = $this->message->header->get('message_id');
168
			$this->set('message_id', $attr ? $attr->first() : '');
169
		}
170
171
		return $this->get('message_id');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('message_id') could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
172
	}
173
174
	/**
175
	 * Get uid.
176
	 *
177
	 * @return int
178
	 */
179
	public function getMsgUid(): int
180
	{
181
		if (!$this->has('uid')) {
182
			$this->set('uid', $this->message->getUid());
183
		}
184
185
		return $this->get('uid');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('uid') could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
186
	}
187
188
	/**
189
	 * Get subject.
190
	 *
191
	 * @return string
192
	 */
193
	public function getSubject(): string
194
	{
195
		if (!$this->has('subject')) {
196
			$this->set('subject', $this->getHeader('subject'));
197
		}
198
199
		return $this->get('subject');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('subject') could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
200
	}
201
202
	/**
203
	 * Get header data.
204
	 *
205
	 * @param string $key
206
	 *
207
	 * @return string
208
	 */
209
	public function getHeader(string $key): string
210
	{
211
		$attr = $this->message->header->get($key);
212
		return $attr ? $attr->__toString() : '';
213
	}
214
215
	/**
216
	 * Get emials by key.
217
	 *
218
	 * @param string $key
219
	 *
220
	 * @return array
221
	 */
222
	public function getEmail(string $key): array
223
	{
224
		if (!$this->has($key)) {
225
			$attr = $this->message->header->get($key);
226
			$this->set('uid', $attr ? array_map(fn ($data) => $data->mail, $attr->all()) : []);
227
		}
228
229
		return $this->get($key);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get($key) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
230
	}
231
232
	/**
233
	 * Get array header data by key.
234
	 *
235
	 * @param string $key
236
	 *
237
	 * @return array
238
	 */
239
	public function getHeaderAsArray(string $key): array
240
	{
241
		$attr = $this->message->header->get($key);
242
		return $attr ? $attr->toArray() : [];
243
	}
244
245
	/**
246
	 * Get header war data.
247
	 *
248
	 * @return string
249
	 */
250
	public function getHeaderRaw(): string
251
	{
252
		return $this->message->header->raw;
253
	}
254
255
	/**
256
	 * Get date.
257
	 *
258
	 * @return string
259
	 */
260
	public function getDate()
261
	{
262
		if (!$this->has('date')) {
263
			$attr = $this->message->header->get('date');
264
			$this->set('date', $attr ? $attr->toDate()->toDateTimeString() : '');
265
		}
266
267
		return $this->get('date');
268
	}
269
270
	/**
271
	 * Set a given flag.
272
	 *
273
	 * @param string $flag
274
	 *
275
	 * @return $this
276
	 */
277
	public function setFlag(string $flag)
278
	{
279
		$this->message->setFlag($flag);
280
		return $this;
281
	}
282
283
	/** {@inheritdoc} */
284
	public function getMailType(): int
285
	{
286
		if (null === $this->mailType) {
287
			$to = false;
288
			$from = (bool) \App\Mail\RecordFinder::findUserEmail($this->getEmail('from'));
289
			foreach (['to', 'cc', 'bcc'] as $header) {
290
				if ($emails = $this->getEmail($header)) {
291
					$to = (bool) \App\Mail\RecordFinder::findUserEmail($emails);
292
					break;
293
				}
294
			}
295
296
			$key = self::MAIL_TYPE_RECEIVED;
297
			if ($from && $to) {
298
				$key = self::MAIL_TYPE_INTERNAL;
299
			} elseif ($from) {
300
				$key = self::MAIL_TYPE_SENT;
301
			}
302
			$this->mailType = $key;
303
		}
304
305
		return $this->mailType;
306
	}
307
308
	/**
309
	 * Get first letter form email.
310
	 *
311
	 * @return void
312
	 */
313
	public function getFirstLetter()
314
	{
315
		return strtoupper(\App\TextUtils::textTruncate(trim(implode(',', $this->getEmail('from'))), 1, false));
0 ignored issues
show
Bug Best Practice introduced by
The expression return strtoupper(App\Te...l('from'))), 1, false)) returns the type string which is incompatible with the documented return type void.
Loading history...
316
	}
317
318
	/**
319
	 * Check if the Message has a html body.
320
	 *
321
	 * @return bool
322
	 */
323
	public function hasHTMLBody(): bool
324
	{
325
		return $this->message->hasHTMLBody();
326
	}
327
328
	/**
329
	 * Get the Message  body.
330
	 *
331
	 * @param bool $purify
332
	 *
333
	 * @return string
334
	 */
335
	public function getBody(bool $purify = true)
336
	{
337
		if (null === $this->body) {
338
			if ($this->hasHTMLBody()) {
339
				$this->body = $this->message->getHTMLBody();
340
				$this->parseBody();
341
			} else {
342
				$this->body = $this->message->getTextBody() ?? '';
343
			}
344
		}
345
346
		return $purify ? \App\Purifier::decodeHtml(\App\Purifier::purifyHtml($this->body)) : $this->body;
347
	}
348
349
	/**
350
	 * Get body raw.
351
	 *
352
	 * @return string
353
	 */
354
	public function getBodyRaw()
355
	{
356
		return $this->hasHTMLBody() ? $this->message->getHTMLBody() : $this->message->getTextBody();
357
	}
358
359
	/**
360
	 * Set body.
361
	 *
362
	 * @param string $body
363
	 *
364
	 * @return $this
365
	 */
366
	public function setBody(string $body)
367
	{
368
		$this->body = $body;
369
		return $this;
370
	}
371
372
	/**
373
	 * Get forlder name (utf8).
374
	 *
375
	 * @return string
376
	 */
377
	public function getFolderName(): string
378
	{
379
		return $this->message->getFolder()->full_name;
380
	}
381
382
	/**
383
	 * Treatment mail content with all images and unnecessary trash.
384
	 */
385
	private function parseBody()
386
	{
387
		$html = $this->body;
388
		$html = preg_replace(
389
			[':<(head|style|script).+?</\1>:is', // remove <head>, <styleand <scriptsections
390
				':<!\[[^]<]+\]>:', // remove <![if !mso]and friends
391
				':<!DOCTYPE[^>]+>:', // remove <!DOCTYPE ... >
392
				':<\?[^>]+>:', // remove <?xml version="1.0" ... >
393
				'~</?html[^>]*>~', // remove html tags
394
				'~</?body[^>]*>~', // remove body tags
395
				'~</?o:[^>]*>~', // remove mso tags
396
				'~\sclass=[\'|\"][^\'\"]+[\'|\"]~i', // remove class attributes
397
			], ['', '', '', '', '', '', '', ''], $html);
398
		$doc = new \DOMDocument('1.0', 'UTF-8');
399
		$previousValue = libxml_use_internal_errors(true);
400
		$doc->loadHTML('<?xml encoding="utf-8"?>' . $html);
401
		libxml_clear_errors();
402
		libxml_use_internal_errors($previousValue);
403
404
		$imgs = $doc->getElementsByTagName('img');
405
		$lenght = $imgs->length;
406
		while ($imgs->length && $lenght) {
407
			--$lenght;
408
			$this->getFileFromImage($imgs->item(0));
409
		}
410
411
		$previousValue = libxml_use_internal_errors(true);
412
		$html = $doc->saveHTML();
413
		libxml_clear_errors();
414
		libxml_use_internal_errors($previousValue);
415
		$html = str_replace('<?xml encoding="utf-8"?>', '', $html);
416
417
		$this->body = $html;
418
	}
419
420
	/**
421
	 * Check if mail has attachments.
422
	 *
423
	 * @return bool
424
	 */
425
	public function hasAttachments(): bool
426
	{
427
		$this->getBody(false);
428
		return !empty($this->files) || $this->message->hasAttachments();
429
	}
430
431
	/**
432
	 * Get attachments.
433
	 *
434
	 * @return array
435
	 */
436
	public function getAttachments(): array
437
	{
438
		foreach ($this->message->getAttachments() as $attachment) {
439
			$this->files[$attachment->id] = \App\Fields\File::loadFromContent($attachment->getContent(), $attachment->getName(), ['validateAllCodeInjection' => true, 'id' => $attachment->id]);
0 ignored issues
show
Bug Best Practice introduced by
The property files does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
440
			$this->message->getAttachments()->forget($attachment->id);
441
		}
442
443
		return $this->files;
444
	}
445
446
	/**
447
	 * Get documents.
448
	 *
449
	 * @return array
450
	 */
451
	public function getDocuments(): array
452
	{
453
		return $this->documents;
454
	}
455
456
	/**
457
	 * Add attachemtns to CRM.
458
	 *
459
	 * @param array $docData
460
	 *
461
	 * @return void
462
	 */
463
	public function saveAttachments(array $docData)
464
	{
465
		$this->getBody(false);
466
		$useTime = $this->getDate();
467
		$userId = \App\User::getCurrentUserRealId();
468
469
		$params = array_merge([
470
			'created_user_id' => $userId,
471
			'assigned_user_id' => $userId,
472
			'modifiedby' => $userId,
473
			'createdtime' => $useTime,
474
			'modifiedtime' => $useTime,
475
			'folderid' => 'T2',
476
		], $docData);
477
478
		$maxSize = \App\Config::getMaxUploadSize();
479
		foreach ($this->getAttachments() as $key => $file) {
480
			if ($maxSize < ($size = $file->getSize())) {
481
				\App\Log::error("Error - downloaded the file is too big '{$file->getName()}', size: {$size}, in mail: {$this->getDate()} | Folder: {$this->getFolderName()} | ID: {$this->getMsgUid()}", __CLASS__);
482
				continue;
483
			}
484
			if ($file->validateAndSecure() && ($id = \App\Fields\File::saveFromContent($file, $params))) {
485
				$this->documents[$key] = $id;
486
				$this->setBody(str_replace(["crm-id=\"{$key}\"", "attachment-id=\"{$key}\""], ["crm-id=\"{$id['crmid']}\"", "attachment-id=\"{$id['attachmentsId']}\""], $this->getBody(false)));
487
			} else {
488
				\App\Log::error("Error downloading the file '{$file->getName()}' in mail: {$this->getDate()} | Folder: {$this->getFolderName()} | ID: {$this->getMsgUid()}", __CLASS__);
489
			}
490
		}
491
	}
492
493
	/**
494
	 * Get file from image.
495
	 *
496
	 * @param DOMElement $element
0 ignored issues
show
Bug introduced by
The type App\Mail\Message\DOMElement was not found. Did you mean DOMElement? If so, make sure to prefix the type with \.
Loading history...
497
	 *
498
	 * @return array
499
	 */
500
	private function getFileFromImage(\DOMElement $element)
501
	{
502
		$src = trim($element->getAttribute('src'), '\'');
503
		$element->removeAttribute('src');
504
		$file = [];
505
		if ('data:' === substr($src, 0, 5)) {
506
			$file = \App\Fields\File::saveFromString($src, ['validateAllowedFormat' => 'image']);
507
		} elseif (filter_var($src, FILTER_VALIDATE_URL)) {
508
			if (\Config\Modules\OSSMailScanner::$attachMailBodyGraphicUrl ?? true) {
509
				$file = \App\Fields\File::loadFromUrl($src, ['validateAllowedFormat' => 'image']);
510
				if (!$file->validateAndSecure()) {
511
					$file = [];
512
				}
513
			} else {
514
				$file = ['url' => $src];
515
			}
516
		} elseif ('cid:' === substr($src, 0, 4)) {
517
			$src = substr($src, 4);
518
			if ($this->message->getAttachments()->has($src)) {
519
				$attachment = $this->message->getAttachments()->get($src);
520
				$fileInstance = \App\Fields\File::loadFromContent($attachment->getContent(), $attachment->getName(), ['validateAllowedFormat' => 'image']);
521
				if ($fileInstance && $fileInstance->validateAndSecure()) {
522
					$file = $fileInstance;
523
					$this->message->getAttachments()->forget($src);
524
				}
525
			} else {
526
				\App\Log::warning("There is no attachment with ID: $src , in mail: {$this->getDate()} | Folder: {$this->getFolderName()} | ID: {$this->message->getMsgUid()}", __CLASS__);
527
			}
528
		} else {
529
			\App\Log::warning("Unsupported photo type, requires verification. ID: $src , in mail: {$this->getDate()} | Folder: {$this->getFolderName()} | ID: {$this->message->getMsgUid()}", __CLASS__);
530
		}
531
		if ($file) {
532
			$yetiforceTag = $element->ownerDocument->createElement('yetiforce');
0 ignored issues
show
Bug introduced by
The method createElement() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

532
			/** @scrutinizer ignore-call */ 
533
   $yetiforceTag = $element->ownerDocument->createElement('yetiforce');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
533
			if ($file instanceof \App\Fields\File) {
534
				$key = sha1($file->getPath());
535
				$yetiforceTag->setAttribute('type', 'Documents');
536
				$yetiforceTag->setAttribute('crm-id', $key);
537
				$yetiforceTag->setAttribute('attachment-id', $key);
538
				if ($element->hasAttribute('width')) {
539
					$yetiforceTag->setAttribute('width', \App\Purifier::encodeHtml($element->getAttribute('width')));
540
				}
541
				if ($element->hasAttribute('height')) {
542
					$yetiforceTag->setAttribute('height', \App\Purifier::encodeHtml($element->getAttribute('height')));
543
				}
544
				$this->files[$key] = $file;
0 ignored issues
show
Bug Best Practice introduced by
The property files does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
545
			} else {
546
				$yetiforceTag->textContent = $file['url'];
547
			}
548
		} else {
549
			$yetiforceTag = $element->cloneNode(true);
550
		}
551
552
		$element->parentNode->replaceChild($yetiforceTag, $element);
0 ignored issues
show
Bug introduced by
The method replaceChild() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

552
		$element->parentNode->/** @scrutinizer ignore-call */ 
553
                        replaceChild($yetiforceTag, $element);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
553
	}
554
}
555