Your IP : 3.139.86.62


Current Path : /var/www/axolotl/data/www/arhangelsk.axolotls.ru/a537b/
Upload File :
Current File : /var/www/axolotl/data/www/arhangelsk.axolotls.ru/a537b/helper.tar

contactsstepper.php000066400000003417147744702230010517 0ustar00<?php

namespace Bitrix\Mail\Helper;

use Bitrix\Main;
use Bitrix\Mail;
use Bitrix\Mail\Internals\MailContactTable;

class ContactsStepper extends Main\Update\Stepper
{
	protected static $moduleId = 'mail';

	public function execute(array &$option)
	{
		$option['steps'] = Mail\MailMessageTable::getCount(array(
			'<=ID' => $option['lastId'] > 0 ? $option['lastId'] : 0,
		));
		$option['count'] = Mail\MailMessageTable::getCount();

		if ($option['steps'] >= $option['count'])
		{
			return false;
		}

		$res = Mail\MailMessageTable::getList(array(
			'select' => array(
				'ID',
				'FIELD_FROM', 'FIELD_REPLY_TO',
				'FIELD_TO', 'FIELD_CC', 'FIELD_BCC',
				'MAILBOX_USER_ID' => 'MAILBOX.USER_ID',
			),
			'filter' => array(
				'>ID' => $option['lastId'] > 0 ? $option['lastId'] : 0,
			),
			'order' => array('ID' => 'ASC'),
			'limit' => 1000,
		));


		$contacts = array();
		while ($item = $res->fetch())
		{
			$option['steps']++;
			$option['lastId'] = $item['ID'];

			@array_push(
				$contacts,
				...MailContact::getContactsData($item['FIELD_FROM'], $item['MAILBOX_USER_ID'], MailContactTable::ADDED_TYPE_FROM),
				...MailContact::getContactsData($item['FIELD_REPLY_TO'], $item['MAILBOX_USER_ID'], MailContactTable::ADDED_TYPE_REPLY_TO),
				...MailContact::getContactsData($item['FIELD_TO'], $item['MAILBOX_USER_ID'], MailContactTable::ADDED_TYPE_TO),
				...MailContact::getContactsData($item['FIELD_CC'], $item['MAILBOX_USER_ID'], MailContactTable::ADDED_TYPE_CC),
				...MailContact::getContactsData($item['FIELD_BCC'], $item['MAILBOX_USER_ID'], MailContactTable::ADDED_TYPE_BCC)
			);

			if (count($contacts) >= 100)
			{
				MailContactTable::addContactsBatch($contacts);

				$contacts = array();
			}
		}

		MailContactTable::addContactsBatch($contacts);

		return true;
	}

}
message.php000066400000025114147744702230006720 0ustar00<?php

namespace Bitrix\Mail\Helper;

use Bitrix\Main;
use Bitrix\Main\Security;
use Bitrix\Mail;

class Message
{

	public static function prepare(&$message)
	{
		$message['__email'] = null;
		foreach (array($message['MAILBOX_EMAIL'], $message['MAILBOX_NAME'], $message['MAILBOX_LOGIN']) as $item)
		{
			$address = new Main\Mail\Address($item);
			if ($address->validate())
			{
				$message['__email'] = $address->getEmail();
				break;
			}
		}

		$fieldsMap = array(
			'__from' => 'FIELD_FROM',
			'__reply_to' => 'FIELD_REPLY_TO',
			'__to' => 'FIELD_TO',
			'__cc' => 'FIELD_CC',
			'__bcc' => 'FIELD_BCC',
			'__rcpt' => 'FIELD_RCPT',
		);

		if ('' != $message['HEADER'])
		{
			foreach ($fieldsMap as $field)
			{
				if (strlen($message[$field]) == 255)
				{
					$parsedHeader = \CMailMessage::parseHeader($message['HEADER'], LANG_CHARSET);

					$message['FIELD_FROM'] = $parsedHeader->getHeader('FROM');
					$message['FIELD_REPLY_TO'] = $parsedHeader->getHeader('REPLY-TO');
					$message['FIELD_TO'] = $parsedHeader->getHeader('TO');
					$message['FIELD_CC'] = $parsedHeader->getHeader('CC');
					$message['FIELD_BCC'] = join(', ', array_merge(
						(array) $parsedHeader->getHeader('X-Original-Rcpt-to'),
						(array) $parsedHeader->getHeader('BCC')
					));

					break;
				}
			}
		}

		foreach ($fieldsMap as $extField => $field)
		{
			$isFromField = in_array($extField, array('__from', '__reply_to'));

			$message[$extField] = array();
			foreach (explode(',', $message[$field]) as $item)
			{
				if (trim($item))
				{
					$address = new Main\Mail\Address($item);
					if ($address->validate())
					{
						if ($isFromField && $address->getEmail() == $message['__email'])
						{
							$message['__is_outcome'] = true;
						}

						$message[$extField][] = array(
							'name' => $address->getName(),
							'email' => $address->getEmail(),
							'formated' => ($address->getName() ? $address->get() : $address->getEmail()),
						);
					}
					else
					{
						$message[$extField][] = array(
							'name'  => $item,
						);
					}
				}
			}
		}

		if (empty($message['__reply_to']))
		{
			$message['__reply_to'] = $message['__from'];
		}

		// @TODO: path
		$message['__href'] = sprintf('/mail/message/%u', $message['ID']);

		$urlManager = Attachment\Storage::getUrlManager();

		if (!empty($message['__files']) && is_array($message['__files']))
		{
			$urlParams = array();

			if (isset($_REQUEST['mail_uf_message_token']) && is_string($_REQUEST['mail_uf_message_token']))
			{
				$urlParams['mail_uf_message_token'] = $_REQUEST['mail_uf_message_token'];
			}

			foreach ($message['__files'] as $k => $item)
			{
				if ($diskFile = Attachment\Storage::getObjectByAttachment($item, true))
				{
					$message['__files'][$k] = array(
						'id' => sprintf('n%u', $diskFile->getId()),
						'name' => $item['FILE_NAME'],
						'url' => $urlManager->getUrlForShowFile($diskFile, $urlParams),
						'size' => \CFile::formatSize($diskFile->getSize()),
						'fileId' => $diskFile->getFileId(),
						'objectId' => $diskFile->getId(),
						'bytes' => $diskFile->getSize(),
					);

					if (\Bitrix\Disk\TypeFile::isImage($diskFile))
					{
						$message['__files'][$k]['preview'] = $urlManager->getUrlForShowFile(
							$diskFile,
							array_merge(
								array('width' => 80, 'height' => 80, 'exact' => 'Y'),
								$urlParams
							)
						);
					}

					$inlineParams = array_merge(
						array('__bxacid' => sprintf('n%u', $diskFile->getId())),
						$urlParams
					);
					$message['BODY_HTML'] = preg_replace(
						sprintf('/("|\')\s*aid:%u\s*\1/i', $item['ID']),
						sprintf('\1%s\1', $urlManager->getUrlForShowFile($diskFile, $inlineParams)),
						$message['BODY_HTML']
					);
				}
				else
				{
					$file = \CFile::getFileArray($item['FILE_ID']);
					if (!empty($file) && is_array($file))
					{
						$message['__files'][$k] = array(
							'id' => $file['ID'],
							'name' => $item['FILE_NAME'],
							'url' => $file['SRC'],
							'size' => \CFile::formatSize($file['FILE_SIZE']),
							'fileId' => $file['ID'],
							'bytes' => $file['FILE_SIZE'],
						);

						if (\CFile::isImage($item['FILE_NAME'], $item['CONTENT_TYPE']))
						{
							$preview = \CFile::resizeImageGet(
								$file, array('width' => 80, 'height' => 80),
								BX_RESIZE_IMAGE_EXACT, false
							);

							if (!empty($preview['src']))
							{
								$message['__files'][$k]['preview'] = $preview['src'];
							}
						}

						$message['BODY_HTML'] = preg_replace(
							sprintf('/("|\')\s*aid:%u\s*\1/i', $item['ID']),
							sprintf('\1%s\1', $file['SRC']),
							$message['BODY_HTML']
						);
					}
					else
					{
						unset($message['__files'][$k]);
					}
				}
			}
		}

		return $message;
	}

	public static function hasAccess(&$message, $userId = null)
	{
		global $USER;

		if (!($userId > 0 || is_object($USER) && $USER->isAuthorized()))
		{
			return false;
		}

		if (!($userId > 0))
		{
			$userId = $USER->getId();
		}

		$access = (bool) Mail\MailboxTable::getUserMailbox($message['MAILBOX_ID'], $userId);

		$message['__access_level'] = $access ? 'full' : false;

		if (!$access && isset($_REQUEST['mail_uf_message_token']))
		{
			$token = $signature = '';
			if (is_string($_REQUEST['mail_uf_message_token']) && strpos($_REQUEST['mail_uf_message_token'], ':') > 0)
			{
				list($token, $signature) = explode(':', $_REQUEST['mail_uf_message_token'], 2);
			}

			if (strlen($token) > 0 && strlen($signature) > 0)
			{
				$excerpt = Mail\Internals\MessageAccessTable::getList(array(
					'select' => array('SECRET', 'MESSAGE_ID'),
					'filter' => array(
						'=TOKEN' => $token,
						'=MAILBOX_ID' => $message['MAILBOX_ID'],
					),
					'limit' => 1,
				))->fetch();

				if (!empty($excerpt['SECRET']))
				{
					$signer = new Security\Sign\Signer(new Security\Sign\HmacAlgorithm('md5'));

					if ($signer->validate($excerpt['SECRET'], $signature, sprintf('user%u', $userId)))
					{
						$access = $message['ID'] == $excerpt['MESSAGE_ID'];

						if (!$access) // check parent access
						{
							$access = (bool) Mail\Internals\MessageClosureTable::getList(array(
								'select' => array('PARENT_ID'),
								'filter' => array(
									'=MESSAGE_ID' => $message['ID'],
									'=PARENT_ID' => $excerpt['MESSAGE_ID'],
								),
							))->fetch();
						}
					}
				}
			}
		}

		if (false === $message['__access_level'])
		{
			$message['__access_level'] = $access ? 'read' : false;
		}

		return $access;
	}

	public static function prepareSearchContent(&$fields)
	{
		// @TODO: filter short words, filter duplicates, str_rot13?
		return str_rot13(join(
			' ',
			array(
				$fields['FIELD_FROM'],
				$fields['FIELD_REPLY_TO'],
				$fields['FIELD_TO'],
				$fields['FIELD_CC'],
				$fields['FIELD_BCC'],
				$fields['SUBJECT'],
				$fields['BODY'],
			)
		));
	}

	public static function prepareSearchString($string)
	{
		return str_rot13($string);
	}

	public static function getTotalUnseenCount($userId)
	{
		$unseenTotal = static::getTotalUnseenForMailboxes($userId);
		$unseen = 0;
		foreach ($unseenTotal as $index => $item)
		{
			$unseen += (int)$item['UNSEEN'];
		}
		return $unseen;
	}

	public static function getTotalUnseenForMailboxes($userId)
	{
		$mailboxes = Mail\MailboxTable::getUserMailboxes($userId);

		if (empty($mailboxes))
		{
			return array();
		}

		$mailboxes = array_combine(
			array_column($mailboxes, 'ID'),
			$mailboxes
		);

		$mailboxFilter = array(
			'LOGIC' => 'OR',
		);
		foreach ($mailboxes as $item)
		{
			$mailboxFilter[] = array(
				'=MAILBOX_ID' => $item['ID'],
				'!@DIR_MD5' => array_map(
					'md5',
					array_merge(
						(array) $item['OPTIONS']['imap'][MessageFolder::TRASH],
						(array) $item['OPTIONS']['imap'][MessageFolder::SPAM]
					)
				),
			);
		}
		$totalUnseen = Mail\MailMessageUidTable::getList(array(
			'select' => array(
				'MAILBOX_ID',
				new \Bitrix\Main\Entity\ExpressionField('TOTAL', 'COUNT(1)'),
				new \Bitrix\Main\Entity\ExpressionField(
					'UNSEEN',
					"COUNT(IF(%s IN('N','U'), 1, NULL))",
					array('IS_SEEN')
				),
			),
			'filter' => array(
				$mailboxFilter,
				'>MESSAGE_ID' => 0,
			),
			'group' => array(
				'MAILBOX_ID',
			),
		))->fetchAll();
		$result = [];
		foreach ($totalUnseen as $index => $item)
		{
			$result[$item['MAILBOX_ID']] = [
				'TOTAL' => $item['TOTAL'],
				'UNSEEN' => $item['UNSEEN'],
			];
		}
		return $result;
	}

	public static function ensureAttachments(&$message)
	{
		if ($message['ATTACHMENTS'] > 0 || !($message['OPTIONS']['attachments'] > 0))
		{
			return;
		}

		if (Main\Config\Option::get('mail', 'save_attachments', B_MAIL_SAVE_ATTACHMENTS) !== 'Y')
		{
			return;
		}

		$mailboxHelper = Mailbox::createInstance($message['MAILBOX_ID'], false);

		$attachments = empty($mailboxHelper) ? false : $mailboxHelper->downloadAttachments($message);

		if (false === $attachments)
		{
			$logEntry = sprintf(
				'Helper\Message: Attachments downloading failed (%u:%s:%u)',
				$message['MAILBOX_ID'],
				$message['DIR_MD5'],
				$message['MSG_UID']
			);

			if (!empty($mailboxHelper) && !$mailboxHelper->getErrors()->isEmpty())
			{
				$logEntry .= PHP_EOL . join(PHP_EOL, $mailboxHelper->getErrors()->toArray());
			}

			addMessage2Log($logEntry, 'mail', 2);

			return false;
		}

		foreach ($attachments as $i => $item)
		{
			$attachFields = array(
				'MESSAGE_ID'   => $message['ID'],
				'FILE_NAME'    => $item['FILENAME'],
				'CONTENT_TYPE' => $item['CONTENT-TYPE'],
				'FILE_DATA'    => $item['BODY'],
				'CONTENT_ID'   => $item['CONTENT-ID'],
			);

			$attachmentId = \CMailMessage::addAttachment($attachFields);

			if ($attachmentId > 0)
			{
				$message['ATTACHMENTS']++;

				$message['BODY_HTML'] = preg_replace(
					sprintf(
						'/<img([^>]+)src\s*=\s*(\'|\")?\s*(http:\/\/cid:%s)\s*\2([^>]*)>/is',
						preg_quote($item['CONTENT-ID'], '/')
					),
					sprintf('<img\1src="aid:%u"\4>', $attachmentId),
					$message['BODY_HTML']
				);
			}
		}

		if ($message['ATTACHMENTS'] > 0)
		{
			\CMailMessage::update($message['ID'], array('BODY_HTML' => $message['BODY_HTML']));

			return $message['ID'];
		}
	}

	public static function resync(&$message)
	{
		$mailboxHelper = Mailbox::createInstance($message['MAILBOX_ID'], false);

		$result = empty($mailboxHelper) ? false : $mailboxHelper->resyncMessage($message);

		if (false === $result)
		{
			$logEntry = sprintf(
				'Helper\Message: Message resync failed (%u:%s:%u)',
				$message['MAILBOX_ID'],
				$message['DIR_MD5'],
				$message['MSG_UID']
			);

			if (!empty($mailboxHelper) && !$mailboxHelper->getErrors()->isEmpty())
			{
				$logEntry .= PHP_EOL . join(PHP_EOL, $mailboxHelper->getErrors()->toArray());
			}

			addMessage2Log($logEntry, 'mail', 3);
		}

		return $result;
	}

}
oauth/google.php000066400000002353147744702230007670 0ustar00<?php

namespace Bitrix\Mail\Helper\OAuth;

use Bitrix\Main;
use Bitrix\Mail;

class Google extends Mail\Helper\OAuth
{

	protected function __construct()
	{
		$this->oauthEntity = new GoogleInterface;

		$this->oauthEntity->addScope(array(
			'email',
			'https://mail.google.com/',
		));
	}

	protected function check()
	{
		$provider = new \CSocServGoogleOAuth;

		return $provider->checkSettings();
	}

	protected function mapUserData(array $userData)
	{
		return array(
			'email' => $userData['email'],
			'first_name' => $userData['given_name'],
			'last_name' => $userData['family_name'],
			'full_name' => $userData['name'],
			'image' => $userData['picture'],
			'error' => $userData['error']['message'],
		);
	}

	public static function getServiceName()
	{
		return 'google';
	}

	public function getControllerUrl()
	{
		return \CSocServAuth::getControllerUrl();
	}

}

if (Main\Loader::includeModule('socialservices'))
{
	class GoogleInterface extends \CGoogleOAuthInterface
	{

		public function getStorageTokens()
		{
			return false;
		}

		public function getTokenData()
		{
			return array(
				'access_token' => $this->access_token,
				'refresh_token' => $this->refresh_token,
				'expires_in' => $this->accessTokenExpires,
			);
		}

	}
}
oauth/yandex.php000066400000002533147744702230007704 0ustar00<?php

namespace Bitrix\Mail\Helper\OAuth;

use Bitrix\Main;
use Bitrix\Mail;

class Yandex extends Mail\Helper\OAuth
{

	protected function __construct()
	{
		$this->oauthEntity = new YandexInterface;
	}

	protected function check()
	{
		$provider = new \CSocServYandexAuth;

		return $provider->checkSettings();
	}

	protected function mapUserData(array $userData)
	{
		return array(
			'email' => $userData['default_email'],
			'first_name' => $userData['first_name'],
			'last_name' => $userData['last_name'],
			'full_name' => $userData['real_name'],
			'image' => sprintf('https://avatars.yandex.net/get-yapic/%s/islands-middle', $userData['default_avatar_id']),
			//'error' => $data['error']['message'],
		);
	}

	public static function getServiceName()
	{
		return 'yandex';
	}

	public function getControllerUrl()
	{
		return \CSocServYandexAuth::CONTROLLER_URL;
	}

}

if (Main\Loader::includeModule('socialservices'))
{
	class YandexInterface extends \CYandexOAuthInterface
	{

		public function getStorageTokens()
		{
			return false;
		}

		public function getTokenData()
		{
			return array(
				'access_token' => $this->access_token,
				'refresh_token' => $this->refresh_token,
				'expires_in' => $this->accessTokenExpires,
			);
		}

		public function getNewAccessToken($refreshToken = false, $userId = 0, $save = false)
		{
			return false;
		}

	}
}
oauth/liveid.php000066400000004127147744702230007671 0ustar00<?php

namespace Bitrix\Mail\Helper\OAuth;

use Bitrix\Main;
use Bitrix\Mail;

class LiveId extends Mail\Helper\OAuth
{

	protected function __construct()
	{
		$this->oauthEntity = new LiveIdInterface;

		$this->oauthEntity->addScope(array(
			'wl.emails',
			'wl.imap',
			'wl.offline_access',
		));
	}

	protected function check()
	{
		$provider = new \CSocServLiveIdOAuth;

		return $provider->checkSettings();
	}

	protected function mapUserData(array $userData)
	{
		return array(
			'email' => $userData['emails']['account'],
			'first_name' => $userData['first_name'],
			'last_name' => $userData['last_name'],
			'full_name' => $userData['name'],
			'image' => sprintf('https://apis.live.net/v5.0/%s/picture?type=small', $userData['id']),
			'error' => $userData['error']['message'],
		);
	}

	public static function getServiceName()
	{
		return 'liveid';
	}

	public function getControllerUrl()
	{
		return \CSocServLiveIdOAuth::CONTROLLER_URL;
	}

}

if (Main\Loader::includeModule('socialservices'))
{
	class_exists('CSocServLiveIdOAuth');

	class LiveIdInterface extends \CLiveIdOAuthInterface
	{

		public function setCode($code)
		{
			$this->code = $code;
		}

		public function setToken($access_token)
		{
			$this->access_token = $access_token;
		}

		public function getStorageTokens()
		{
			return false;
		}

		public function getTokenData()
		{
			return array(
				'access_token' => $this->access_token,
				'refresh_token' => $this->refresh_token,
				'expires_in' => time() + $this->accessTokenExpires,
			);
		}

		public function getCurrentUser()
		{
			if (empty($this->access_token))
			{
				return false;
			}

			$httpClient = new \Bitrix\Main\Web\HttpClient();
			$httpClient->setHeader('Authorization', 'Bearer ' . $this->access_token);

			$result = $httpClient->get(static::CONTACTS_URL);

			if (!empty($result))
			{
				try
				{
					$result = \Bitrix\Main\Web\Json::decode($result);
				}
				catch (Exception $e)
				{
					$result = null;
				}
			}

			if (is_array($result))
			{
				$result = array_merge(
					$result,
					$this->getTokenData()
				);
			}

			return $result;
		}

	}
}
oauth/mailru.php000066400000003077147744702230007711 0ustar00<?php

namespace Bitrix\Mail\Helper\OAuth;

use Bitrix\Main;
use Bitrix\Mail;

class Mailru extends Mail\Helper\OAuth
{

	protected function __construct()
	{
		$this->oauthEntity = new MailruInterface(
			\CSocServMailRu2::getOption('mailru2_client_id'),
			\CSocServMailRu2::getOption('mailru2_client_secret')
		);

		$this->oauthEntity->addScope(array(
			'userinfo',
			'mail.imap',
		));
	}

	protected function check()
	{
		$provider = new \CSocServMailRu2;

		return $provider->checkSettings();
	}

	protected function mapUserData(array $userData)
	{
		return array(
			'email' => $userData['email'],
			'first_name' => $userData['first_name'],
			'last_name' => $userData['last_name'],
			'full_name' => $userData['name'],
			'image' => $userData['image'],
			'error' => $userData['error_description'],
		);
	}

	public static function getServiceName()
	{
		return 'mailru';
	}

	public function getControllerUrl()
	{
		return \CSocServMailRu2::CONTROLLER_URL;
	}

}

if (Main\Loader::includeModule('socialservices'))
{
	class_exists('CSocServMailRu2');

	class MailruInterface extends \CMailRu2Interface
	{

		public function getStorageTokens()
		{
			return false;
		}

		public function getTokenData()
		{
			return array(
				'access_token' => $this->access_token,
				'refresh_token' => $this->refresh_token,
				'expires_in' => $this->accessTokenExpires,
			);
		}

		public function getCurrentUser()
		{
			$result = parent::getCurrentUser();

			if (is_array($result))
			{
				$result = array_merge(
					$result,
					$this->getTokenData()
				);
			}

			return $result;
		}

	}
}
mailbox/mailboxsyncmanager.php000064400000010177147744702230012613 0ustar00<?php
namespace Bitrix\Mail\Helper\Mailbox;

use COption;
use CUserOptions;

class MailboxSyncManager
{
	private $userId;
	private $mailCheckInterval;
	private $syncOptionCategory = 'global';
	private $syncOptionName = 'user_mailboxes_sync_info';

	public function __construct($userId)
	{
		$this->userId = $userId;
		$this->mailCheckInterval = COption::getOptionString('intranet', 'mail_check_period', 10) * 60;
	}

	public function getFailedToSyncMailboxes()
	{
		$mailboxes = [];
		$mailboxesSyncInfo = $this->getMailboxesSyncInfo();

		foreach ($mailboxesSyncInfo as $mailboxId => $lastMailCheckData)
		{
			if (!$lastMailCheckData['isSuccess'])
			{
				$mailboxes[$mailboxId] = $lastMailCheckData;
			}
		}
		return $mailboxes;
	}

	public function getSuccessSyncedMailboxes()
	{
		$mailboxesToSync = [];
		$mailboxesSyncInfo = $this->getMailboxesSyncInfo();

		foreach ($mailboxesSyncInfo as $mailboxId => $lastMailCheckData)
		{
			if ($lastMailCheckData['isSuccess'])
			{
				$mailboxesToSync[$mailboxId] = $lastMailCheckData;
			}
		}
		return $mailboxesToSync;
	}

	public function getNeedToBeSyncedMailboxes()
	{
		$mailboxesSyncData = $this->getSuccessSyncedMailboxes();
		$mailboxesToSync = [];
		foreach ($mailboxesSyncData as $mailboxId => $lastMailCheckData)
		{
			if ($lastMailCheckData['timeStarted'] >= 0 && (time() - intval($lastMailCheckData['timeStarted']) >= $this->mailCheckInterval))
			{
				$mailboxesToSync[$mailboxId] = $lastMailCheckData;
			}
		}
		return $mailboxesToSync;
	}

	public function getMailCheckInterval()
	{
		return $this->mailCheckInterval;
	}

	public function deleteSyncData($mailboxId)
	{
		$mailboxesOptions = $this->getMailboxesSyncInfo();
		if (empty($mailboxesOptions))
		{
			return;
		}
		unset($mailboxesOptions[$mailboxId]);
		if (empty($mailboxesOptions))
		{
			CUserOptions::deleteOption($this->syncOptionCategory, $this->syncOptionName, false, $this->userId);
		}
		else
		{
			$this->setOption($mailboxesOptions);
		}
	}

	public function setDefaultSyncData($mailboxId)
	{
		$mailboxesOptions = $this->getMailboxesSyncInfo();
		$mailboxesOptions[$mailboxId] = ['isSuccess' => true, 'timeStarted' => 0];
		$this->setOption($mailboxesOptions);
	}

	public function setSyncStartedData($mailboxId, $time = null)
	{
		$mailboxesOptions = $this->getMailboxesSyncInfo();
		$mailboxesOptions[$mailboxId] = ['isSuccess' => true, 'timeStarted' => $time !== null && (int)$time >= 0 ? (int)$time : time()];
		$this->setOption($mailboxesOptions);
	}

	public function setSyncStatus($mailboxId, $isSuccess, $time = null)
	{
		$mailboxesOptions = $this->getMailboxesSyncInfo();
		$mailboxesOptions[$mailboxId] = ['isSuccess' => $isSuccess, 'timeStarted' => $time !== null && (int)$time >= 0 ? (int)$time : time()];
		$this->setOption($mailboxesOptions);
	}

	private function setOption($mailboxesSyncInfo)
	{
		CUserOptions::setOption($this->syncOptionCategory, $this->syncOptionName, $mailboxesSyncInfo, false, $this->userId);
	}

	/**
	 * @return mixed
	 */
	private function getMailboxesSyncInfo()
	{
		return CUserOptions::getOption($this->syncOptionCategory, $this->syncOptionName, [], $this->userId);
	}

	public function getNextTimeToSync($lastMailCheckData)
	{
		return intval($lastMailCheckData['timeStarted']) + $this->mailCheckInterval - time();
	}

	/**
	 * @return null|int
	 */
	public function getFirstFailedToSyncMailboxId()
	{
		$mailboxesIdsFailedToSync = array_keys($this->getFailedToSyncMailboxes());
		return !empty($mailboxesIdsFailedToSync) && count($mailboxesIdsFailedToSync) > 0
			? (int)$mailboxesIdsFailedToSync[0]
			: null;
	}

	public function getLastMailboxSyncIsSuccessStatus($mailboxId)
	{
		$mailboxesOptions = $this->getMailboxesSyncInfo();
		if (!(isset($mailboxesOptions[$mailboxId]) && array_key_exists('isSuccess', $mailboxesOptions[$mailboxId])))
		{
			return null;
		}
		return $mailboxesOptions[$mailboxId]['isSuccess'];
	}

	public function getLastMailboxSyncTime($mailboxId)
	{
		$mailboxesOptions = $this->getMailboxesSyncInfo();
		if (!(isset($mailboxesOptions[$mailboxId]) && array_key_exists('timeStarted', $mailboxesOptions[$mailboxId])))
		{
			return null;
		}
		return $mailboxesOptions[$mailboxId]['timeStarted'];
	}
}mailbox/sharedmailboxesmanager.php000066400000003572147744702230013440 0ustar00<?php
namespace Bitrix\Mail\Helper\Mailbox;

use Bitrix\Mail\Internals\MailboxAccessTable;
use Bitrix\Mail\MailboxTable;
use Bitrix\Main\Entity\Query\Filter\Expression\Column;
use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Main\ORM\Query\Query;

class SharedMailboxesManager
{
	public static function getSharedMailboxesCount()
	{
		$count = static::getBaseQueryForSharedMailboxes()
			->addSelect(Query::expr()->countDistinct('MAILBOX_ID'), 'CNT')
			->exec()
			->fetch();
		return !empty($count['CNT']) ? $count['CNT'] : 0;
	}

	public static function getSharedMailboxesIds()
	{
		$mailboxesIds = static::getBaseQueryForSharedMailboxes()
			->addSelect('MAILBOX_ID')
			->addGroup('MAILBOX_ID')
			->exec()
			->fetchAll();
		return array_map('intval', array_column($mailboxesIds, 'MAILBOX_ID'));
	}

	public static function getUserIdsWithAccessToMailbox($mailboxId)
	{
		$userCodes = MailboxAccessTable::query()
			->addSelect('ACCESS_CODE')
			->where('MAILBOX_ID', $mailboxId)
			->whereLike('ACCESS_CODE', 'U%')
			->exec()
			->fetchAll();
		$results = [];
		foreach ($userCodes as $userAccessCode)
		{
			// @TODO: departments
			if (preg_match('#U[0-9]+#', $userAccessCode['ACCESS_CODE']) === 1)
			{
				$results[] = substr($userAccessCode['ACCESS_CODE'], 1);
			}
		}
		return $results;
	}

	/**
	 * @return \Bitrix\Mail\Internals\EO_MailboxAccess_Query|Query
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\SystemException
	 */
	private static function getBaseQueryForSharedMailboxes()
	{
		return MailboxAccessTable::query()
			->registerRuntimeField('', new ReferenceField('ref', MailboxTable::class, ['=this.MAILBOX_ID' => 'ref.ID'], ['join_type' => 'INNER']))
			->where(new ExpressionField('ac', 'CONCAT("U", %s)', 'ref.USER_ID'), '!=', new Column('ACCESS_CODE'))
			->where('ref.ACTIVE', 'Y')
			->where('ref.LID', SITE_ID);
	}
}mailbox/responseparser.php000064400000002033147744702230011773 0ustar00<?php

namespace Bitrix\Mail\Helper\Mailbox;

class ResponseParser
{
	public function getOldToNewUidsMap($copyUid)
	{
		$uIds = [];
		$dirUidValidity = '';
		if ($responseLine = stristr($copyUid, 'COPYUID'))
		{
			$data = explode(' ', stristr($copyUid, 'COPYUID'));
			if (isset($data[1]) && isset($data[2]) && isset($data[3]))
			{
				$dirUidValidity = $data[1];
				$idsFrom = $this->getIdsSet($data[2]);
				$idsTo = $this->getIdsSet(str_replace(']', '', $data[3]));
				$uIds = array_combine($idsFrom, $idsTo);
			}
		}

		return [
			'uids' => $uIds,
			'dirUid' => $dirUidValidity,
		];
	}

	private function getIdsSet($line)
	{
		$idsFrom = [];
		$idsFromParsed = explode(',', $line);
		foreach ($idsFromParsed as $_index => $_idFrom)
		{
			$sequence = explode(':', $_idFrom);
			if (count($sequence) == 2)
			{
				$idsFrom = array_merge($idsFrom, range(min($sequence[0], $sequence[1]), max($sequence[0], $sequence[1]), 1));
			}
			elseif (count($sequence) == 1)
			{
				$idsFrom[] = intval($sequence[0]);
			}
		}
		return $idsFrom;
	}
}mailbox/imap.php000066400000111274147744702230007660 0ustar00<?php

namespace Bitrix\Mail\Helper\Mailbox;

use Bitrix\Mail\Helper\MessageFolder;
use Bitrix\Main;
use Bitrix\Mail;

class Imap extends Mail\Helper\Mailbox
{
	const MESSAGE_PARTS_TEXT = 1;
	const MESSAGE_PARTS_ATTACHMENT = 2;
	const MESSAGE_PARTS_ALL = -1;

	protected $client;

	protected function __construct($mailbox)
	{
		parent::__construct($mailbox);

		$mailbox = &$this->mailbox;

		$this->client = new Mail\Imap(
			$mailbox['SERVER'],
			$mailbox['PORT'],
			$mailbox['USE_TLS'] == 'Y' || $mailbox['USE_TLS'] == 'S',
			$mailbox['USE_TLS'] == 'Y',
			$mailbox['LOGIN'],
			$mailbox['PASSWORD']
		);
	}

	protected function normalizeMailboxOptions()
	{
		$options = &$this->mailbox['OPTIONS'];

		if (empty($options['imap']) || !is_array($options['imap']))
		{
			$options['imap'] = array();
		}

		$imapOptions = &$options['imap'];
		if (empty($imapOptions[MessageFolder::INCOME]) || !is_array($imapOptions[MessageFolder::INCOME]))
		{
			$imapOptions[MessageFolder::INCOME] = array();
		}
		if (empty($imapOptions[MessageFolder::OUTCOME]) || !is_array($imapOptions[MessageFolder::OUTCOME]))
		{
			$imapOptions[MessageFolder::OUTCOME] = array();
		}
	}

	public function getSyncStatus()
	{
		$meta = Mail\MailMessageUidTable::getList(array(
			'select' => array(
				new Main\Entity\ExpressionField('TOTAL', 'COUNT(1)'),
			),
			'filter' => array(
				'=MAILBOX_ID' => $this->mailbox['ID'],
			),
		))->fetch();

		if ($meta['TOTAL'] > 0 && $this->mailbox['OPTIONS']['imap']['total'] > 0)
		{
			return $meta['TOTAL'] / $this->mailbox['OPTIONS']['imap']['total'];
		}
		else
		{
			return parent::getSyncStatus();
		}
	}

	protected function syncInternal()
	{
		$count = $this->syncMailbox();
		if (false === $count)
		{
			$this->errors = new Main\ErrorCollection($this->client->getErrors()->toArray());
		}

		return $count;
	}

	protected function createMessage(Main\Mail\Mail $message, array $fields = array())
	{
		$dir = reset($this->mailbox['OPTIONS']['imap'][MessageFolder::OUTCOME]) ?: 'INBOX';

		$fields = array_merge(
			$fields,
			array(
				'DIR_MD5' => md5($dir),
				'DIR_UIDV' => 0,
				'MSG_UID' => 0,
			)
		);

		return parent::createMessage($message, $fields);
	}

	public function syncOutgoing()
	{
		$this->cacheDirs();

		parent::syncOutgoing();
	}

	public function uploadMessage(Main\Mail\Mail $message, array &$excerpt = null)
	{
		$dir = reset($this->mailbox['OPTIONS']['imap'][MessageFolder::OUTCOME]) ?: 'INBOX';

		$data = $this->client->select($dir, $error);

		if (false === $data)
		{
			$this->errors = new Main\ErrorCollection($this->client->getErrors()->toArray());

			return false;
		}

		if (!empty($excerpt['__unique_headers']))
		{
			if ($this->client->searchByHeader(false, $dir, $excerpt['__unique_headers'], $error))
			{
				return false;
			}
		}

		if (!empty($excerpt['ID']))
		{
			class_exists('Bitrix\Mail\Helper');

			Mail\DummyMail::overwriteMessageHeaders(
				$message,
				array(
					'X-Bitrix-Mail-Message-UID' => $excerpt['ID'],
				)
			);
		}

		$result = $this->client->append(
			$dir,
			array('\Seen'),
			new \DateTime,
			sprintf(
				'%1$s%3$s%3$s%2$s',
				$message->getHeaders(),
				$message->getBody(),
				$message->getMailEol()
			),
			$error
		);

		if (false === $result)
		{
			$this->errors = new Main\ErrorCollection($this->client->getErrors()->toArray());

			return false;
		}

		$this->syncDir($dir);

		return $result;
	}

	public function downloadMessage(array &$excerpt)
	{
		if (empty($excerpt['MSG_UID']) || empty($excerpt['DIR_MD5']))
		{
			return false;
		}

		$dir = MessageFolder::getFolderNameByHash($excerpt['DIR_MD5'], $this->mailbox['OPTIONS']);
		if (empty($dir))
		{
			return false;
		}

		$body = $this->client->fetch(true, $dir, $excerpt['MSG_UID'], '(BODY.PEEK[])', $error);

		if (false === $body)
		{
			$this->errors = new Main\ErrorCollection($this->client->getErrors()->toArray());

			return false;
		}

		return empty($body['BODY[]']) ? null : $body['BODY[]'];
	}

	public function downloadMessageParts(array &$excerpt, Mail\Imap\BodyStructure $bodystructure, $flags = Imap::MESSAGE_PARTS_ALL)
	{
		if (empty($excerpt['MSG_UID']) || empty($excerpt['DIR_MD5']))
		{
			return false;
		}

		$dir = MessageFolder::getFolderNameByHash($excerpt['DIR_MD5'], $this->mailbox['OPTIONS']);
		if (empty($dir))
		{
			return false;
		}

		$rfc822Parts = array();

		$select = array_filter(
			$bodystructure->traverse(
				function (Mail\Imap\BodyStructure $item) use ($flags, &$rfc822Parts)
				{
					if ($item->isMultipart())
					{
						return;
					}

					$isTextItem = $item->isText() && !$item->isAttachment();
					if ($flags & ($isTextItem ? Imap::MESSAGE_PARTS_TEXT : Imap::MESSAGE_PARTS_ATTACHMENT))
					{
						// due to yandex bug
						if ('message' === $item->getType() && 'rfc822' === $item->getSubtype())
						{
							$rfc822Parts[] = $item;

							return sprintf('BODY.PEEK[%1$s.HEADER] BODY.PEEK[%1$s.TEXT]', $item->getNumber());
						}

						return sprintf('BODY.PEEK[%1$s.MIME] BODY.PEEK[%1$s]', $item->getNumber());
					}
				},
				true
			)
		);

		if (empty($select))
		{
			return array();
		}

		$parts = $this->client->fetch(
			true,
			$dir,
			$excerpt['MSG_UID'],
			sprintf('(%s)', join(' ', $select)),
			$error
		);

		if (false === $parts)
		{
			$this->errors = new Main\ErrorCollection($this->client->getErrors()->toArray());

			return false;
		}

		foreach ($rfc822Parts as $item)
		{
			$headerKey = sprintf('BODY[%s.HEADER]', $item->getNumber());
			$bodyKey = sprintf('BODY[%s.TEXT]', $item->getNumber());

			if (array_key_exists($headerKey, $parts) || array_key_exists($bodyKey, $parts))
			{
				$partMime = 'Content-Type: message/rfc822';
				if (!empty($item->getParams()['name']))
				{
					$partMime .= sprintf('; name="%s"', $item->getParams()['name']);
				}

				if (!empty($item->getDisposition()[0]))
				{
					$partMime .= sprintf("\r\nContent-Disposition: ", $item->getDisposition()[0]);
					if (!empty($item->getDisposition()[1]) && is_array($item->getDisposition()[1]))
					{
						foreach ($item->getDisposition()[1] as $name => $value)
						{
							$partMime .= sprintf('; %s="%s"', $name, $value);
						}
					}
				}

				$parts[sprintf('BODY[%1$s.MIME]', $item->getNumber())] = $partMime;
				$parts[sprintf('BODY[%1$s]', $item->getNumber())] = sprintf(
					"%s\r\n\r\n%s",
					rtrim($parts[$headerKey], "\r\n"),
					ltrim($parts[$bodyKey], "\r\n")
				);

				unset($parts[$headerKey], $parts[$bodyKey]);
			}
		}

		return $parts;
	}

	public function cacheDirs()
	{
		$dirs = $this->client->listMailboxes('*', $error, true);
		if (false === $dirs)
		{
			$this->errors = new Main\ErrorCollection($this->client->getErrors()->toArray());

			return false;
		}

		$imapDirs = array();
		$disabledDirs = array();
		$availableDirs = array();

		$outcomeDirs = array();
		$draftsDirs = array();
		$trashDirs = array();
		$spamDirs = array();
		$inboxDirs = array();
		$inboxTrees = array();

		foreach ($dirs as $i => $item)
		{
			if (strtoupper($item['name']) === 'INBOX')
			{
				if (!$inboxDirs)
				{
					$inboxDirs = [$item['name']];
				}
				else
				{
					$disabledDirs[] = $item['name'];
				}
			}
		}

		foreach ($dirs as $i => $item)
		{
			$imapDirs[$item['name']] = $item['path'];

			if (in_array(reset($item['path']), $inboxDirs))
			{
				$inboxTrees[$item['name']] = $item['path'];
			}

			if (preg_grep('/^ \x5c Noselect $/ix', $item['flags']))
			{
				$disabledDirs[] = $item['name'];
			}

			if (preg_grep('/^ \x5c Sent $/ix', $item['flags']))
			{
				$outcomeDirs[] = $item['name'];
			}

			if (preg_grep('/^ \x5c Drafts $/ix', $item['flags']))
			{
				$draftsDirs[] = $item['name'];
			}

			if (preg_grep('/^ \x5c Trash $/ix', $item['flags']))
			{
				$trashDirs[] = $item['name'];
			}

			if (preg_grep('/^ \x5c ( Junk | Spam ) $/ix', $item['flags']))
			{
				$spamDirs[] = $item['name'];
			}
		}

		// @TODO: filter disabled from income, outcome etc.

		$this->reloadMailboxOptions();

		$options = &$this->mailbox['OPTIONS'];

		$options['imap']['dirs'] = $imapDirs = $inboxTrees + $imapDirs;
		$options['imap']['disabled'] = $disabledDirs;

		$availableDirs = array_diff(array_keys($imapDirs), $disabledDirs);

		$options['imap'][MessageFolder::INCOME] = $inboxDirs;

		$options['imap'][MessageFolder::OUTCOME] = array_intersect(
			(array) $options['imap'][MessageFolder::OUTCOME],
			$availableDirs
		) ?: $outcomeDirs;

		$options['imap'][MessageFolder::DRAFTS] = array_intersect(
			(array) $options['imap'][MessageFolder::DRAFTS],
			$availableDirs
		) ?: $draftsDirs;
		$options['imap'][MessageFolder::DRAFTS] = $draftsDirs; // @TODO: remove if drafts dir settings implemented

		$options['imap'][MessageFolder::TRASH] = array_intersect(
			(array) $options['imap'][MessageFolder::TRASH],
			$availableDirs
		) ?: $trashDirs;

		$options['imap'][MessageFolder::SPAM] = array_intersect(
			(array) $options['imap'][MessageFolder::SPAM],
			$availableDirs
		) ?: $spamDirs;

		if (!empty($options['imap']['!sync_dirs']))
		{
			$options['imap']['sync_dirs'] = $options['imap']['!sync_dirs'];
		}

		if (!empty($options['imap']['sync_dirs']))
		{
			$options['imap']['ignore'] = array_values(array_diff(
				array_keys($imapDirs),
				(array) $options['imap']['sync_dirs']
			));

			unset($options['imap']['sync_dirs']);
		}

		if (!array_key_exists('ignore', $options['imap']))
		{
			$options['imap']['ignore'] = array_merge(
				(array) $options['imap'][MessageFolder::DRAFTS],
				(array) $options['imap'][MessageFolder::TRASH],
				(array) $options['imap'][MessageFolder::SPAM]
			);
		}

		$options['imap']['ignore'] = array_unique(array_merge(
			(array) $options['imap']['ignore'],
			$disabledDirs
		));

		Mail\MailboxTable::update(
			$this->mailbox['ID'],
			array(
				'OPTIONS' => $options,
			)
		);

		return $dirs;
	}

	public function cacheMeta()
	{
		$dirs = $this->cacheDirs();
		if (false === $dirs)
		{
			return false;
		}

		$meta = array();

		foreach ($dirs as $i => $item)
		{
			if (!in_array($item['name'], (array) $this->mailbox['OPTIONS']['imap']['ignore']))
			{
				if (!preg_grep('/^ \x5c Noselect $/ix', $item['flags']))
				{
					$data = $this->client->examine($item['name'], $error);
					if (false === $data)
					{
						$this->warnings->add($this->client->getErrors()->toArray());
					}
					else
					{
						$item = array_merge($item, $data);
					}
				}
			}

			$meta[$item['name']] = $item;
		}

		$this->reloadMailboxOptions();

		$options = &$this->mailbox['OPTIONS'];

		$options['imap']['total'] = array_reduce(
			$meta,
			function ($sum, $item) use (&$options)
			{
				return $sum + (in_array($item['name'], (array) $options['imap']['ignore']) ? 0 : $item['exists']);
			},
			0
		);

		Mail\MailboxTable::update(
			$this->mailbox['ID'],
			array(
				'OPTIONS' => $options,
			)
		);

		return $meta;
	}

	protected function getFolderToMessagesMap($messages)
	{
		if (isset($messages['MSG_UID']))
		{
			$messages = [$messages];
		}
		$data = [];
		$result = new Main\Result();
		foreach ($messages as $message)
		{
			$id = $message['MSG_UID'];
			$folderFrom = MessageFolder::getFolderNameByHash($message['DIR_MD5'], $this->mailbox['OPTIONS']);
			$data[$folderFrom][] = $id;
			$results[$folderFrom][] = $message;
		}
		return $result->setData($data);
	}

	public function markUnseen($messages)
	{
		$result = $this->getFolderToMessagesMap($messages);
		foreach ($result->getData() as $folderFrom => $ids)
		{
			$result = $this->client->unseen($ids, $folderFrom);
			if (!$result->isSuccess() || !$this->client->getErrors()->isEmpty())
			{
				break;
			}
		}
		return $result;
	}

	public function markSeen($messages)
	{
		$result = $this->getFolderToMessagesMap($messages);
		foreach ($result->getData() as $folderFrom => $ids)
		{
			$result = $this->client->seen($ids, $folderFrom);
			if (!$result->isSuccess() || !$this->client->getErrors()->isEmpty())
			{
				break;
			}
		}
		return $result;
	}

	public function moveMailsToFolder($messages, $folderTo)
	{
		$result = $this->getFolderToMessagesMap($messages);
		$moveResult = new Main\Result();
		foreach ($result->getData() as $folderFrom => $ids)
		{
			$moveResult = $this->client->moveMails($ids, $folderFrom, $folderTo);
			if (!$moveResult->isSuccess() || !$this->client->getErrors()->isEmpty())
			{
				break;
			}
		}

		return $moveResult;
	}

	public function deleteMails($messages)
	{
		$result = $this->getFolderToMessagesMap($messages);

		foreach ($result->getData() as $folderName => $messageId)
		{
			$result = $this->client->delete($messageId, $folderName);
		}

		return $result;
	}

	public function syncMailbox()
	{
		if (!$this->client->authenticate($error))
		{
			return false;
		}

		$meta = $this->cacheMeta();
		if (false === $meta)
		{
			return false;
		}

		$count = 0;

		$queue = array(
			'inbox' => array(),
			MessageFolder::OUTCOME => array(),
			'other' => array(),
			MessageFolder::TRASH => array(),
			MessageFolder::SPAM => array(),
		);
		foreach ($meta as $dir => $item)
		{
			if (in_array($dir, (array) $this->mailbox['OPTIONS']['imap']['ignore']))
			{
				continue;
			}

			if ('inbox' == strtolower(reset($item['path'])))
			{
				$queue['inbox'][$dir] = $item;
			}
			else if (in_array($dir, (array) $this->mailbox['OPTIONS']['imap'][MessageFolder::OUTCOME]))
			{
				$queue[MessageFolder::OUTCOME][$dir] = $item;
			}
			else if (in_array($dir, (array) $this->mailbox['OPTIONS']['imap'][MessageFolder::TRASH]))
			{
				$queue[MessageFolder::TRASH][$dir] = $item;
			}
			else if (in_array($dir, (array) $this->mailbox['OPTIONS']['imap'][MessageFolder::SPAM]))
			{
				$queue[MessageFolder::SPAM][$dir] = $item;
			}
			else
			{
				$queue['other'][$dir] = $item;
			}
		}

		$meta = call_user_func_array('array_merge', $queue);

		foreach ($meta as $dir => $item)
		{
			if ($item['exists'] > 0)
			{
				$count += $this->syncDir($dir);

				if ($this->isTimeQuotaExceeded())
				{
					break;
				}
			}
		}

		$this->lastSyncResult = ['newMessages' => $count, 'updatedMessages' => 0, 'deletedMessages' => 0];

		if (!$this->isTimeQuotaExceeded())
		{
			$result = $this->unregisterMessages(array(
				'!@DIR_MD5' => array_map(
					'md5',
					array_diff(
						array_keys($meta),
						(array) $this->mailbox['OPTIONS']['imap']['ignore']
					)
				),
			));

			$countDeleted = $result ? $result->getCount() : 0;

			$this->lastSyncResult['deletedMessages'] += $countDeleted;

			if (!empty($this->syncParams['full']))
			{
				foreach ($meta as $dir => $item)
				{
					$this->resyncDir($dir);

					if ($this->isTimeQuotaExceeded())
					{
						break;
					}
				}
			}
		}

		return $count;
	}

	public function syncDir($dir)
	{
		$count = 0;

		if (in_array($dir, (array) $this->mailbox['OPTIONS']['imap']['ignore']))
		{
			return $count;
		}

		while ($range = $this->getSyncRange($dir, $uidtoken))
		{
			$reverse = $range[0] > $range[1];

			sort($range);

			$messages = $this->client->fetch(
				true,
				$dir,
				join(':', $range),
				'(UID FLAGS INTERNALDATE RFC822.SIZE BODYSTRUCTURE BODY.PEEK[HEADER])',
				$error
			);

			if (empty($messages))
			{
				if (false === $messages)
				{
					$this->warnings->add($this->client->getErrors()->toArray());
				}
				else
				{
					// @TODO: log
				}

				break;
			}

			$reverse ? krsort($messages) : ksort($messages);

			$this->parseHeaders($messages);

			$this->blacklistMessages($dir, $messages);

			$this->prepareMessages($dir, $uidtoken, $messages);

			$hashesMap = array();
			foreach ($messages as $id => $item)
			{
				if ($this->syncMessage($dir, $uidtoken, $item, $hashesMap))
				{
					$count++;
				}

				if ($this->isTimeQuotaExceeded())
				{
					break 2;
				}
			}
		}

		if (false === $range)
		{
			$this->warnings->add($this->client->getErrors()->toArray());
		}

		return $count;
	}

	public function resyncDir($dir)
	{
		if (in_array($dir, (array) $this->mailbox['OPTIONS']['imap']['ignore']))
		{
			$result = $this->unregisterMessages(array(
				'=DIR_MD5' => md5($dir),
			));

			$countDeleted = $result ? $result->getCount() : 0;

			$this->lastSyncResult['deletedMessages'] += $countDeleted;

			return;
		}

		$meta = $this->client->select($dir, $error);
		if (false === $meta)
		{
			$this->warnings->add($this->client->getErrors()->toArray());

			return;
		}

		$uidtoken = $meta['uidvalidity'];

		if ($meta['exists'] > 0)
		{
			if ($uidtoken > 0)
			{
				$result = $this->unregisterMessages(array(
					'=DIR_MD5'  => md5($dir),
					'<DIR_UIDV' => $uidtoken,
				));

				$countDeleted = $result ? $result->getCount() : 0;

				$this->lastSyncResult['deletedMessages'] += $countDeleted;
			}
		}
		else
		{
			if ($this->client->ensureEmpty($dir, $error))
			{
				$result = $this->unregisterMessages(array(
					'=DIR_MD5' => md5($dir),
				));

				$countDeleted = $result ? $result->getCount() : 0;

				$this->lastSyncResult['deletedMessages'] += $countDeleted;
			}

			return;
		}

		$fetcher = function ($range) use ($dir)
		{
			$messages = $this->client->fetch(false, $dir, $range, '(UID FLAGS)', $error);

			if (empty($messages))
			{
				if (false === $messages)
				{
					$this->warnings->add($this->client->getErrors()->toArray());
				}
				else
				{
					// @TODO: log
				}

				return;
			}

			krsort($messages);

			return $messages;
		};

		$messages = $fetcher($meta['exists'] > 10000 ? sprintf('1,%u', $meta['exists']) : '1:*');

		if (empty($messages))
		{
			return;
		}

		$range = array(
			reset($messages)['UID'],
			end($messages)['UID'],
		);
		sort($range);

		$result = $this->unregisterMessages(array(
			'=DIR_MD5' => md5($dir),
			'>MSG_UID' => 0,
			array(
				'LOGIC' => 'OR',
				'<MSG_UID' => $range[0],
				'>MSG_UID' => $range[1],
			),
		));

		$countDeleted = $result ? $result->getCount() : 0;

		$this->lastSyncResult['deletedMessages'] += $countDeleted;

		if (!($meta['exists'] > 10000))
		{
			$this->resyncMessages($dir, $uidtoken, $messages);

			return;
		}

		$range1 = $meta['exists'];
		while ($range1 > 0)
		{
			$rangeSize = $range1 > 10000 ? 8000 : $range1;
			$range0 = max($range1 - $rangeSize, 1);

			$messages = $fetcher(sprintf('%u:%u', $range0, $range1));

			if (empty($messages))
			{
				return;
			}

			$this->resyncMessages($dir, $uidtoken, $messages);

			if ($this->isTimeQuotaExceeded())
			{
				return;
			}

			$range1 -= $rangeSize;
		}
	}

	protected function parseHeaders(&$messages)
	{
		foreach ($messages as $id => $item)
		{
			$messages[$id]['__header'] = \CMailMessage::parseHeader($item['BODY[HEADER]'], $this->mailbox['LANG_CHARSET']);
			$messages[$id]['__from'] = array_unique(array_map('strtolower', array_filter(array_merge(
				\CMailUtil::extractAllMailAddresses($messages[$id]['__header']->getHeader('FROM')),
				\CMailUtil::extractAllMailAddresses($messages[$id]['__header']->getHeader('REPLY-TO'))
			), 'trim')));
		}
	}

	protected function blacklistMessages($dir, &$messages)
	{
		$trashDirs = (array) $this->mailbox['OPTIONS']['imap'][MessageFolder::TRASH];
		$spamDirs = (array) $this->mailbox['OPTIONS']['imap'][MessageFolder::SPAM];

		$targetDir = reset($spamDirs) ?: reset($trashDirs) ?: null;

		if (empty($targetDir) || in_array($dir, array_merge($trashDirs, $spamDirs)))
		{
			return;
		}

		$blacklist = array(
			'email'  => array(),
			'domain' => array(),
		);

		$blacklistEmails = Mail\BlacklistTable::query()
			->addSelect('*')
			->setFilter(array(
				'=SITE_ID' => $this->mailbox['LID'],
				array(
					'LOGIC' => 'OR',
					'=MAILBOX_ID' => $this->mailbox['ID'],
					array(
						'=MAILBOX_ID' => 0,
						'@USER_ID' => array(0, $this->mailbox['USER_ID']),
					),
				),
			))
			->exec()
			->fetchCollection();
		foreach ($blacklistEmails as $blacklistEmail)
		{
			if ($blacklistEmail->isDomainType())
			{
				$blacklist['domain'][] = $blacklistEmail;
			}
			else
			{
				$blacklist['email'][] = $blacklistEmail;
			}
		}

		if (empty($blacklist['email']) && empty($blacklist['domain']))
		{
			return;
		}

		$targetMessages = [];
		$emailAddresses = array_map(function ($element)
		{
			/** @var Mail\Internals\Entity\BlacklistEmail $element */
			return $element->getItemValue();
		}, $blacklist['email']);
		$domains = array_map(function ($element)
		{
			/** @var Mail\Internals\Entity\BlacklistEmail $element */
			return $element->getItemValue();
		}, $blacklist['domain']);

		foreach ($messages as $id => $item)
		{
			if (!empty($blacklist['email']))
			{
				if (array_intersect($messages[$id]['__from'], $emailAddresses))
				{
					$targetMessages[$id] = $item['UID'];

					continue;
				}
				else
				{
					foreach ($blacklist['email'] as $blacklistMail)
					{
						/** @var Mail\Internals\Entity\BlacklistEmail $blacklistMail */
						if (array_intersect($messages[$id]['__from'], [$blacklistMail->convertDomainToPunycode()]))
						{
							$targetMessages[$id] = $item['UID'];
							continue;
						}
					}
				}
			}

			if (!empty($blacklist['domain']))
			{
				foreach ($messages[$id]['__from'] as $email)
				{
					$domain = substr($email, strrpos($email, '@'));
					if (in_array($domain, $domains))
					{
						$targetMessages[$id] = $item['UID'];

						continue 2;
					}
				}
			}
		}

		if (!empty($targetMessages))
		{
			if ($this->client->moveMails($targetMessages, $dir, $targetDir)->isSuccess())
			{
				$messages = array_diff_key($messages, $targetMessages);
			}
		}
	}

	protected function prepareMessages($dir, $uidtoken, &$messages)
	{
		$excerpt = array();

		$range = array(
			reset($messages)['UID'],
			end($messages)['UID'],
		);
		sort($range);

		$result = $this->listMessages(array(
			'select' => array('ID'),
			'filter' => array(
				'=DIR_MD5' => md5($dir),
				'=DIR_UIDV' => $uidtoken,
				'>=MSG_UID' => $range[0],
				'<=MSG_UID' => $range[1],
			),
		), false);

		while ($item = $result->fetch())
		{
			$excerpt[] = $item['ID'];
		}

		$uids = array();
		$hashes = array();
		foreach ($messages as $id => $item)
		{
			$messageUid = md5(sprintf('%s:%u:%u', $dir, $uidtoken, $item['UID']));

			if (in_array($messageUid, $excerpt))
			{
				unset($messages[$id]);
				continue;
			}

			$excerpt[] = $uids[$id] = $messageUid;

			$hashes[$id] = md5(sprintf(
				'%s:%s:%u',
				trim($item['BODY[HEADER]']),
				$item['INTERNALDATE'],
				$item['RFC822.SIZE']
			));

			$messages[$id]['__internaldate'] = Main\Type\DateTime::createFromPhp(
				\DateTime::createFromFormat(
					'j-M-Y H:i:s O',
					ltrim(trim($item['INTERNALDATE']), '0')
				) ?: new \DateTime
			);

			$messages[$id]['__fields'] = array(
				'ID'           => $messageUid,
				'DIR_MD5'      => md5($dir),
				'DIR_UIDV'     => $uidtoken,
				'MSG_UID'      => $item['UID'],
				'INTERNALDATE' => $messages[$id]['__internaldate'],
				'IS_SEEN'      => preg_grep('/^ \x5c Seen $/ix', $item['FLAGS']) ? 'Y' : 'N',
				'HEADER_MD5'   => $hashes[$id],
				'MESSAGE_ID'   => 0,
			);

			if (preg_match('/X-Bitrix-Mail-Message-UID:\s*([a-f0-9]+)/i', $item['BODY[HEADER]'], $matches))
			{
				$messages[$id]['__replaces'] = $matches[1];
			}
		}

		$hashesMap = array();
		foreach ($hashes as $id => $hash)
		{
			if (!array_key_exists($hash, $hashesMap))
			{
				$hashesMap[$hash] = array();
			}

			$hashesMap[$hash][] = $id;
		}

		$result = $this->listMessages(array(
			'select' => array('HEADER_MD5', 'MESSAGE_ID', 'DATE_INSERT'),
			'filter' => array(
				'@HEADER_MD5' => array_keys($hashesMap),
			),
		), false);

		while ($item = $result->fetch())
		{
			foreach ((array) $hashesMap[$item['HEADER_MD5']] as $id)
			{
				$messages[$id]['__created'] = $item['DATE_INSERT'];
				$messages[$id]['__fields']['MESSAGE_ID'] = $item['MESSAGE_ID'];
			}
		}

		$result = $this->listMessages(array(
			'select' => array('ID', 'MESSAGE_ID', 'DATE_INSERT'),
			'filter' => array(
				'@ID' => array_values($uids),
				// DIR_MD5 can be empty in DB
			),
		), false);

		while ($item = $result->fetch())
		{
			$id = array_search($item['ID'], $uids);
			$messages[$id]['__created'] = $item['DATE_INSERT'];
			$messages[$id]['__fields']['MESSAGE_ID'] = $item['MESSAGE_ID'];
			$messages[$id]['__replaces'] = $item['ID'];
		}
	}

	protected function resyncMessages($dir, $uidtoken, &$messages)
	{
		$excerpt = array();

		$range = array(
			reset($messages)['UID'],
			end($messages)['UID'],
		);
		sort($range);

		$result = $this->listMessages(array(
			'select' => array('ID', 'MESSAGE_ID', 'IS_SEEN'),
			'filter' => array(
				'=DIR_MD5' => md5($dir),
				'=DIR_UIDV' => $uidtoken,
				'>=MSG_UID' => $range[0],
				'<=MSG_UID' => $range[1],
			),
		), false);

		while ($item = $result->fetch())
		{
			$item['MAILBOX_USER_ID'] = $this->mailbox['USER_ID'];
			$excerpt[$item['ID']] = $item;
		}

		$update = array(
			'Y' => array(),
			'N' => array(),
			'S' => array(),
			'U' => array(),
		);
		foreach ($messages as $id => $item)
		{
			$messageUid = md5(sprintf('%s:%u:%u', $dir, $uidtoken, $item['UID']));

			if (array_key_exists($messageUid, $excerpt))
			{
				$excerptSeen = $excerpt[$messageUid]['IS_SEEN'];
				$excerptSeenYN = in_array($excerptSeen, array('Y', 'S')) ? 'Y' : 'N';
				$messageSeen = preg_grep('/^ \x5c Seen $/ix', $item['FLAGS']) ? 'Y' : 'N';

				if ($messageSeen != $excerptSeen)
				{
					if (in_array($excerptSeen, array('S', 'U')))
					{
						$excerpt[$messageUid]['IS_SEEN'] = $excerptSeenYN;
						$update[$excerptSeenYN][$messageUid] = $excerpt[$messageUid];

						if ($messageSeen != $excerptSeenYN)
						{
							$update[$excerptSeen][] = $item['UID'];
						}
					}
					else
					{
						$excerpt[$messageUid]['IS_SEEN'] = $messageSeen;
						$update[$messageSeen][$messageUid] = $excerpt[$messageUid];
					}
				}

				unset($excerpt[$messageUid]);
			}
			else
			{
				/*
				addMessage2Log(
					sprintf(
						'IMAP: message lost (%u:%s:%u:%s)',
						$this->mailbox['ID'], $dir, $uidtoken, $item['UID']
					),
					'mail', 0, false
				);
				*/
			}
		}

		$countUpdated = 0;
		$countDeleted = count($excerpt);

		foreach ($update as $seen => $items)
		{
			if (!empty($items))
			{
				if (in_array($seen, array('S', 'U')))
				{
					$method = 'S' == $seen ? 'seen' : 'unseen';
					$this->client->$method($items, $dir);
				}
				else
				{
					$countUpdated += count($items);

					$this->updateMessagesRegistry(
						array(
							'@ID' => array_keys($items),
						),
						array(
							'IS_SEEN' => $seen,
						),
						$items = array() // @TODO: fix lazyload in MessageEventManager::processOnMailMessageModified()
					);
				}
			}
		}

		if (!empty($excerpt))
		{
			$result = $this->unregisterMessages(
				array(
					'@ID' => array_keys($excerpt),
				),
				$excerpt
			);

			$countDeleted += $result ? $result->getCount() : 0;
		}

		$this->lastSyncResult['updatedMessages'] += $countUpdated;
		$this->lastSyncResult['deletedMessages'] += $countDeleted;
	}

	protected function syncMessage($dir, $uidtoken, $message, &$hashesMap = array())
	{
		$fields = $message['__fields'];

		if ($fields['MESSAGE_ID'] > 0)
		{
			$hashesMap[$fields['HEADER_MD5']] = $fields['MESSAGE_ID'];
		}
		else
		{
			if (array_key_exists($fields['HEADER_MD5'], $hashesMap) && $hashesMap[$fields['HEADER_MD5']] > 0)
			{
				$fields['MESSAGE_ID'] = $hashesMap[$fields['HEADER_MD5']];
			}
		}

		if (!$this->registerMessage($fields, isset($message['__replaces']) ? $message['__replaces'] : null))
		{
			return false;
		}

		if (Mail\Helper\LicenseManager::getSyncOldLimit() > 0)
		{
			if ($message['__internaldate']->getTimestamp() < strtotime(sprintf('-%u days', Mail\Helper\LicenseManager::getSyncOldLimit())))
			{
				return false;
			}
		}

		if (!empty($this->mailbox['OPTIONS']['sync_from']))
		{
			if ($message['__internaldate']->getTimestamp() < $this->mailbox['OPTIONS']['sync_from'])
			{
				return false;
			}
		}

		if (!empty($message['__created']) && !empty($this->mailbox['OPTIONS']['resync_from']))
		{
			if ($message['__created']->getTimestamp() < $this->mailbox['OPTIONS']['resync_from'])
			{
				return false;
			}
		}

		if ($fields['MESSAGE_ID'] > 0)
		{
			return true;
		}

		$messageId = 0;

		if (!empty($message['BODYSTRUCTURE']) && !empty($message['BODY[HEADER]']))
		{
			$message['__bodystructure'] = new Mail\Imap\BodyStructure($message['BODYSTRUCTURE']);

			$message['__parts'] = $this->downloadMessageParts(
				$message['__fields'],
				$message['__bodystructure'],
				$this->isSupportLazyAttachments() ? self::MESSAGE_PARTS_TEXT : self::MESSAGE_PARTS_ALL
			);

			// #119474
			if (!$message['__bodystructure']->isMultipart())
			{
				if (is_array($message['__parts']) && !empty($message['__parts']['BODY[1]']))
				{
					$message['__parts']['BODY[1.MIME]'] = $message['BODY[HEADER]'];
				}
			}
		}
		else
		{
			// fallback
			$message['__parts'] = $this->downloadMessage($message['__fields']) ?: false;
		}

		if (false !== $message['__parts'])
		{
			$messageId = $this->cacheMessage(
				$message,
				array(
					'timestamp' => $message['__internaldate']->getTimestamp(),
					'size' => $message['RFC822.SIZE'],
					'outcome' => in_array($this->mailbox['EMAIL'], $message['__from']),
					'draft' => in_array($dir, $this->mailbox['OPTIONS']['imap'][MessageFolder::DRAFTS]) || preg_grep('/^ \x5c Draft $/ix', $message['FLAGS']),
					'trash' => in_array($dir, $this->mailbox['OPTIONS']['imap'][MessageFolder::TRASH]),
					'spam' => in_array($dir, $this->mailbox['OPTIONS']['imap'][MessageFolder::SPAM]),
					'seen' => $fields['IS_SEEN'] == 'Y',
					'hash' => $fields['HEADER_MD5'],
					'lazy_attachments' => $this->isSupportLazyAttachments(),
					'excerpt' => $fields,
				)
			);
		}

		if ($messageId > 0)
		{
			$hashesMap[$fields['HEADER_MD5']] = $messageId;

			$this->linkMessage($fields['ID'], $messageId);
		}

		return $messageId > 0;
	}

	public function downloadAttachments(array &$excerpt)
	{
		if (empty($excerpt['MSG_UID']) || empty($excerpt['DIR_MD5']))
		{
			return false;
		}

		$dir = MessageFolder::getFolderNameByHash($excerpt['DIR_MD5'], $this->mailbox['OPTIONS']);
		if (empty($dir))
		{
			return false;
		}

		$message = $this->client->fetch(true, $dir, $excerpt['MSG_UID'], '(BODYSTRUCTURE)', $error);
		if (empty($message['BODYSTRUCTURE']))
		{
			// @TODO: fallback

			if (false === $message)
			{
				$this->errors = new Main\ErrorCollection($this->client->getErrors()->toArray());
			}

			return false;
		}

		if (!is_array($message['BODYSTRUCTURE']))
		{
			$this->errors = new Main\ErrorCollection(array(
				new Main\Error('Helper\Mailbox\Imap: Invalid BODYSTRUCTURE', 0),
				new Main\Error((string) $message['BODYSTRUCTURE'], -1),
			));
			return false;
		}

		$message['__bodystructure'] = new Mail\Imap\BodyStructure($message['BODYSTRUCTURE']);

		$parts = $this->downloadMessageParts(
			$excerpt,
			$message['__bodystructure'],
			self::MESSAGE_PARTS_ATTACHMENT
		);

		$attachments = array();

		$message['__bodystructure']->traverse(
			function (Mail\Imap\BodyStructure $item) use (&$parts, &$attachments)
			{
				if ($item->isMultipart() || $item->isText() && !$item->isAttachment())
				{
					return;
				}

				$attachments[] = \CMailMessage::decodeMessageBody(
					\CMailMessage::parseHeader(
						$parts[sprintf('BODY[%s.MIME]', $item->getNumber())],
						$this->mailbox['LANG_CHARSET']
					),
					$parts[sprintf('BODY[%s]', $item->getNumber())],
					$this->mailbox['LANG_CHARSET']
				);
			}
		);

		return $attachments;
	}

	protected function cacheMessage(&$message, $params = array())
	{
		if (!is_array($message))
		{
			return parent::cacheMessage($message, $params);
		}

		if (!is_array($message['__parts']))
		{
			return parent::cacheMessage($message['__parts'], $params);
		}

		if (empty($message['__header']))
		{
			return false;
		}

		if (empty($message['__bodystructure']) || !($message['__bodystructure'] instanceof Mail\Imap\BodyStructure))
		{
			return false;
		}

		$complete = function (&$html, &$text)
		{
			if ('' !== $html && '' === $text)
			{
				$text = html_entity_decode(
					htmlToTxt($html),
					ENT_QUOTES | ENT_HTML401,
					$this->mailbox['LANG_CHARSET']
				);
			}
			else if ('' === $html && '' !== $text)
			{
				$html = txtToHtml($text, false, 120);
			}
		};

		list($bodyHtml, $bodyText, $attachments) = $message['__bodystructure']->traverse(
			function (Mail\Imap\BodyStructure $item, &$subparts) use (&$message, &$complete)
			{
				$parts = &$message['__parts'];

				$html = '';
				$text = '';
				$attachments = array();

				if ($item->isMultipart())
				{
					if ('alternative' === $item->getSubtype())
					{
						foreach ($subparts as $part)
						{
							$part = $part[0];

							if ('' !== $part[0])
							{
								$html = $part[0];
							}

							if ('' !== $part[1])
							{
								$text = $part[1];
							}

							if (!empty($part[2]))
							{
								$attachments = array_merge($attachments, $part[2]);
							}
						}

						$complete($html, $text);
					}
					else
					{
						foreach ($subparts as $part)
						{
							$part = $part[0];

							$complete($part[0], $part[1]);

							if ('' !== $part[0] || '' !== $part[1])
							{
								$html .= $part[0] . "\r\n\r\n";
								$text .= $part[1] . "\r\n\r\n";
							}

							$attachments = array_merge($attachments, $part[2]);
						}
					}

					$html = trim($html);
					$text = trim($text);
				}
				else
				{
					if (array_key_exists(sprintf('BODY[%s]', $item->getNumber()), $parts))
					{
						$part = \CMailMessage::decodeMessageBody(
							\CMailMessage::parseHeader(
								$parts[sprintf('BODY[%s.MIME]', $item->getNumber())],
								$this->mailbox['LANG_CHARSET']
							),
							$parts[sprintf('BODY[%s]', $item->getNumber())],
							$this->mailbox['LANG_CHARSET']
						);
					}

					if (!$item->isText() || $item->isAttachment())
					{
						$attachments[] = empty($part) ? $item->getNumber() : $part;
					}
					else if (!empty($part))
					{
						if ('html' === $item->getSubtype())
						{
							$html = $part['BODY'];
						}
						else
						{
							$text = $part['BODY'];
						}
					}
				}

				return array($html, $text, $attachments);
			}
		)[0];

		$complete($bodyHtml, $bodyText);

		$dummyBody;
		return \CMailMessage::saveMessage(
			$this->mailbox['ID'],
			$dummyBody,
			$message['__header'],
			$bodyHtml,
			$bodyText,
			$attachments,
			$params
		);
	}

	protected function getSyncRange($dir, &$uidtoken)
	{
		$meta = $this->client->select($dir, $error);
		if (false === $meta)
		{
			$this->warnings->add($this->client->getErrors()->toArray());

			return null;
		}

		if (!($meta['exists'] > 0))
		{
			return null;
		}

		$uidtoken = $meta['uidvalidity'];

		$rangeGetter = function ($min, $max) use ($dir, $uidtoken, &$rangeGetter)
		{
			$size = $max - $min + 1;

			$set = array();
			$d = $size < 1000 ? 100 : pow(10, round(ceil(log10($size) - 0.7) / 2) * 2 - 2);
			for ($i = $min; $i <= $max; $i = $i + $d)
			{
				$set[] = $i;
			}

			if (count($set) > 1 && end($set) + 100 >= $max)
			{
				array_pop($set);
			}

			$set[] = $max;

			$set = $this->client->fetch(false, $dir, join(',', $set), '(UID)', $error);
			if (empty($set))
			{
				return false;
			}

			ksort($set);

			static $uidMin, $uidMax;

			if (!isset($uidMin, $uidMax))
			{
				$minmax = $this->getUidRange($dir, $uidtoken);

				if ($minmax)
				{
					$uidMin = $minmax['MIN'];
					$uidMax = $minmax['MAX'];
				}
				else
				{
					$uidMin = $uidMax = (end($set)['UID'] + 1);
				}
			}

			if (count($set) == 1)
			{
				$uid = reset($set)['UID'];

				if ($uid > $uidMax || $uid < $uidMin)
				{
					return array($uid, $uid);
				}
			}
			else if (end($set)['UID'] > $uidMax)
			{
				$max = current($set)['id'];
				do
				{
					$exmax = $max;

					$max = current($set)['id'];
					$min = prev($set)['id'];
				}
				while (current($set)['UID'] > $uidMax && prev($set) && next($set));

				if ($max - $min > 200)
				{
					return $rangeGetter($min, $max);
				}
				else
				{
					if ($set[$max]['UID'] - $uidMax < 100)
					{
						$max = $exmax;
					}

					return array(
						max($set[$min]['UID'], $uidMax + 1),
						$set[$max]['UID'],
					);
				}
			}
			else if (reset($set)['UID'] < $uidMin)
			{
				$min = current($set)['id'];
				do
				{
					$exmin = $min;

					$min = current($set)['id'];
					$max = next($set)['id'];
				}
				while (current($set)['UID'] < $uidMin && next($set) && prev($set));

				if ($max - $min > 200)
				{
					return $rangeGetter($min, $max);
				}
				else
				{
					if ($uidMin - $set[$min]['UID'] < 100)
					{
						$min = $exmin;
					}

					return array(
						min($set[$max]['UID'], $uidMin - 1),
						$set[$min]['UID'],
					);
				}
			}

			return null;
		};

		return $rangeGetter(1, $meta['exists']);
	}

	protected function getUidRange($dir, $uidtoken)
	{
		$filter = array(
			'=DIR_MD5' => md5($dir),
			'=DIR_UIDV' => $uidtoken,
			'>MSG_UID' => 0,
		);

		$min = $this->listMessages(
			array(
				'select' => array(
					'MIN' => 'MSG_UID',
				),
				'filter' => $filter,
				'order' => array(
					'MSG_UID' => 'ASC',
				),
				'limit' => 1,
			),
			false
		)->fetch();

		$max = $this->listMessages(
			array(
				'select' => array(
					'MAX' => 'MSG_UID',
				),
				'filter' => $filter,
				'order' => array(
					'MSG_UID' => 'DESC',
				),
				'limit' => 1,
			),
			false
		)->fetch();

		if ($min && $max)
		{
			return array(
				'MIN' => $min['MIN'],
				'MAX' => $max['MAX'],
			);
		}

		return null;
	}

}
messageeventmanager.php000066400000014102147744702230011310 0ustar00<?php
namespace Bitrix\Mail\Helper;

use Bitrix\Mail\MailMessageTable;
use Bitrix\Mail\MailMessageUidTable;
use Bitrix\Main\Event;
use Bitrix\Main\Loader;
use Bitrix\Main\Type\DateTime;

class MessageEventManager
{
	const EVENT_DELETE_MESSAGES = 'onMailMessageDeleted';

	/** Dispatches OnMessageModified event for compatibility
	 * If event parameters do not have ['HEADER_MD5', 'MAILBOX_USER_ID']
	 * data will be obtained from database
	 * @param Event $event
	 * @return MessageEventManager
	 */
	public static function onMailMessageDeleted(Event $event)
	{
		$manager = new static();
		$manager->processOnMailMessageDeletedEvent($event);
		return $manager;
	}

	private function processOnMailMessageDeletedEvent(Event $event)
	{
		$params = $event->getParameters();
		$filter = empty($params['DELETED_BY_FILTER']) ? [] : $params['DELETED_BY_FILTER'];
		$fieldsData = empty($params['MAIL_FIELDS_DATA']) ? [] : $params['MAIL_FIELDS_DATA'];
		$this->handleRemovedEvent($fieldsData, $filter);
	}

	/** Dispatches OnMessageObsolete event for compatibility
	 * If messages data from event parameters do not have ['HEADER_MD5', 'MAILBOX_USER_ID', 'IS_SEEN']
	 * data will be obtained from database
	 * @param Event $event
	 * @return MessageEventManager
	 */
	public static function onMailMessageModified(Event $event)
	{
		$manager = new static();
		$manager->processOnMailMessageModified($event);
		return $manager;
	}

	private function processOnMailMessageModified(Event $event)
	{
		$params = $event->getParameters();
		$updatedFieldValues = empty($params['UPDATED_FIELDS_VALUES']) ? [] : $params['UPDATED_FIELDS_VALUES'];
		$fieldsData = empty($params['MAIL_FIELDS_DATA']) ? [] : $params['MAIL_FIELDS_DATA'];
		$filter = empty($params['UPDATED_BY_FILTER']) ? [] : $params['UPDATED_BY_FILTER'];
		if (!empty($updatedFieldValues) && isset($updatedFieldValues['IS_SEEN']))
		{
			$fieldsData = $this->getMailsFieldsData($fieldsData, ['HEADER_MD5', 'MAILBOX_USER_ID', 'IS_SEEN'], $filter);
			$this->sendMessageModifiedEvent($fieldsData);
		}
		if (!empty($updatedFieldValues) && isset($updatedFieldValues['DIR_MD5']))
		{
			$folderHash = empty($updatedFieldValues['DIR_MD5']) ? null : $updatedFieldValues['DIR_MD5'];
			$mailboxOptions = !empty($fieldsData[0]) && !empty($fieldsData[0]['MAILBOX_OPTIONS']) ? $fieldsData[0]['MAILBOX_OPTIONS'] : [];
			if (!empty($folderHash) && !empty($mailboxOptions))
			{
				$isTrashFolder = $folderHash === MessageFolder::getFolderHashByType(MessageFolder::TRASH, $mailboxOptions);
				$isSpamFolder = $folderHash === MessageFolder::getFolderHashByType(MessageFolder::SPAM, $mailboxOptions);
				$folderName = MessageFolder::getFolderNameByHash($folderHash, $mailboxOptions);
				$isDisabledFolder = MessageFolder::isDisabledFolder($folderName, $mailboxOptions);

				if ($isTrashFolder || $isSpamFolder || $isDisabledFolder)
				{
					$this->handleRemovedEvent($fieldsData, $filter);
				}
			}
		}
	}

	protected function sendMessageModifiedEvent($fieldsData)
	{
		foreach ($fieldsData as $fields)
		{
			$event = new Event(
				'mail', 'OnMessageModified',
				[
					'user' => $fields['MAILBOX_USER_ID'],
					'hash' => $fields['HEADER_MD5'],
					'seen' => $fields['IS_SEEN'] === 'Y',
				]
			);
			$event->send();
		}
	}

	private function handleRemovedEvent($fieldsData, $filter)
	{
		$this->sendMessageDeletedEvent($fieldsData);
	}

	protected function sendMessageDeletedEvent($fieldsData)
	{
		foreach ($fieldsData as $fields)
		{
			$event = new Event(
				'mail', 'OnMessageObsolete',
				[
					'user' => $fields['MAILBOX_USER_ID'],
					'hash' => $fields['HEADER_MD5'],
				]
			);
			$event->send();
		}
	}

	private function getMailsFieldsData($eventData, $requiredKeys, $filter)
	{
		$fieldsData = $eventData;
		$missingKeys = $requiredKeys;
		$messagesCount = count($eventData);
		if ($messagesCount)
		{
			foreach ($requiredKeys as $requiredKey)
			{
				if (count(array_column($eventData, $requiredKey)) === $messagesCount)
				{
					$missingKeys = array_diff($missingKeys, [$requiredKey]);
				}
			}
		}

		if (!empty($missingKeys) && !empty($filter))
		{
			$fieldsData = $this->getMailMessagesList($filter, $missingKeys);
		}
		$results = [];
		foreach ($fieldsData as $index => $mailFieldsData)
		{
			$results[$mailFieldsData['HEADER_MD5']] = $mailFieldsData;
		}
		return $results;
	}

	protected function getMailMessagesList($filter, $selectingFields)
	{
		$dateLastMonth = new DateTime();
		$dateLastMonth->add('-1 MONTH');
		foreach ($selectingFields as $index => $selectingField)
		{
			if (strncmp('MAILBOX_', $selectingField, 8) === 0)
			{
				$selectingFields[$selectingField] = 'MAILBOX.' . substr($selectingField, 8);
				unset($selectingFields[$index]);
			}
		}

		return MailMessageUidTable::getList([
				'select' => $selectingFields,
				'filter' => array_merge($filter, [
					'>=INTERNALDATE' => $dateLastMonth,
				]),
			]
		)->fetchAll();
	}

	/**
	 * @param array $data
	 * @return array
	 * @throws \Bitrix\Main\ObjectException
	 * @throws \Exception
	 */
	public static function onMailEventMailRead(array $data)
	{
		$messageId = $data['msgid'];
		if($messageId)
		{
			$message = MailMessageTable::getList([
				'select' => [
					'OPTIONS', 'ID', 'READ_CONFIRMED',
				],
				'filter' => [
					'=MSG_ID' => $messageId,
					'READ_CONFIRMED' => null,
				]
			])->fetch();
			if($message)
			{
				$readTime = new DateTime();
				$result = MailMessageTable::update($message['ID'], [
					'READ_CONFIRMED' => $readTime,
				]);
				if($result->isSuccess())
				{
					if(Loader::includeModule("pull"))
					{
						\CPullWatch::addToStack(static::getPullTagName($message['ID']), [
							'module_id' => 'mail',
							'command' => 'onMessageRead',
							'params' => [
								'messageId' => $message['ID'],
								'readTime' => $readTime->getTimestamp(),
							],
						]);
					}
				}
			}
		}

		return $data;
	}

	public static function getPullTagName($messageId)
	{
		return 'MAILMESSAGEREADED'.$messageId;
	}

	public static function getRequiredFieldNamesForEvent($eventName)
	{
		if ($eventName === static::EVENT_DELETE_MESSAGES)
		{
			return array('HEADER_MD5', 'MESSAGE_ID', 'MAILBOX_USER_ID');
		}
	}
}messagefolder.php000066400000005066147744702230010120 0ustar00<?php

namespace Bitrix\Mail\Helper;

use Bitrix\Main\Localization\Loc;

/**
 * Class MessageFolder
 */
class MessageFolder
{
	const TRASH = 'trash';
	const SPAM = 'spam';
	const INCOME = 'income';
	const OUTCOME = 'outcome';
	const DRAFTS = 'drafts';

	/**
	 * @param array $message
	 * @param array $mailboxOptions
	 * @return string
	 */
	public static function getFolderNameByHash($messageFolderHash, $mailboxOptions)
	{
		$folderName = '';
		if (!empty($mailboxOptions['imap']['dirsMd5']))
		{
			$names = array_filter(
				$mailboxOptions['imap']['dirsMd5'],
				function ($hash) use ($messageFolderHash)
				{
					return $hash == $messageFolderHash;
				}
			);
			if (count($names) == 1)
			{
				$folderName = array_keys($names)[0];
			}
		}
		return $folderName;
	}

	public static function getFolderHashByType($folderType, $mailboxOptions)
	{
		$folderHash = '';
		if (!empty($mailboxOptions['imap']['dirsMd5']))
		{
			$name = static::getFolderNameByType($folderType, $mailboxOptions);
			$hashes = array_filter(
				$mailboxOptions['imap']['dirsMd5'],
				function ($_name) use ($name)
				{
					return $_name == $name;
				},
				ARRAY_FILTER_USE_KEY
			);
			if (count($hashes) == 1)
			{
				$folderHash = array_values($hashes)[0];
			}
		}

		return $folderHash;
	}

	public static function getFolderNameByType($folderType, $mailboxOptions)
	{
		if (!empty($mailboxOptions['imap']) && is_array($mailboxOptions['imap']))
		{
			$imapOptions = $mailboxOptions['imap'];
			if (!empty($imapOptions[$folderType]) && isset($imapOptions[$folderType][0]))
			{
				return $imapOptions[$folderType][0];
			}
		}
		return null;
	}

	public static function getDisabledFolders($mailboxOptions)
	{
		$disabled = empty($mailboxOptions['imap']['disabled']) ? [] : $mailboxOptions['imap']['disabled'];
		$ignore = empty($mailboxOptions['imap']['ignore']) ? [] : $mailboxOptions['imap']['ignore'];
		return array_merge($disabled, $ignore);
	}

	public static function isDisabledFolder($folder, $mailboxOptions)
	{
		return in_array($folder, static::getDisabledFolders($mailboxOptions), true);
	}

	public static function getFormattedPath(array $path, $mailboxOptions)
	{
		$root = array_shift($path);

		if (strtolower($root) == 'inbox' && !static::isDisabledFolder($root, $mailboxOptions))
		{
			$root = Loc::getMessage('MAIL_CLIENT_INBOX_ALIAS');
		}

		array_unshift($path, $root);

		return $path;
	}

	public static function getFormattedName(array $path, $mailboxOptions, $full = true)
	{
		$path = static::getFormattedPath($path, $mailboxOptions);

		return $full ? join(' / ', $path) : end($path);
	}

}
recipient.php000066400000014065147744702230007261 0ustar00<?php

namespace Bitrix\Mail\Helper;

use Bitrix\Main;
use Bitrix\Mail;

/**
 * Class Recipient
 * @package Bitrix\Mail\Helper
 */
class Recipient
{
	/**
	 * Builds unique code for BX.UI.Selector selector.
	 *
	 * @param string $email Email.
	 *
	 * @return string
	 */
	public static function buildUniqueEmailCode($email)
	{
//		return 'U' . md5($email);
		return 'MC' . $email;
	}

	/**
	 * Load last used Rcpt.
	 *
	 * @param string $emailTo Add this email to the result list.
	 * @param integer $limit Limit list length.
	 *
	 * @return array Data preformed for BX.UI.Selector selector with email only mode enabled.
	 *
	 * @throws Main\SystemException
	 */
	public static function loadLastRcpt($emailTo = null, $limit = 10)
	{
		global $APPLICATION;

		$result = array();

		$currentUser = \Bitrix\Main\Engine\CurrentUser::get();

		$lastRcptResult = \Bitrix\Main\FinderDestTable::getList(array(
			'filter' => array(
				'=USER_ID' => $currentUser->getId(),
				'=CONTEXT' => 'MAIL_LAST_RCPT',
			),
			'select' => array('CODE'),
			'order' => array('LAST_USE_DATE' => 'DESC'),
			'limit' => $limit,
		));

		$emailUsersIds = array();
		while ($item = $lastRcptResult->fetch())
		{
			$emailUsersIds[] = (int) str_replace('MC', '', $item['CODE']);
		}

		if (count($emailUsersIds) > 0)
		{
			$mailContacts = Mail\Internals\MailContactTable::getList([
				'filter' => array(
					'@ID' => $emailUsersIds,
					'=USER_ID' => $currentUser->getId(),
				),
				'select' => ['ID', 'NAME', 'EMAIL', 'ICON'],
				'limit' => $limit,
			])->fetchAll();

			$contactAvatars = $resultsMailContacts = [];
			foreach ($mailContacts as $mailContact)
			{
				$resultsMailContacts[$mailContact['EMAIL']] = $mailContact;
			}
			foreach ($resultsMailContacts as $mailContact)
			{
				$email = $mailContact['EMAIL'];
				if ($contactAvatars[$email] === null)
				{
					ob_start();
					$APPLICATION->includeComponent('bitrix:mail.contact.avatar', '', array(
						'mailContact' => $mailContact,
					));
					$contactAvatars[$email] = ob_get_clean();
				}
				$id = static::buildUniqueEmailCode($email);
				$result[$id] = [
					'id' => $id,
					'entityType' => 'email',
					'entityId' => $mailContact['ID'],
					'name' => htmlspecialcharsbx($mailContact['NAME']),
					'iconCustom' => $contactAvatars[$email],
					'email' => htmlspecialcharsbx($mailContact['EMAIL']),
					'desc' => htmlspecialcharsbx($mailContact['EMAIL']),
					'isEmail' => 'Y',
				];
			}
		}

		return $result;
	}


	/**
	 * Load mail contacts - users with EXTERNAL_AUTH_ID = email.
	 *
	 * @param array $filter Filter.
	 * @param integer $limit Limit list length.
	 *
	 * @return array  Data preformed for BX.UI.Selector selector with email only mode enabled.
	 *
	 * @throws Main\SystemException
	 */
	public static function loadMailContacts($filter = [], $limit = 20)
	{
		global $APPLICATION;

		$result = array();

		$mailContacts = \Bitrix\Main\UserTable::getList(array(
			'select' => array(
				'ID',
				'NAME',
				'LAST_NAME',
				'SECOND_NAME',
				'EMAIL',
				'PERSONAL_PHOTO',
			),
			'filter' => array_merge(
				array(
					'=ACTIVE' => 'Y',
					'=EXTERNAL_AUTH_ID' => 'email',
				),
				$filter
			),
			'order' => array(
				'LAST_NAME' => 'ASC',
			),
			'limit' => $limit,
		));

		$contactAvatars = array();
		while ($mailContact = $mailContacts->fetch())
		{
			$email = $mailContact['EMAIL'];
			if ($contactAvatars[$email] === null)
			{
				ob_start();
				$APPLICATION->includeComponent('bitrix:mail.contact.avatar', '',
					[
						'mailContact' => array(
							'FILE_ID' => $mailContact['PERSONAL_PHOTO'],
							'name' => \CUser::formatName(\CSite::getNameFormat(), $mailContact),
							'email' => $mailContact['EMAIL'],
						),
					]);
				$contactAvatars[$email] = ob_get_clean();
			}

			$id = static::buildUniqueEmailCode($email);
			$result[$id] = array(
				'id' => $id,
				'entityType' => 'mailContacts',
				'entityId' => $mailContact['ID'],
				'name' => \CUser::formatName(\CSite::getNameFormat(), $mailContact, true, true),
				'iconCustom' => $contactAvatars[$email],
				'email' => htmlspecialcharsbx($mailContact['EMAIL']),
				'desc' => htmlspecialcharsbx($mailContact['EMAIL']),
				'isEmail' => 'Y',
			);
		}

		return $result;
	}

	/**
	 * Load mail contacts from CRM - users with EXTERNAL_AUTH_ID = email and contact crm entity.
	 *
	 * @param array $filter Filter.
	 * @param integer $limit Limit list length.

	 * @return array  Data preformed for BX.UI.Selector selector with email only mode enabled.
	 *
	 * @throws Main\LoaderException
	 */
	public static function loadCrmMailContacts($filter = [], $limit = 20)
	{
		global $APPLICATION;

		$result = array();

		if (Main\Loader::includeModule('crm'))
		{
			$mailContacts = \Bitrix\Main\UserTable::getList(array(
				'select' => array(
					'ID',
					'NAME',
					'LAST_NAME',
					'SECOND_NAME',
					'EMAIL',
					'PERSONAL_PHOTO',
				),
				'filter' => array_merge(
					array(
						'=ACTIVE' => 'Y',
						'=EXTERNAL_AUTH_ID' => 'email',
					),
					$filter
				),
				'order' => array(
					'LAST_NAME' => 'ASC',
				),
				'limit' => $limit,
			));

			$contactAvatars = array();
			while ($mailContact = $mailContacts->fetch())
			{
				$email = $mailContact['EMAIL'];
				if (empty($email))
				{
					continue;
				}

				$crmCommunications = \CSocNetLogDestination::searchCrmEntities(array(
					'SEARCH' => $email,
					'ENTITIES' => array('CONTACT'),
				));
				foreach ($crmCommunications as $communication)
				{
					$email = $communication['email'];

					if ($contactAvatars[$email] === null)
					{
						ob_start();
						$APPLICATION->includeComponent('bitrix:mail.contact.avatar', '',
							[
								'mailContact' => array(
									'FILE_ID' => $mailContact['PERSONAL_PHOTO'],
									'name' => \CUser::formatName(\CSite::getNameFormat(), $mailContact),
									'email' => $email,
								),
							]);
						$contactAvatars[$email] = ob_get_clean();
						$communication['iconCustom'] = $contactAvatars[$email];
					}

					$id = static::buildUniqueEmailCode($email);
					$communication['id'] = $id;
					$result[$id] = $communication;
				}
			}
		}

		return $result;
	}
}
mailbox.php000066400000070302147744702230006726 0ustar00<?php

namespace Bitrix\Mail\Helper;

use Bitrix\Main;
use Bitrix\Main\ORM;
use Bitrix\Mail;

abstract class Mailbox
{

	const SYNC_TIMEOUT = 300;
	const SYNC_TIME_QUOTA = 280;

	protected $mailbox;
	protected $filters;
	protected $session;
	protected $startTime, $syncTimeout, $checkpoint;
	protected $syncParams = array();
	protected $errors, $warnings;
	protected $lastSyncResult = array(
		'newMessages' => 0,
		'deletedMessages' => 0,
		'updatedMessages' => 0
	);

	/**
	 * Creates active mailbox helper instance by ID
	 *
	 * @param int $id Mailbox ID.
	 * @param bool $throw Throw exception on error.
	 * @return \Bitrix\Mail\Helper\Mailbox|false
	 * @throws \Exception
	 */
	public static function createInstance($id, $throw = true)
	{
		return static::rawInstance(array('=ID' => (int) $id, '=ACTIVE' => 'Y'), $throw);
	}

	/**
	 * Creates mailbox helper instance
	 *
	 * @param mixed $filter Filter.
	 * @param bool $throw Throw exception on error.
	 * @return \Bitrix\Mail\Helper\Mailbox|false
	 * @throws \Exception
	 */
	public static function rawInstance($filter, $throw = true)
	{
		try
		{
			$mailbox = static::prepareMailbox($filter);

			return static::instance($mailbox);
		}
		catch (\Exception $e)
		{
			if ($throw)
			{
				throw $e;
			}
			else
			{
				return false;
			}
		}
	}

	protected static function instance(array $mailbox)
	{
		// @TODO: other SERVER_TYPE
		$types = array(
			'imap' => 'Bitrix\Mail\Helper\Mailbox\Imap',
			'controller' => 'Bitrix\Mail\Helper\Mailbox\Imap',
			'domain' => 'Bitrix\Mail\Helper\Mailbox\Imap',
			'crdomain' => 'Bitrix\Mail\Helper\Mailbox\Imap',
		);

		if (empty($mailbox))
		{
			throw new Main\ObjectException('no mailbox');
		}

		if (empty($mailbox['SERVER_TYPE']) || !array_key_exists($mailbox['SERVER_TYPE'], $types))
		{
			throw new Main\ObjectException('unsupported mailbox type');
		}

		return new $types[$mailbox['SERVER_TYPE']]($mailbox);
	}

	protected static function prepareMailbox($filter)
	{
		if (is_scalar($filter))
		{
			$filter = array('=ID' => (int) $filter);
		}

		$mailbox = Mail\MailboxTable::getList(array(
			'filter' => $filter,
			'select' => array('*', 'LANG_CHARSET' => 'SITE.CULTURE.CHARSET'),
			'limit' => 1,
		))->fetch() ?: array();

		if (!empty($mailbox))
		{
			if (in_array($mailbox['SERVER_TYPE'], array('controller', 'crdomain', 'domain')))
			{
				$result = \CMailDomain2::getImapData(); // @TODO: request controller for 'controller' and 'crdomain'

				$mailbox['SERVER']  = $result['server'];
				$mailbox['PORT']    = $result['port'];
				$mailbox['USE_TLS'] = $result['secure'];
			}

			Mail\MailboxTable::normalizeEmail($mailbox);
		}

		return $mailbox;
	}

	public function setSyncParams(array $params = array())
	{
		$this->syncParams = $params;
	}

	protected function __construct($mailbox)
	{
		$this->startTime = time();
		if (defined('START_EXEC_PROLOG_BEFORE_1') && preg_match('/ (\d+)$/', START_EXEC_PROLOG_BEFORE_1, $matches))
		{
			$startTime = $matches[1];
			if ($startTime > 0 && $this->startTime >= $startTime)
			{
				$this->startTime = $startTime;
			}
		}

		$this->syncTimeout = min(max(0, ini_get('max_execution_time')) ?: static::SYNC_TIMEOUT, static::SYNC_TIMEOUT);

		$this->mailbox = $mailbox;

		$this->normalizeMailboxOptions();

		$this->setCheckpoint();

		$this->session = md5(uniqid(''));
		$this->errors = new Main\ErrorCollection();
		$this->warnings = new Main\ErrorCollection();
	}

	protected function reloadMailboxOptions()
	{
		$mailbox = static::prepareMailbox(array('=ID' => $this->mailbox['ID']));

		if (!empty($mailbox['OPTIONS']) && is_array($mailbox['OPTIONS']))
		{
			$this->mailbox['OPTIONS'] = $mailbox['OPTIONS'];
		}

		$this->normalizeMailboxOptions();
	}

	protected function normalizeMailboxOptions()
	{
		if (empty($this->mailbox['OPTIONS']) || !is_array($this->mailbox['OPTIONS']))
		{
			$this->mailbox['OPTIONS'] = array();
		}
	}

	public function getMailbox()
	{
		return $this->mailbox;
	}

	protected function isTimeQuotaExceeded()
	{
		return time() - $this->startTime > ceil($this->syncTimeout * 0.9);
	}

	public function setCheckpoint()
	{
		$this->checkpoint = time();
	}

	public function sync()
	{
		global $DB;

		if (!LicenseManager::isSyncAvailable())
		{
			$this->mailbox['OPTIONS']['next_sync'] = time() + 3600 * 24;

			return 0;
		}

		if (time() - $this->mailbox['SYNC_LOCK'] < $this->syncTimeout)
		{
			return 0;
		}

		$this->mailbox['SYNC_LOCK'] = time();

		if ($this->isTimeQuotaExceeded())
		{
			return 0;
		}

		$this->syncOutgoing();

		$lockSql = sprintf(
			'UPDATE b_mail_mailbox SET SYNC_LOCK = %u WHERE ID = %u AND (SYNC_LOCK IS NULL OR SYNC_LOCK < %u)',
			$this->mailbox['SYNC_LOCK'], $this->mailbox['ID'], $this->mailbox['SYNC_LOCK'] - $this->syncTimeout
		);
		if (!$DB->query($lockSql)->affectedRowsCount())
		{
			return 0;
		}

		$mailboxSyncManager = new Mailbox\MailboxSyncManager($this->mailbox['USER_ID']);
		if ($this->mailbox['USER_ID'] > 0)
		{
			$mailboxSyncManager->setSyncStartedData($this->mailbox['ID']);
		}

		$this->session = md5(uniqid(''));

		$count = $this->syncInternal();
		$success = $count !== false && $this->errors->isEmpty();

		$syncUnlock = $this->isTimeQuotaExceeded() ? 0 : -1;

		$interval = max(1, (int) $this->mailbox['PERIOD_CHECK']) * 60;
		$syncErrors = max(0, (int) $this->mailbox['OPTIONS']['sync_errors']);

		if ($count === false)
		{
			$syncErrors++;

			$maxInterval = 3600 * 24 * 7;
			for ($i = 1; $i < $syncErrors && $interval < $maxInterval; $i++)
			{
				$interval = min($interval * ($i + 1), $maxInterval);
			}
		}
		else
		{
			$syncErrors = 0;

			$interval = $syncUnlock < 0 ? $interval : min($count > 0 ? 60 : 600, $interval);
		}

		$this->mailbox['OPTIONS']['sync_errors'] = $syncErrors;
		$this->mailbox['OPTIONS']['next_sync'] = time() + $interval;

		$optionsValue = $this->mailbox['OPTIONS'];
		unset($optionsValue['imap']['dirsMd5']);
		$unlockSql = sprintf(
			"UPDATE b_mail_mailbox SET SYNC_LOCK = %d, OPTIONS = '%s' WHERE ID = %u AND SYNC_LOCK = %u",
			$syncUnlock,
			$DB->forSql(serialize($optionsValue)),
			$this->mailbox['ID'],
			$this->mailbox['SYNC_LOCK']
		);
		if ($DB->query($unlockSql)->affectedRowsCount())
		{
			$this->mailbox['SYNC_LOCK'] = $syncUnlock;
		}

		$lastSyncResult = $this->getLastSyncResult();

		$this->pushSyncStatus(
			array(
				'new' => $count,
				'updated' => $lastSyncResult['updatedMessages'],
				'deleted' => $lastSyncResult['deletedMessages'],
				'complete' => $this->mailbox['SYNC_LOCK'] < 0,
			),
			true
		);

		if ($this->mailbox['USER_ID'] > 0)
		{
			$mailboxSyncManager->setSyncStatus($this->mailbox['ID'], $success, time());
		}

		$usersWithAccessToMailbox = Mailbox\SharedMailboxesManager::getUserIdsWithAccessToMailbox($this->mailbox['ID']);
		foreach ($usersWithAccessToMailbox as $userId)
		{
			\CUserCounter::set(
				$userId,
				'mail_unseen',
				Message::getTotalUnseenCount($userId),
				$this->mailbox['LID']
			);
		}

		return $count;
	}

	public function getSyncStatus()
	{
		return -1;
	}

	protected function pushSyncStatus($params, $force = false)
	{
		if (Main\Loader::includeModule('pull'))
		{
			$status = $this->getSyncStatus();

			\CPullWatch::addToStack(
				'mail_mailbox_' . $this->mailbox['ID'],
				array(
					'module_id' => 'mail',
					'command' => 'mailbox_sync_status',
					'params' => array_merge(
						array(
							'id' => $this->mailbox['ID'],
							'status' => sprintf('%.3f', $status),
						),
						$params
					),
				)
			);

			if ($force)
			{
				\Bitrix\Pull\Event::send();
			}
		}
	}

	public function dismissOldMessages()
	{
		global $DB;

		if (!Mail\Helper\LicenseManager::isCleanupOldEnabled())
		{
			return true;
		}

		$startTime = time();

		if (time() - $this->mailbox['SYNC_LOCK'] < $this->syncTimeout)
		{
			return false;
		}

		if ($this->isTimeQuotaExceeded())
		{
			return false;
		}

		$syncUnlock = $this->mailbox['SYNC_LOCK'];

		$lockSql = sprintf(
			'UPDATE b_mail_mailbox SET SYNC_LOCK = %u WHERE ID = %u AND (SYNC_LOCK IS NULL OR SYNC_LOCK < %u)',
			$startTime, $this->mailbox['ID'], $startTime - $this->syncTimeout
		);
		if ($DB->query($lockSql)->affectedRowsCount())
		{
			$this->mailbox['SYNC_LOCK'] = $startTime;
		}
		else
		{
			return false;
		}

		$result = true;

		$entity = Mail\MailMessageUidTable::getEntity();
		$connection = $entity->getConnection();

		$where = sprintf(
			' (%s) AND NOT EXISTS (SELECT 1 FROM %s WHERE (%s) AND (%s)) ',
			ORM\Query\Query::buildFilterSql(
				$entity,
				array(
					'=MAILBOX_ID' => $this->mailbox['ID'],
					'>MESSAGE_ID' => 0,
					'<INTERNALDATE' => Main\Type\DateTime::createFromTimestamp(strtotime(sprintf('-%u days', Mail\Helper\LicenseManager::getSyncOldLimit()))),
				)
			),
			$connection->getSqlHelper()->quote(Mail\Internals\MessageAccessTable::getTableName()),
			ORM\Query\Query::buildFilterSql(
				$entity,
				array(
					'=MAILBOX_ID' => new Main\DB\SqlExpression('?#', 'MAILBOX_ID'),
					'=MESSAGE_ID' => new Main\DB\SqlExpression('?#', 'MESSAGE_ID'),
				)
			),
			ORM\Query\Query::buildFilterSql(
				Mail\Internals\MessageAccessTable::getEntity(),
				array(
					'=ENTITY_TYPE' => array(
						Mail\Internals\MessageAccessTable::ENTITY_TYPE_TASKS_TASK,
						Mail\Internals\MessageAccessTable::ENTITY_TYPE_BLOG_POST,
					),
				)
			)
		);

		do
		{
			$connection->query(sprintf(
				'INSERT IGNORE INTO %s (ID, MAILBOX_ID, MESSAGE_ID)
				(SELECT ID, MAILBOX_ID, MESSAGE_ID FROM %s WHERE %s ORDER BY ID LIMIT 1000)',
				$connection->getSqlHelper()->quote(Mail\Internals\MessageDeleteQueueTable::getTableName()),
				$connection->getSqlHelper()->quote($entity->getDbTableName()),
				$where
			));

			$connection->query(sprintf(
				'UPDATE %s SET MESSAGE_ID = 0 WHERE %s ORDER BY ID LIMIT 1000',
				$connection->getSqlHelper()->quote($entity->getDbTableName()),
				$where
			));

			if ($this->isTimeQuotaExceeded() || time() - $this->checkpoint > 20)
			{
				$result = false;

				break;
			}
		}
		while ($connection->getAffectedRowsCount() >= 1000);

		$unlockSql = sprintf(
			"UPDATE b_mail_mailbox SET SYNC_LOCK = %d WHERE ID = %u AND SYNC_LOCK = %u",
			$syncUnlock, $this->mailbox['ID'], $this->mailbox['SYNC_LOCK']
		);
		if ($DB->query($unlockSql)->affectedRowsCount())
		{
			$this->mailbox['SYNC_LOCK'] = $syncUnlock;
		}

		return $result;
	}

	public function cleanup()
	{
		do
		{
			$res = Mail\Internals\MessageDeleteQueueTable::getList(array(
				'runtime' => array(
					new ORM\Fields\Relations\Reference(
						'MESSAGE_UID',
						'Bitrix\Mail\MailMessageUidTable',
						array(
							'=this.MAILBOX_ID' => 'ref.MAILBOX_ID',
							'=this.MESSAGE_ID' => 'ref.MESSAGE_ID',
						)
					),
				),
				'select' => array('MESSAGE_ID', 'UID' => 'MESSAGE_UID.ID'),
				'filter' => array(
					'=MAILBOX_ID' => $this->mailbox['ID'],
				),
				'limit' => 100,
			));

			$count = 0;
			while ($item = $res->fetch())
			{
				$count++;

				if (empty($item['UID']))
				{
					\CMailMessage::delete($item['MESSAGE_ID']);
				}

				Mail\Internals\MessageDeleteQueueTable::deleteList(array(
					'=MAILBOX_ID' => $this->mailbox['ID'],
					'=MESSAGE_ID' => $item['MESSAGE_ID'],
				));

				if ($this->isTimeQuotaExceeded() || time() - $this->checkpoint > 60)
				{
					return false;
				}
			}
		}
		while ($count > 0);

		return true;
	}

	protected function listMessages($params = array(), $fetch = true)
	{
		$filter = array(
			'=MAILBOX_ID' => $this->mailbox['ID'],
		);

		if (!empty($params['filter']))
		{
			$filter = array_merge((array) $params['filter'], $filter);
		}

		$params['filter'] = $filter;

		$result = Mail\MailMessageUidTable::getList($params);

		return $fetch ? $result->fetchAll() : $result;
	}

	protected function registerMessage(&$fields, $replaces = null)
	{
		$now = new Main\Type\DateTime();

		if (!empty($replaces))
		{
			if (!is_array($replaces))
			{
				$replaces = array(
					'=ID' => $replaces,
				);
			}

			$exists = Mail\MailMessageUidTable::getList(array(
				'select' => array(
					'ID',
					'MESSAGE_ID',
				),
				'filter' => array(
					$replaces,
					'=MAILBOX_ID' => $this->mailbox['ID'],
				),
			))->fetch();
		}

		if (!empty($exists))
		{
			$fields['MESSAGE_ID'] = $exists['MESSAGE_ID'];

			$result = (bool) Mail\MailMessageUidTable::updateList(
				array(
					'=ID' => $exists['ID'],
					'=MAILBOX_ID' => $this->mailbox['ID'],
				),
				array_merge(
					$fields,
					array(
						'TIMESTAMP_X' => $now,
					)
				),
				array_merge(
					$exists,
					array(
						'MAILBOX_USER_ID' => $this->mailbox['USER_ID'],
					)
				)
			);
		}
		else
		{
			$result = Mail\MailMessageUidTable::add(array_merge(
				array(
					'MESSAGE_ID'  => 0,
				),
				$fields,
				array(
					'MAILBOX_ID'  => $this->mailbox['ID'],
					'SESSION_ID'  => $this->session,
					'TIMESTAMP_X' => $now,
					'DATE_INSERT' => $now,
				)
			))->isSuccess();
		}

		return $result;
	}

	protected function updateMessagesRegistry(array $filter, array $fields, $mailData = array())
	{
		return Mail\MailMessageUidTable::updateList(
			array_merge(
				$filter,
				array(
					'=MAILBOX_ID' => $this->mailbox['ID'],
				)
			),
			$fields,
			$mailData
		);
	}

	protected function unregisterMessages($filter, $eventData = [])
	{
		return Mail\MailMessageUidTable::deleteList(
			array_merge(
				$filter,
				array(
					'=MAILBOX_ID' => $this->mailbox['ID'],
				)
			),
			array_map(
				function ($item)
				{
					$item['MAILBOX_ID'] = $this->mailbox['ID'];
					return $item;
				},
				$eventData
			)
		);
	}

	protected function linkMessage($uid, $id)
	{
		$result = Mail\MailMessageUidTable::update(
			array(
				'ID' => $uid,
				'MAILBOX_ID' => $this->mailbox['ID'],
			),
			array(
				'MESSAGE_ID' => $id,
			)
		);

		return $result->isSuccess();
	}

	protected function cacheMessage(&$body, $params = array())
	{
		if (empty($params['origin']) && empty($params['replaces']))
		{
			$params['lazy_attachments'] = $this->isSupportLazyAttachments();
		}

		return \CMailMessage::addMessage(
			$this->mailbox['ID'],
			$body,
			$this->mailbox['CHARSET'] ?: $this->mailbox['LANG_CHARSET'],
			$params
		);
	}

	public function mail(array $params)
	{
		class_exists('Bitrix\Mail\Helper');

		$message = new Mail\DummyMail($params);

		$messageUid = $this->createMessage($message);

		Mail\Internals\MessageUploadQueueTable::add(array(
			'ID' => $messageUid,
			'MAILBOX_ID' => $this->mailbox['ID'],
		));

		\CAgent::addAgent(
			sprintf(
				'Bitrix\Mail\Helper::syncOutgoingAgent(%u);',
				$this->mailbox['ID']
			),
			'mail', 'N', 60
		);
	}

	protected function createMessage(Main\Mail\Mail $message, array $fields = array())
	{
		$messageUid = sprintf('%x%x', time(), rand(0, 0xffffffff));

		$messageId = $this->cacheMessage(
			sprintf(
				'%1$s%3$s%3$s%2$s',
				$message->getHeaders(),
				$message->getBody(),
				$message->getMailEol()
			),
			array(
				'outcome' => true,
				'draft' => false,
				'trash' => false,
				'spam' => false,
				'seen' => true,
				'trackable' => true,
				'origin' => true,
			)
		);

		$fields = array_merge(
			$fields,
			array(
				'ID' => $messageUid,
				'INTERNALDATE' => new Main\Type\DateTime,
				'IS_SEEN' => 'Y',
				'MESSAGE_ID' => $messageId,
			)
		);

		$this->registerMessage($fields);

		return $messageUid;
	}

	public function syncOutgoing()
	{
		$res = $this->listMessages(
			array(
				'runtime' => array(
					new \Bitrix\Main\Entity\ReferenceField(
						'UPLOAD_QUEUE',
						'Bitrix\Mail\Internals\MessageUploadQueueTable',
						array(
							'=this.ID' => 'ref.ID',
							'=this.MAILBOX_ID' => 'ref.MAILBOX_ID',
						),
						array(
							'join_type' => 'INNER',
						)
					),
				),
				'select' => array(
					'*',
					'__' => 'MESSAGE.*',
					'UPLOAD_LOCK' => 'UPLOAD_QUEUE.SYNC_LOCK',
					'UPLOAD_STAGE' => 'UPLOAD_QUEUE.SYNC_STAGE',
					'UPLOAD_ATTEMPTS' => 'UPLOAD_QUEUE.ATTEMPTS',
				),
				'filter' => array(
					'>=UPLOAD_QUEUE.SYNC_STAGE' => 0,
					'<UPLOAD_QUEUE.SYNC_LOCK' => time() - $this->syncTimeout,
					'<UPLOAD_QUEUE.ATTEMPTS' => 5,
				),
				'order' => array(
					'UPLOAD_QUEUE.SYNC_LOCK' => 'ASC',
					'UPLOAD_QUEUE.SYNC_STAGE' => 'ASC',
					'UPLOAD_QUEUE.ATTEMPTS' => 'ASC',
				),
			),
			false
		);

		while ($excerpt = $res->fetch())
		{
			$n = $excerpt['UPLOAD_ATTEMPTS'] + 1;
			$interval = min($this->syncTimeout * pow($n, $n), 3600 * 24 * 7);

			if ($excerpt['UPLOAD_LOCK'] > time() - $interval)
			{
				continue;
			}

			$this->syncOutgoingMessage($excerpt);

			if ($this->isTimeQuotaExceeded())
			{
				break;
			}
		}
	}

	protected function syncOutgoingMessage($excerpt)
	{
		global $DB;

		$lockSql = sprintf(
			"UPDATE b_mail_message_upload_queue SET SYNC_LOCK = %u, SYNC_STAGE = %u, ATTEMPTS = ATTEMPTS + 1
				WHERE ID = '%s' AND MAILBOX_ID = %u AND SYNC_LOCK < %u",
			$syncLock = time(),
			max(1, $excerpt['UPLOAD_STAGE']),
			$DB->forSql($excerpt['ID']),
			$excerpt['MAILBOX_ID'],
			$syncLock - $this->syncTimeout
		);
		if (!$DB->query($lockSql)->affectedRowsCount())
		{
			return;
		}

		$outgoingBody = $excerpt['__BODY_HTML'];

		$excerpt['__files'] = Mail\Internals\MailMessageAttachmentTable::getList(array(
			'select' => array(
				'ID', 'FILE_ID', 'FILE_NAME',
			),
			'filter' => array(
				'=MESSAGE_ID' => $excerpt['__ID'],
			),
		))->fetchAll();

		$attachments = array();
		if (!empty($excerpt['__files']) && is_array($excerpt['__files']))
		{
			$hostname = \COption::getOptionString('main', 'server_name', 'localhost');
			if (defined('BX24_HOST_NAME') && BX24_HOST_NAME != '')
			{
				$hostname = BX24_HOST_NAME;
			}
			else if (defined('SITE_SERVER_NAME') && SITE_SERVER_NAME != '')
			{
				$hostname = SITE_SERVER_NAME;
			}

			foreach ($excerpt['__files'] as $item)
			{
				$file = \CFile::makeFileArray($item['FILE_ID']);

				$contentId = sprintf(
					'bxacid.%s@%s.mail',
					hash('crc32b', $file['external_id'].$file['size'].$file['name']),
					hash('crc32b', $hostname)
				);

				$attachments[] = array(
					'ID'           => $contentId,
					'NAME'         => $item['FILE_NAME'],
					'PATH'         => $file['tmp_name'],
					'CONTENT_TYPE' => $file['type'],
				);

				$outgoingBody = preg_replace(
					sprintf('/aid:%u/i', $item['ID']),
					sprintf('cid:%s', $contentId),
					$outgoingBody
				);
			}
		}

		foreach (array('FROM', 'REPLY_TO', 'TO', 'CC', 'BCC') as $field)
		{
			$field = sprintf('__FIELD_%s', $field);

			if (strlen($excerpt[$field]) == 255 && '' != $excerpt['__HEADER'] && empty($parsedHeader))
			{
				$parsedHeader = \CMailMessage::parseHeader($excerpt['__HEADER'], LANG_CHARSET);

				$excerpt['__FIELD_FROM'] = $parsedHeader->getHeader('FROM');
				$excerpt['__FIELD_REPLY_TO'] = $parsedHeader->getHeader('REPLY-TO');
				$excerpt['__FIELD_TO'] = $parsedHeader->getHeader('TO');
				$excerpt['__FIELD_CC'] = $parsedHeader->getHeader('CC');
				$excerpt['__FIELD_BCC'] = join(', ', array_merge(
					(array) $parsedHeader->getHeader('X-Original-Rcpt-to'),
					(array) $parsedHeader->getHeader('BCC')
				));
			}

			$excerpt[$field] = explode(',', $excerpt[$field]);

			foreach ($excerpt[$field] as $k => $item)
			{
				unset($excerpt[$field][$k]);

				$address = new Main\Mail\Address($item);

				if ($address->validate())
				{
					if ($address->getName())
					{
						$excerpt[$field][] = sprintf(
							'%s <%s>',
							sprintf('=?%s?B?%s?=', SITE_CHARSET, base64_encode($address->getName())),
							$address->getEmail()
						);
					}
					else
					{
						$excerpt[$field][] = $address->getEmail();
					}
				}
			}

			$excerpt[$field] = join(', ', $excerpt[$field]);
		}

		$outgoingParams = array(
			'CHARSET'      => LANG_CHARSET,
			'CONTENT_TYPE' => 'html',
			'ATTACHMENT'   => $attachments,
			'TO'           => $excerpt['__FIELD_TO'],
			'SUBJECT'      => $excerpt['__SUBJECT'],
			'BODY'         => $outgoingBody,
			'HEADER'       => array(
				'From'       => $excerpt['__FIELD_FROM'],
				'Reply-To'   => $excerpt['__FIELD_REPLY_TO'],
				//'To'         => $excerpt['__FIELD_TO'],
				'Cc'         => $excerpt['__FIELD_CC'],
				'Bcc'        => $excerpt['__FIELD_BCC'],
				//'Subject'    => $excerpt['__SUBJECT'],
				'Message-Id' => sprintf('<%s>', $excerpt['__MSG_ID']),
				'In-Reply-To' => sprintf('<%s>', $excerpt['__IN_REPLY_TO']),
				'X-Bitrix-Mail-Message-UID' => $excerpt['ID'],
			),
		);

		$context = new Main\Mail\Context();
		$context->setCategory(Main\Mail\Context::CAT_EXTERNAL);
		$context->setPriority(Main\Mail\Context::PRIORITY_NORMAL);

		$eventManager = \Bitrix\Main\EventManager::getInstance();
		$eventKey = $eventManager->addEventHandler(
			'main',
			'OnBeforeMailSend',
			function () use (&$excerpt)
			{
				if ($excerpt['UPLOAD_STAGE'] >= 2)
				{
					return new Main\EventResult(Main\EventResult::ERROR);
				}
			}
		);

		$success = Main\Mail\Mail::send(array_merge(
			$outgoingParams,
			array(
				'TRACK_READ' => array(
					'MODULE_ID' => 'mail',
					'FIELDS'    => array('msgid' => $excerpt['__MSG_ID']),
					'URL_PAGE' => '/pub/mail/read.php',
				),
				//'TRACK_CLICK' => array(
				//	'MODULE_ID' => 'mail',
				//	'FIELDS'    => array('msgid' => $excerpt['__MSG_ID']),
				//),
				'CONTEXT' => $context,
			)
		));

		$eventManager->removeEventHandler('main', 'OnBeforeMailSend', $eventKey);

		if ($excerpt['UPLOAD_STAGE'] < 2 && !$success)
		{
			return false;
		}

		$needUpload = true;
		if ($context->getSmtp() && $context->getSmtp()->getFrom() == $this->mailbox['EMAIL'])
		{
			$needUpload = !in_array('deny_upload', (array) $this->mailbox['OPTIONS']['flags']);
		}

		if ($needUpload)
		{
			if ($excerpt['UPLOAD_STAGE'] < 2)
			{
				Mail\Internals\MessageUploadQueueTable::update(
					array(
						'ID' => $excerpt['ID'],
						'MAILBOX_ID' => $excerpt['MAILBOX_ID'],
					),
					array(
						'SYNC_STAGE' => 2,
						'ATTEMPTS' => 1,
					)
				);
			}

			class_exists('Bitrix\Mail\Helper');

			$message = new Mail\DummyMail(array_merge(
				$outgoingParams,
				array(
					'HEADER' => array_merge(
						$outgoingParams['HEADER'],
						array(
							'To'      => $outgoingParams['TO'],
							'Subject' => $outgoingParams['SUBJECT'],
						)
					),
				)
			));

			if ($this->uploadMessage($message, $excerpt))
			{
				Mail\Internals\MessageUploadQueueTable::delete(array(
					'ID' => $excerpt['ID'],
					'MAILBOX_ID' => $excerpt['MAILBOX_ID'],
				));
			}
		}
		else
		{
			Mail\Internals\MessageUploadQueueTable::update(
				array(
					'ID' => $excerpt['ID'],
					'MAILBOX_ID' => $excerpt['MAILBOX_ID'],
				),
				array(
					'SYNC_STAGE' => -1,
					'SYNC_LOCK' => 0,
				)
			);
		}

		return;
	}

	public function resyncMessage(array &$excerpt)
	{
		$body = $this->downloadMessage($excerpt);
		if (!empty($body))
		{
			return $this->cacheMessage(
				$body,
				array(
					'replaces' => $excerpt['ID'],
				)
			);
		}

		return false;
	}

	public function downloadAttachments(array &$excerpt)
	{
		$body = $this->downloadMessage($excerpt);
		if (!empty($body))
		{
			list(,,, $attachments) = \CMailMessage::parseMessage($body, $this->mailbox['LANG_CHARSET']);

			return $attachments;
		}

		return false;
	}

	public function isSupportLazyAttachments()
	{
		foreach ($this->getFilters() as $filter)
		{
			foreach ($filter['__actions'] as $action)
			{
				if (empty($action['LAZY_ATTACHMENTS']))
				{
					return false;
				}
			}
		}

		return true;
	}

	public function getFilters($force = false)
	{
		if (is_null($this->filters) || $force)
		{
			$this->filters = Mail\MailFilterTable::getList(array(
				'filter' => ORM\Query\Query::filter()
					->where('ACTIVE', 'Y')
					->where(
						ORM\Query\Query::filter()->logic('or')
							->where('MAILBOX_ID', $this->mailbox['ID'])
							->where('MAILBOX_ID', null)
					),
				'order' => array(
					'SORT' => 'ASC',
					'ID' => 'ASC',
				),
			))->fetchAll();

			foreach ($this->filters as $k => $item)
			{
				$this->filters[$k]['__actions'] = array();

				$res = \CMailFilter::getFilterList($item['ACTION_TYPE']);
				while ($row = $res->fetch())
				{
					$this->filters[$k]['__actions'][] = $row;
				}
			}
		}

		return $this->filters;
	}

	/**
	 * @deprecated
	 */
	public function resortTree($message = null)
	{
		global $DB;

		$worker = function ($id, $msgId, &$i)
		{
			global $DB;

			$stack = array(
				array(
					array($id, $msgId, false),
				),
			);

			$excerpt = array();

			do
			{
				$level = array_pop($stack);

				while ($level)
				{
					list($id, $msgId, $skip) = array_shift($level);

					if (!$skip)
					{
						$excerpt[] = $id;

						$DB->query(sprintf(
							'UPDATE b_mail_message SET LEFT_MARGIN = %2$u, RIGHT_MARGIN = %3$u WHERE ID = %1$u',
							$id, ++$i, ++$i
						));

						if (!empty($msgId))
						{
							$replies = array();

							$res = Mail\MailMessageTable::getList(array(
								'select' => array(
									'ID',
									'MSG_ID',
								),
								'filter' => array(
									'=MAILBOX_ID' => $this->mailbox['ID'],
									'=IN_REPLY_TO' => $msgId,
								),
								'order' => array(
									'FIELD_DATE' => 'ASC',
								),
							));

							while ($item = $res->fetch())
							{
								if (!in_array($item['ID'], $excerpt))
								{
									$replies[] = array($item['ID'], $item['MSG_ID'], false);
								}
							}

							if ($replies)
							{
								array_unshift($level, array($id, $msgId, true));

								array_push($stack, $level, $replies);
								$i--;

								continue 2;
							}
						}
					}
					else
					{
						$DB->query(sprintf(
							'UPDATE b_mail_message SET RIGHT_MARGIN = %2$u WHERE ID = %1$u',
							$id, ++$i
						));
					}
				}
			}
			while ($stack);
		};

		if (!empty($message))
		{
			if (empty($message['ID']))
			{
				throw new Main\ArgumentException("Argument 'message' is not valid");
			}

			$item = $DB->query(sprintf(
				'SELECT GREATEST(M1, M2) AS I FROM (SELECT
					(SELECT RIGHT_MARGIN FROM b_mail_message WHERE MAILBOX_ID = %1$u AND RIGHT_MARGIN > 0 ORDER BY LEFT_MARGIN ASC LIMIT 1) M1,
					(SELECT RIGHT_MARGIN FROM b_mail_message WHERE MAILBOX_ID = %1$u AND RIGHT_MARGIN > 0 ORDER BY LEFT_MARGIN DESC LIMIT 1) M2
				) M',
				$this->mailbox['ID']
			))->fetch();

			$i = empty($item['I']) ? 0 : $item['I'];

			$worker($message['ID'], $message['MSG_ID'], $i);
		}
		else
		{
			$DB->query(sprintf(
				'UPDATE b_mail_message SET LEFT_MARGIN = 0, RIGHT_MARGIN = 0 WHERE MAILBOX_ID = %u',
				$this->mailbox['ID']
			));

			$i = 0;

			$res = $DB->query(sprintf(
				"SELECT ID, MSG_ID FROM b_mail_message M WHERE MAILBOX_ID = %u AND (
					IN_REPLY_TO IS NULL OR IN_REPLY_TO = '' OR NOT EXISTS (
						SELECT 1 FROM b_mail_message WHERE MAILBOX_ID = M.MAILBOX_ID AND MSG_ID = M.IN_REPLY_TO
					)
				)",
				$this->mailbox['ID']
			));

			while ($item = $res->fetch())
			{
				$worker($item['ID'], $item['MSG_ID'], $i);
			}

			// crosslinked messages
			$query = sprintf(
				'SELECT ID, MSG_ID FROM b_mail_message
					WHERE MAILBOX_ID = %u AND LEFT_MARGIN = 0
					ORDER BY FIELD_DATE ASC LIMIT 1',
				$this->mailbox['ID']
			);
			while ($item = $DB->query($query)->fetch())
			{
				$worker($item['ID'], $item['MSG_ID'], $i);
			}
		}
	}

	/**
	 * @deprecated
	 */
	public function incrementTree($message)
	{
		if (empty($message['ID']))
		{
			throw new Main\ArgumentException("Argument 'message' is not valid");
		}

		if (!empty($message['IN_REPLY_TO']))
		{
			$item = Mail\MailMessageTable::getList(array(
				'select' => array(
					'ID', 'MSG_ID', 'LEFT_MARGIN', 'RIGHT_MARGIN',
				),
				'filter' => array(
					'=MAILBOX_ID' => $this->mailbox['ID'],
					'=MSG_ID' => $message['IN_REPLY_TO'],
				),
				'order' => array(
					'LEFT_MARGIN' => 'ASC',
				),
			))->fetch();

			if (!empty($item))
			{
				$message = $item;

				$item = Mail\MailMessageTable::getList(array(
					'select' => array(
						'ID', 'MSG_ID',
					),
					'filter' => array(
						'=MAILBOX_ID' => $this->mailbox['ID'],
						'<LEFT_MARGIN' => $item['LEFT_MARGIN'],
						'>RIGHT_MARGIN' => $item['RIGHT_MARGIN'],
					),
					'order' => array(
						'LEFT_MARGIN' => 'ASC',
					),
					'limit' => 1,
				))->fetch();

				if (!empty($item))
				{
					$message = $item;
				}
			}
		}

		$this->resortTree($message);
	}

	abstract protected function syncInternal();
	abstract public function uploadMessage(Main\Mail\Mail $message, array &$excerpt);
	abstract public function downloadMessage(array &$excerpt);

	public function getErrors()
	{
		return $this->errors;
	}

	public function getWarnings()
	{
		return $this->warnings;
	}

	public function getLastSyncResult()
	{
		return $this->lastSyncResult;
	}

	protected function setLastSyncResult(array $data)
	{
		$this->lastSyncResult = array_merge($this->lastSyncResult, $data);
	}
}
mailcontact.php000064400000006234147744702230007572 0ustar00<?php

namespace Bitrix\Mail\Helper;
/**
 * Class MailContact
 * @package Bitrix\Mail\Helper
 */
class MailContact
{
	const COLOR_GREEN       = '#9dcf01';
	const COLOR_BLUE        = '#2fc6f6';
	const COLOR_LIGHT_BLUE  = '#56d1e0';
	const COLOR_ORANGE      = '#ffa900';
	const COLOR_CYAN        = '#47e4c2';
	const COLOR_PINK        = '#ff5b55';
	const COLOR_PURPLE      = '#9985dd';
	const COLOR_GREY        = '#a8adb4';
	const COLOR_BROWN       = '#af7e00';
	const COLOR_RED         = '#F44336';
	const COLOR_DEEP_PURPLE = '#673AB7';
	const COLOR_INDIGO      = '#3F51B5';
	const COLOR_TEAL        = '#009688';
	const COLOR_LIGHT_GREEN = '#8BC34A';
	const COLOR_LIME        = '#CDDC39';
	const COLOR_YELLOW      = '#FFEB3B';
	const COLOR_AMBER       = '#FFC107';
	const COLOR_DEEP_ORANGE = '#FF5722';
	const COLOR_BLUE_GREY   = '#607D8B';

	/**
	 * @param $email
	 * @param $name
	 * @param null $lastName
	 * @return array
	 * @throws \ReflectionException
	 */
	public static function getIconData($email, $name, $lastName = null)
	{
		return [
			'INITIALS' => static::getInitials($email, $name, $lastName),
			'COLOR' => static::getRandomColor(),
		];
	}

	/**
	 * @return mixed
	 * @throws \ReflectionException
	 */
	public static function getRandomColor()
	{
		static $colors = null;
		if (is_null($colors))
		{
			$reflect = new \ReflectionClass(static::class);
			foreach ($reflect->getConstants() as $name => $value)
			{
				if (strncmp($name, 'COLOR', 5) === 0)
				{
					$colors[] = $value;
				}
			}
		}
		return $colors[rand(0, count($colors) - 1)];
	}

	/** return two symbols from name and last name, or 1 - from name or email
	 * @param $email
	 * @param $name
	 * @param null $lastName
	 * @return string
	 */
	public static function getInitials($email, $name = null, $lastName = null)
	{
		if ($lastName && substr($lastName, 0, 1) && $name && substr($name, 0, 1))
		{
			return strtoupper(substr($name, 0, 1) . substr($lastName, 0, 1));
		}
		$name = explode(' ', $name);

		if (is_array($name) && isset($name[0]) && $name[0])
		{
			if (isset($name[1]) && $name[1])
			{
				return strtoupper(substr($name[0], 0, 1) . substr($name[1], 0, 1));
			}
			else
			{
				return strtoupper(substr($name[0], 0, 1));
			}
		}
		return strtoupper(substr($email, 0, 1));
	}

	/** returns array of fields and their values for adding to database
	 * @param $mailsField
	 * @param $userId
	 * @param $addedFrom
	 * @return array
	 * @throws \ReflectionException
	 */
	public static function getContactsData($mailsField, $userId, $addedFrom)
	{
		if (!$mailsField)
		{
			return [];
		}
		$mails = explode(',', $mailsField);
		$contacts = [];
		foreach ($mails as $mail)
		{
			$mail = trim($mail);
			$address = new \Bitrix\Main\Mail\Address($mail);
			$emailToAdd = $nameToAdd = '';
			if ($address->validate())
			{
				$emailToAdd = $address->getEmail();
				$nameToAdd = trim($address->getName());
			}
			if ($emailToAdd)
			{
				$contacts[] = [
					'USER_ID' => intval($userId),
					'NAME' => $nameToAdd ? $nameToAdd : explode('@', $emailToAdd)[0],
					'ICON' => static::getIconData($emailToAdd, $nameToAdd),
					'EMAIL' => $emailToAdd,
					'ADDED_FROM' => $addedFrom,
				];
			}
		}
		return $contacts;
	}
}
oauth.php000066400000023722147744702230006417 0ustar00<?php

namespace Bitrix\Mail\Helper;

use Bitrix\Main;
use Bitrix\Mail;

abstract class OAuth
{

	protected $oauthEntity;

	protected $service, $storedUid;

	/**
	 * Returns the list of supported services
	 *
	 * @return array
	 */
	public static function getKnownServices()
	{
		static $knownServices;

		if (is_null($knownServices))
		{
			$knownServices = array(
				OAuth\Google::getServiceName(),
				OAuth\LiveId::getServiceName(),
				OAuth\Yandex::getServiceName(),
				OAuth\Mailru::getServiceName(),
			);
		}

		return $knownServices;
	}

	/**
	 * Returns helper instance
	 *
	 * @param string $service Service name.
	 * @return \Bitrix\Mail\Helper\OAuth|false
	 */
	public static function getInstance($service = null)
	{
		if (get_called_class() != get_class())
		{
			$className = get_called_class();
			$service = $className::getServiceName();
		}
		else
		{
			if (!in_array($service, self::getKnownServices()))
			{
				return false;
			}

			$className = sprintf('%s\OAuth\%s', __NAMESPACE__, $service);
		}

		if (!Main\Loader::includeModule('socialservices'))
		{
			return false;
		}

		$instance = new $className;

		$instance->service = $service;
		$instance->storedUid = sprintf('%x%x', time(), rand(0, 0xffffffff));

		if (!$instance->check())
		{
			return false;
		}

		return $instance;
	}

	/**
	 * Returns helper instance by packed metadata
	 *
	 * @param string $meta Packed metadata.
	 * @return \Bitrix\Mail\Helper\OAuth|false
	 */
	public static function getInstanceByMeta($meta)
	{
		if ($meta = static::parseMeta($meta))
		{
			if ($instance = self::getInstance($meta['service']))
			{
				if ('oauthb' == $meta['type'])
				{
					$instance->storedUid = $meta['key'];
				}

				return $instance;
			}
		}
	}

	/**
	 * Returns packed metadata for instance
	 *
	 * @return string
	 */
	public function buildMeta()
	{
		return sprintf(
			"\x00oauthb\x00%s\x00%s",
			$this->getServiceName(),
			$this->getStoredUid()
		);
	}

	/**
	 * Returns unpacked metadata
	 *
	 * @param string $meta Packed metadata.
	 * @return array
	 */
	public static function parseMeta($meta)
	{
		$regex = sprintf(
			'/^\x00(oauthb)\x00(%s)\x00([a-f0-9]+)$/',
			join(
				'|',
				array_map(
					function ($item)
					{
						return preg_quote($item, '/');
					},
					self::getKnownServices()
				)
			)
		);

		if (!preg_match($regex, $meta, $matches))
		{
			if (!preg_match('/^\x00(oauth)\x00(google|liveid)\x00(\d+)$/', $meta, $matches))
			{
				return null;
			}
		}

		return array(
			'type' => $matches[1],
			'service' => $matches[2],
			'key' => $matches[3],
		);
	}

	/**
	 * Returns token from socialservices
	 *
	 * @param string $service Service name.
	 * @param string $key User ID.
	 * @return string|null
	 */
	private static function getSocservToken($service, $key)
	{
		if (Main\Loader::includeModule('socialservices'))
		{
			switch ($service)
			{
				case 'google':
					$oauthClient = new \CSocServGoogleOAuth($key);
					$oauthClient->getUrl('modal', array('https://mail.google.com/'));
					break;
				case 'liveid':
					$oauthClient = new \CSocServLiveIDOAuth($key);
					$oauthClient->getUrl('modal', array('wl.imap', 'wl.offline_access'));
					break;
			}

			if (!empty($oauthClient))
			{
				return $oauthClient->getStorageToken() ?: false;
			}
		}

		return null;
	}

	/**
	 * Returns token by packed metadata
	 *
	 * @param string $meta Packed metadata.
	 * @return string|null
	 */
	public static function getTokenByMeta($meta)
	{
		if ($meta = static::parseMeta($meta))
		{
			if ('oauthb' == $meta['type'])
			{
				if ($oauthHelper = self::getInstance($meta['service']))
				{
					return $oauthHelper->getStoredToken($meta['key']) ?: false;
				}
			}
			else if ('oauth' == $meta['type'])
			{
				return self::getSocservToken($meta['service'], $meta['key']);
			}
		}
	}

	/**
	 * Returns user data by packed metadata
	 *
	 * @param string $meta Packed metadata.
	 * @param boolean $secure Strip raw data (includes tokens).
	 * @return array|null
	 */
	public static function getUserDataByMeta($meta, $secure = true)
	{
		if ($meta = static::parseMeta($meta))
		{
			if ($oauthHelper = self::getInstance($meta['service']))
			{
				if ('oauthb' == $meta['type'])
				{
					$oauthHelper->getStoredToken($meta['key']);
				}
				else if ('oauth' == $meta['type'])
				{
					if ($token = self::getSocservToken($meta['service'], $meta['key']))
					{
						$oauthHelper->getOAuthEntity()->setToken($token);
					}
				}

				return $oauthHelper->getUserData($secure);
			}
		}

		return null;
	}

	/**
	 * Returns service interface entity
	 *
	 * @return mixed
	 */
	public function getOAuthEntity()
	{
		return $this->oauthEntity;
	}

	/**
	 * Returns instance UID
	 *
	 * @return string
	 */
	public function getStoredUid()
	{
		return $this->storedUid;
	}

	/**
	 * Returns server OAuth handler URI
	 *
	 * @param boolean $final Skip Bitrix24 proxy.
	 * @return string
	 */
	public function getRedirect($final = true)
	{
		return isModuleInstalled('bitrix24') && !$final
			? $this->getControllerUrl() . '/redirect.php'
			: $this->getHostUrl() . '/bitrix/tools/mail_oauth.php';
	}

	public function getHostUrl()
	{
		$request = Main\Context::getCurrent()->getRequest();

		$uri = new Main\Web\Uri(sprintf(
			'%s://%s:%u',
			$request->isHttps() ? 'https' : 'http',
			$request->getHttpHost(),
			Main\Context::getCurrent()->getServer()->getServerPort() // $request->getServerPort()
		));

		return rtrim($uri->getLocator(), '/');
	}

	/**
	 * Returns service OAuth endpoint URI
	 *
	 * @return string
	 */
	public function getUrl()
	{
		global $APPLICATION;

		\CSocServAuthManager::setUniqueKey();

		if (isModuleInstalled('bitrix24'))
		{
			$state = sprintf(
				'%s?%s',
				$this->getRedirect(),
				http_build_query(array(
					'check_key' => $_SESSION['UNIQUE_KEY'],
					'dummy' => 'https://dummy.bitrix24.com/',
					'state' => rawurlencode(http_build_query(array(
						'service' => $this->service,
						'uid' => $this->storedUid,
					))),
				))
			);
		}
		else
		{
			$state = http_build_query(array(
				'check_key' => $_SESSION['UNIQUE_KEY'],
				'service' => $this->service,
				'uid' => $this->storedUid,
			));
		}

		return $this->oauthEntity->getAuthUrl($this->getRedirect(false), $state);
	}

	/**
	 * Fetches token by instance UID from DB
	 *
	 * @return array|false
	 */
	protected function fetchStoredToken()
	{
		return Mail\Internals\OAuthTable::getList(array(
			'filter' => array(
				'=UID' => $this->storedUid,
			),
			'order' => array(
				'ID' => 'DESC',
			),
		))->fetch();
	}

	/**
	 * Returns token by instance UID
	 *
	 * @param string $uid Instance UID.
	 * @return string|null
	 */
	public function getStoredToken($uid = null)
	{
		$token = null;

		if (!empty($uid))
		{
			$this->storedUid = $uid;
		}

		$item = $this->fetchStoredToken();

		if (!empty($item))
		{
			$this->oauthEntity->setToken($token = $item['TOKEN']);
			$this->oauthEntity->setRefreshToken($item['REFRESH_TOKEN']);

			if (empty($token) || $item['TOKEN_EXPIRES'] > 0 && $item['TOKEN_EXPIRES'] < time())
			{
				$this->oauthEntity->setToken(null);

				if (!empty($item['REFRESH_TOKEN']))
				{
					if ($this->oauthEntity->getNewAccessToken($item['REFRESH_TOKEN']))
					{
						$tokenData = $this->oauthEntity->getTokenData();

						Mail\Internals\OAuthTable::update(
							$item['ID'],
							array(
								'TOKEN' => $tokenData['access_token'],
								'REFRESH_TOKEN' => $tokenData['refresh_token'],
								'TOKEN_EXPIRES' => $tokenData['expires_in'],
							)
						);
					}
				}

				$token = $this->oauthEntity->getToken();
			}
		}

		return $token;
	}

	/**
	 * Obtains tokens from service
	 *
	 * @param string $code Service authorization code.
	 * @return boolean
	 */
	public function getAccessToken($code = null)
	{
		if ($code)
		{
			$this->oauthEntity->setCode($code);
		}

		$oauthData = $_SESSION['OAUTH_DATA'];

		$result = $this->oauthEntity->getAccessToken($this->getRedirect(false));

		$_SESSION['OAUTH_DATA'] = $oauthData;

		return $result;
	}

	/**
	 * Returns user data
	 *
	 * @param boolean $secure Strip raw data (includes tokens).
	 * @return array|null
	 */
	public function getUserData($secure = true)
	{
		try
		{
			$userData = $this->oauthEntity->getCurrentUser();
		}
		catch (Main\SystemException $e)
		{
		}

		if (!empty($userData))
		{
			return array_merge(
				$this->mapUserData($userData),
				$secure ? array() : array('__data' => $userData)
			);
		}
	}

	/**
	 * Returns unified user data
	 *
	 * @param array $userData User data.
	 * @return array
	 */
	abstract protected function mapUserData(array $userData);

	/**
	 * Returns service name
	 *
	 * @throws \Bitrix\Main\ObjectException
	 * @return void
	 */
	public static function getServiceName()
	{
		throw new Main\ObjectException('abstract');
	}

	/**
	 * Handles service response
	 *
	 * @param string $state Response data.
	 * @return void
	 */
	public function handleResponse($state)
	{
		$this->storedUid = $state['uid'];

		if ($item = $this->fetchStoredToken())
		{
			$this->oauthEntity->setRefreshToken($item['REFRESH_TOKEN']);
		}

		if (!empty($_REQUEST['code']) && \CSocServAuthManager::checkUniqueKey())
		{
			$this->getAccessToken($_REQUEST['code']);

			if ($userData = $this->getUserData(false))
			{
				$fields = array(
					'UID' => $this->getStoredUid(),
					'TOKEN' => $userData['__data']['access_token'],
					'REFRESH_TOKEN' => $userData['__data']['refresh_token'],
					'TOKEN_EXPIRES' => $userData['__data']['expires_in'],
				);

				if (empty($item))
				{
					Mail\Internals\OAuthTable::add($fields);
				}
				else
				{
					Mail\Internals\OAuthTable::update($item['ID'], $fields);
				}

				unset($userData['__data']);

				?>

				<script type="text/javascript">

				targetWindow = window.opener ? window.opener : window;

				targetWindow.BX.onCustomEvent(
					'OnMailOAuthBCompleted',
					[
						'<?=\CUtil::jsEscape($this->getStoredUid()) ?>',
						'<?=\CUtil::jsEscape($this->getUrl()) ?>',
						<?=Main\Web\Json::encode($userData) ?>
					]
				);

				if (targetWindow !== window)
				{
					window.close();
				}

				</script>

				<?
			}
		}
	}

}
messageindexstepper.php000066400000002157147744702230011355 0ustar00<?php

namespace Bitrix\Mail\Helper;

use Bitrix\Main;
use Bitrix\Mail;

class MessageIndexStepper extends Main\Update\Stepper
{
	const INDEX_VERSION = 1;

	protected static $moduleId = 'mail';

	public function execute(array &$option)
	{
		$option['steps'] = Mail\MailMessageTable::getCount(array(
			'=INDEX_VERSION' => static::INDEX_VERSION,
		));
		$option['count'] = Mail\MailMessageTable::getCount(array(
			'<=INDEX_VERSION' => static::INDEX_VERSION,
		));

		if ($option['steps'] >= $option['count'])
		{
			return false;
		}

		$res = Mail\MailMessageTable::getList(array(
			'select' => array(
				'ID',
				'FIELD_FROM', 'FIELD_REPLY_TO',
				'FIELD_TO', 'FIELD_CC', 'FIELD_BCC',
				'SUBJECT', 'BODY',
			),
			'filter' => array(
				'<INDEX_VERSION' => static::INDEX_VERSION,
			),
			'order' => array('ID' => 'ASC'),
			'limit' => 1000,
		));

		while ($item = $res->fetch())
		{
			$option['steps']++;

			$fields = array(
				'SEARCH_CONTENT' => Message::prepareSearchContent($item),
				'INDEX_VERSION' => static::INDEX_VERSION,
			);

			Mail\MailMessageTable::update($item['ID'], $fields);
		}

		return true;
	}

}
messageclosurestepper.php000066400000006401147744702230011716 0ustar00<?php

namespace Bitrix\Mail\Helper;

use Bitrix\Main;
use Bitrix\Mail;

class MessageClosureStepper extends Main\Update\Stepper
{
	protected static $moduleId = 'mail';

	public function execute(array &$option)
	{
		global $DB, $pPERIOD;

		$pPERIOD = 10;

		$option['count'] = Mail\MailMessageTable::getCount();
		$option['steps'] = Mail\Internals\MessageClosureTable::getList(array(
			'select' => array(
				new Main\Entity\ExpressionField('CNT', 'COUNT(DISTINCT %s)', 'MESSAGE_ID')
			),
		))->fetch()['CNT'];

		if ($option['steps'] < $option['count'])
		{
			if (!($option['mailboxId'] > 0) || $option['stage'] < 1)
			{
				$option['mailboxId'] = $DB->query(
					'SELECT MAILBOX_ID FROM b_mail_message M WHERE NOT EXISTS (
						SELECT 1 FROM b_mail_message_closure WHERE MESSAGE_ID = M.ID
					) LIMIT 1'
				)->fetch()['MAILBOX_ID'];

				$option['stage'] = 1;
			}
		}
		else
		{
			$option['mailboxId'] = false;

			$option['stage'] = 3;
		}

		if ($option['mailboxId'] > 0 && 1 == $option['stage'])
		{
			$res = $DB->query(sprintf(
				'INSERT IGNORE INTO b_mail_message_closure (MESSAGE_ID, PARENT_ID)
				(
					SELECT M.ID, M.ID FROM b_mail_message M
					WHERE M.MAILBOX_ID = %u AND (
						M.IN_REPLY_TO IS NULL OR M.IN_REPLY_TO = "" OR M.IN_REPLY_TO = M.MSG_ID OR NOT EXISTS (
							SELECT 1 FROM b_mail_message WHERE MAILBOX_ID = M.MAILBOX_ID AND MSG_ID = M.IN_REPLY_TO
						)
					) AND NOT EXISTS (SELECT 1 FROM b_mail_message_closure WHERE MESSAGE_ID = M.ID)
					LIMIT 40000
				)',
				$option['mailboxId']
			))->affectedRowsCount();

			$option['stage'] = $res < 40000 ? 2 : 1;
			$option['steps'] += $res;

			return self::CONTINUE_EXECUTION;
		}

		if ($option['mailboxId'] > 0 && 2 == $option['stage'])
		{
			$res = $DB->query(sprintf(
				'INSERT IGNORE INTO b_mail_message_closure (MESSAGE_ID, PARENT_ID)
				(
					SELECT DISTINCT M.ID, C.PARENT_ID
					FROM b_mail_message M
						LEFT JOIN b_mail_message R ON M.MAILBOX_ID = R.MAILBOX_ID AND M.IN_REPLY_TO = R.MSG_ID
						LEFT JOIN b_mail_message_closure C ON R.ID = C.MESSAGE_ID
					WHERE M.MAILBOX_ID = %u
						AND EXISTS (SELECT 1 FROM b_mail_message_closure WHERE MESSAGE_ID = R.ID)
						AND NOT EXISTS (SELECT 1 FROM b_mail_message_closure WHERE MESSAGE_ID = M.ID)
				)',
				$option['mailboxId']
			))->affectedRowsCount();

			$option['stage'] = $res > 0 ? 3 : 4;

			return self::CONTINUE_EXECUTION;
		}

		if (3 == $option['stage'])
		{
			$res = $DB->query(
				'INSERT IGNORE INTO b_mail_message_closure (MESSAGE_ID, PARENT_ID)
				(
					SELECT DISTINCT C.MESSAGE_ID, C.MESSAGE_ID
					FROM b_mail_message_closure C
					WHERE NOT EXISTS (SELECT 1 FROM b_mail_message_closure WHERE PARENT_ID = C.MESSAGE_ID)
				)'
			)->affectedRowsCount();

			$option['stage'] = $res > 0 ? 2 : 4;
		}

		if (4 == $option['stage'])
		{
			$res = $DB->query(sprintf(
				'INSERT IGNORE INTO b_mail_message_closure (MESSAGE_ID, PARENT_ID)
				(
					SELECT M.ID, M.ID FROM b_mail_message M
					WHERE M.MAILBOX_ID = %u
						AND NOT EXISTS (SELECT 1 FROM b_mail_message_closure WHERE MESSAGE_ID = M.ID)
					ORDER BY FIELD_DATE ASC LIMIT 1
				)',
				$option['mailboxId']
			))->affectedRowsCount();

			$option['stage'] = $res > 0 ? 2 : 0;
		}

		return $option['mailboxId'] > 0 ? self::CONTINUE_EXECUTION : self::FINISH_EXECUTION;
	}

}
licensemanager.php000066400000002637147744702230010256 0ustar00<?php

namespace Bitrix\Mail\Helper;

use Bitrix\Main;
use Bitrix\Bitrix24;

/**
 * Class LicenseManager
 */
class LicenseManager
{

	/**
	 * Checks if mailboxes synchronization is available
	 *
	 * @return bool
	 */
	public static function isSyncAvailable()
	{
		if (!Main\Loader::includeModule('bitrix24'))
		{
			return true;
		}

		return (bool) Bitrix24\Feature::isFeatureEnabled('mail_mailbox_sync');
	}

	public static function getSharedMailboxesLimit()
	{
		if (!Main\Loader::includeModule('bitrix24'))
		{
			return -1;
		}

		return (int) Bitrix24\Feature::getVariable('mail_shared_mailboxes_limit');
	}

	/**
	 * How many mailboxes a user can connect
	 *
	 * @return int
	 */
	public static function getUserMailboxesLimit()
	{
		if (!Main\Loader::includeModule('bitrix24'))
		{
			return -1;
		}

		if (!static::isSyncAvailable())
		{
			return 0;
		}

		return (int) Bitrix24\Feature::getVariable('mail_user_mailboxes_limit');
	}

	/**
	 * Returns the number of days to store messages
	 *
	 * @return int
	 */
	public static function getSyncOldLimit()
	{
		if (!Main\Loader::includeModule('bitrix24'))
		{
			return (int) Main\Config\Option::get('mail', 'sync_old_limit2', -1);
		}

		return (int) Bitrix24\Feature::getVariable('mail_sync_old_limit');
	}

	/**
	 * Checks if old messages should be deleted
	 *
	 * @return bool
	 */
	public static function isCleanupOldEnabled()
	{
		return static::getSyncOldLimit() > 0;
	}

}
attachment/storage.php000066400000007340147744702230011071 0ustar00<?php

namespace Bitrix\Mail\Helper\Attachment;

use Bitrix\Main;
use Bitrix\Main\Localization\Loc;
use Bitrix\Mail;

class Storage
{

	/**
	 * Returns attachments disk storage
	 *
	 * @return \Bitrix\Disk\Storage|false
	 */
	public static function getStorage()
	{
		static $storage;

		if (!is_null($storage))
		{
			return $storage;
		}

		$storage = false;

		if (!Main\Loader::includeModule('disk'))
		{
			return $storage;
		}

		$storageId = Main\Config\Option::get('mail', 'disk_attachment_storage_id', 0);
		if ($storageId > 0)
		{
			$storage = \Bitrix\Disk\Storage::loadById($storageId);
			if (!$storage || $storage->getModuleId() != 'mail')
			{
				$storage = false;
			}
		}

		if (!$storage)
		{
			$driver = \Bitrix\Disk\Driver::getInstance();

			$storage = $driver->addStorageIfNotExist(array(
				'NAME' => Loc::getMessage('MAIL_ATTACHMENT_STORAGE_NAME'),
				'USE_INTERNAL_RIGHTS' => false,
				'MODULE_ID' => 'mail',
				'ENTITY_TYPE' => Mail\Disk\ProxyType\Mail::className(),
				'ENTITY_ID' => 'mail',
			));
			if ($storage)
			{
				Main\Config\Option::set('mail', 'disk_attachment_storage_id', $storage->getId());
			}
			else
			{
				$storage = false;
			}
		}

		return $storage;
	}

	/**
	 * Returns disk url manager
	 *
	 * @return \Bitrix\Disk\UrlManager|false
	 */
	public static function getUrlManager()
	{
		static $urlManager;

		if (!is_null($urlManager))
		{
			return $urlManager;
		}

		$urlManager = false;

		if (!Main\Loader::includeModule('disk'))
		{
			return $urlManager;
		}

		$urlManager = \Bitrix\Disk\Driver::getInstance()->getUrlManager();

		return $urlManager;
	}

	/**
	 * Returns disk objects by file ID
	 *
	 * @param int $fileId File ID.
	 * @param int $limit Limit.
	 * @return array
	 */
	public static function getObjectsByFileId($fileId, $limit = 0)
	{
		$storage = static::getStorage();

		if (!$storage)
		{
			return array();
		}

		return \Bitrix\Disk\File::getModelList(array(
			'filter' => array(
				'=STORAGE_ID' => $storage->getId(),
				'=TYPE' => \Bitrix\Disk\Internals\ObjectTable::TYPE_FILE,
				'=FILE_ID' => $fileId,
			),
			'limit' => $limit,
		));
	}

	/**
	 * Returns disk object by attachment file data (creates one if not exists)
	 *
	 * @param array $attachment Attachment file data.
	 * @param boolean $create Create object if not exists.
	 * @return \Bitrix\Disk\File|false|null
	 */
	public static function getObjectByAttachment(array $attachment, $create = false)
	{
		$object = reset(static::getObjectsByFileId($attachment['FILE_ID'], 1));

		if (empty($object) && $create)
		{
			$object = static::registerAttachment($attachment);
		}

		return $object;
	}

	/**
	 * Creates disk object for attachment file
	 *
	 * @param array $attachment Attachment file data.
	 * @return \Bitrix\Disk\File|false|null
	 */
	public static function registerAttachment(array $attachment)
	{
		$storage = static::getStorage();

		if (!$storage)
		{
			return false;
		}

		$folder = $storage->getChild(array(
			'=NAME' => date('Y-m'),
			'=TYPE' => \Bitrix\Disk\Internals\FolderTable::TYPE,
		));

		if (!$folder)
		{
			$folder = $storage->addFolder(array(
				'NAME' => date('Y-m'),
				'CREATED_BY' => 1, // @TODO
			));
		}

		if (!$folder)
		{
			$folder = $storage;
		}

		return $folder->addFile(
			array(
				'NAME' => \Bitrix\Disk\Ui\Text::correctFilename($attachment['FILE_NAME']) ?: sprintf('%x', rand(0, 0xffffff)),
				'FILE_ID' => $attachment['FILE_ID'],
				'SIZE' => $attachment['FILE_SIZE'],
				'CREATED_BY' => 1, // @TODO
			),
			array(),
			true
		);
	}

	/**
	 * Deletes disk objects by file ID
	 *
	 * @param int $fileId File ID.
	 * @return void
	 */
	public static function unregisterAttachment($fileId)
	{
		foreach (static::getObjectsByFileId($fileId) as $item)
		{
			$item->delete(1); // @TODO
		}
	}

}