OSSMail_Mail_Model::getFolder()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * Mail Scanner bind email action.
4
 *
5
 * @package Model
6
 *
7
 * @copyright YetiForce S.A.
8
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
9
 * @author    Mariusz Krzaczkowski <[email protected]>
10
 * @author    Radosław Skrzypczak <[email protected]>
11
 */
12
13
/**
14
 * Mail Scanner bind email action.
15
 */
16
class OSSMail_Mail_Model extends \App\Base
17
{
18
	/** @var string[] Ignored mail addresses */
19
	public const IGNORED_MAILS = ['@', 'undisclosed-recipients',  'Undisclosed-recipients', 'undisclosed-recipients@', 'Undisclosed-recipients@', 'Undisclosed recipients@,@', 'undisclosed recipients@,@'];
20
21
	/** @var array Mail account. */
22
	protected $mailAccount = [];
23
24
	/** @var string Mail folder. */
25
	protected $mailFolder = '';
26
27
	/** @var bool|int Mail crm id. */
28
	protected $mailCrmId = false;
29
30
	/** @var array Action result. */
31
	protected $actionResult = [];
32
33
	/** @var int Mail type. */
34
	protected $mailType;
35
36
	/**
37
	 * Set account.
38
	 *
39
	 * @param array $account
40
	 */
41
	public function setAccount($account)
42
	{
43
		$this->mailAccount = $account;
44
	}
45
46
	/**
47
	 * Set folder.
48
	 *
49
	 * @param string $folder
50
	 */
51
	public function setFolder($folder)
52
	{
53
		$this->mailFolder = $folder;
54
	}
55
56
	/**
57
	 * Add action result.
58
	 *
59
	 * @param string $type
60
	 * @param string $result
61
	 */
62
	public function addActionResult($type, $result)
63
	{
64
		$this->actionResult[$type] = $result;
65
	}
66
67
	/**
68
	 * Get account.
69
	 *
70
	 * @return array
71
	 */
72
	public function getAccount()
73
	{
74
		return $this->mailAccount;
75
	}
76
77
	/**
78
	 * Get folder.
79
	 *
80
	 * @return string
81
	 */
82
	public function getFolder()
83
	{
84
		return $this->mailFolder;
85
	}
86
87
	/**
88
	 * Get action result.
89
	 *
90
	 * @param string $action
91
	 *
92
	 * @return array
93
	 */
94
	public function getActionResult($action = false)
95
	{
96
		if ($action && isset($this->actionResult[$action])) {
97
			return $this->actionResult[$action];
98
		}
99
		return $this->actionResult;
100
	}
101
102
	/**
103
	 * Get type email.
104
	 *
105
	 * @param bool $returnText
106
	 *
107
	 * @return int|string
108
	 */
109
	public function getTypeEmail($returnText = false)
110
	{
111
		if (isset($this->mailType)) {
112
			if ($returnText) {
113
				$cacheKey = 'Received';
114
				switch ($this->mailType) {
115
					case 0:
116
						$cacheKey = 'Sent';
117
						break;
118
					case 2:
119
						$cacheKey = 'Internal';
120
						break;
121
				}
122
				return $cacheKey;
123
			}
124
			return $this->mailType;
125
		}
126
		$account = $this->getAccount();
127
		$fromEmailUser = $this->findEmailUser($this->get('from_email'));
128
		$toEmailUser = $this->findEmailUser($this->get('to_email'));
129
		$ccEmailUser = $this->findEmailUser($this->get('cc_email'));
130
		$bccEmailUser = $this->findEmailUser($this->get('bcc_email'));
131
		$existIdentitie = false;
132
		foreach (OSSMailScanner_Record_Model::getIdentities($account['user_id']) as $identitie) {
133
			if ($identitie['email'] == $this->get('from_email')) {
134
				$existIdentitie = true;
135
			}
136
		}
137
		if ($fromEmailUser && ($toEmailUser || $ccEmailUser || $bccEmailUser)) {
138
			$key = 2;
139
			$cacheKey = 'Internal';
140
		} elseif ($existIdentitie || $fromEmailUser) {
141
			$key = 0;
142
			$cacheKey = 'Sent';
143
		} else {
144
			$key = 1;
145
			$cacheKey = 'Received';
146
		}
147
		$this->mailType = $key;
148
		if ($returnText) {
149
			return $cacheKey;
150
		}
151
		return $key;
152
	}
153
154
	/**
155
	 * Find email user.
156
	 *
157
	 * @param string $emails
158
	 *
159
	 * @return bool
160
	 */
161
	public static function findEmailUser($emails)
162
	{
163
		$notFound = 0;
164
		if (!empty($emails)) {
165
			foreach (explode(',', $emails) as $email) {
166
				if (!\Users_Module_Model::checkMailExist($email)) {
167
					++$notFound;
168
				}
169
			}
170
		}
171
		return 0 === $notFound;
172
	}
173
174
	/**
175
	 * Get account owner.
176
	 *
177
	 * @return int
178
	 */
179
	public function getAccountOwner()
180
	{
181
		$account = $this->getAccount();
182
		if ($account['crm_user_id']) {
183
			return $account['crm_user_id'];
184
		}
185
		return \App\User::getCurrentUserId();
186
	}
187
188
	/**
189
	 * Generation crm unique id.
190
	 *
191
	 * @return string
192
	 */
193
	public function getUniqueId()
194
	{
195
		if ($this->has('cid')) {
196
			return $this->get('cid');
197
		}
198
		$uid = hash('sha256', $this->get('from_email') . '|' . $this->get('date') . '|' . $this->get('subject') . '|' . $this->get('message_id'));
199
		$this->set('cid', $uid);
200
		return $uid;
201
	}
202
203
	/**
204
	 * Get mail crm id.
205
	 *
206
	 * @return bool|int
207
	 */
208
	public function getMailCrmId()
209
	{
210
		if ($this->mailCrmId) {
211
			return $this->mailCrmId;
212
		}
213
		if (empty($this->get('message_id')) || \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...
214
			$query = (new \App\Db\Query())->select(['ossmailviewid'])->from('vtiger_ossmailview')->where(['cid' => $this->getUniqueId()])->limit(1);
215
		} else {
216
			$query = (new \App\Db\Query())->select(['ossmailviewid'])->from('vtiger_ossmailview')->where(['uid' => $this->get('message_id'), 'rc_user' => $this->getAccountOwner()])->limit(1);
217
		}
218
		return $this->mailCrmId = $query->scalar();
0 ignored issues
show
Documentation Bug introduced by
It seems like $query->scalar() can also be of type string. However, the property $mailCrmId is declared as type boolean|integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
Bug Best Practice introduced by
The expression return $this->mailCrmId = $query->scalar() also could return the type string which is incompatible with the documented return type boolean|integer.
Loading history...
219
	}
220
221
	/**
222
	 * Set mail crm id.
223
	 *
224
	 * @param int $id
225
	 */
226
	public function setMailCrmId($id)
227
	{
228
		$this->mailCrmId = $id;
229
	}
230
231
	/**
232
	 * Get email.
233
	 *
234
	 * @param string $cacheKey
235
	 *
236
	 * @return string
237
	 */
238
	public function getEmail($cacheKey)
239
	{
240
		$header = $this->get('header');
241
		$text = '';
242
		if (property_exists($header, $cacheKey)) {
243
			$text = $header->{$cacheKey};
244
		}
245
		$return = '';
246
		if (\is_array($text)) {
247
			foreach ($text as $row) {
248
				if ('' != $return) {
249
					$return .= ',';
250
				}
251
				$return .= $row->mailbox . '@' . $row->host;
252
			}
253
		}
254
		return $return;
255
	}
256
257
	/**
258
	 * Search crmids by emails.
259
	 *
260
	 * @param string   $moduleName
261
	 * @param string   $fieldName
262
	 * @param string[] $emails
263
	 *
264
	 * @return array crmids
265
	 */
266
	public function searchByEmails(string $moduleName, string $fieldName, array $emails)
267
	{
268
		$return = [];
269
		$cacheKey = 'MailSearchByEmails' . $moduleName . '_' . $fieldName;
270
		foreach ($emails as $email) {
271
			if (empty($email) || \in_array($email, self::IGNORED_MAILS)) {
272
				continue;
273
			}
274
			if (App\Cache::staticHas($cacheKey, $email)) {
275
				$cache = App\Cache::staticGet($cacheKey, $email);
276
				if (0 != $cache) {
277
					$return = array_merge($return, $cache);
278
				}
279
			} else {
280
				$ids = [];
281
				$queryGenerator = new \App\QueryGenerator($moduleName);
282
				if ($queryGenerator->getModuleField($fieldName)) {
283
					$queryGenerator->setFields(['id']);
284
					$queryGenerator->addCondition($fieldName, $email, 'e');
285
					$ids = $queryGenerator->createQuery()->column();
286
					$return = array_merge($return, $ids);
287
				}
288
				if (empty($ids)) {
289
					$ids = 0;
290
				}
291
				App\Cache::staticSave($cacheKey, $email, $ids);
292
			}
293
		}
294
		return $return;
295
	}
296
297
	/**
298
	 * Search crmids from domains.
299
	 *
300
	 * @param string   $moduleName
301
	 * @param string   $fieldName
302
	 * @param string[] $emails
303
	 *
304
	 * @return int[] CRM ids
305
	 */
306
	public function searchByDomains(string $moduleName, string $fieldName, array $emails)
307
	{
308
		$cacheKey = 'MailSearchByDomains' . $moduleName . '_' . $fieldName;
309
		$crmids = [];
310
		foreach ($emails as $email) {
311
			if (empty($email) || \in_array($email, self::IGNORED_MAILS)) {
312
				continue;
313
			}
314
			$domain = mb_strtolower(explode('@', $email)[1]);
315
			if (!$domain) {
316
				continue;
317
			}
318
			if (App\Cache::staticHas($cacheKey, $domain)) {
319
				$cache = App\Cache::staticGet($cacheKey, $domain);
320
				if (0 != $cache) {
321
					$crmids = array_merge($crmids, $cache);
322
				}
323
			} else {
324
				$crmids = App\Fields\MultiDomain::findIdByDomain($moduleName, $fieldName, $domain);
325
				App\Cache::staticSave($cacheKey, $domain, $crmids);
326
			}
327
		}
328
		return $crmids;
329
	}
330
331
	/**
332
	 * Find email address.
333
	 *
334
	 * @param string $field
335
	 * @param string $searchModule
336
	 * @param bool   $returnArray
337
	 *
338
	 * @return array|string
339
	 */
340
	public function findEmailAddress($field, $searchModule = false, $returnArray = true)
341
	{
342
		$return = [];
343
		$emails = $this->get($field);
344
		if (empty($emails)) {
345
			return [];
346
		}
347
		if (strpos($emails, ',')) {
348
			$emails = explode(',', $emails);
349
		} else {
350
			$emails = (array) $emails;
351
		}
352
		$emailSearchList = OSSMailScanner_Record_Model::getEmailSearchList();
353
		if (!empty($emailSearchList)) {
354
			foreach ($emailSearchList as $field) {
0 ignored issues
show
introduced by
$field is overwriting one of the parameters of this function.
Loading history...
355
				$enableFind = true;
356
				$row = explode('=', $field);
357
				$moduleName = $row[1];
358
				$fieldName = $row[0];
359
				$fieldModel = Vtiger_Module_Model::getInstance($moduleName)->getField($row[0]);
360
				if ($searchModule && $searchModule !== $moduleName) {
361
					$enableFind = false;
362
				}
363
				if ($enableFind) {
364
					if (319 === $fieldModel->getUIType()) {
365
						$return = array_merge($return, $this->searchByDomains($moduleName, $fieldName, $emails));
366
					} else {
367
						$return = array_merge($return, $this->searchByEmails($moduleName, $fieldName, $emails));
368
					}
369
				}
370
			}
371
		}
372
		if (!$returnArray) {
373
			return implode(',', $return);
374
		}
375
		return $return;
376
	}
377
378
	/**
379
	 * Function to saving attachments.
380
	 */
381
	public function saveAttachments()
382
	{
383
		$userId = $this->getAccountOwner();
384
		$useTime = $this->get('date');
385
		$files = $this->get('files');
386
		$params = [
387
			'created_user_id' => $userId,
388
			'assigned_user_id' => $userId,
389
			'modifiedby' => $userId,
390
			'createdtime' => $useTime,
391
			'modifiedtime' => $useTime,
392
			'folderid' => 'T2',
393
		];
394
		if ($attachments = $this->get('attachments')) {
395
			$maxSize = \App\Config::getMaxUploadSize();
396
			foreach ($attachments as $attachment) {
397
				if ($maxSize < ($size = \strlen($attachment['attachment']))) {
398
					\App\Log::error("Error - downloaded the file is too big '{$attachment['filename']}', size: {$size}, in mail: {$this->get('date')} | Folder: {$this->getFolder()} | ID: {$this->get('id')}", __CLASS__);
399
					continue;
400
				}
401
				$fileInstance = \App\Fields\File::loadFromContent($attachment['attachment'], $attachment['filename'], ['validateAllCodeInjection' => true]);
402
				if ($fileInstance && $fileInstance->validateAndSecure() && ($id = App\Fields\File::saveFromContent($fileInstance, $params))) {
403
					$files[] = $id;
404
				} else {
405
					\App\Log::error("Error downloading the file '{$attachment['filename']}' in mail: {$this->get('date')} | Folder: {$this->getFolder()} | ID: {$this->get('id')}", __CLASS__);
406
				}
407
			}
408
		}
409
		$db = App\Db::getInstance();
410
		foreach ($files as $file) {
411
			$db->createCommand()->insert('vtiger_ossmailview_files', [
412
				'ossmailviewid' => $this->mailCrmId,
413
				'documentsid' => $file['crmid'],
414
				'attachmentsid' => $file['attachmentsId'],
415
			])->execute();
416
		}
417
		return $files;
418
	}
419
420
	/**
421
	 * Treatment mail content with all images and unnecessary trash.
422
	 *
423
	 * @return string
424
	 */
425
	public function getContent(): string
426
	{
427
		if ($this->has('parsedContent')) {
428
			return $this->get('parsedContent');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->get('parsedContent') 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...
429
		}
430
		$html = $this->get('body');
431
		if (!\App\Utils::isHtml($html) || !$this->get('isHtml')) {
432
			$html = nl2br($html);
433
		}
434
		$attachments = $this->get('attachments');
435
		if (\Config\Modules\OSSMailScanner::$attachHtmlAndTxtToMessageBody && \count($attachments) < 2) {
436
			foreach ($attachments as $key => $attachment) {
437
				if (('.html' === substr($attachment['filename'], -5)) || ('.txt' === substr($attachment['filename'], -4))) {
438
					$html .= $attachment['attachment'] . '<hr />';
439
					unset($attachments[$key]);
440
				}
441
			}
442
		}
443
		$encoding = mb_detect_encoding($html, mb_list_encodings(), true);
444
		if ($encoding && 'UTF-8' !== $encoding) {
445
			$html = mb_convert_encoding($html, 'UTF-8', $encoding);
446
		}
447
		$html = preg_replace(
448
			[':<(head|style|script).+?</\1>:is', // remove <head>, <styleand <scriptsections
449
				':<!\[[^]<]+\]>:', // remove <![if !mso]and friends
450
				':<!DOCTYPE[^>]+>:', // remove <!DOCTYPE ... >
451
				':<\?[^>]+>:', // remove <?xml version="1.0" ... >
452
				'~</?html[^>]*>~', // remove html tags
453
				'~</?body[^>]*>~', // remove body tags
454
				'~</?o:[^>]*>~', // remove mso tags
455
				'~\sclass=[\'|\"][^\'\"]+[\'|\"]~i', // remove class attributes
456
			], ['', '', '', '', '', '', '', ''], $html);
457
		$doc = new \DOMDocument('1.0', 'UTF-8');
458
		$previousValue = libxml_use_internal_errors(true);
459
		$doc->loadHTML('<?xml encoding="utf-8"?>' . $html);
460
		libxml_clear_errors();
461
		libxml_use_internal_errors($previousValue);
462
		$params = [
463
			'created_user_id' => $this->getAccountOwner(),
464
			'assigned_user_id' => $this->getAccountOwner(),
465
			'modifiedby' => $this->getAccountOwner(),
466
			'createdtime' => $this->get('date'),
467
			'modifiedtime' => $this->get('date'),
468
			'folderid' => \Config\Modules\OSSMailScanner::$mailBodyGraphicDocumentsFolder ?? 'T2',
469
		];
470
		$files = [];
471
		foreach ($doc->getElementsByTagName('img') as $img) {
472
			if ($file = $this->getFileFromImage($img, $params, $attachments)) {
473
				$files[] = $file;
474
			}
475
		}
476
		$this->set('files', $files);
477
		$this->set('attachments', $attachments);
478
		$previousValue = libxml_use_internal_errors(true);
479
		$html = $doc->saveHTML();
480
		libxml_clear_errors();
481
		libxml_use_internal_errors($previousValue);
482
		$html = \App\Purifier::purifyHtml(str_replace('<?xml encoding="utf-8"?>', '', $html));
483
		$this->set('parsedContent', $html);
484
		return $html;
485
	}
486
487
	/**
488
	 * Get file from image.
489
	 *
490
	 * @param DOMElement $element
491
	 * @param array      $params
492
	 * @param array      $attachments
493
	 *
494
	 * @return array
495
	 */
496
	private function getFileFromImage(DOMElement $element, array $params, array &$attachments): array
497
	{
498
		$src = trim($element->getAttribute('src'), '\'');
499
		$element->removeAttribute('src');
500
		$file = [];
501
		if ('data:' === substr($src, 0, 5)) {
502
			if ($fileInstance = \App\Fields\File::saveFromString($src, ['validateAllowedFormat' => 'image'])) {
503
				$params['titlePrefix'] = 'base64_';
504
				if ($file = \App\Fields\File::saveFromContent($fileInstance, $params)) {
505
					$file['srcType'] = 'base64';
506
				}
507
			}
508
		} elseif (filter_var($src, FILTER_VALIDATE_URL)) {
509
			$params['param'] = ['validateAllowedFormat' => 'image'];
510
			$params['titlePrefix'] = 'url_';
511
			if (\Config\Modules\OSSMailScanner::$attachMailBodyGraphicUrl ?? true) {
512
				if ($file = App\Fields\File::saveFromUrl($src, $params)) {
513
					$file['srcType'] = 'image';
514
				}
515
			} else {
516
				$file = [
517
					'srcType' => 'url',
518
					'url' => $src,
519
				];
520
			}
521
		} elseif ('cid:' === substr($src, 0, 4)) {
522
			$src = substr($src, 4);
523
			if (isset($attachments[$src])) {
524
				$fileInstance = App\Fields\File::loadFromContent($attachments[$src]['attachment'], $attachments[$src]['filename'], ['validateAllowedFormat' => 'image']);
525
				if ($fileInstance && $fileInstance->validateAndSecure()) {
526
					$params['titlePrefix'] = 'content_';
527
					if ($file = App\Fields\File::saveFromContent($fileInstance, $params)) {
528
						$file['srcType'] = 'cid';
529
					}
530
					unset($attachments[$src]);
531
				}
532
			} else {
533
				\App\Log::warning("There is no attachment with ID: $src , in mail: {$this->get('date')} | Folder: {$this->getFolder()} | ID: {$this->get('id')}", __CLASS__);
534
			}
535
		} else {
536
			\App\Log::warning("Unsupported photo type, requires verification. ID: $src , in mail: {$this->get('date')} | Folder: {$this->getFolder()} | ID: {$this->get('id')}", __CLASS__);
537
		}
538
		if ($file) {
539
			$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

539
			/** @scrutinizer ignore-call */ 
540
   $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...
540
			if ('url' === $file['srcType']) {
541
				$yetiforceTag->textContent = $file['url'];
542
			} else {
543
				$yetiforceTag->setAttribute('type', 'Documents');
544
				$yetiforceTag->setAttribute('crm-id', $file['crmid']);
545
				$yetiforceTag->setAttribute('attachment-id', $file['attachmentsId']);
546
			}
547
			$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

547
			$element->parentNode->/** @scrutinizer ignore-call */ 
548
                         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...
548
		} else {
549
			$file = [];
550
		}
551
		return $file;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $file could return the type false which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
552
	}
553
554
	/**
555
	 * Post process function.
556
	 */
557
	public function postProcess()
558
	{
559
	}
560
}
561