507 lines
14 KiB
PHP
507 lines
14 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* Copyright 2005-2017 Horde LLC (http://www.horde.org/)
|
||
|
*
|
||
|
* See the enclosed file COPYING for license information (LGPL). If you
|
||
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
||
|
*
|
||
|
* @category Horde
|
||
|
* @copyright 2005-2017 Horde LLC
|
||
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
||
|
* @package Imap_Client
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* A Horde_Cache implementation for caching IMAP/POP data.
|
||
|
* Requires the Horde_Cache package.
|
||
|
*
|
||
|
* @author Michael Slusarz <slusarz@horde.org>
|
||
|
* @category Horde
|
||
|
* @copyright 2005-2017 Horde LLC
|
||
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
||
|
* @package Imap_Client
|
||
|
*/
|
||
|
class Horde_Imap_Client_Cache_Backend_Cache
|
||
|
extends Horde_Imap_Client_Cache_Backend
|
||
|
{
|
||
|
/** Cache structure version. */
|
||
|
const VERSION = 3;
|
||
|
|
||
|
/**
|
||
|
* The cache object.
|
||
|
*
|
||
|
* @var Horde_Cache
|
||
|
*/
|
||
|
protected $_cache;
|
||
|
|
||
|
/**
|
||
|
* The working data for the current pageload. All changes take place to
|
||
|
* this data.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $_data = array();
|
||
|
|
||
|
/**
|
||
|
* The list of cache slices loaded.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $_loaded = array();
|
||
|
|
||
|
/**
|
||
|
* The mapping of UIDs to slices.
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $_slicemap = array();
|
||
|
|
||
|
/**
|
||
|
* The list of items to update:
|
||
|
* - add: (array) List of IDs that were added.
|
||
|
* - slice: (array) List of slices that were modified.
|
||
|
* - slicemap: (boolean) Was slicemap info changed?
|
||
|
*
|
||
|
* @var array
|
||
|
*/
|
||
|
protected $_update = array();
|
||
|
|
||
|
/**
|
||
|
* Constructor.
|
||
|
*
|
||
|
* @param array $params Configuration parameters:
|
||
|
* <pre>
|
||
|
* - REQUIRED Parameters:
|
||
|
* - cacheob: (Horde_Cache) The cache object to use.
|
||
|
*
|
||
|
* - Optional Parameters:
|
||
|
* - lifetime: (integer) The lifetime of the cache data (in seconds).
|
||
|
* DEFAULT: 1 week (604800 seconds)
|
||
|
* - slicesize: (integer) The slicesize to use.
|
||
|
* DEFAULT: 50
|
||
|
* </pre>
|
||
|
*/
|
||
|
public function __construct(array $params = array())
|
||
|
{
|
||
|
// Default parameters.
|
||
|
$params = array_merge(array(
|
||
|
'lifetime' => 604800,
|
||
|
'slicesize' => 50
|
||
|
), array_filter($params));
|
||
|
|
||
|
if (!isset($params['cacheob'])) {
|
||
|
throw new InvalidArgumentException('Missing cacheob parameter.');
|
||
|
}
|
||
|
|
||
|
foreach (array('lifetime', 'slicesize') as $val) {
|
||
|
$params[$val] = intval($params[$val]);
|
||
|
}
|
||
|
|
||
|
parent::__construct($params);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initialization tasks.
|
||
|
*/
|
||
|
protected function _initOb()
|
||
|
{
|
||
|
$this->_cache = $this->_params['cacheob'];
|
||
|
register_shutdown_function(array($this, 'save'));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Updates the cache.
|
||
|
*/
|
||
|
public function save()
|
||
|
{
|
||
|
$lifetime = $this->_params['lifetime'];
|
||
|
|
||
|
foreach ($this->_update as $mbox => $val) {
|
||
|
$s = &$this->_slicemap[$mbox];
|
||
|
|
||
|
try {
|
||
|
if (!empty($val['add'])) {
|
||
|
if ($s['c'] <= $this->_params['slicesize']) {
|
||
|
$val['slice'][] = $s['i'];
|
||
|
$this->_loadSlice($mbox, $s['i']);
|
||
|
}
|
||
|
$val['slicemap'] = true;
|
||
|
|
||
|
foreach (array_keys(array_flip($val['add'])) as $uid) {
|
||
|
if ($s['c']++ > $this->_params['slicesize']) {
|
||
|
$s['c'] = 0;
|
||
|
$val['slice'][] = ++$s['i'];
|
||
|
$this->_loadSlice($mbox, $s['i']);
|
||
|
}
|
||
|
$s['s'][$uid] = $s['i'];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!empty($val['slice'])) {
|
||
|
$d = &$this->_data[$mbox];
|
||
|
$val['slicemap'] = true;
|
||
|
|
||
|
foreach (array_keys(array_flip($val['slice'])) as $slice) {
|
||
|
$data = array();
|
||
|
foreach (array_keys($s['s'], $slice) as $uid) {
|
||
|
$data[$uid] = is_array($d[$uid])
|
||
|
? serialize($d[$uid])
|
||
|
: $d[$uid];
|
||
|
}
|
||
|
$this->_cache->set($this->_getCid($mbox, $slice), serialize($data), $lifetime);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!empty($val['slicemap'])) {
|
||
|
$this->_cache->set($this->_getCid($mbox, 'slicemap'), serialize($s), $lifetime);
|
||
|
}
|
||
|
} catch (Horde_Exception $e) {
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->_update = array();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*/
|
||
|
public function get($mailbox, $uids, $fields, $uidvalid)
|
||
|
{
|
||
|
$ret = array();
|
||
|
$this->_loadUids($mailbox, $uids, $uidvalid);
|
||
|
|
||
|
if (empty($this->_data[$mailbox])) {
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
if (!empty($fields)) {
|
||
|
$fields = array_flip($fields);
|
||
|
}
|
||
|
$ptr = &$this->_data[$mailbox];
|
||
|
|
||
|
foreach (array_intersect($uids, array_keys($ptr)) as $val) {
|
||
|
if (is_string($ptr[$val])) {
|
||
|
try {
|
||
|
$ptr[$val] = @unserialize($ptr[$val]);
|
||
|
} catch (Exception $e) {}
|
||
|
}
|
||
|
|
||
|
$ret[$val] = (empty($fields) || empty($ptr[$val]))
|
||
|
? $ptr[$val]
|
||
|
: array_intersect_key($ptr[$val], $fields);
|
||
|
}
|
||
|
|
||
|
return $ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*/
|
||
|
public function getCachedUids($mailbox, $uidvalid)
|
||
|
{
|
||
|
$this->_loadSliceMap($mailbox, $uidvalid);
|
||
|
return array_unique(array_merge(
|
||
|
array_keys($this->_slicemap[$mailbox]['s']),
|
||
|
(isset($this->_update[$mailbox]) ? $this->_update[$mailbox]['add'] : array())
|
||
|
));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*/
|
||
|
public function set($mailbox, $data, $uidvalid)
|
||
|
{
|
||
|
$update = array_keys($data);
|
||
|
|
||
|
try {
|
||
|
$this->_loadUids($mailbox, $update, $uidvalid);
|
||
|
} catch (Horde_Imap_Client_Exception $e) {
|
||
|
// Ignore invalidity - just start building the new cache
|
||
|
}
|
||
|
|
||
|
$d = &$this->_data[$mailbox];
|
||
|
$s = &$this->_slicemap[$mailbox]['s'];
|
||
|
$add = $updated = array();
|
||
|
|
||
|
foreach ($data as $k => $v) {
|
||
|
if (isset($d[$k])) {
|
||
|
if (is_string($d[$k])) {
|
||
|
try {
|
||
|
$d[$k] = @unserialize($d[$k]);
|
||
|
} catch (Exception $e) {}
|
||
|
}
|
||
|
$d[$k] = is_array($d[$k])
|
||
|
? array_merge($d[$k], $v)
|
||
|
: $v;
|
||
|
if (isset($s[$k])) {
|
||
|
$updated[$s[$k]] = true;
|
||
|
}
|
||
|
} else {
|
||
|
$d[$k] = $v;
|
||
|
$add[] = $k;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->_toUpdate($mailbox, 'add', $add);
|
||
|
$this->_toUpdate($mailbox, 'slice', array_keys($updated));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*/
|
||
|
public function getMetaData($mailbox, $uidvalid, $entries)
|
||
|
{
|
||
|
$this->_loadSliceMap($mailbox, $uidvalid);
|
||
|
|
||
|
return empty($entries)
|
||
|
? $this->_slicemap[$mailbox]['d']
|
||
|
: array_intersect_key($this->_slicemap[$mailbox]['d'], array_flip($entries));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*/
|
||
|
public function setMetaData($mailbox, $data)
|
||
|
{
|
||
|
$this->_loadSliceMap($mailbox, isset($data['uidvalid']) ? $data['uidvalid'] : null);
|
||
|
$this->_slicemap[$mailbox]['d'] = array_merge($this->_slicemap[$mailbox]['d'], $data);
|
||
|
$this->_toUpdate($mailbox, 'slicemap', true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*/
|
||
|
public function deleteMsgs($mailbox, $uids)
|
||
|
{
|
||
|
if (empty($uids)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$this->_loadSliceMap($mailbox);
|
||
|
|
||
|
$slicemap = &$this->_slicemap[$mailbox];
|
||
|
$deleted = array_intersect_key($slicemap['s'], array_flip($uids));
|
||
|
|
||
|
if (isset($this->_update[$mailbox])) {
|
||
|
$this->_update[$mailbox]['add'] = array_diff(
|
||
|
$this->_update[$mailbox]['add'],
|
||
|
$uids
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (empty($deleted)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
$this->_loadUids($mailbox, array_keys($deleted));
|
||
|
$d = &$this->_data[$mailbox];
|
||
|
|
||
|
foreach (array_keys($deleted) as $id) {
|
||
|
unset($d[$id], $slicemap['s'][$id]);
|
||
|
}
|
||
|
|
||
|
foreach (array_unique($deleted) as $slice) {
|
||
|
/* Get rid of slice if less than 10% of capacity. */
|
||
|
if (($slice != $slicemap['i']) &&
|
||
|
($slice_uids = array_keys($slicemap['s'], $slice)) &&
|
||
|
($this->_params['slicesize'] * 0.1) > count($slice_uids)) {
|
||
|
$this->_toUpdate($mailbox, 'add', $slice_uids);
|
||
|
$this->_cache->expire($this->_getCid($mailbox, $slice));
|
||
|
foreach ($slice_uids as $val) {
|
||
|
unset($slicemap['s'][$val]);
|
||
|
}
|
||
|
} else {
|
||
|
$this->_toUpdate($mailbox, 'slice', array($slice));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*/
|
||
|
public function deleteMailbox($mailbox)
|
||
|
{
|
||
|
$this->_loadSliceMap($mailbox);
|
||
|
$this->_deleteMailbox($mailbox);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
*/
|
||
|
public function clear($lifetime)
|
||
|
{
|
||
|
$this->_cache->clear();
|
||
|
$this->_data = $this->_loaded = $this->_slicemap = $this->_update = array();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create the unique ID used to store the data in the cache.
|
||
|
*
|
||
|
* @param string $mailbox The mailbox to cache.
|
||
|
* @param string $slice The cache slice.
|
||
|
*
|
||
|
* @return string The cache ID.
|
||
|
*/
|
||
|
protected function _getCid($mailbox, $slice)
|
||
|
{
|
||
|
return implode('|', array(
|
||
|
'horde_imap_client',
|
||
|
$this->_params['username'],
|
||
|
$mailbox,
|
||
|
$this->_params['hostspec'],
|
||
|
$this->_params['port'],
|
||
|
$slice,
|
||
|
self::VERSION
|
||
|
));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Delete a mailbox from the cache.
|
||
|
*
|
||
|
* @param string $mbox The mailbox to delete.
|
||
|
*/
|
||
|
protected function _deleteMailbox($mbox)
|
||
|
{
|
||
|
foreach (array_merge(array_keys(array_flip($this->_slicemap[$mbox]['s'])), array('slicemap')) as $slice) {
|
||
|
$cid = $this->_getCid($mbox, $slice);
|
||
|
$this->_cache->expire($cid);
|
||
|
unset($this->_loaded[$cid]);
|
||
|
}
|
||
|
|
||
|
unset(
|
||
|
$this->_data[$mbox],
|
||
|
$this->_slicemap[$mbox],
|
||
|
$this->_update[$mbox]
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load UIDs by regenerating from the cache.
|
||
|
*
|
||
|
* @param string $mailbox The mailbox to load.
|
||
|
* @param array $uids The UIDs to load.
|
||
|
* @param integer $uidvalid The IMAP uidvalidity value of the mailbox.
|
||
|
*/
|
||
|
protected function _loadUids($mailbox, $uids, $uidvalid = null)
|
||
|
{
|
||
|
if (!isset($this->_data[$mailbox])) {
|
||
|
$this->_data[$mailbox] = array();
|
||
|
}
|
||
|
|
||
|
$this->_loadSliceMap($mailbox, $uidvalid);
|
||
|
|
||
|
if (!empty($uids)) {
|
||
|
foreach (array_unique(array_intersect_key($this->_slicemap[$mailbox]['s'], array_flip($uids))) as $slice) {
|
||
|
$this->_loadSlice($mailbox, $slice);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load UIDs from a cache slice.
|
||
|
*
|
||
|
* @param string $mailbox The mailbox to load.
|
||
|
* @param integer $slice The slice to load.
|
||
|
*/
|
||
|
protected function _loadSlice($mailbox, $slice)
|
||
|
{
|
||
|
$cache_id = $this->_getCid($mailbox, $slice);
|
||
|
|
||
|
if (!empty($this->_loaded[$cache_id])) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (($data = $this->_cache->get($cache_id, 0)) !== false) {
|
||
|
try {
|
||
|
$data = @unserialize($data);
|
||
|
} catch (Exception $e) {}
|
||
|
}
|
||
|
|
||
|
if (($data !== false) && is_array($data)) {
|
||
|
$this->_data[$mailbox] += $data;
|
||
|
$this->_loaded[$cache_id] = true;
|
||
|
} else {
|
||
|
$ptr = &$this->_slicemap[$mailbox];
|
||
|
|
||
|
// Slice data is corrupt; remove from slicemap.
|
||
|
foreach (array_keys($ptr['s'], $slice) as $val) {
|
||
|
unset($ptr['s'][$val]);
|
||
|
}
|
||
|
|
||
|
if ($slice == $ptr['i']) {
|
||
|
$ptr['c'] = 0;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load the slicemap for a given mailbox. The slicemap contains
|
||
|
* the uidvalidity information, the UIDs->slice lookup table, and any
|
||
|
* metadata that needs to be saved for the mailbox.
|
||
|
*
|
||
|
* @param string $mailbox The mailbox.
|
||
|
* @param integer $uidvalid The IMAP uidvalidity value of the mailbox.
|
||
|
*/
|
||
|
protected function _loadSliceMap($mailbox, $uidvalid = null)
|
||
|
{
|
||
|
if (!isset($this->_slicemap[$mailbox]) &&
|
||
|
(($data = $this->_cache->get($this->_getCid($mailbox, 'slicemap'), 0)) !== false)) {
|
||
|
try {
|
||
|
if (($slice = @unserialize($data)) &&
|
||
|
is_array($slice)) {
|
||
|
$this->_slicemap[$mailbox] = $slice;
|
||
|
}
|
||
|
} catch (Exception $e) {}
|
||
|
}
|
||
|
|
||
|
if (isset($this->_slicemap[$mailbox])) {
|
||
|
$ptr = &$this->_slicemap[$mailbox];
|
||
|
if (is_null($ptr['d']['uidvalid'])) {
|
||
|
$ptr['d']['uidvalid'] = $uidvalid;
|
||
|
return;
|
||
|
} elseif (!is_null($uidvalid) &&
|
||
|
($ptr['d']['uidvalid'] != $uidvalid)) {
|
||
|
$this->_deleteMailbox($mailbox);
|
||
|
} else {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->_slicemap[$mailbox] = array(
|
||
|
// Tracking count for purposes of determining slices
|
||
|
'c' => 0,
|
||
|
// Metadata storage
|
||
|
// By default includes UIDVALIDITY of mailbox.
|
||
|
'd' => array('uidvalid' => $uidvalid),
|
||
|
// The ID of the last slice.
|
||
|
'i' => 0,
|
||
|
// The slice list.
|
||
|
's' => array()
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add update entry for a mailbox.
|
||
|
*
|
||
|
* @param string $mailbox The mailbox.
|
||
|
* @param string $type 'add', 'slice', or 'slicemap'.
|
||
|
* @param mixed $data The data to update.
|
||
|
*/
|
||
|
protected function _toUpdate($mailbox, $type, $data)
|
||
|
{
|
||
|
if (!isset($this->_update[$mailbox])) {
|
||
|
$this->_update[$mailbox] = array(
|
||
|
'add' => array(),
|
||
|
'slice' => array()
|
||
|
);
|
||
|
}
|
||
|
|
||
|
$this->_update[$mailbox][$type] = ($type == 'slicemap')
|
||
|
? $data
|
||
|
: array_merge($this->_update[$mailbox][$type], $data);
|
||
|
}
|
||
|
|
||
|
/* Serializable methods. */
|
||
|
|
||
|
/**
|
||
|
*/
|
||
|
public function serialize()
|
||
|
{
|
||
|
$this->save();
|
||
|
return parent::serialize();
|
||
|
}
|
||
|
|
||
|
}
|