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