1: <?php
2:
3: /**
4: * IMAP modules
5: * @package modules
6: * @subpackage imap
7: */
8:
9: /**
10: * Represent a message structure by parsing the results from the IMAP
11: * BODYSTRUCTURE command
12: * @subpackage imap/lib
13: */
14: class Hm_IMAP_Struct {
15:
16: /* Holds the completed structure */
17: private $struct = array();
18:
19: /* Holds the current part number */
20: private $part_number = '-1';
21:
22: /* Holds the parent container type, if any */
23: private $parent_type = false;
24:
25: /* Valid top level MIME types */
26: private $mime_types = array('application', 'audio', 'binary', 'image', 'message', 'model', 'multipart', 'text', 'video');
27:
28: /* These are "readable" MESSAGE subtypes that should be treated as text */
29: private $readable_message_types = array('delivery-status', 'external-body', 'disposition-notification', 'rfc822-headers');
30:
31: /* Field order of a single non-text message part */
32: private $single_format = array( 'type' => 0, 'subtype' => 1, 'attributes' => 2, 'id' => 3, 'description' => 4,
33: 'encoding' => 5, 'size' => 6, 'md5' => 7, 'file_attributes' => 8, 'langauge' => 9, 'location' => 10,);
34:
35: /* Field order of a single text message part */
36: private $text_format = array( 'type' => 0, 'subtype' => 1, 'attributes' => 2, 'id' => 3, 'description' => 4,
37: 'encoding' => 5, 'size' => 6, 'lines' => 7, 'md5' => 8, 'disposition' => 9, 'file_attributes' => 10,
38: 'langauge' => 11, 'location' => 12,);
39:
40: /* Field order of an RFC822 container part */
41: private $rfc822_format = array( 'type' => 0, 'subtype' => 1, 'attributes' => 2, 'id' => 3, 'description' => 4,
42: 'encoding' => 5, 'size' => 6, 'envelope' => 7, 'body_lines' => 9, 'body_attributes' => 10, 'disposition' => 11,
43: 'language' => 12, 'location' => 13);
44:
45: /* Fields in a multipart container part */
46: private $multipart_format = array( 'subtype', 'attributes', 'disposition', 'language', 'location');
47:
48: /* Address fields in an RFC822 ENVELOPE */
49: private $envelope_addresses = array( 'from', 'sender', 'reply-to', 'to', 'cc', 'bcc', 'in-reply-to');
50:
51: /* Fields order of an ENVELOPE address */
52: private $address_format = array( 'name' => 0, 'route' => 1, 'mailbox' => 2, 'domain' => 3,);
53:
54: /* Fields order of an ENVELOPE */
55: private $envelope_format = array( 'date' => 0, 'subject' => 1, 'from' => 2, 'sender' => 3, 'reply-to' => 4,
56: 'to' => 5, 'cc' => 6, 'bcc' => 7, 'in-reply-to' => 8, 'message_id' => 9);
57:
58: /* Hm_IMAP object */
59: private $imap = false;
60:
61: /**
62: * Constructor. Takes the BODYSTRUCTURE response and builds a data representation
63: * @param array $struct_response low-level parsed IMAP response
64: * @return void
65: */
66: public function __construct($struct_response, $imap) {
67: $this->imap = $imap;
68: list($struct, $_) = $this->build($struct_response);
69: $this->struct = $this->id_parts($struct);
70: }
71:
72: /**
73: * Builds a nested array based on parens in the input
74: * @param array $array low-level parsed IMAP response
75: * @param int $index position in the list
76: * @return array tuple of the parsed result and index
77: */
78: private function build($array, $index=0) {
79: $res = array();
80: $len = count($array);
81: for ($i = $index; $i < $len; $i++ ) {
82: $val = $array[$i];
83: if ($val == '(') {
84: list($result, $new_index) = $this->build($array, ($i+1));
85: $res[] = $result;
86: $i = $new_index;
87: }
88: elseif ($val == ')') {
89: return array($res, $i);
90: }
91: else {
92: $res[] = $val;
93: }
94: }
95: return array($res, $i);
96: }
97: /**
98: * Create a name => value attribute set
99: * @param array $vals set of attributes
100: *
101: * @return array
102: */
103: private function attribute_set($vals) {
104: $res = array();
105: $len = count($vals);
106: for ($i = 0; $i < $len; $i++) {
107: if (isset($vals[$i + 1])) {
108: $res[mb_strtolower($vals[$i])] = $this->set_value($vals[$i + 1]);
109: $i++;
110: }
111: }
112: return $res;
113: }
114:
115: /**
116: * Parse an ENVELOPE address
117: * @param array $vals parts of an address
118: * @return string
119: */
120: private function envelope_address($vals) {
121: $res = array();
122: foreach ($vals as $addy) {
123: $parts = array();
124: $result = '';
125: foreach ($this->address_format as $name => $pos) {
126: if (isset($addy[$pos])) {
127: $parts[$name] = $this->set_value($addy[$pos]);
128: }
129: else {
130: $parts[$name] = false;
131: }
132: }
133: if ($parts['name']) {
134: $result = '"'.$parts['name'].'" ';
135: }
136: if ($parts['mailbox'] && $parts['domain']) {
137: $result .= $parts['mailbox'].'@'.$parts['domain'];
138: }
139: $res[] = $result;
140: }
141: return implode(',', $res);
142: }
143:
144: /**
145: * Prepare a value to be added to the structure
146: * @param mixed $val value to add
147: * @param string $type optional value type
148: * @return prepared value
149: */
150: private function set_value($val, $type=false) {
151: if ($type == 'envelope') {
152: return $this->envelope($val);
153: }
154: elseif (is_array($val) && in_array($type, array('attributes', 'body_attributes', 'disposition', 'file_attributes'), true)) {
155: return $this->attribute_set($val);
156: }
157: elseif (is_array($val) && in_array($type, $this->envelope_addresses, true)) {
158: return $this->envelope_address($val);
159: }
160: elseif ($val === 'NIL') {
161: return false;
162: }
163: elseif (!is_array($val)) {
164: if ($type == 'type' || $type == 'subtype') {
165: $val = mb_strtolower($val);
166: }
167: return $this->imap->decode_fld($val);
168: }
169: else {
170: return $val;
171: }
172: }
173:
174: /**
175: * Parse an RFC822 ENVELOPE section
176: * @param array $vals low-level IMAP response
177: * @return array
178: */
179: private function envelope($vals) {
180: $res = array();
181: foreach ($this->envelope_format as $name => $pos) {
182: if (isset($vals[$pos])) {
183: $res[$name] = $this->set_value($vals[$pos], $name);
184: }
185: }
186: return $res;
187: }
188:
189: /**
190: * Determine if this is a multipart, rfc822 message, or single part type
191: * @param array $vals low-level IMAP response
192: * @return string
193: */
194: private function get_part_type($vals) {
195: if (count($vals) > 1 && is_string($vals[0]) && is_string($vals[1])) {
196: $type = mb_strtolower($vals[0]);
197: $subtype = mb_strtolower($vals[1]);
198: if ($type == 'message' && !in_array($subtype, $this->readable_message_types, true)) {
199: return 'message';
200: }
201: elseif (in_array($type, $this->mime_types, true) || preg_match("/^x-.+$/", $type)) {
202: return 'single';
203: }
204: }
205: elseif (is_array($vals[0])) {
206: return 'multi';
207: }
208: return false;
209: }
210:
211: /**
212: * Parse an RFC822 message part
213: * @param array $vals low-level IMAP response
214: * @return array
215: */
216: private function id_rfc822_part($vals) {
217: $res = array();
218: foreach($this->rfc822_format as $name => $pos) {
219: if (isset($vals[$pos])) {
220: $res[$name] = $this->set_value($vals[$pos], $name);
221: }
222: else {
223: $res[$name] = false;
224: }
225: }
226: $part_number = $this->part_number;
227: $len = count($vals);
228: $this->part_number .= '.0';
229: $this->parent_type = 'message';
230: $subs = array();
231: if (isset($vals[8]) && is_array($vals[8])) {
232: $subs = array_merge($subs, $this->id_parts(array($vals[8])));
233: }
234: if (!empty($subs)) {
235: $res['subs'] = $subs;
236: }
237: $this->part_number = $part_number;
238: $this->parent_type = false;
239: return $res;
240: }
241:
242: /**
243: * Parse multipart message part
244: * @param array $vals low-level IMAP response
245: * @param bool $increment flag to control part ids
246: * @return array
247: */
248: private function id_multi_part($vals, $increment=false) {
249: if ($increment) {
250: $part_number = $this->part_number;
251: $this->part_number .= '.0';
252: }
253: list($index, $subs) = $this->parse_multi_part_subs($vals);
254: $res = $this->parse_multi_part_flds($index, $vals);
255: $res['subs'] = $subs;
256: if ($increment) {
257: $this->part_number = $part_number;
258: }
259: return $res;
260: }
261:
262: /**
263: * Parse multipart message parts
264: * @param array $vals low-level IMAP response
265: * @return array last index and subs array
266: */
267: private function parse_multi_part_subs($vals) {
268: $index = 0;
269: $subs = array();
270: $this->parent_type = 'multi';
271: foreach($vals as $index => $val) {
272: if (!is_array($val)) {
273: break;
274: }
275: else {
276: $subs = array_merge($subs, $this->id_parts(array($val)));
277: }
278: }
279: return array($index, $subs);
280: }
281:
282: /**
283: * Parse multipart message fields
284: * @param int $index position in the array
285: * @param array $vals low-level parsed IMAP response
286: * @return array
287: */
288: private function parse_multi_part_flds($index, $vals) {
289: $res = array('type' => 'multipart');
290: if ($index) {
291: foreach ($this->multipart_format as $fld) {
292: if (isset($vals[$index])) {
293: $res[$fld] = $this->set_value($vals[$index], $fld);
294: }
295: else {
296: $res[$fld] = false;
297: }
298: $index++;
299: }
300: }
301: return $res;
302: }
303:
304: /**
305: * Parse single message part
306: * @param array $vals low-level IMAP response
307: * @return array
308: */
309: private function id_single_part($vals) {
310: $res = array();
311: $single_format = false;
312: if (isset($vals[0]) && mb_strtolower($vals[0]) == 'text') {
313: $flds = $this->text_format;
314: }
315: else {
316: $flds = $this->single_format;
317: $single_format = true;
318: }
319: foreach($flds as $name => $pos) {
320: if (isset($vals[$pos])) {
321: $res[$name] = $this->set_value($vals[$pos], $name);
322: }
323: else {
324: $res[$name] = false;
325: }
326: }
327: // This is an edge case for improperly formatted parts
328: if ($single_format && !$res['size'] && is_numeric($res['encoding'])) {
329: $res['file_attributes'] = $res['md5'];
330: $res['md5'] = $res['size'];
331: $res['size'] = $res['encoding'];
332: $res['encoding'] = $res['description'];
333: $res['description'] = false;
334: }
335: return $res;
336: }
337:
338: /**
339: * parse the message parts at the current "level"
340: * @param array $struct low-level IMAP response
341: * @return array
342: */
343: private function id_parts($struct) {
344: $res = array();
345: foreach ($struct as $val) {
346: if (is_array($val)) {
347: $part_type = $this->get_part_type($val);
348: if ($part_type == 'message') {
349: $res[$this->increment_part_number()] = $this->id_rfc822_part($val);
350: }
351: elseif ( $part_type == 'single' ) {
352: if ($this->part_number == '-1') {
353: $this->part_number = '0';
354: }
355: $res[$this->increment_part_number()] = $this->id_single_part($val);
356: }
357: elseif ( $part_type == 'multi' ) {
358: if ($this->parent_type == 'message') {
359: $res[$this->part_number] = $this->id_multi_part($val);
360: }
361: else {
362: $res[$this->increment_part_number()] = $this->id_multi_part($val, true);
363: }
364: }
365: }
366: }
367: return $res;
368: }
369:
370: /**
371: * Increment the message part number in the weird way IMAP does.
372: * @return string
373: */
374: private function increment_part_number() {
375: $part = $this->part_number;
376: if (!mb_strstr($part, '.')) {
377: $part++;
378: }
379: else {
380: $parts = explode('.', $part);
381: $parts[(count($parts) - 1)]++;
382: $part = implode('.', $parts);
383: }
384: $part = (string) $part;
385: $this->part_number = $part;
386: return $part;
387: }
388:
389: /**
390: * Search a parsed BODYSTRUCTURE response
391: * @param array $struct the response to search
392: * @param array $flds key => value list of fields and values to search for
393: * @param bool $all true to return all matching parts
394: * @param array $res holds results during recursive iterations
395: * @return array list of matching parts
396: */
397: public function recursive_search($struct, $flds, $all, $res, $parent=false) {
398: foreach ($struct as $msg_id => $vals) {
399: $matches = 0;
400: if (isset($flds['imap_part_number'])) {
401: if ($msg_id === $flds['imap_part_number'] || (string) preg_replace("/^0\.{1}/", '', $msg_id) === (string) $flds['imap_part_number']) {
402: $matches++;
403: }
404: }
405: foreach ($flds as $name => $fld_val) {
406: if (isset($vals[$name]) && mb_stristr($vals[$name], $fld_val)) {
407: $matches++;
408: }
409: }
410: if (array_key_exists('envelope', $vals)) {
411: $parent = $vals;
412: }
413: if ($matches === count($flds)) {
414: $part = $vals;
415: if (isset($part['subs'])) {
416: $part['subs'] = count($part['subs']);
417: }
418: if (is_array($parent) && array_key_exists('envelope', $parent)) {
419: $part['envelope'] = $parent['envelope'];
420: }
421: $res[preg_replace("/^0\.{1}/", '', $msg_id)] = $part;
422: if (!$all) {
423: return $res;
424: }
425:
426: }
427: if (isset($vals['subs'])) {
428: $res = $this->recursive_search($vals['subs'], $flds, $all, $res, $parent);
429: }
430: }
431: return $res;
432: }
433:
434: /**
435: * Return structure
436: * @return array
437: */
438: public function data () {
439: return $this->struct;
440: }
441:
442: /**
443: * Public search function, returns a list of matching parts
444: * @param array $flds key => value pairs of fields and values to search on
445: * @param bool $all true to return all matches
446: * @return array
447: */
448: public function search($flds, $all=true) {
449: return $this->recursive_search($this->struct, $flds, $all, $res=array());
450: }
451: }
452: