1: <?php
2:
3: /**
4: * IMAP modules
5: * @package modules
6: * @subpackage imap
7: */
8:
9: /**
10: * Cache related methods
11: * @subpackage imap/lib
12: */
13: class Hm_IMAP_Cache extends Hm_IMAP_Parser {
14:
15: /**
16: * Store the imap command used to fetch a sorted message list in absence of
17: * the SORT imap extension. This needs to be removed from cache in some cases.
18: */
19: public function set_fetch_command($newval){
20: $newval = str_replace(array("\r", "\n"), array(''), preg_replace("/^A\d+ /", '', $newval));
21: $this->cache_data['INTERNAL_CYPHT_USE']['SORT_BY_FETCH'] = $newval;
22: }
23: public function get_fetch_command(){
24: if (isset($this->cache_data['INTERNAL_CYPHT_USE'])){
25: if (isset($this->cache_data['INTERNAL_CYPHT_USE']['SORT_BY_FETCH'])){
26: return $this->cache_data['INTERNAL_CYPHT_USE']['SORT_BY_FETCH'];
27: }
28: }
29: return false;
30: }
31:
32: /**
33: * update the cache untagged QRESYNC FETCH responses
34: * @param array $data low level parsed IMAP response segment
35: * @return int 1 if the cache was updated
36: */
37: protected function update_cache_data($data) {
38: if (!$this->use_cache) {
39: return 0;
40: }
41: $res = 0;
42: if (in_array('VANISHED', $data)) {
43: $uid = $this->get_adjacent_response_value($data, -1, 'VANISHED');
44: if ($this->is_clean($uid, 'uid')) {
45: if (isset($this->cache_data[$this->selected_mailbox['name']])) {
46: $key = $this->selected_mailbox['name'];
47: foreach ($this->cache_data[$key] as $command => $result) {
48: if (mb_strstr($command, 'UID FETCH')) {
49: if (isset($result[$uid])) {
50: unset($this->cache_data[$key][$command][$uid]);
51: $this->debug[] = sprintf('Removed message from cache using QRESYNC response (uid: %s)', $uid);
52: $res = 1;
53: }
54: }
55: elseif (mb_strstr($command, 'UID SORT')) {
56: $index = array_search($uid, $result);
57: if ($index !== false) {
58: unset($this->cache_data[$key][$command][$index]);
59: $this->debug[] = sprintf('Removed message from cache using QRESYNC response (uid: %s)', $uid);
60: $res = 1;
61: }
62: }
63: elseif (mb_strstr($command, 'UID SEARCH')) {
64: $index = array_search($uid, $result);
65: if ($index !== false) {
66: unset($this->cache_data[$key][$command][$index]);
67: $this->debug[] = sprintf('Removed message from cache using QRESYNC response (uid: %s)', $uid);
68: $res = 1;
69: }
70: }
71: }
72: }
73: }
74: }
75: else {
76: $flags = array();
77: $uid = $this->get_adjacent_response_value($data, -1, 'UID');
78: if ($this->is_clean($uid, 'uid')) {
79: $flag_start = array_search('FLAGS', $data);
80: if ($flag_start !== false) {
81: $flags = $this->get_flag_values(array_slice($data, $flag_start));
82: }
83: }
84: if ($uid) {
85: if (isset($this->cache_data[$this->selected_mailbox['name']])) {
86: $key = $this->selected_mailbox['name'];
87: foreach ($this->cache_data[$key] as $command => $result) {
88: if (mb_strstr($command, 'UID FETCH')) {
89: if (isset($result[$uid]['flags'])) {
90: $this->cache_data[$key][$command][$uid]['flags'] = implode(' ', $flags);
91: $this->debug[] = sprintf('Updated cache data from QRESYNC response (uid: %s)', $uid);
92: $res = 1;
93: }
94: elseif (isset($result['flags'])) {
95: $this->cache_data[$key][$command]['flags'] = implode(' ', $flags);
96: $this->debug[] = sprintf('Updated cache data from QRESYNC response (uid: %s)', $uid);
97: $res = 1;
98: }
99: elseif (isset($result['Flags'])) {
100: $this->cache_data[$key][$command]['Flags'] = implode(' ', $flags);
101: $this->debug[] = sprintf('Updated cache data from QRESYNC response (uid: %s)', $uid);
102: $res = 1;
103: }
104: }
105: }
106: }
107: }
108: }
109: return $res;
110: }
111:
112: /**
113: * cache certain IMAP command return values for re-use
114: * @param array $res low level parsed IMAP response
115: * @return array initial low level parsed IMAP response argument
116: */
117: protected function cache_return_val($res, $command) {
118: if (!$this->use_cache) {
119: return $res;
120: }
121: $command = str_replace(array("\r", "\n"), array(''), preg_replace("/^A\d+ /", '', $command));
122: if (preg_match("/^SELECT/", $command)) {
123: $this->cache_data['SELECT'][$command] = $res;
124: }
125: elseif (preg_match("/^EXAMINE/", $command)) {
126: $this->cache_data['EXAMINE'][$command] = $res;
127: }
128: elseif (preg_match("/^LIST/", $command)) {
129: $this->cache_data['LIST'][$command] = $res;
130: }
131: elseif (preg_match("/^LSUB/", $command)) {
132: $this->cache_data['LSUB'][$command] = $res;
133: }
134: elseif (preg_match("/^NAMESPACE/", $command)) {
135: $this->cache_data['NAMESPACE'] = $res;
136: }
137: elseif ($this->selected_mailbox) {
138: $key = $this->selected_mailbox['name'];
139: $this->cache_data[$key][$command] = $res;
140: }
141: $count = 0;
142: foreach ($this->cache_data as $commands) {
143: $count += count($commands);
144: }
145: if ($count > $this->cache_limit) {
146: $this->prune_cache($count);
147: }
148: return $res;
149: }
150:
151: /**
152: * search for cache entries to prune
153: * @param int $count current number of cache entries
154: * @param array $exclude list of cache keys to skip
155: * @return array list of key tuples of cache entries to prune
156: */
157: protected function collect_cache_entries_to_prune($count, $exclude) {
158: $to_remove = array();
159: if ($count > $this->cache_limit) {
160: foreach ($this->cache_data as $key => $commands) {
161: if ( in_array( $key, $exclude ) ) {
162: continue;
163: }
164: foreach ($commands as $command => $value) {
165: $to_remove[] = array($key, $command);
166: $count--;
167: if ($count == $this->cache_limit) {
168: break 2;
169: }
170: }
171: }
172: }
173: return $to_remove;
174: }
175:
176: /**
177: * prune the IMAP cache if it needs it
178: * @return void
179: */
180: protected function prune_cache($count) {
181: $current_key = false;
182: $to_remove = array();
183: if (isset($this->selected_mailbox['name'])) {
184: if (isset($this->cache_data[$this->selected_mailbox['name']])) {
185: $current_key = $this->selected_mailbox['name'];
186: }
187: }
188: $to_remove = $this->collect_cache_entries_to_prune($count, array($current_key, 'LIST', 'LSUB', 'NAMESPACE' ));
189: $count -= count($to_remove);
190: if ($count > $this->cache_limit) {
191: $to_remove = $this->collect_cache_entries_to_prune($count, array($current_key));
192: $count -= count($to_remove);
193: if ($count > $this->cache_limit) {
194: $to_remove = $this->collect_cache_entries_to_prune($count, array());
195: $count -= count($to_remove);
196: }
197: }
198: if (!empty($to_remove)) {
199: foreach($to_remove as $keys) {
200: $this->debug[] = sprintf('Unset cache at (%s) for key (%s)', $keys[0], $keys[1]);
201: unset($this->cache_data[$keys[0]][$keys[1]]);
202: }
203: }
204: }
205:
206: /**
207: * determine if the current command can be served from cache
208: * @param string $command IMAP command to check
209: * @param bool $check_only flag to avoid double logging
210: * @return mixed cached result or false if there is no cached data to use
211: */
212: protected function check_cache($command, $check_only=false) {
213: if (!$this->use_cache) {
214: return false;
215: }
216: $command = str_replace(array("\r", "\n"), array(''), preg_replace("/^A\d+ /", '', $command));
217: $res = false;
218: $msg = '';
219: if (preg_match("/^SELECT/", $command) && isset($this->cache_data['SELECT'][$command])) {
220: $res = $this->cache_data['SELECT'][$command];
221: $msg = 'Found cached mailbox state: '.$command;
222: }
223: elseif (preg_match("/^EXAMINE/", $command) && isset($this->cache_data['EXAMINE'][$command])) {
224: $res = $this->cache_data['EXAMINE'][$command];
225: $msg = 'Found cached mailbox state: '.$command;
226: }
227: elseif (preg_match("/^LIST/ ", $command) && isset($this->cache_data['LIST'][$command])) {
228: $msg = 'IMAP Cache hit for: '.$command;
229: $res = $this->cache_data['LIST'][$command];
230: }
231: elseif (preg_match("/^LSUB /", $command) && isset($this->cache_data['LSUB'][$command])) {
232: $msg = 'IMAP Cache hit for: '.$command;
233: $res = $this->cache_data['LSUB'][$command];
234: }
235: elseif (preg_match("/^NAMESPACE/", $command) && isset($this->cache_data['NAMESPACE'])) {
236: $msg = 'IMAP Cache hit for: '.$command;
237: $res = $this->cache_data['NAMESPACE'];
238: }
239: elseif ($this->selected_mailbox) {
240:
241: $box = $this->selected_mailbox['name'];
242:
243: if (isset($this->cache_data[$box][$command])) {
244: $msg = 'IMAP Cache hit for: '.$box.' with: '.$command;
245: $res = $this->cache_data[$box][$command];
246: }
247: }
248: if ($msg) {
249: Hm_Debug::add($msg, 'info');
250: $this->cached_response = true;
251: $this->debug[] = $msg;
252: }
253: if ($check_only) {
254: return $res ? true : false;
255: }
256: return $res;
257: }
258:
259: /**
260: * invalidate parts of the data cache
261: * @param string $type can be one of LIST, LSUB, ALL, or a mailbox name
262: * @return void
263: */
264: public function bust_cache($type, $full=true) {
265: if (!$this->use_cache) {
266: return;
267: }
268: switch($type) {
269: case 'LIST':
270: if (isset($this->cache_data['LIST'])) {
271: unset($this->cache_data['LIST']);
272: $this->debug[] = 'cache busted: '.$type;
273: }
274: break;
275: case 'LSUB':
276: if (isset($this->cache_data['LSUB'])) {
277: unset($this->cache_data['LSUB']);
278: $this->debug[] = 'cache busted: '.$type;
279: }
280: break;
281: case 'ALL':
282: $this->cache_data = array();
283: $this->debug[] = 'cache busted: '.$type;
284: break;
285: default:
286: if (isset($this->cache_data[$type])) {
287: if (!$full) {
288: foreach ($this->cache_data[$type] as $command => $res) {
289: if (!preg_match("/^UID FETCH/", $command)) {
290: unset($this->cache_data[$type][$command]);
291: $this->debug[] = 'Partial cache flush: '.$command;
292: }
293: if ($this->get_fetch_command()) {
294: if (mb_strstr($command, $this->get_fetch_command())){
295: unset($this->cache_data[$type][$command]);
296: $this->debug[] = 'Partial cache flush: '.$command;
297: }
298: }
299: }
300: }
301: else {
302: unset($this->cache_data[$type]);
303: $this->debug[] = 'cache busted: '.$type;
304: }
305: }
306: break;
307: }
308: }
309:
310: /**
311: * output a string version of the cache that can be re-used
312: * @return string serialized version of the cache data
313: */
314: public function dump_cache( $type = 'string') {
315: if ($type == 'array') {
316: return $this->cache_data;
317: }
318: elseif ($type == 'gzip') {
319: return gzcompress(serialize($this->cache_data));
320: }
321: else {
322: return serialize($this->cache_data);
323: }
324: }
325:
326: /**
327: * load cache data from the output of dump_cache()
328: * @param string $data serialized cache data from dump_cache()
329: * @return void
330: */
331: public function load_cache($data, $type='string') {
332: if ($type == 'array') {
333: if (is_array($data)) {
334: $this->cache_data = $data;
335: $this->debug[] = 'Cache loaded: '.count($this->cache_data);
336: }
337: }
338: elseif ($type == 'gzip') {
339: $data = unserialize(gzuncompress($data));
340: if (is_array($data)) {
341: $this->cache_data = $data;
342: $this->debug[] = 'Cache loaded: '.count($this->cache_data);
343: }
344: }
345: else {
346: $data = unserialize($data);
347: if (is_array($data)) {
348: $this->cache_data = $data;
349: $this->debug[] = 'Cache loaded: '.count($this->cache_data);
350: }
351: }
352: }
353: }
354: