1<?php 2 3declare(strict_types=1); 4 5/** 6 * @author Lukas Reschke <lukas@owncloud.com> 7 * @author Thomas Müller <thomas.mueller@tmit.eu> 8 * 9 * Mail 10 * 11 * This code is free software: you can redistribute it and/or modify 12 * it under the terms of the GNU Affero General Public License, version 3, 13 * as published by the Free Software Foundation. 14 * 15 * This program is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU Affero General Public License for more details. 19 * 20 * You should have received a copy of the GNU Affero General Public License, version 3, 21 * along with this program. If not, see <http://www.gnu.org/licenses/> 22 * 23 */ 24 25namespace OCA\Mail\Cache; 26 27use Exception; 28use Horde_Imap_Client_Cache_Backend; 29use Horde_Imap_Client_Exception; 30use InvalidArgumentException; 31 32/** 33 * This class is inspired by Horde_Imap_Client_Cache_Backend_Cache of the Horde Project 34 */ 35class Cache extends Horde_Imap_Client_Cache_Backend { 36 37 /** Cache structure version. */ 38 public const VERSION = 3; 39 40 /** 41 * The cache object. 42 * 43 * @var \OCP\ICache 44 */ 45 protected $_cache; 46 47 /** 48 * The working data for the current pageload. All changes take place to 49 * this data. 50 * 51 * @var array 52 */ 53 protected $_data = []; 54 55 /** 56 * The list of cache slices loaded. 57 * 58 * @var array 59 */ 60 protected $_loaded = []; 61 62 /** 63 * The mapping of UIDs to slices. 64 * 65 * @var array 66 */ 67 protected $_slicemap = []; 68 69 /** 70 * The list of items to update: 71 * - add: (array) List of IDs that were added. 72 * - slice: (array) List of slices that were modified. 73 * - slicemap: (boolean) Was slicemap info changed? 74 * 75 * @var array 76 */ 77 protected $_update = []; 78 79 /** 80 * Constructor. 81 * 82 * @param array $params Configuration parameters: 83 */ 84 public function __construct(array $params = []) { 85 // Default parameters. 86 $params = array_merge([ 87 'lifetime' => 604800, 88 'slicesize' => 50 89 ], array_filter($params)); 90 91 if (!isset($params['cacheob'])) { 92 throw new InvalidArgumentException('Missing cacheob parameter.'); 93 } 94 95 foreach (['lifetime', 'slicesize'] as $val) { 96 $params[$val] = intval($params[$val]); 97 } 98 99 parent::__construct($params); 100 } 101 102 /** 103 * Initialization tasks. 104 */ 105 protected function _initOb() { 106 $this->_cache = $this->_params['cacheob']; 107 register_shutdown_function([$this, 'save']); 108 } 109 110 /** 111 * Updates the cache. 112 */ 113 public function save(): void { 114 $lifetime = $this->_params['lifetime']; 115 116 foreach ($this->_update as $mbox => $val) { 117 $s = &$this->_slicemap[$mbox]; 118 119 if (!empty($val['add'])) { 120 if ($s['c'] <= $this->_params['slicesize']) { 121 $val['slice'][] = $s['i']; 122 $this->_loadSlice($mbox, $s['i']); 123 } 124 $val['slicemap'] = true; 125 126 foreach (array_keys(array_flip($val['add'])) as $uid) { 127 if ($s['c']++ > $this->_params['slicesize']) { 128 $s['c'] = 0; 129 $val['slice'][] = ++$s['i']; 130 $this->_loadSlice($mbox, $s['i']); 131 } 132 $s['s'][$uid] = $s['i']; 133 } 134 } 135 136 if (!empty($val['slice'])) { 137 $d = &$this->_data[$mbox]; 138 $val['slicemap'] = true; 139 140 foreach (array_keys(array_flip($val['slice'])) as $slice) { 141 $data = []; 142 foreach (array_keys($s['s'], $slice) as $uid) { 143 $data[$uid] = is_array($d[$uid]) 144 ? serialize($d[$uid]) 145 : $d[$uid]; 146 } 147 $this->_cache->set($this->_getCid($mbox, $slice), serialize($data), $lifetime); 148 } 149 } 150 151 if (!empty($val['slicemap'])) { 152 $this->_cache->set($this->_getCid($mbox, 'slicemap'), serialize($s), $lifetime); 153 } 154 } 155 156 $this->_update = []; 157 } 158 159 /** {@inheritDoc} */ 160 public function get($mailbox, $uids, $fields, $uidvalid) { 161 $ret = []; 162 $this->_loadUids($mailbox, $uids, $uidvalid); 163 164 if (empty($this->_data[$mailbox])) { 165 return $ret; 166 } 167 168 if (!empty($fields)) { 169 $fields = array_flip($fields); 170 } 171 $ptr = &$this->_data[$mailbox]; 172 173 foreach (array_intersect($uids, array_keys($ptr)) as $val) { 174 if (is_string($ptr[$val])) { 175 try { 176 $ptr[$val] = @unserialize($ptr[$val]); 177 } catch (Exception $e) { 178 } 179 } 180 181 $ret[$val] = (empty($fields) || empty($ptr[$val])) 182 ? $ptr[$val] 183 : array_intersect_key($ptr[$val], $fields); 184 } 185 186 return $ret; 187 } 188 189 /** {@inheritDoc} */ 190 public function getCachedUids($mailbox, $uidvalid) { 191 $this->_loadSliceMap($mailbox, $uidvalid); 192 return array_unique(array_merge( 193 array_keys($this->_slicemap[$mailbox]['s']), 194 (isset($this->_update[$mailbox]) ? $this->_update[$mailbox]['add'] : []) 195 )); 196 } 197 198 /** 199 * {@inheritDoc} 200 * 201 * @return void 202 */ 203 public function set($mailbox, $data, $uidvalid) { 204 $update = array_keys($data); 205 206 try { 207 $this->_loadUids($mailbox, $update, $uidvalid); 208 } catch (Horde_Imap_Client_Exception $e) { 209 // Ignore invalidity - just start building the new cache 210 } 211 212 $d = &$this->_data[$mailbox]; 213 $s = &$this->_slicemap[$mailbox]['s']; 214 $add = $updated = []; 215 216 foreach ($data as $k => $v) { 217 if (isset($d[$k])) { 218 if (is_string($d[$k])) { 219 try { 220 $d[$k] = @unserialize($d[$k]); 221 } catch (Exception $e) { 222 } 223 } 224 $d[$k] = is_array($d[$k]) 225 ? array_merge($d[$k], $v) 226 : $v; 227 if (isset($s[$k])) { 228 $updated[$s[$k]] = true; 229 } 230 } else { 231 $d[$k] = $v; 232 $add[] = $k; 233 } 234 } 235 236 $this->_toUpdate($mailbox, 'add', $add); 237 $this->_toUpdate($mailbox, 'slice', array_keys($updated)); 238 } 239 240 /** {@inheritDoc} */ 241 public function getMetaData($mailbox, $uidvalid, $entries) { 242 $this->_loadSliceMap($mailbox, $uidvalid); 243 244 return empty($entries) 245 ? $this->_slicemap[$mailbox]['d'] 246 : array_intersect_key($this->_slicemap[$mailbox]['d'], array_flip($entries)); 247 } 248 249 /** 250 * {@inheritDoc} 251 * 252 * @return void 253 */ 254 public function setMetaData($mailbox, $data) { 255 $this->_loadSliceMap($mailbox, isset($data['uidvalid']) ? $data['uidvalid'] : null); 256 $this->_slicemap[$mailbox]['d'] = array_merge($this->_slicemap[$mailbox]['d'], $data); 257 $this->_toUpdate($mailbox, 'slicemap', true); 258 } 259 260 /** 261 * {@inheritDoc} 262 * 263 * @return void 264 */ 265 public function deleteMsgs($mailbox, $uids) { 266 $this->_loadSliceMap($mailbox); 267 268 $slicemap = &$this->_slicemap[$mailbox]; 269 $deleted = array_intersect_key($slicemap['s'], array_flip($uids)); 270 271 if (isset($this->_update[$mailbox])) { 272 $this->_update[$mailbox]['add'] = array_diff( 273 $this->_update[$mailbox]['add'], 274 $uids 275 ); 276 } 277 278 if (empty($deleted)) { 279 return; 280 } 281 282 $this->_loadUids($mailbox, array_keys($deleted)); 283 $d = &$this->_data[$mailbox]; 284 285 foreach (array_keys($deleted) as $id) { 286 unset($d[$id], $slicemap['s'][$id]); 287 } 288 289 foreach (array_unique($deleted) as $slice) { 290 /* Get rid of slice if less than 10% of capacity. */ 291 if (($slice !== $slicemap['i']) && 292 ($slice_uids = array_keys($slicemap['s'], $slice)) && 293 ($this->_params['slicesize'] * 0.1) > count($slice_uids)) { 294 $this->_toUpdate($mailbox, 'add', $slice_uids); 295 $this->_cache->remove($this->_getCid($mailbox, $slice)); 296 foreach ($slice_uids as $val) { 297 unset($slicemap['s'][$val]); 298 } 299 } else { 300 $this->_toUpdate($mailbox, 'slice', [$slice]); 301 } 302 } 303 } 304 305 /** 306 * {@inheritDoc} 307 * 308 * @return void 309 */ 310 public function deleteMailbox($mailbox) { 311 $this->_loadSliceMap($mailbox); 312 $this->_deleteMailbox($mailbox); 313 } 314 315 /** 316 * {@inheritDoc} 317 * 318 * @return void 319 */ 320 public function clear($lifetime) { 321 $this->_cache->clear(); 322 $this->_data = $this->_loaded = $this->_slicemap = $this->_update = []; 323 } 324 325 /** 326 * Create the unique ID used to store the data in the cache. 327 * 328 * @param string $mailbox The mailbox to cache. 329 * @param string $slice The cache slice. 330 * 331 * @return string The cache ID. 332 */ 333 protected function _getCid($mailbox, $slice) { 334 return implode('|', [ 335 'horde_imap_client', 336 $this->_params['username'], 337 $mailbox, 338 $this->_params['hostspec'], 339 $this->_params['port'], 340 $slice, 341 self::VERSION 342 ]); 343 } 344 345 /** 346 * Delete a mailbox from the cache. 347 * 348 * @param string $mbox The mailbox to delete. 349 * 350 * @return void 351 */ 352 protected function _deleteMailbox($mbox): void { 353 foreach (array_merge(array_keys(array_flip($this->_slicemap[$mbox]['s'])), ['slicemap']) as $slice) { 354 $cid = $this->_getCid($mbox, $slice); 355 $this->_cache->remove($cid); 356 unset($this->_loaded[$cid]); 357 } 358 359 unset( 360 $this->_data[$mbox], 361 $this->_slicemap[$mbox], 362 $this->_update[$mbox] 363 ); 364 } 365 366 /** 367 * Load UIDs by regenerating from the cache. 368 * 369 * @param string $mailbox The mailbox to load. 370 * @param array $uids The UIDs to load. 371 * @param integer $uidvalid The IMAP uidvalidity value of the mailbox. 372 * 373 * @return void 374 */ 375 protected function _loadUids($mailbox, $uids, $uidvalid = null): void { 376 if (!isset($this->_data[$mailbox])) { 377 $this->_data[$mailbox] = []; 378 } 379 380 $this->_loadSliceMap($mailbox, $uidvalid); 381 382 if (!empty($uids)) { 383 foreach (array_unique(array_intersect_key($this->_slicemap[$mailbox]['s'], array_flip($uids))) as $slice) { 384 $this->_loadSlice($mailbox, $slice); 385 } 386 } 387 } 388 389 /** 390 * Load UIDs from a cache slice. 391 * 392 * @param string $mailbox The mailbox to load. 393 * @param integer $slice The slice to load. 394 * 395 * @return void 396 */ 397 protected function _loadSlice($mailbox, $slice) { 398 $cache_id = $this->_getCid($mailbox, $slice); 399 400 if (!empty($this->_loaded[$cache_id])) { 401 return; 402 } 403 404 if (($data = $this->_cache->get($cache_id)) !== false) { 405 try { 406 if (is_string($data)) { 407 $data = @unserialize($data); 408 } 409 } catch (Exception $e) { 410 } 411 } 412 413 if (($data !== false) && is_array($data)) { 414 $this->_data[$mailbox] += $data; 415 $this->_loaded[$cache_id] = true; 416 } else { 417 $ptr = &$this->_slicemap[$mailbox]; 418 419 // Slice data is corrupt; remove from slicemap. 420 foreach (array_keys($ptr['s'], $slice) as $val) { 421 unset($ptr['s'][$val]); 422 } 423 424 if ($slice === $ptr['i']) { 425 $ptr['c'] = 0; 426 } 427 } 428 } 429 430 /** 431 * Load the slicemap for a given mailbox. The slicemap contains 432 * the uidvalidity information, the UIDs->slice lookup table, and any 433 * metadata that needs to be saved for the mailbox. 434 * 435 * @param string $mailbox The mailbox. 436 * @param integer $uidvalid The IMAP uidvalidity value of the mailbox. 437 * 438 * @return void 439 */ 440 protected function _loadSliceMap($mailbox, $uidvalid = null) { 441 if (!isset($this->_slicemap[$mailbox]) && 442 (($data = $this->_cache->get($this->_getCid($mailbox, 'slicemap'))) !== false)) { 443 try { 444 if (is_string($data) && 445 ($slice = @unserialize($data)) && 446 is_array($slice)) { 447 $this->_slicemap[$mailbox] = $slice; 448 } 449 } catch (Exception $e) { 450 } 451 } 452 453 if (isset($this->_slicemap[$mailbox])) { 454 $ptr = &$this->_slicemap[$mailbox]; 455 if (is_null($ptr['d']['uidvalid'])) { 456 $ptr['d']['uidvalid'] = $uidvalid; 457 return; 458 } elseif (!is_null($uidvalid) && 459 ($ptr['d']['uidvalid'] !== $uidvalid)) { 460 $this->_deleteMailbox($mailbox); 461 } else { 462 return; 463 } 464 } 465 466 $this->_slicemap[$mailbox] = [ 467 // Tracking count for purposes of determining slices 468 'c' => 0, 469 // Metadata storage 470 // By default includes UIDVALIDITY of mailbox. 471 'd' => ['uidvalid' => $uidvalid], 472 // The ID of the last slice. 473 'i' => 0, 474 // The slice list. 475 's' => [] 476 ]; 477 } 478 479 /** 480 * Add update entry for a mailbox. 481 * 482 * @param string $mailbox The mailbox. 483 * @param string $type 'add', 'slice', or 'slicemap'. 484 * @param mixed $data The data to update. 485 * 486 * @return void 487 */ 488 protected function _toUpdate($mailbox, $type, $data): void { 489 if (!isset($this->_update[$mailbox])) { 490 $this->_update[$mailbox] = [ 491 'add' => [], 492 'slice' => [] 493 ]; 494 } 495 496 $this->_update[$mailbox][$type] = ($type === 'slicemap') 497 ? $data 498 : array_merge($this->_update[$mailbox][$type], $data); 499 } 500 501 /* Serializable methods. */ 502 503 /** 504 */ 505 public function serialize() { 506 $this->save(); 507 return parent::serialize(); 508 } 509} 510