1<?php
2/**
3 * Copyright 2013-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file LICENSE for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @category  Horde
9 * @copyright 2013-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 * @package   Imap_Client
12 */
13
14/**
15 * A MongoDB database implementation for caching IMAP/POP data.
16 *
17 * Requires the Horde_Mongo class.
18 *
19 * @author    Michael Slusarz <slusarz@horde.org>
20 * @category  Horde
21 * @copyright 2013-2017 Horde LLC
22 * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
23 * @package   Imap_Client
24 */
25class Horde_Imap_Client_Cache_Backend_Mongo
26extends Horde_Imap_Client_Cache_Backend
27implements Horde_Mongo_Collection_Index
28{
29    /** Mongo collection names. */
30    const BASE = 'horde_imap_client_cache_data';
31    const MD = 'horde_imap_client_cache_metadata';
32    const MSG = 'horde_imap_client_cache_message';
33
34    /** Mongo field names: BASE collection. */
35    const BASE_HOSTSPEC = 'hostspec';
36    const BASE_MAILBOX = 'mailbox';
37    const BASE_MODIFIED = 'modified';
38    const BASE_PORT = 'port';
39    const BASE_UID = 'data';
40    const BASE_USERNAME = 'username';
41
42    /** Mongo field names: MD collection. */
43    const MD_DATA = 'data';
44    const MD_FIELD = 'field';
45    const MD_UID = 'uid';
46
47    /** Mongo field names: MSG collection. */
48    const MSG_DATA = 'data';
49    const MSG_MSGUID = 'msguid';
50    const MSG_UID = 'uid';
51
52    /**
53     * The MongoDB object for the cache data.
54     *
55     * @var MongoDB
56     */
57    protected $_db;
58
59    /**
60     * The list of indices.
61     *
62     * @var array
63     */
64    protected $_indices = array(
65        self::BASE => array(
66            'base_index_1' => array(
67                self::BASE_HOSTSPEC => 1,
68                self::BASE_MAILBOX => 1,
69                self::BASE_PORT => 1,
70                self::BASE_USERNAME => 1,
71            )
72        ),
73        self::MSG => array(
74            'msg_index_1' => array(
75                self::MSG_MSGUID => 1,
76                self::MSG_UID => 1
77            )
78        )
79    );
80
81    /**
82     * Constructor.
83     *
84     * @param array $params  Configuration parameters:
85     * <pre>
86     *   - REQUIRED parameters:
87     *     - mongo_db: (Horde_Mongo_Client) A MongoDB client object.
88     * </pre>
89     */
90    public function __construct(array $params = array())
91    {
92        if (!isset($params['mongo_db'])) {
93            throw new InvalidArgumentException('Missing mongo_db parameter.');
94        }
95
96        parent::__construct($params);
97    }
98
99    /**
100     */
101    protected function _initOb()
102    {
103        $this->_db = $this->_params['mongo_db']->selectDB(null);
104    }
105
106    /**
107     */
108    public function get($mailbox, $uids, $fields, $uidvalid)
109    {
110        $this->getMetaData($mailbox, $uidvalid, array('uidvalid'));
111
112        if (!($uid = $this->_getUid($mailbox))) {
113            return array();
114        }
115
116        $out = array();
117        $query = array(
118            self::MSG_MSGUID => array('$in' => array_map('strval', $uids)),
119            self::MSG_UID => $uid
120        );
121
122        try {
123            $cursor = $this->_db->selectCollection(self::MSG)->find(
124                $query,
125                array(self::MSG_DATA => true, self::MSG_MSGUID => true)
126            );
127            foreach ($cursor as $val) {
128                try {
129                    $out[$val[self::MSG_MSGUID]] = $this->_value($val[self::MSG_DATA]);
130                } catch (Exception $e) {}
131            }
132        } catch (MongoException $e) {}
133
134        return $out;
135    }
136
137    /**
138     */
139    public function getCachedUids($mailbox, $uidvalid)
140    {
141        $this->getMetaData($mailbox, $uidvalid, array('uidvalid'));
142
143        if (!($uid = $this->_getUid($mailbox))) {
144            return array();
145        }
146
147        $out = array();
148        $query = array(
149            self::MSG_UID => $uid
150        );
151
152        try {
153            $cursor = $this->_db->selectCollection(self::MSG)->find(
154                $query, array(self::MSG_MSGUID => true)
155            );
156            foreach ($cursor as $val) {
157                $out[] = $val[self::MSG_MSGUID];
158            }
159        } catch (MongoException $e) {}
160
161        return $out;
162    }
163
164    /**
165     */
166    public function set($mailbox, $data, $uidvalid)
167    {
168        if ($uid = $this->_getUid($mailbox)) {
169            $res = $this->get($mailbox, array_keys($data), array(), $uidvalid);
170        } else {
171            $res = array();
172            $uid = $this->_createUid($mailbox);
173        }
174
175        $coll = $this->_db->selectCollection(self::MSG);
176
177        foreach ($data as $key => $val) {
178            try {
179                if (isset($res[$key])) {
180                    $coll->update(array(
181                        self::MSG_MSGUID => strval($key),
182                        self::MSG_UID => $uid
183                    ), array(
184                        self::MSG_DATA => $this->_value(array_merge($res[$key], $val)),
185                        self::MSG_MSGUID => strval($key),
186                        self::MSG_UID => $uid
187                    ));
188                } else {
189                    $doc = array(
190                        self::MSG_DATA => $this->_value($val),
191                        self::MSG_MSGUID => strval($key),
192                        self::MSG_UID => $uid
193                    );
194                    $coll->insert($doc);
195                }
196            } catch (MongoException $e) {}
197        }
198
199        /* Update modified time. */
200        try {
201            $this->_db->selectCollection(self::BASE)->update(
202                array(self::BASE_UID => $uid),
203                array(self::BASE_MODIFIED => time())
204            );
205        } catch (MongoException $e) {}
206
207        /* Update uidvalidity. */
208        $this->setMetaData($mailbox, array('uidvalid' => $uidvalid));
209    }
210
211    /**
212     */
213    public function getMetaData($mailbox, $uidvalid, $entries)
214    {
215        if (!($uid = $this->_getUid($mailbox))) {
216            return array();
217        }
218
219        $out = array();
220        $query = array(
221            self::MD_UID => $uid
222        );
223
224        if (!empty($entries)) {
225            $entries[] = 'uidvalid';
226            $query[self::MD_FIELD] = array(
227                '$in' => array_unique($entries)
228            );
229        }
230
231        try {
232            $cursor = $this->_db->selectCollection(self::MD)->find(
233                $query,
234                array(self::MD_DATA => true, self::MD_FIELD => true)
235            );
236            foreach ($cursor as $val) {
237                try {
238                    $out[$val[self::MD_FIELD]] = $this->_value($val[self::MD_DATA]);
239                } catch (Exception $e) {}
240            }
241
242            if (is_null($uidvalid) ||
243                !isset($out['uidvalid']) ||
244                ($out['uidvalid'] == $uidvalid)) {
245                return $out;
246            }
247
248            $this->deleteMailbox($mailbox);
249        } catch (MongoException $e) {}
250
251        return array();
252    }
253
254    /**
255     */
256    public function setMetaData($mailbox, $data)
257    {
258        if (!($uid = $this->_getUid($mailbox))) {
259            $uid = $this->_createUid($mailbox);
260        }
261
262        $coll = $this->_db->selectCollection(self::MD);
263
264        foreach ($data as $key => $val) {
265            try {
266                $coll->update(
267                    array(
268                        self::MD_FIELD => $key,
269                        self::MD_UID => $uid
270                    ),
271                    array(
272                        self::MD_DATA => $this->_value($val),
273                        self::MD_FIELD => $key,
274                        self::MD_UID => $uid
275                    ),
276                    array('upsert' => true)
277                );
278            } catch (MongoException $e) {}
279        }
280    }
281
282    /**
283     */
284    public function deleteMsgs($mailbox, $uids)
285    {
286        if (!empty($uids) && ($uid = $this->_getUid($mailbox))) {
287            try {
288                $this->_db->selectCollection(self::MSG)->remove(array(
289                    self::MSG_MSGUID => array(
290                        '$in' => array_map('strval', $uids)
291                    ),
292                    self::MSG_UID => $uid
293                ));
294            } catch (MongoException $e) {}
295        }
296    }
297
298    /**
299     */
300    public function deleteMailbox($mailbox)
301    {
302        if (!($uid = $this->_getUid($mailbox))) {
303            return;
304        }
305
306        foreach (array(self::BASE, self::MD, self::MSG) as $val) {
307            try {
308                $this->_db->selectCollection($val)
309                    ->remove(array('uid' => $uid));
310            } catch (MongoException $e) {}
311        }
312    }
313
314    /**
315     */
316    public function clear($lifetime)
317    {
318        if (is_null($lifetime)) {
319            foreach (array(self::BASE, self::MD, self::MSG) as $val) {
320                $this->_db->selectCollection($val)->drop();
321            }
322            return;
323        }
324
325        $query = array(
326            self::BASE_MODIFIED => array('$lt' => (time() - $lifetime))
327        );
328        $uids = array();
329
330        try {
331            $cursor = $this->_db->selectCollection(self::BASE)->find($query);
332            foreach ($cursor as $val) {
333                $uids[] = strval($val['_id']);
334            }
335        } catch (MongoException $e) {}
336
337        if (empty($uids)) {
338            return;
339        }
340
341        foreach (array(self::BASE, self::MD, self::MSG) as $val) {
342            try {
343                $this->_db->selectCollection($val)
344                    ->remove(array('uid' => array('$in' => $uids)));
345            } catch (MongoException $e) {}
346        }
347    }
348
349    /**
350     * Return the UID for a mailbox/user/server combo.
351     *
352     * @param string $mailbox  Mailbox name.
353     *
354     * @return string  UID from base table.
355     */
356    protected function _getUid($mailbox)
357    {
358        $query = array(
359            self::BASE_HOSTSPEC => $this->_params['hostspec'],
360            self::BASE_MAILBOX => $mailbox,
361            self::BASE_PORT => $this->_params['port'],
362            self::BASE_USERNAME => $this->_params['username']
363        );
364
365        try {
366            if ($result = $this->_db->selectCollection(self::BASE)->findOne($query)) {
367                return strval($result['_id']);
368            }
369        } catch (MongoException $e) {}
370
371        return null;
372    }
373
374    /**
375     * Create and return the UID for a mailbox/user/server combo.
376     *
377     * @param string $mailbox  Mailbox name.
378     *
379     * @return string  UID from base table.
380     */
381    protected function _createUid($mailbox)
382    {
383        $doc = array(
384            self::BASE_HOSTSPEC => $this->_params['hostspec'],
385            self::BASE_MAILBOX => $mailbox,
386            self::BASE_PORT => $this->_params['port'],
387            self::BASE_USERNAME => $this->_params['username']
388        );
389        $this->_db->selectCollection(self::BASE)->insert($doc);
390
391        return $this->_getUid($mailbox);
392    }
393
394    /**
395     * Convert data from/to storage format.
396     *
397     * @param mixed|MongoBinData $data  The data object.
398     *
399     * @return mixed|MongoBinData  The converted data.
400     */
401    protected function _value($data)
402    {
403        static $compress;
404
405        if (!isset($compress)) {
406            $compress = new Horde_Compress_Fast();
407        }
408
409        return ($data instanceof MongoBinData)
410            ? @unserialize($compress->decompress($data->bin))
411            : new MongoBinData(
412                $compress->compress(serialize($data)), MongoBinData::BYTE_ARRAY
413            );
414    }
415
416    /* Horde_Mongo_Collection_Index methods. */
417
418    /**
419     */
420    public function checkMongoIndices()
421    {
422        foreach ($this->_indices as $key => $val) {
423            if (!$this->_params['mongo_db']->checkIndices($key, $val)) {
424                return false;
425            }
426        }
427
428        return true;
429    }
430
431    /**
432     */
433    public function createMongoIndices()
434    {
435        foreach ($this->_indices as $key => $val) {
436            $this->_params['mongo_db']->createIndices($key, $val);
437        }
438    }
439
440}
441