1: <?php
2:
3: /**
4: * IMAP modules
5: * @package modules
6: * @subpackage imap
7: */
8:
9: /**
10: * IMAP specific parsing routines
11: * @subpackage imap/lib
12: */
13: class Hm_IMAP_Parser extends Hm_IMAP_Base {
14:
15: /**
16: * helper method to grab values from the SELECT response
17: * @param array $vals low level parsed select response segment
18: * @param int $offset offset in the list to search for a value
19: * @param string $key value in the array to start from
20: * @return int the adjacent value
21: */
22: protected function get_adjacent_response_value($vals, $offset, $key) {
23: foreach ($vals as $i => $v) {
24: $i += $offset;
25: if (isset($vals[$i]) && $vals[$i] == $key) {
26: return $v;
27: }
28: }
29: return 0;
30: }
31:
32: /**
33: * helper function to cllect flags from the SELECT response
34: * @param array $vals low level parsed select response segment
35: * @return array list of flags
36: */
37: protected function get_flag_values($vals) {
38: $collect_flags = false;
39: $res = array();
40: foreach ($vals as $i => $v) {
41: if ($v == ')') {
42: $collect_flags = false;
43: }
44: if ($collect_flags) {
45: $res[] = $v;
46: }
47: if ($v == '(') {
48: $collect_flags = true;
49: }
50: }
51: return $res;
52: }
53:
54: /**
55: * compare filter keywords against message flags
56: * @param string $filter message type to filter by
57: * @param string $flags IMAP flag value string
58: * @return bool true if the message matches the filter
59: */
60: protected function flag_match($filter, $flags) {
61: $res = false;
62: switch($filter) {
63: case 'ANSWERED':
64: case 'SEEN':
65: case 'DRAFT':
66: case 'DELETED':
67: case 'FLAGGED':
68: $res = mb_stristr($flags, $filter);
69: break;
70: case 'UNSEEN':
71: case 'UNDRAFT':
72: case 'UNDELETED':
73: case 'UNFLAGGED':
74: case 'UNANSWERED':
75: $res = !mb_stristr($flags, str_replace('UN', '', $filter));
76: break;
77: }
78: return $res;
79: }
80:
81: /**
82: * build a list of IMAP extensions from the capability response
83: * @return void
84: */
85: protected function parse_extensions_from_capability() {
86: $extensions = array();
87: foreach (explode(' ', $this->capability) as $word) {
88: if (!in_array(mb_strtolower($word), array('*', 'ok', 'completed', 'imap4rev1', 'capability'), true)) {
89: $extensions[] = mb_strtolower($word);
90: }
91: }
92: $this->supported_extensions = $extensions;
93: }
94:
95: /**
96: * build a QRESYNC IMAP extension paramater for a SELECT statement
97: * @return string param to use
98: */
99: protected function build_qresync_params() {
100: $param = '';
101: if (isset($this->selected_mailbox['detail'])) {
102: $box = $this->selected_mailbox['detail'];
103: if (isset($box['uidvalidity']) && $box['uidvalidity'] && isset($box['modseq']) && $box['modseq']) {
104: if ($this->is_clean($box['uidvalidity'], 'uid') && $this->is_clean($box['modseq'], 'uid')) {
105: $param = sprintf(' (QRESYNC (%s %s))', $box['uidvalidity'], $box['modseq']);
106: }
107: }
108: }
109: return $param;
110: }
111:
112: /**
113: * parse GETQUOTAROOT and GETQUOTA responses
114: * @param array $data low level parsed IMAP response segment
115: * @return array list of properties
116: */
117: protected function parse_quota_response($vals) {
118: $current = 0;
119: $max = 0;
120: $name = '';
121: if (in_array('QUOTA', $vals)) {
122: $name = $this->get_adjacent_response_value($vals, -1, 'QUOTA');
123: if ($name == '(') {
124: $name = '';
125: }
126: }
127: if (in_array('STORAGE', $vals)) {
128: $current = $this->get_adjacent_response_value($vals, -1, 'STORAGE');
129: $max = $this->get_adjacent_response_value($vals, -2, 'STORAGE');
130: }
131: return array($name, $max, $current);
132: }
133:
134: /**
135: * collect useful untagged responses about mailbox state from certain command responses
136: * @param array $data low level parsed IMAP response segment
137: * @return array list of properties
138: */
139: protected function parse_untagged_responses($data) {
140: $cache_updates = 0;
141: $cache_updated = 0;
142: $qresync = false;
143: $attributes = array(
144: 'uidnext' => false,
145: 'unseen' => false,
146: 'uidvalidity' => false,
147: 'exists' => false,
148: 'pflags' => false,
149: 'recent' => false,
150: 'modseq' => false,
151: 'flags' => false,
152: 'nomodseq' => false
153: );
154: foreach($data as $vals) {
155: if (in_array('NOMODSEQ', $vals)) {
156: $attributes['nomodseq'] = true;
157: }
158: if (in_array('MODSEQ', $vals)) {
159: $attributes['modseq'] = $this->get_adjacent_response_value($vals, -2, 'MODSEQ');
160: }
161: if (in_array('HIGHESTMODSEQ', $vals)) {
162: $attributes['modseq'] = $this->get_adjacent_response_value($vals, -1, 'HIGHESTMODSEQ');
163: }
164: if (in_array('UIDNEXT', $vals)) {
165: $attributes['uidnext'] = $this->get_adjacent_response_value($vals, -1, 'UIDNEXT');
166: }
167: if (in_array('UNSEEN', $vals)) {
168: $attributes['unseen'] = $this->get_adjacent_response_value($vals, -1, 'UNSEEN');
169: }
170: if (in_array('UIDVALIDITY', $vals)) {
171: $attributes['uidvalidity'] = $this->get_adjacent_response_value($vals, -1, 'UIDVALIDITY');
172: }
173: if (in_array('EXISTS', $vals)) {
174: $attributes['exists'] = $this->get_adjacent_response_value($vals, 1, 'EXISTS');
175: }
176: if (in_array('RECENT', $vals)) {
177: $attributes['recent'] = $this->get_adjacent_response_value($vals, 1, 'RECENT');
178: }
179: if (in_array('PERMANENTFLAGS', $vals)) {
180: $attributes['pflags'] = $this->get_flag_values($vals);
181: }
182: if (in_array('FLAGS', $vals) && !in_array('MODSEQ', $vals)) {
183: $attributes['flags'] = $this->get_flag_values($vals);
184: }
185: if (in_array('FETCH', $vals) || in_array('VANISHED', $vals)) {
186: $cache_updates++;
187: $cache_updated += $this->update_cache_data($vals);
188: }
189: }
190: if ($cache_updates && $cache_updates == $cache_updated) {
191: $qresync = true;
192: }
193: return array($qresync, $attributes);
194: }
195:
196: /**
197: * parse untagged ESEARCH/ESORT responses
198: * @param array $vals low level parsed IMAP response segment
199: * @return array list of ESEARCH response values
200: */
201: protected function parse_esearch_response($vals) {
202: $res = array();
203: if (in_array('MIN', $vals)) {
204: $res['min'] = $this->get_adjacent_response_value($vals, -1, 'MIN');
205: }
206: if (in_array('MAX', $vals)) {
207: $res['max'] = $this->get_adjacent_response_value($vals, -1, 'MAX');
208: }
209: if (in_array('COUNT', $vals)) {
210: $res['count'] = $this->get_adjacent_response_value($vals, -1, 'COUNT');
211: }
212: if (in_array('ALL', $vals)) {
213: $res['all'] = $this->get_adjacent_response_value($vals, -1, 'ALL');
214: }
215: return $res;
216: }
217:
218: /**
219: * examine NOOP/SELECT/EXAMINE untagged responses to determine if the mailbox state changed
220: * @param array $attributes list of attribute name/value pairs
221: * @return void
222: */
223: protected function check_mailbox_state_change($attributes, $cached_state=false, $mailbox=false) {
224: if (!$cached_state) {
225: if ($this->selected_mailbox) {
226: $cached_state = $this->selected_mailbox;
227: }
228: if (!$cached_state) {
229: return;
230: }
231: }
232: $full_change = false;
233: $partial_change = false;
234:
235: foreach($attributes as $name => $value) {
236: if ($value !== false) {
237: if (isset($cached_state[$name]) && $cached_state[$name] != $value) {
238: if ($name == 'uidvalidity') {
239: $full_change = true;
240: }
241: else {
242: $partial_change = true;
243: }
244: }
245: }
246: }
247: if ($full_change || (isset($attributes['nomodseq']) && $attributes['nomodseq'])) {
248: $this->bust_cache($mailbox);
249: }
250: elseif ($partial_change) {
251: $this->bust_cache($mailbox, false);
252: }
253: }
254:
255: /**
256: * helper function to build IMAP LIST commands
257: * @param bool $lsub flag to use LSUB
258: * @return array IMAP LIST/LSUB commands
259: */
260: protected function build_list_commands($lsub, $mailbox, $keyword, $children_capability=true) {
261: $commands = array();
262: if ($lsub) {
263: $imap_command = 'LSUB';
264: }
265: else {
266: $imap_command = 'LIST';
267: }
268: $namespaces = $this->get_namespaces();
269: foreach ($namespaces as $nsvals) {
270:
271: /* build IMAP command */
272: $namespace = $nsvals['prefix'];
273: $delim = $nsvals['delim'];
274: $ns_class = $nsvals['class'];
275: $mailbox = $this->utf7_encode(str_replace('"', '\"', $mailbox));
276: if (mb_strtoupper(mb_substr($namespace, 0, 5)) == 'INBOX') {
277: $namespace = '';
278: }
279:
280: /* send command to the IMAP server and fetch the response */
281: if ($mailbox && $namespace) {
282: $namespace .= $delim.$mailbox;
283: }
284: elseif ($mailbox) {
285: $namespace .= $mailbox.$delim;
286: }
287: if ($this->is_supported('LIST-STATUS')) {
288: $status = ' RETURN (';
289: if ($this->is_supported('LIST-EXTENDED')) {
290: $status .= 'CHILDREN ';
291: }
292: if ($this->is_supported('SPECIAL-USE')) {
293: $status .= 'SPECIAL-USE ';
294: }
295: $status .= 'STATUS (MESSAGES UNSEEN UIDVALIDITY UIDNEXT RECENT))';
296: }
297: else {
298: $status = '';
299: }
300:
301: if (!$children_capability && $namespace) {
302: $commands[] = array($imap_command.' " " "'.$namespace."$keyword\"$status\r\n", $namespace);
303: } else {
304: $commands[] = array($imap_command.' "'.$namespace."\" \"$keyword\"$status\r\n", $namespace);
305: }
306: }
307: return $commands;
308: }
309:
310: /**
311: * parse an untagged STATUS response
312: * @param array $response low level parsed IMAP response segment
313: * @return array list of mailbox attributes
314: */
315: protected function parse_status_response($response) {
316: $attributes = array(
317: 'messages' => false,
318: 'uidvalidity' => false,
319: 'uidnext' => false,
320: 'recent' => false,
321: 'unseen' => false
322: );
323: foreach ($response as $vals) {
324: if (in_array('MESSAGES', $vals)) {
325: $res = $this->get_adjacent_response_value($vals, -1, 'MESSAGES');
326: if (ctype_digit((string)$res)) {
327: $attributes['messages'] = $this->get_adjacent_response_value($vals, -1, 'MESSAGES');
328: }
329: }
330: if (in_array('UIDNEXT', $vals)) {
331: $res = $this->get_adjacent_response_value($vals, -1, 'UIDNEXT');
332: if (ctype_digit((string)$res)) {
333: $attributes['uidnext'] = $this->get_adjacent_response_value($vals, -1, 'UIDNEXT');
334: }
335: }
336: if (in_array('UIDVALIDITY', $vals)) {
337: $res = $this->get_adjacent_response_value($vals, -1, 'UIDVALIDITY');
338: if (ctype_digit((string)$res)) {
339: $attributes['uidvalidity'] = $this->get_adjacent_response_value($vals, -1, 'UIDVALIDITY');
340: }
341: }
342: if (in_array('RECENT', $vals)) {
343: $res = $this->get_adjacent_response_value($vals, -1, 'RECENT');
344: if (ctype_digit((string)$res)) {
345: $attributes['recent'] = $this->get_adjacent_response_value($vals, -1, 'RECENT');
346: }
347: }
348: if (in_array('UNSEEN', $vals)) {
349: $res = $this->get_adjacent_response_value($vals, -1, 'UNSEEN');
350: if (ctype_digit((string)$res)) {
351: $attributes['unseen'] = $this->get_adjacent_response_value($vals, -1, 'UNSEEN');
352: }
353: }
354: }
355: return $attributes;
356: }
357: }
358: