OSSMail_Record_Model::getMailAccountDetail()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
/**
3
 * OSSMail record model file.
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    Radosław Skrzypczak <[email protected]>
10
 * @author    Mariusz Krzaczkowski <[email protected]>
11
 */
12
/**
13
 * OSSMail record model class.
14
 */
15
class OSSMail_Record_Model extends Vtiger_Record_Model
16
{
17
	/** @var int Mailbox Status: Active */
18
	const MAIL_BOX_STATUS_ACTIVE = 0;
19
20
	/** @var int Mailbox Status: Invalid access data */
21
	const MAIL_BOX_STATUS_INVALID_ACCESS = 1;
22
23
	/** @var int Mailbox Status: Blocked temporarily */
24
	const MAIL_BOX_STATUS_BLOCKED_TEMP = 2;
25
26
	/** @var int Mailbox Status: Disabled */
27
	const MAIL_BOX_STATUS_DISABLED = 3;
28
29
	/** @var int Mailbox Status: Blocked permanently */
30
	const MAIL_BOX_STATUS_BLOCKED_PERM = 4;
31
32
	/** @var string[] Mailbox status labels */
33
	const MAIL_BOX_STATUS_LABELS = [
34
		self::MAIL_BOX_STATUS_INVALID_ACCESS => 'LBL_ACCOUNT_INVALID_ACCESS',
35
		self::MAIL_BOX_STATUS_DISABLED => 'LBL_ACCOUNT_IS_DISABLED',
36
		self::MAIL_BOX_STATUS_BLOCKED_TEMP => 'LBL_ACCOUNT_IS_BLOCKED_TEMP',
37
		self::MAIL_BOX_STATUS_BLOCKED_PERM => 'LBL_ACCOUNT_IS_BLOCKED_PERM',
38
	];
39
40
	/**
41
	 * Get status label.
42
	 *
43
	 * @param int $status
44
	 *
45
	 * @return string
46
	 */
47
	public static function getStatusLabel(int $status): string
48
	{
49
		return self::MAIL_BOX_STATUS_LABELS[$status];
50
	}
51
52
	/**
53
	 * Return accounts array.
54
	 *
55
	 * @param int|bool $user
56
	 * @param bool     $onlyMy
57
	 * @param bool     $password
58
	 * @param bool     $onlyActive
59
	 *
60
	 * @return array
61
	 */
62
	public static function getAccountsList($user = false, bool $onlyMy = false, bool $password = false, bool $onlyActive = true)
63
	{
64
		$users = [];
65
		$query = (new \App\Db\Query())->from('roundcube_users');
66
		if ($onlyActive) {
67
			$query->where(['crm_status' => [self::MAIL_BOX_STATUS_INVALID_ACCESS, self::MAIL_BOX_STATUS_ACTIVE]]);
68
		}
69
		if ($user) {
70
			$query->andWhere(['user_id' => $user]);
71
		}
72
		if ($onlyMy) {
73
			$userModel = \App\User::getCurrentUserModel();
74
			$crmUsers = $userModel->getGroups();
75
			$crmUsers[] = $userModel->getId();
76
			$query->innerJoin('roundcube_users_autologin', 'roundcube_users_autologin.rcuser_id = roundcube_users.user_id');
77
			$query->andWhere(['roundcube_users_autologin.crmuser_id' => $crmUsers]);
78
		}
79
		if ($password) {
80
			$query->andWhere(['<>', 'password', '']);
81
		}
82
		$dataReader = $query->createCommand()->query();
83
		while ($row = $dataReader->read()) {
84
			$row['actions'] = empty($row['actions']) ? [] : explode(',', $row['actions']);
85
			$users[$row['user_id']] = $row;
86
		}
87
		$dataReader->close();
88
		return $users;
89
	}
90
91
	/**
92
	 * Returns Roundcube configuration.
93
	 *
94
	 * @return array
95
	 */
96
	public static function loadRoundcubeConfig()
97
	{
98
		$configMail = \App\Config::module('OSSMail');
99
		if (!\defined('RCMAIL_VERSION') && file_exists(RCUBE_INSTALL_PATH . '/program/include/iniset.php')) {
0 ignored issues
show
Bug introduced by
The constant RCUBE_INSTALL_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
100
			// read rcube version from iniset
101
			$iniset = file_get_contents(RCUBE_INSTALL_PATH . '/program/include/iniset.php');
102
			if (preg_match('/define\(.RCMAIL_VERSION.,\s*.([0-9.]+[a-z-]*)?/', $iniset, $matches)) {
103
				$rcubeVersion = str_replace('-git', '.999', $matches[1]);
104
				\define('RCMAIL_VERSION', $rcubeVersion);
105
				\define('RCUBE_VERSION', $rcubeVersion);
106
			} else {
107
				throw new \App\Exceptions\AppException('Unable to find a Roundcube version');
108
			}
109
		}
110
		include 'public_html/modules/OSSMail/roundcube/config/defaults.inc.php';
111
		return $configMail + $config;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $config does not exist. Did you maybe mean $configMail?
Loading history...
112
	}
113
114
	/**
115
	 * Imap connection cache.
116
	 *
117
	 * @var array
118
	 */
119
	protected static $imapConnectCache = [];
120
121
	/**
122
	 * $imapConnectMailbox.
123
	 *
124
	 * @var string
125
	 */
126
	public static $imapConnectMailbox = '';
127
128
	/**
129
	 * Return imap connection resource.
130
	 *
131
	 * @param string $user
132
	 * @param string $password
133
	 * @param string $host
134
	 * @param string $folder     Character encoding UTF7-IMAP
135
	 * @param bool   $dieOnError
136
	 * @param array  $config
137
	 * @param array  $account
138
	 *
139
	 * @return IMAP\Connection|false
0 ignored issues
show
Bug introduced by
The type IMAP\Connection 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...
140
	 */
141
	public static function imapConnect($user, $password, $host = '', $folder = 'INBOX', $dieOnError = true, $config = [], array $account = [])
142
	{
143
		\App\Log::trace("Entering OSSMail_Record_Model::imapConnect($user , '****' , $folder) method ...");
144
		if (!$config) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $config of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
145
			$config = self::loadRoundcubeConfig();
146
		}
147
		$cacheName = $user . $host . $folder;
148
		if (isset(self::$imapConnectCache[$cacheName])) {
149
			return self::$imapConnectCache[$cacheName];
150
		}
151
		$parseHost = parse_url($host);
152
		if (empty($parseHost['host'])) {
153
			$hosts = [];
154
			if ($imapHost = $config['imap_host'] ?? '') {
155
				$hosts = \is_string($imapHost) ? [$imapHost => $imapHost] : $imapHost;
156
			}
157
			foreach ($hosts as $configHost => $hostDomain) {
158
				$parsedConfigHost = parse_url($configHost);
159
				if (isset($parsedConfigHost['host']) && $parsedConfigHost['host'] === $host) {
160
					$parseHost = $parsedConfigHost;
161
					break;
162
				}
163
			}
164
		}
165
		$port = 143;
166
		$sslMode = 'tls';
167
		if (!empty($parseHost['host'])) {
168
			$host = $parseHost['host'];
169
			$sslMode = (isset($parseHost['scheme']) && \in_array($parseHost['scheme'], ['ssl', 'imaps', 'tls'])) ? $parseHost['scheme'] : null;
170
			if (!empty($parseHost['port'])) {
171
				$port = $parseHost['port'];
172
			} elseif ($sslMode && 'tls' !== $sslMode) {
173
				$port = 993;
174
			}
175
		}
176
		$validateCert = '';
177
		if (!$config['validate_cert'] && $config['imap_open_add_connection_type']) {
178
			$validateCert = '/novalidate-cert';
179
		}
180
		if ($config['imap_open_add_connection_type'] && $sslMode) {
181
			$sslMode = '/' . $sslMode;
182
		} else {
183
			$sslMode = '';
184
		}
185
		imap_timeout(IMAP_OPENTIMEOUT, 5);
186
		$maxRetries = $options = 0;
187
		if (isset($config['imap_max_retries'])) {
188
			$maxRetries = $config['imap_max_retries'];
189
		}
190
		$params = [];
191
		if (isset($config['imap_params'])) {
192
			$params = $config['imap_params'];
193
		}
194
		static::$imapConnectMailbox = "{{$host}:{$port}/imap{$sslMode}{$validateCert}}{$folder}";
195
		\App\Log::trace('imap_open(({' . static::$imapConnectMailbox . ", $user , '****'. $options, $maxRetries, " . var_export($params, true) . ') method ...');
196
		\App\Log::beginProfile(__METHOD__ . '|imap_open|' . $user, 'Mail|IMAP');
197
		$mbox = imap_open(static::$imapConnectMailbox, $user, $password, $options, $maxRetries, $params);
198
		\App\Log::endProfile(__METHOD__ . '|imap_open|' . $user, 'Mail|IMAP');
199
		self::$imapConnectCache[$cacheName] = $mbox;
200
		if ($mbox) {
0 ignored issues
show
introduced by
$mbox is of type resource, thus it always evaluated to false.
Loading history...
201
			if ($account) {
202
				\App\Db::getInstance()->createCommand()
203
					->update('roundcube_users', ['crm_error' => null, 'crm_status' => self::MAIL_BOX_STATUS_ACTIVE], ['user_id' => $account['user_id']])
204
					->execute();
205
			}
206
			\App\Log::trace('Exit OSSMail_Record_Model::imapConnect() method ...');
207
			register_shutdown_function(function () use ($mbox, $user) {
208
				try {
209
					\App\Log::beginProfile('OSSMail_Record_Model|imap_close|' . $user, 'Mail|IMAP');
210
					imap_close($mbox);
211
					\App\Log::endProfile('OSSMail_Record_Model|imap_close|' . $user, 'Mail|IMAP');
212
				} catch (\Throwable $e) {
213
					\App\Log::error($e->getMessage() . PHP_EOL . $e->__toString());
214
					throw $e;
215
				}
216
			});
217
		} else {
218
			if ($account) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $account of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
219
				$status = self::MAIL_BOX_STATUS_ACTIVE == $account['crm_status'] ? self::MAIL_BOX_STATUS_INVALID_ACCESS : self::MAIL_BOX_STATUS_BLOCKED_TEMP;
220
				[$date] = explode('||', $account['crm_error'] ?: '');
221
				if (empty($date) || false === strtotime($date)) {
222
					$date = date('Y-m-d H:i:s');
223
				}
224
				if (self::MAIL_BOX_STATUS_BLOCKED_TEMP === $status && strtotime('-' . (OSSMailScanner_Record_Model::getConfig('blocked')['permanentTime'] ?? '2 day')) > strtotime($date)) {
225
					$status = self::MAIL_BOX_STATUS_BLOCKED_PERM;
226
				}
227
				\App\Db::getInstance()->createCommand()
228
					->update('roundcube_users', [
229
						'crm_error' => \App\TextUtils::textTruncate($date . '||' . imap_last_error(), 250),
230
						'crm_status' => $status,
231
						'failed_login' => date('Y-m-d H:i:s'),
232
					], ['user_id' => $account['user_id']])
233
					->execute();
234
			}
235
			\App\Log::error('Error OSSMail_Record_Model::imapConnect(' . static::$imapConnectMailbox . '): ' . imap_last_error());
236
			if ($dieOnError) {
237
				throw new \App\Exceptions\AppException('IMAP_ERROR' . ': ' . imap_last_error());
238
			}
239
		}
240
		return $mbox;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $mbox returns the type resource which is incompatible with the documented return type IMAP\Connection|false.
Loading history...
241
	}
242
243
	/**
244
	 * Update mailbox mesages info for users.
245
	 *
246
	 * @param array $users
247
	 *
248
	 * @return array
249
	 */
250
	public static function updateMailBoxCounter(array $users): array
251
	{
252
		if (empty($users)) {
253
			return [];
254
		}
255
		$dbCommand = \App\Db::getInstance()->createCommand();
256
		$config = Settings_Mail_Config_Model::getConfig('mailIcon');
257
		$interval = $config['timeCheckingMail'] ?? 30;
258
		$date = strtotime("-{$interval} seconds");
259
		$counter = [];
260
		$all = (new \App\Db\Query())->from('u_#__mail_quantities')->where(['userid' => $users])->indexBy('userid')->all();
261
		foreach ($users as $user) {
262
			if (empty($all[$user]['date']) || $date > strtotime($all[$user]['date'])) {
263
				if ($account = self::getMailAccountDetail($user)) {
264
					if (empty($all[$user])) {
265
						$dbCommand->insert('u_#__mail_quantities', ['userid' => $user, 'num' => 0, 'date' => date('Y-m-d H:i:s')])->execute();
266
					} else {
267
						$dbCommand->update('u_#__mail_quantities', ['date' => date('Y-m-d H:i:s')], ['userid' => $user])->execute();
268
					}
269
					try {
270
						$mbox = self::imapConnect($account['username'], \App\Encryption::getInstance()->decrypt($account['password']), $account['mail_host'], 'INBOX', false, [], $account);
271
						if ($mbox) {
272
							\App\Log::beginProfile(__METHOD__ . '|imap_status|' . $user, 'Mail|IMAP');
273
							$info = imap_status($mbox, static::$imapConnectMailbox, SA_UNSEEN);
0 ignored issues
show
Bug introduced by
$mbox of type IMAP\Connection is incompatible with the type resource expected by parameter $imap_stream of imap_status(). ( Ignorable by Annotation )

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

273
							$info = imap_status(/** @scrutinizer ignore-type */ $mbox, static::$imapConnectMailbox, SA_UNSEEN);
Loading history...
274
							\App\Log::endProfile(__METHOD__ . '|imap_status|' . $user, 'Mail|IMAP');
275
							$counter[$user] = $info->unseen ?? 0;
276
							$dbCommand->update('u_#__mail_quantities', ['num' => $counter[$user], 'date' => date('Y-m-d H:i:s')], ['userid' => $user])->execute();
277
						}
278
					} catch (\Throwable $th) {
279
						\App\Log::error($th->__toString());
280
					}
281
				}
282
			} else {
283
				$counter[$user] = $all[$user]['num'] ?? 0;
284
			}
285
		}
286
		return $counter;
287
	}
288
289
	/**
290
	 * @param resource $mbox
291
	 * @param int      $id
292
	 * @param int      $msgno
293
	 * @param bool     $fullMode
294
	 *
295
	 * @return bool|\OSSMail_Mail_Model
296
	 */
297
	public static function getMail($mbox, $id, $msgno = false, bool $fullMode = true)
298
	{
299
		if (!$msgno) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $msgno of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
300
			\App\Log::beginProfile(__METHOD__ . '|imap_msgno', 'Mail|IMAP');
301
			$msgno = imap_msgno($mbox, $id);
302
			\App\Log::endProfile(__METHOD__ . '|imap_msgno', 'Mail|IMAP');
303
		}
304
		if (!$id) {
305
			\App\Log::beginProfile(__METHOD__ . '|imap_uid', 'Mail|IMAP');
306
			$id = imap_uid($mbox, $msgno);
307
			\App\Log::endProfile(__METHOD__ . '|imap_uid', 'Mail|IMAP');
308
		}
309
		if (!$msgno) {
310
			return false;
311
		}
312
		\App\Log::beginProfile(__METHOD__ . '|imap_headerinfo', 'Mail|IMAP');
313
		$header = imap_headerinfo($mbox, $msgno);
314
		\App\Log::endProfile(__METHOD__ . '|imap_headerinfo', 'Mail|IMAP');
315
		$messageId = '';
316
		if (property_exists($header, 'message_id')) {
317
			$messageId = $header->message_id;
318
		}
319
		$mail = new OSSMail_Mail_Model();
320
		$mail->set('header', $header);
321
		$mail->set('id', $id);
322
		$mail->set('Msgno', $header->Msgno);
323
		$mail->set('message_id', $messageId ? \App\Purifier::purifyByType($messageId, 'MailId') : '');
324
		$mail->set('to_email', \App\Purifier::purify($mail->getEmail('to')));
325
		$mail->set('from_email', \App\Purifier::purify($mail->getEmail('from')));
326
		$mail->set('reply_toaddress', \App\Purifier::purify($mail->getEmail('reply_to')));
327
		$mail->set('cc_email', \App\Purifier::purify($mail->getEmail('cc')));
328
		$mail->set('bcc_email', \App\Purifier::purify($mail->getEmail('bcc')));
329
		$mail->set('firstLetterBg', strtoupper(\App\TextUtils::textTruncate(trim(strip_tags(App\Purifier::purify($mail->getEmail('from')))), 1, false)));
330
		$mail->set('subject', isset($header->subject) ? \App\TextUtils::textTruncate(\App\Purifier::purify(self::decodeText($header->subject)), 65535, false) : '');
331
		$mail->set('date', date('Y-m-d H:i:s', $header->udate));
332
		if ($fullMode) {
333
			$structure = self::getBodyAttach($mbox, $id, $msgno);
334
			$mail->set('body', $structure['body']);
335
			$mail->set('attachments', $structure['attachment']);
336
			$mail->set('isHtml', $structure['isHtml']);
337
338
			$clean = '';
339
			\App\Log::beginProfile(__METHOD__ . '|imap_fetch_overview', 'Mail|IMAP');
340
			$msgs = imap_fetch_overview($mbox, $msgno);
341
			\App\Log::endProfile(__METHOD__ . '|imap_fetch_overview', 'Mail|IMAP');
342
343
			foreach ($msgs as $msg) {
344
				\App\Log::beginProfile(__METHOD__ . '|imap_fetchheader', 'Mail|IMAP');
345
				$clean .= imap_fetchheader($mbox, $msg->msgno);
346
				\App\Log::endProfile(__METHOD__ . '|imap_fetchheader', 'Mail|IMAP');
347
			}
348
			$mail->set('clean', $clean);
349
		}
350
		return $mail;
351
	}
352
353
	/**
354
	 * Users cache.
355
	 *
356
	 * @var array
357
	 */
358
	protected static $usersCache = [];
359
360
	/**
361
	 * Return user account detal.
362
	 *
363
	 * @param int $userid
364
	 *
365
	 * @return array
366
	 */
367
	public static function getMailAccountDetail($userid)
368
	{
369
		if (isset(self::$usersCache[$userid])) {
370
			return self::$usersCache[$userid];
371
		}
372
		$user = (new \App\Db\Query())->from('roundcube_users')->where(['user_id' => $userid, 'crm_status' => [self::MAIL_BOX_STATUS_INVALID_ACCESS, self::MAIL_BOX_STATUS_ACTIVE]])->one();
373
		self::$usersCache[$userid] = $user;
374
		return $user;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $user could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
375
	}
376
377
	/**
378
	 * Convert text encoding.
379
	 *
380
	 * @param string $text
381
	 *
382
	 * @return string
383
	 */
384
	public static function decodeText($text)
385
	{
386
		$data = imap_mime_header_decode($text);
387
		$text = '';
388
		foreach ($data as &$row) {
389
			$charset = ('default' == $row->charset) ? 'ASCII' : $row->charset;
390
			if (\function_exists('mb_convert_encoding') && \in_array($charset, mb_list_encodings())) {
391
				$text .= mb_convert_encoding($row->text, 'utf-8', $charset);
392
			} else {
393
				$text .= iconv($charset, 'UTF-8', $row->text);
394
			}
395
		}
396
		return $text;
397
	}
398
399
	/**
400
	 * Return full name.
401
	 *
402
	 * @param string $text
403
	 *
404
	 * @return string
405
	 */
406
	public static function getFullName($text)
407
	{
408
		$return = '';
409
		foreach ($text as $row) {
0 ignored issues
show
Bug introduced by
The expression $text of type string is not traversable.
Loading history...
410
			if ('' != $return) {
411
				$return .= ',';
412
			}
413
			if ('' == $row->personal) {
414
				$return .= $row->mailbox . '@' . $row->host;
415
			} else {
416
				$return .= self::decodeText($row->personal) . ' - ' . $row->mailbox . '@' . $row->host;
417
			}
418
		}
419
		return $return;
420
	}
421
422
	/**
423
	 * Return body and attachments.
424
	 *
425
	 * @param resource $mbox
426
	 * @param int      $id
427
	 * @param int      $msgno
428
	 *
429
	 * @return array
430
	 */
431
	public static function getBodyAttach($mbox, $id, $msgno)
0 ignored issues
show
Unused Code introduced by
The parameter $msgno is not used and could be removed. ( Ignorable by Annotation )

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

431
	public static function getBodyAttach($mbox, $id, /** @scrutinizer ignore-unused */ $msgno)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
432
	{
433
		\App\Log::beginProfile(__METHOD__ . '|imap_fetchstructure', 'Mail|IMAP');
434
		$struct = imap_fetchstructure($mbox, $id, FT_UID);
435
		\App\Log::endProfile(__METHOD__ . '|imap_fetchstructure', 'Mail|IMAP');
436
		$mail = ['id' => $id];
437
		if (empty($struct->parts)) {
438
			$mail = self::initMailPart($mbox, $mail, $struct, 0);
439
		} else {
440
			foreach ($struct->parts as $partNum => $partStructure) {
441
				$mail = self::initMailPart($mbox, $mail, $partStructure, $partNum + 1);
442
			}
443
		}
444
		$body = '';
445
		$body = (!empty($mail['textPlain'])) ? $mail['textPlain'] : $body;
446
		$body = (!empty($mail['textHtml'])) ? $mail['textHtml'] : $body;
447
		$attachment = (isset($mail['attachments'])) ? $mail['attachments'] : [];
448
449
		return [
450
			'body' => $body,
451
			'attachment' => $attachment,
452
			'isHtml' => !empty($mail['textHtml']),
453
		];
454
	}
455
456
	/**
457
	 * Init mail part.
458
	 *
459
	 * @param resource $mbox
460
	 * @param array    $mail
461
	 * @param object   $partStructure
462
	 * @param int      $partNum
463
	 *
464
	 * @return array
465
	 */
466
	protected static function initMailPart($mbox, $mail, $partStructure, $partNum)
467
	{
468
		if ($partNum) {
469
			\App\Log::beginProfile(__METHOD__ . '|imap_fetchbody', 'Mail|IMAP');
470
			$data = $orgData = imap_fetchbody($mbox, $mail['id'], $partNum, FT_UID | FT_PEEK);
471
			\App\Log::endProfile(__METHOD__ . '|imap_fetchbody', 'Mail|IMAP');
472
		} else {
473
			\App\Log::beginProfile(__METHOD__ . '|imap_body', 'Mail|IMAP');
474
			$data = $orgData = imap_body($mbox, $mail['id'], FT_UID | FT_PEEK);
475
			\App\Log::endProfile(__METHOD__ . '|imap_body', 'Mail|IMAP');
476
		}
477
		if (1 == $partStructure->encoding) {
478
			$data = imap_utf8($data);
479
		} elseif (2 == $partStructure->encoding) {
480
			$data = imap_binary($data);
481
		} elseif (3 == $partStructure->encoding) {
482
			$data = imap_base64($data);
483
		} elseif (4 == $partStructure->encoding) {
484
			$data = imap_qprint($data);
485
		}
486
		$params = [];
487
		if (!empty($partStructure->parameters)) {
488
			foreach ($partStructure->parameters as $param) {
489
				$params[strtolower($param->attribute)] = $param->value;
490
			}
491
		}
492
		if (!empty($partStructure->dparameters)) {
493
			foreach ($partStructure->dparameters as $param) {
494
				$paramName = strtolower(preg_match('~^(.*?)\*~', $param->attribute, $matches) ? $matches[1] : $param->attribute);
495
				if (isset($params[$paramName])) {
496
					$params[$paramName] .= $param->value;
497
				} else {
498
					$params[$paramName] = $param->value;
499
				}
500
			}
501
		}
502
		if (!empty($params['charset']) && 'utf-8' !== strtolower($params['charset'])) {
503
			if (\function_exists('mb_convert_encoding') && \in_array($params['charset'], mb_list_encodings())) {
504
				$encodedData = mb_convert_encoding($data, 'UTF-8', $params['charset']);
505
			} else {
506
				$encodedData = iconv($params['charset'], 'UTF-8', $data);
507
			}
508
			if ($encodedData) {
509
				$data = $encodedData;
510
			}
511
		}
512
		$attachmentId = $partStructure->ifid ? trim($partStructure->id, ' <>') : (isset($params['filename']) || isset($params['name']) ? random_int(0, PHP_INT_MAX) . random_int(0, PHP_INT_MAX) : null);
513
		if ($attachmentId) {
514
			if (empty($params['filename']) && empty($params['name'])) {
515
				$fileName = $attachmentId . '.' . strtolower($partStructure->subtype);
516
			} else {
517
				$fileName = !empty($params['filename']) ? $params['filename'] : $params['name'];
518
				$fileName = self::decodeText($fileName);
519
				$fileName = self::decodeRFC2231($fileName);
520
			}
521
			$mail['attachments'][$attachmentId]['filename'] = $fileName;
522
			$mail['attachments'][$attachmentId]['attachment'] = $data;
523
		} elseif (0 == $partStructure->type && $data) {
524
			if (preg_match('/^([a-zA-Z0-9]{76} )+[a-zA-Z0-9]{76}$/', $data) && base64_decode($data, true)) {
525
				$data = base64_decode($data);
526
			}
527
			if ('plain' == strtolower($partStructure->subtype)) {
528
				$uuDecode = self::uuDecode($data);
529
				if (isset($uuDecode['attachments'])) {
530
					$mail['attachments'] = $uuDecode['attachments'];
531
				}
532
				if (!isset($mail['textPlain'])) {
533
					$mail['textPlain'] = '';
534
				}
535
				if (isset($params['format']) && 'flowed' === $params['format']) {
536
					$uuDecode['text'] = self::unfoldFlowed($uuDecode['text'], isset($params['delsp']) && 'yes' === strtolower($params['delsp']));
537
				}
538
				$mail['textPlain'] .= $uuDecode['text'];
539
			} else {
540
				if (!isset($mail['textHtml'])) {
541
					$mail['textHtml'] = '';
542
				}
543
				if ($data && '<' !== $data[0] && '<' === $orgData[0]) {
544
					$data = $orgData;
545
				}
546
				$mail['textHtml'] .= $data;
547
			}
548
		} elseif (2 == $partStructure->type && $data) {
549
			if (!isset($mail['textPlain'])) {
550
				$mail['textPlain'] = '';
551
			}
552
			$mail['textPlain'] .= trim($data);
553
		}
554
		if (!empty($partStructure->parts)) {
555
			foreach ($partStructure->parts as $subPartNum => $subPartStructure) {
556
				if (2 == $partStructure->type && 'RFC822' == $partStructure->subtype) {
557
					$mail = self::initMailPart($mbox, $mail, $subPartStructure, $partNum);
558
				} else {
559
					$mail = self::initMailPart($mbox, $mail, $subPartStructure, $partNum . '.' . ($subPartNum + 1));
0 ignored issues
show
Bug introduced by
$partNum . '.' . $subPartNum + 1 of type string is incompatible with the type integer expected by parameter $partNum of OSSMail_Record_Model::initMailPart(). ( Ignorable by Annotation )

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

559
					$mail = self::initMailPart($mbox, $mail, $subPartStructure, /** @scrutinizer ignore-type */ $partNum . '.' . ($subPartNum + 1));
Loading history...
560
				}
561
			}
562
		}
563
		return $mail;
564
	}
565
566
	/**
567
	 * Decode string.
568
	 *
569
	 * @param string $input
570
	 *
571
	 * @return array
572
	 */
573
	protected static function uuDecode($input)
574
	{
575
		$attachments = [];
576
		$uu_regexp_begin = '/begin [0-7]{3,4} ([^\r\n]+)\r?\n/s';
577
		$uu_regexp_end = '/`\r?\nend((\r?\n)|($))/s';
578
579
		while (preg_match($uu_regexp_begin, $input, $matches, PREG_OFFSET_CAPTURE)) {
580
			$startpos = $matches[0][1];
581
			if (!preg_match($uu_regexp_end, $input, $m, PREG_OFFSET_CAPTURE, $startpos)) {
582
				break;
583
			}
584
585
			$endpos = $m[0][1];
586
			$begin_len = \strlen($matches[0][0]);
587
			$end_len = \strlen($m[0][0]);
588
589
			// extract attachment body
590
			$filebody = substr($input, $startpos + $begin_len, $endpos - $startpos - $begin_len - 1);
591
			$filebody = str_replace("\r\n", "\n", $filebody);
592
593
			// remove attachment body from the message body
594
			$input = substr_replace($input, '', $startpos, $endpos + $end_len - $startpos);
595
596
			// add attachments to the structure
597
			$attachments[] = [
598
				'filename' => trim($matches[1][0]),
599
				'attachment' => convert_uudecode($filebody),
600
			];
601
		}
602
		return ['attachments' => $attachments, 'text' => $input];
603
	}
604
605
	/**
606
	 * Parse format=flowed message body.
607
	 *
608
	 * @param string $text
609
	 * @param bool   $delSp
610
	 *
611
	 * @return string
612
	 */
613
	protected static function unfoldFlowed(string $text, bool $delSp = false): string
614
	{
615
		$text = preg_split('/\r?\n/', $text);
616
		$last = -1;
617
		$qLevel = 0;
618
		foreach ($text as $idx => $line) {
619
			if ($q = strspn($line, '>')) {
620
				$line = substr($line, $q);
621
				if (isset($line[0]) && ' ' === $line[0]) {
622
					$line = substr($line, 1);
623
				}
624
				if ($q == $qLevel
625
					&& isset($text[$last]) && ' ' == $text[$last][\strlen($text[$last]) - 1]
626
					&& !preg_match('/^>+ {0,1}$/', $text[$last])
627
				) {
628
					if ($delSp) {
629
						$text[$last] = substr($text[$last], 0, -1);
630
					}
631
					$text[$last] .= $line;
632
					unset($text[$idx]);
633
				} else {
634
					$last = $idx;
635
				}
636
			} else {
637
				if ('-- ' == $line) {
638
					$last = $idx;
639
				} else {
640
					if (isset($line[0]) && ' ' === $line[0]) {
641
						$line = substr($line, 1);
642
					}
643
					if (isset($text[$last]) && $line && !$qLevel
644
						&& '-- ' !== $text[$last]
645
						&& isset($text[$last][\strlen($text[$last]) - 1]) && ' ' === $text[$last][\strlen($text[$last]) - 1]
646
					) {
647
						if ($delSp) {
648
							$text[$last] = substr($text[$last], 0, -1);
649
						}
650
						$text[$last] .= $line;
651
						unset($text[$idx]);
652
					} else {
653
						$text[$idx] = $line;
654
						$last = $idx;
655
					}
656
				}
657
			}
658
			$qLevel = $q;
659
		}
660
661
		return implode("\r\n", $text);
662
	}
663
664
	/**
665
	 * Check if url is encoded.
666
	 *
667
	 * @param string $string
668
	 *
669
	 * @return bool
670
	 */
671
	public static function isUrlEncoded($string)
672
	{
673
		$string = str_replace('%20', '+', $string);
674
		$decoded = urldecode($string);
675
676
		return $decoded != $string && urlencode($decoded) == $string;
677
	}
678
679
	/**
680
	 * decode RFC2231 formatted string.
681
	 *
682
	 * @param string $string
683
	 * @param string $charset
684
	 *
685
	 * @return string
686
	 */
687
	protected static function decodeRFC2231($string, $charset = 'utf-8')
688
	{
689
		if (preg_match("/^(.*?)'.*?'(.*?)$/", $string, $matches)) {
690
			$encoding = $matches[1];
691
			$data = $matches[2];
692
			if (self::isUrlEncoded($data)) {
693
				$string = iconv(strtoupper($encoding), $charset, urldecode($data));
694
			}
695
		}
696
		return $string;
697
	}
698
699
	/**
700
	 * Return user folders.
701
	 *
702
	 * @param int $user
703
	 *
704
	 * @return array
705
	 */
706
	public static function getFolders($user)
707
	{
708
		$account = self::getAccountsList($user);
709
		$account = reset($account);
710
		$folders = false;
711
		$mbox = self::imapConnect($account['username'], \App\Encryption::getInstance()->decrypt($account['password']), $account['mail_host'], 'INBOX', false, [], $account);
712
		if ($mbox) {
713
			$folders = [];
714
			$ref = '{' . $account['mail_host'] . '}';
715
			$list = imap_list($mbox, $ref, '*');
0 ignored issues
show
Bug introduced by
$mbox of type IMAP\Connection is incompatible with the type resource expected by parameter $imap_stream of imap_list(). ( Ignorable by Annotation )

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

715
			$list = imap_list(/** @scrutinizer ignore-type */ $mbox, $ref, '*');
Loading history...
716
			foreach ($list as $mailboxname) {
717
				$name = str_replace($ref, '', $mailboxname);
718
				$name = \App\Utils::convertCharacterEncoding($name, 'UTF7-IMAP', 'UTF-8');
719
				$folders[$name] = $name;
720
			}
721
		}
722
		return $folders;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $folders could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
723
	}
724
725
	/**
726
	 * Return site URL.
727
	 *
728
	 * @return string
729
	 */
730
	public static function getSiteUrl()
731
	{
732
		$site_URL = App\Config::main('site_URL');
733
		if ('/' != substr($site_URL, -1)) {
734
			$site_URL = $site_URL . '/';
735
		}
736
		return $site_URL;
737
	}
738
739
	/**
740
	 * Fetch mails from IMAP.
741
	 *
742
	 * @param int|null $user
743
	 *
744
	 * @return array
745
	 */
746
	public static function getMailsFromIMAP(?int $user = null)
747
	{
748
		$accounts = self::getAccountsList(false, true);
749
		$mails = [];
750
		$mailLimit = 5;
751
		if ($accounts) {
752
			if ($user && isset($accounts[$user])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
753
				$account = $accounts[$user];
754
			} else {
755
				$account = reset($accounts);
756
			}
757
			$imap = self::imapConnect($account['username'], \App\Encryption::getInstance()->decrypt($account['password']), $account['mail_host'], 'INBOX', true, [], $account);
758
			\App\Log::beginProfile(__METHOD__ . '|imap_num_msg', 'Mail|IMAP');
759
			$numMessages = imap_num_msg($imap);
0 ignored issues
show
Bug introduced by
$imap of type IMAP\Connection|false is incompatible with the type resource expected by parameter $imap_stream of imap_num_msg(). ( Ignorable by Annotation )

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

759
			$numMessages = imap_num_msg(/** @scrutinizer ignore-type */ $imap);
Loading history...
760
			\App\Log::endProfile(__METHOD__ . '|imap_num_msg', 'Mail|IMAP');
761
			if ($numMessages < $mailLimit) {
762
				$mailLimit = $numMessages;
763
			}
764
			for ($i = $numMessages; $i > ($numMessages - $mailLimit); --$i) {
765
				$mail = self::getMail($imap, false, $i);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $id of OSSMail_Record_Model::getMail(). ( Ignorable by Annotation )

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

765
				$mail = self::getMail($imap, /** @scrutinizer ignore-type */ false, $i);
Loading history...
Bug introduced by
$imap of type IMAP\Connection|false is incompatible with the type resource expected by parameter $mbox of OSSMail_Record_Model::getMail(). ( Ignorable by Annotation )

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

765
				$mail = self::getMail(/** @scrutinizer ignore-type */ $imap, false, $i);
Loading history...
766
				$mails[] = $mail;
767
			}
768
		}
769
		return $mails;
770
	}
771
772
	/**
773
	 * Get mail account detail by hash ID.
774
	 *
775
	 * @param string $hash
776
	 *
777
	 * @return bool|array
778
	 */
779
	public static function getAccountByHash($hash)
780
	{
781
		if (preg_match('/^[_a-zA-Z0-9.,]+$/', $hash)) {
782
			$result = (new \App\Db\Query())
783
				->from('roundcube_users')
784
				->where(['like', 'preferences', "%:\"$hash\";%", false])
785
				->one();
786
			if ($result) {
787
				return $result;
788
			}
789
		}
790
		return false;
791
	}
792
793
	/**
794
	 * Update user data for account.
795
	 *
796
	 * @param int   $userId
797
	 * @param array $data
798
	 *
799
	 * @return bool
800
	 */
801
	public static function setAccountUserData(int $userId, array $data): bool
802
	{
803
		return \App\Db::getInstance()->createCommand()->update('roundcube_users', $data, ['user_id' => $userId])->execute();
0 ignored issues
show
Bug Best Practice introduced by
The expression return App\Db::getInstan...=> $userId))->execute() returns the type integer which is incompatible with the type-hinted return boolean.
Loading history...
804
	}
805
}
806