1<?php
2
3declare(strict_types=1);
4
5/**
6 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
7 *
8 * Mail
9 *
10 * This code is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU Affero General Public License, version 3,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
18 *
19 * You should have received a copy of the GNU Affero General Public License, version 3,
20 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21 *
22 */
23
24namespace OCA\Mail\IMAP\Sync;
25
26use Horde_Imap_Client;
27use Horde_Imap_Client_Base;
28use Horde_Imap_Client_Exception;
29use Horde_Imap_Client_Exception_Sync;
30use Horde_Imap_Client_Ids;
31use Horde_Imap_Client_Mailbox;
32use OCA\Mail\Exception\UidValidityChangedException;
33use OCA\Mail\Exception\MailboxDoesNotSupportModSequencesException;
34use OCA\Mail\IMAP\MessageMapper;
35use function array_chunk;
36use function array_merge;
37
38class Synchronizer {
39
40	/**
41	 * This determines how many UIDs we send to IMAP for a check of changed or
42	 * vanished messages. The number needs a balance between good performance
43	 * (few chunks) and staying below the IMAP command size limits. 15k has
44	 * shown to cause IMAP errors for some accounts where the UID list can't be
45	 * compressed much by Horde.
46	 */
47	private const UID_CHUNK_SIZE = 10000;
48
49	/** @var MessageMapper */
50	private $messageMapper;
51
52	public function __construct(MessageMapper $messageMapper) {
53		$this->messageMapper = $messageMapper;
54	}
55
56	/**
57	 * @param Horde_Imap_Client_Base $imapClient
58	 * @param Request $request
59	 * @param int $criteria
60	 *
61	 * @return Response
62	 * @throws Horde_Imap_Client_Exception
63	 * @throws Horde_Imap_Client_Exception_Sync
64	 * @throws UidValidityChangedException
65	 * @throws MailboxDoesNotSupportModSequencesException
66	 */
67	public function sync(Horde_Imap_Client_Base $imapClient,
68						 Request $request,
69						 int $criteria = Horde_Imap_Client::SYNC_NEWMSGSUIDS | Horde_Imap_Client::SYNC_FLAGSUIDS | Horde_Imap_Client::SYNC_VANISHEDUIDS): Response {
70		$mailbox = new Horde_Imap_Client_Mailbox($request->getMailbox());
71		try {
72			if ($criteria & Horde_Imap_Client::SYNC_NEWMSGSUIDS) {
73				$newUids = $this->getNewMessageUids($imapClient, $mailbox, $request);
74			} else {
75				$newUids = [];
76			}
77			if ($criteria & Horde_Imap_Client::SYNC_FLAGSUIDS) {
78				$changedUids = $this->getChangedMessageUids($imapClient, $mailbox, $request);
79			} else {
80				$changedUids = [];
81			}
82			if ($criteria & Horde_Imap_Client::SYNC_VANISHEDUIDS) {
83				$vanishedUids = $this->getVanishedMessageUids($imapClient, $mailbox, $request);
84			} else {
85				$vanishedUids = [];
86			}
87		} catch (Horde_Imap_Client_Exception_Sync $e) {
88			if ($e->getCode() === Horde_Imap_Client_Exception_Sync::UIDVALIDITY_CHANGED) {
89				throw new UidValidityChangedException();
90			}
91			throw $e;
92		} catch (Horde_Imap_Client_Exception $e) {
93			if ($e->getCode() === Horde_Imap_Client_Exception::MBOXNOMODSEQ) {
94				throw new MailboxDoesNotSupportModSequencesException($e->getMessage(), $e->getCode(), $e);
95			}
96			throw $e;
97		}
98
99		$newMessages = $this->messageMapper->findByIds($imapClient, $request->getMailbox(), $newUids);
100		$changedMessages = $this->messageMapper->findByIds($imapClient, $request->getMailbox(), $changedUids);
101		$vanishedMessageUids = $vanishedUids;
102
103		return new Response($newMessages, $changedMessages, $vanishedMessageUids);
104	}
105
106	/**
107	 * @param Horde_Imap_Client_Base $imapClient
108	 * @param Horde_Imap_Client_Mailbox $mailbox
109	 * @param Request $request
110	 *
111	 * @return array
112	 * @throws Horde_Imap_Client_Exception
113	 * @throws Horde_Imap_Client_Exception_Sync
114	 */
115	private function getNewMessageUids(Horde_Imap_Client_Base $imapClient, Horde_Imap_Client_Mailbox $mailbox, Request $request): array {
116		$newUids = $imapClient->sync($mailbox, $request->getToken(), [
117			'criteria' => Horde_Imap_Client::SYNC_NEWMSGSUIDS,
118		])->newmsgsuids->ids;
119		return $newUids;
120	}
121
122	/**
123	 * @param Horde_Imap_Client_Base $imapClient
124	 * @param Horde_Imap_Client_Mailbox $mailbox
125	 * @param Request $request
126	 *
127	 * @return array
128	 */
129	private function getChangedMessageUids(Horde_Imap_Client_Base $imapClient, Horde_Imap_Client_Mailbox $mailbox, Request $request): array {
130		$changedUids = array_merge(
131			[], // for php<7.4 https://www.php.net/manual/en/function.array-merge.php
132			...array_map(
133				function (array $uids) use ($imapClient, $mailbox, $request) {
134					return $imapClient->sync($mailbox, $request->getToken(), [
135						'criteria' => Horde_Imap_Client::SYNC_FLAGSUIDS,
136						'ids' => new Horde_Imap_Client_Ids($uids),
137					])->flagsuids->ids;
138				},
139				array_chunk($request->getUids(), self::UID_CHUNK_SIZE)
140			)
141		);
142		return $changedUids;
143	}
144
145	/**
146	 * @param Horde_Imap_Client_Base $imapClient
147	 * @param Horde_Imap_Client_Mailbox $mailbox
148	 * @param Request $request
149	 *
150	 * @return array
151	 */
152	private function getVanishedMessageUids(Horde_Imap_Client_Base $imapClient, Horde_Imap_Client_Mailbox $mailbox, Request $request): array {
153		$vanishedUids = array_merge(
154			[], // for php<7.4 https://www.php.net/manual/en/function.array-merge.php
155			...array_map(
156				function (array $uids) use ($imapClient, $mailbox, $request) {
157					return $imapClient->sync($mailbox, $request->getToken(), [
158						'criteria' => Horde_Imap_Client::SYNC_VANISHEDUIDS,
159						'ids' => new Horde_Imap_Client_Ids($uids),
160					])->vanisheduids->ids;
161				},
162				array_chunk($request->getUids(), self::UID_CHUNK_SIZE)
163			)
164		);
165		return $vanishedUids;
166	}
167}
168