Overview

Packages

  • Imap
    • Client

Classes

  • Horde_Imap_Client
  • Horde_Imap_Client_Auth_DigestMD5
  • Horde_Imap_Client_Base
  • Horde_Imap_Client_Cache
  • Horde_Imap_Client_Data_Acl
  • Horde_Imap_Client_Data_AclCommon
  • Horde_Imap_Client_Data_AclNegative
  • Horde_Imap_Client_Data_AclRights
  • Horde_Imap_Client_Data_Envelope
  • Horde_Imap_Client_Data_Fetch
  • Horde_Imap_Client_Data_Fetch_Pop3
  • Horde_Imap_Client_Data_Thread
  • Horde_Imap_Client_DateTime
  • Horde_Imap_Client_Exception
  • Horde_Imap_Client_Exception_NoSupportExtension
  • Horde_Imap_Client_Fetch_Query
  • Horde_Imap_Client_Ids
  • Horde_Imap_Client_Ids_Pop3
  • Horde_Imap_Client_Mailbox
  • Horde_Imap_Client_Search_Query
  • Horde_Imap_Client_Socket
  • Horde_Imap_Client_Socket_Pop3
  • Horde_Imap_Client_Sort
  • Horde_Imap_Client_Translation
  • Horde_Imap_Client_Utf7imap
  • Horde_Imap_Client_Utils
  • Horde_Imap_Client_Utils_Pop3
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Utility functions for the Horde IMAP client.
  4:  *
  5:  * Copyright 2008-2012 Horde LLC (http://www.horde.org/)
  6:  *
  7:  * getBaseSubject() code adapted from imap-base-subject.c (Dovecot 1.2)
  8:  *   Original code released under the LGPL-2.0.1
  9:  *   Copyright (c) 2002-2008 Timo Sirainen <tss@iki.fi>
 10:  *
 11:  * See the enclosed file COPYING for license information (LGPL). If you
 12:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 13:  *
 14:  * @author   Michael Slusarz <slusarz@horde.org>
 15:  * @category Horde
 16:  * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
 17:  * @package  Imap_Client
 18:  */
 19: class Horde_Imap_Client_Utils
 20: {
 21:     /**
 22:      * Create an IMAP message sequence string from a list of indices.
 23:      *
 24:      * Index Format: range_start:range_end,uid,uid2,...
 25:      *
 26:      * Mailbox Format: {mbox_length}[mailbox]range_start:range_end,uid,uid2,...
 27:      *
 28:      * @param mixed $in       An array of indices (or a single index). See
 29:      *                        'mailbox' below.
 30:      * @param array $options  Additional options:
 31:      *   - mailbox: (boolean) If true, store mailbox information with the
 32:      *              ID list.  $in should be an array of arrays, with keys as
 33:      *              mailbox names and values as IDs.
 34:      *              DEFAULT: false
 35:      *   - nosort: (boolean) Do not numerically sort the IDs before creating
 36:      *             the range?
 37:      *             DEFAULT: false
 38:      *
 39:      * @return string  The IMAP message sequence string.
 40:      */
 41:     public function toSequenceString($in, $options = array())
 42:     {
 43:         if (empty($in)) {
 44:             return '';
 45:         }
 46: 
 47:         if (!empty($options['mailbox'])) {
 48:             $str = '';
 49:             unset($options['mailbox']);
 50: 
 51:             foreach ($in as $mbox => $ids) {
 52:                 $str .= '{' . strlen($mbox) . '}' . $mbox . $this->toSequenceString($ids, $options);
 53:             }
 54: 
 55:             return $str;
 56:         }
 57: 
 58:         // Make sure IDs are unique
 59:         $in = is_array($in)
 60:             ? array_keys(array_flip($in))
 61:             : array($in);
 62: 
 63:         if (empty($options['nosort'])) {
 64:             sort($in, SORT_NUMERIC);
 65:         }
 66: 
 67:         $first = $last = array_shift($in);
 68:         $i = count($in) - 1;
 69:         $out = array();
 70: 
 71:         reset($in);
 72:         while (list($key, $val) = each($in)) {
 73:             if (($last + 1) == $val) {
 74:                 $last = $val;
 75:             }
 76: 
 77:             if (($i == $key) || ($last != $val)) {
 78:                 if ($last == $first) {
 79:                     $out[] = $first;
 80:                     if ($i == $key) {
 81:                         $out[] = $val;
 82:                     }
 83:                 } else {
 84:                     $out[] = $first . ':' . $last;
 85:                     if (($i == $key) && ($last != $val)) {
 86:                         $out[] = $val;
 87:                     }
 88:                 }
 89:                 $first = $last = $val;
 90:             }
 91:         }
 92: 
 93:         return empty($out)
 94:             ? $first
 95:             : implode(',', $out);
 96:     }
 97: 
 98:     /**
 99:      * Parse an IMAP message sequence string into a list of indices.
100:      * See toSequenceString() for allowed formats.
101:      *
102:      * @see toSequenceString()
103:      *
104:      * @param string $str  The IMAP message sequence string.
105:      *
106:      * @return array  An array of indices.  If string contains mailbox info,
107:      *                return value will be an array of arrays, with keys as
108:      *                mailbox names and values as IDs. Otherwise, return the
109:      *                list of IDs.
110:      */
111:     public function fromSequenceString($str)
112:     {
113:         $ids = array();
114:         $str = trim($str);
115: 
116:         if (!strlen($str)) {
117:             return $ids;
118:         }
119: 
120:         if ($str[0] == '{') {
121:             $i = strpos($str, '}');
122:             $count = intval(substr($str, 1, $i - 1));
123:             $mbox = substr($str, $i + 1, $count);
124:             $i += $count + 1;
125:             $end = strpos($str, '{', $i);
126: 
127:             if ($end === false) {
128:                 $uidstr = substr($str, $i);
129:             } else {
130:                 $uidstr = substr($str, $i, $end - $i);
131:                 $ids = $this->fromSequenceString(substr($str, $end));
132:             }
133: 
134:             $ids[$mbox] = $this->fromSequenceString($uidstr);
135: 
136:             return $ids;
137:         }
138: 
139:         $idarray = explode(',', $str);
140: 
141:         reset($idarray);
142:         while (list(,$val) = each($idarray)) {
143:             $range = explode(':', $val);
144:             if (isset($range[1])) {
145:                 for ($i = min($range), $j = max($range); $i <= $j; ++$i) {
146:                     $ids[] = $i;
147:                 }
148:             } else {
149:                 $ids[] = $val;
150:             }
151:         }
152: 
153:         return $ids;
154:     }
155: 
156:     /**
157:      * Remove "bare newlines" from a string.
158:      *
159:      * @param string $str  The original string.
160:      *
161:      * @return string  The string with all bare newlines removed.
162:      */
163:     public function removeBareNewlines($str)
164:     {
165:         return str_replace(array("\r\n", "\n"), array("\n", "\r\n"), $str);
166:     }
167: 
168:     /**
169:      * Escape IMAP output via a quoted string (see RFC 3501 [4.3]). Note that
170:      * IMAP quoted strings support 7-bit characters only and can not contain
171:      * either CR or LF.
172:      *
173:      * @param string $str     The unescaped string.
174:      * @param boolean $force  Always add quotes?
175:      *
176:      * @return string  The escaped string.
177:      */
178:     public function escape($str, $force = false)
179:     {
180:         if (!strlen($str)) {
181:             return '""';
182:         }
183: 
184:         $newstr = addcslashes($str, '"\\');
185:         return (!$force && ($str == $newstr))
186:             ? $str
187:             : '"' . $newstr . '"';
188:     }
189: 
190:     /**
191:      * Given a string, will strip out any characters that are not allowed in
192:      * the IMAP 'atom' definition (RFC 3501 [9]).
193:      *
194:      * @param string $str  An ASCII string.
195:      *
196:      * @return string  The string with the disallowed atom characters stripped
197:      *                 out.
198:      */
199:     public function stripNonAtomChars($str)
200:     {
201:         return str_replace(array('(', ')', '{', ' ', '%', '*', '"', '\\', ']'), '', preg_replace('/[\x00-\x1f\x7f]/', '', $str));
202:     }
203: 
204:     /**
205:      * Return the "base subject" defined in RFC 5256 [2.1].
206:      *
207:      * @param string $str     The original subject string.
208:      * @param array $options  Additional options:
209:      *   - keepblob: (boolean) Don't remove any "blob" information (i.e. text
210:      *               leading text between square brackets) from string.
211:      *
212:      * @return string  The cleaned up subject string.
213:      */
214:     public function getBaseSubject($str, $options = array())
215:     {
216:         // Rule 1a: MIME decode to UTF-8.
217:         $str = Horde_Mime::decode($str, 'UTF-8');
218: 
219:         // Rule 1b: Remove superfluous whitespace.
220:         $str = preg_replace("/[\t\r\n ]+/", ' ', $str);
221: 
222:         if (!strlen($str)) {
223:             return '';
224:         }
225: 
226:         do {
227:             /* (2) Remove all trailing text of the subject that matches the
228:              * the subj-trailer ABNF, repeat until no more matches are
229:              * possible. */
230:             $str = preg_replace("/(?:\s*\(fwd\)\s*)+$/i", '', $str);
231: 
232:             do {
233:                 /* (3) Remove all prefix text of the subject that matches the
234:                  * subj-leader ABNF. */
235:                 $found = $this->_removeSubjLeader($str, !empty($options['keepblob']));
236: 
237:                 /* (4) If there is prefix text of the subject that matches
238:                  * the subj-blob ABNF, and removing that prefix leaves a
239:                  * non-empty subj-base, then remove the prefix text. */
240:                 $found = (empty($options['keepblob']) && $this->_removeBlobWhenNonempty($str)) || $found;
241: 
242:                 /* (5) Repeat (3) and (4) until no matches remain. */
243:             } while ($found);
244: 
245:             /* (6) If the resulting text begins with the subj-fwd-hdr ABNF and
246:              * ends with the subj-fwd-trl ABNF, remove the subj-fwd-hdr and
247:              * subj-fwd-trl and repeat from step (2). */
248:         } while ($this->_removeSubjFwdHdr($str));
249: 
250:         return $str;
251:     }
252: 
253:     /**
254:      * Parse a POP3 (RFC 2384) or IMAP (RFC 5092/5593) URL.
255:      *
256:      * Absolute IMAP URLs takes one of the following forms:
257:      *   - imap://<iserver>[/]
258:      *   - imap://<iserver>/<enc-mailbox>[<uidvalidity>][?<enc-search>]
259:      *   - imap://<iserver>/<enc-mailbox>[<uidvalidity>]<iuid>[<isection>][<ipartial>][<iurlauth>]
260:      *
261:      * POP URLs take one of the following forms:
262:      *   - pop://<user>;auth=<auth>@<host>:<port>
263:      *
264:      * @param string $url  A URL string.
265:      *
266:      * @return mixed  False if the URL is invalid.  If valid, an array with
267:      *                the following fields:
268:      *   - auth: (string) The authentication method to use.
269:      *   - hostspec: (string) The remote server. (Not present for relative
270:      *               URLs).
271:      *   - mailbox: (string) The IMAP mailbox.
272:      *   - partial: (string) A byte range for use with IMAP FETCH.
273:      *   - port: (integer) The remote port. (Not present for relative URLs).
274:      *   - relative: (boolean) True if this is a relative URL.
275:      *   - search: (string) A search query to be run with IMAP SEARCH.
276:      *   - section: (string) A MIME part ID.
277:      *   - type: (string) Either 'imap' or 'pop'. (Not present for relative
278:      *           URLs).
279:      *   - username: (string) The username to use on the remote server.
280:      *   - uid: (string) The IMAP UID.
281:      *   - uidvalidity: (integer) The IMAP UIDVALIDITY for the given mailbox.
282:      *   - urlauth: (string) URLAUTH info (not parsed).
283:      */
284:     public function parseUrl($url)
285:     {
286:         $data = parse_url(trim($url));
287: 
288:         if (isset($data['scheme'])) {
289:             $type = strtolower($data['scheme']);
290:             if (!in_array($type, array('imap', 'pop'))) {
291:                 return false;
292:             }
293:             $relative = false;
294:         } else {
295:             $type = null;
296:             $relative = true;
297:         }
298: 
299:         $ret_array = array(
300:             'hostspec' => isset($data['host']) ? $data['host'] : null,
301:             'port' => isset($data['port']) ? $data['port'] : 143,
302:             'relative' => $relative,
303:             'type' => $type
304:         );
305: 
306:         /* Check for username/auth information. */
307:         if (isset($data['user'])) {
308:             if (($pos = stripos($data['user'], ';AUTH=')) !== false) {
309:                 $auth = substr($data['user'], $pos + 6);
310:                 if ($auth != '*') {
311:                     $ret_array['auth'] = $auth;
312:                 }
313:                 $data['user'] = substr($data['user'], 0, $pos);
314:             }
315: 
316:             if (strlen($data['user'])) {
317:                 $ret_array['username'] = $data['user'];
318:             }
319:         }
320: 
321:         /* IMAP-only information. */
322:         if (!$type || ($type == 'imap')) {
323:             if (isset($data['path'])) {
324:                 $data['path'] = ltrim($data['path'], '/');
325:                 $parts = explode('/;', $data['path']);
326: 
327:                 $mbox = array_shift($parts);
328:                 if (($pos = stripos($mbox, ';UIDVALIDITY=')) !== false) {
329:                     $ret_array['uidvalidity'] = substr($mbox, $pos + 13);
330:                     $mbox = substr($mbox, 0, $pos);
331:                 }
332:                 $ret_array['mailbox'] = urldecode($mbox);
333: 
334:             }
335: 
336:             if (count($parts)) {
337:                 foreach ($parts as $val) {
338:                     list($k, $v) = explode('=', $val);
339:                     $ret_array[strtolower($k)] = $v;
340:                 }
341:             } elseif (isset($data['query'])) {
342:                 $ret_array['search'] = urldecode($data['query']);
343:             }
344:         }
345: 
346:         return $ret_array;
347:     }
348: 
349:     /**
350:      * Create a POP3 (RFC 2384) or IMAP (RFC 5092/5593) URL.
351:      *
352:      * @param array $data  The data used to create the URL. See the return
353:      *                     value from parseUrl() for the available fields.
354:      *
355:      * @return string  A URL string.
356:      */
357:     public function createUrl($data)
358:     {
359:         $url = '';
360: 
361:         if (isset($data['type'])) {
362:             $url = $data['type'] . '://';
363: 
364:             if (isset($data['username'])) {
365:                 $url .= $data['username'];
366:             }
367: 
368:             if (isset($data['auth'])) {
369:                 $url .= ';AUTH=' . $data['auth'] . '@';
370:             } elseif (isset($data['username'])) {
371:                 $url .= '@';
372:             }
373: 
374:             $url .= $data['hostspec'];
375: 
376:             if (isset($data['port']) && ($data['port'] != 143)) {
377:                 $url .= ':' . $data['port'];
378:             }
379:         }
380: 
381:         $url .= '/';
382: 
383:         if (!isset($data['type']) || ($data['type'] == 'imap')) {
384:             $url .= urlencode($data['mailbox']);
385: 
386:             if (!empty($data['uidvalidity'])) {
387:                 $url .= ';UIDVALIDITY=' . $data['uidvalidity'];
388:             }
389: 
390:             if (isset($data['search'])) {
391:                 $url .= '?' . urlencode($data['search']);
392:             } else {
393:                 if (isset($data['uid'])) {
394:                     $url .= '/;UID=' . $data['uid'];
395:                 }
396: 
397:                 if (isset($data['section'])) {
398:                     $url .= '/;SECTION=' . $data['section'];
399:                 }
400: 
401:                 if (isset($data['partial'])) {
402:                     $url .= '/;PARTIAL=' . $data['partial'];
403:                 }
404: 
405:                 if (isset($data['urlauth'])) {
406:                     $url .= '/;URLAUTH=' . $data['urlauth'];
407:                 }
408:             }
409:         }
410: 
411: 
412:         return $url;
413:     }
414: 
415:     /**
416:      * Parses a client command array to create a server command string.
417:      *
418:      * @since 1.2.0
419:      *
420:      * @param string $out         The unprocessed command string.
421:      * @param callback $callback  A callback function to use if literal data
422:      *                            is found. Two arguments are passed: the
423:      *                            command string (as built so far) and the
424:      *                            literal data. The return value should be the
425:      *                            new value for the current command string.
426:      * @param array $query        An array with the following format:
427:      * <ul>
428:      *  <li>
429:      *   Array
430:      *   <ul>
431:      *    <li>
432:      *     Array with keys 't' and 'v'
433:      *     <ul>
434:      *      <li>t: IMAP data type (Horde_Imap_Client::DATA_* constants)</li>
435:      *      <li>v: Data value</li>
436:      *     </ul>
437:      *    </li>
438:      *    <li>
439:      *     Array with only values
440:      *     <ul>
441:      *      <li>Treated as a parenthesized list</li>
442:      *     </ul>
443:      *    </li>
444:      *   </ul>
445:      *  </li>
446:      *  <li>
447:      *   Null
448:      *   <ul>
449:      *    <li>Ignored</li>
450:      *   </ul>
451:      * </li>
452:      *  <li>
453:      *   Resource
454:      *   <ul>
455:      *    <li>Treated as literal data</li>
456:      *   </ul>
457:      * </li>
458:      *  <li>
459:      *   String
460:      *   <ul>
461:      *    <li>Output as-is (raw)</li>
462:      *   </ul>
463:      *  </li>
464:      * </ul>
465:      *
466:      * @return string  The command string.
467:      */
468:     public function parseCommandArray($query, $callback = null, $out = '')
469:     {
470:         foreach ($query as $val) {
471:             if (is_null($val)) {
472:                 continue;
473:             }
474: 
475:             if (is_array($val)) {
476:                 if (isset($val['t'])) {
477:                     if ($val['t'] == Horde_Imap_Client::DATA_NUMBER) {
478:                         $out .= intval($val['v']);
479:                     } elseif (($val['t'] != Horde_Imap_Client::DATA_ATOM) &&
480:                               preg_match('/[\x80-\xff\n\r]/', $val['v'])) {
481:                         if (is_callable($callback)) {
482:                             $out = call_user_func_array($callback, array($out, $val['v']));
483:                         }
484:                     } else {
485:                         switch ($val['t']) {
486:                         case Horde_Imap_Client::DATA_ASTRING:
487:                         case Horde_Imap_Client::DATA_MAILBOX:
488:                             /* Only requires quoting if an atom-special is
489:                              * present (besides resp-specials). */
490:                             $out .= $this->escape($val['v'], preg_match('/[\x00-\x1f\x7f\(\)\{\s%\*"\\\\]/', $val['v']));
491:                             break;
492: 
493: 
494:                         case Horde_Imap_Client::DATA_ATOM:
495:                             $out .= $val['v'];
496:                             break;
497: 
498:                         case Horde_Imap_Client::DATA_STRING:
499:                             /* IMAP strings MUST be quoted. */
500:                             $out .= $this->escape($val['v'], true);
501:                             break;
502: 
503:                         case Horde_Imap_Client::DATA_DATETIME:
504:                             $out .= '"' . $val['v'] . '"';
505:                             break;
506: 
507:                         case Horde_Imap_Client::DATA_LISTMAILBOX:
508:                             $out .= $this->escape($val['v'], preg_match('/[\x00-\x1f\x7f\(\)\{\s"\\\\]/', $val['v']));
509:                             break;
510: 
511:                         case Horde_Imap_Client::DATA_NSTRING:
512:                             $out .= strlen($val['v'])
513:                                 ? $this->escape($val['v'], true)
514:                                 : 'NIL';
515:                             break;
516:                         }
517:                     }
518:                 } else {
519:                     $out = rtrim($this->parseCommandArray($val, $callback, $out . '(')) . ')';
520:                 }
521: 
522:                 $out .= ' ';
523:             } elseif (is_resource($val)) {
524:                 /* Resource indicates literal data. */
525:                 if (is_callable($callback)) {
526:                     $out = call_user_func_array($callback, array($out, $val)) . ' ';
527:                 }
528:             } else {
529:                 $out .= $val . ' ';
530:             }
531:         }
532: 
533:         return $out;
534:     }
535: 
536:     /* Internal methods. */
537: 
538:     /**
539:      * Remove all prefix text of the subject that matches the subj-leader
540:      * ABNF.
541:      *
542:      * @param string &$str       The subject string.
543:      * @param boolean $keepblob  Remove blob information?
544:      *
545:      * @return boolean  True if string was altered.
546:      */
547:     protected function _removeSubjLeader(&$str, $keepblob = false)
548:     {
549:         $ret = false;
550: 
551:         if (!strlen($str)) {
552:             return $ret;
553:         }
554: 
555:         if ($len = strspn($str, " \t")) {
556:             $str = substr($str, $len);
557:             $ret = true;
558:         }
559: 
560:         $i = 0;
561: 
562:         if (!$keepblob) {
563:             while (isset($str[$i]) && ($str[$i] == '[')) {
564:                 if (($i = $this->_removeBlob($str, $i)) === false) {
565:                     return $ret;
566:                 }
567:             }
568:         }
569: 
570:         if (stripos($str, 're', $i) === 0) {
571:             $i += 2;
572:         } elseif (stripos($str, 'fwd', $i) === 0) {
573:             $i += 3;
574:         } elseif (stripos($str, 'fw', $i) === 0) {
575:             $i += 2;
576:         } else {
577:             return $ret;
578:         }
579: 
580:         $i += strspn($str, " \t", $i);
581: 
582:         if (!$keepblob) {
583:             while (isset($str[$i]) && ($str[$i] == '[')) {
584:                 if (($i = $this->_removeBlob($str, $i)) === false) {
585:                     return $ret;
586:                 }
587:             }
588:         }
589: 
590:         if (!isset($str[$i]) || ($str[$i] != ':')) {
591:             return $ret;
592:         }
593: 
594:         $str = substr($str, ++$i);
595: 
596:         return true;
597:     }
598: 
599:     /**
600:      * Remove "[...]" text.
601:      *
602:      * @param string &$str  The subject string.
603:      *
604:      * @return boolean  True if string was altered.
605:      */
606:     protected function _removeBlob($str, $i)
607:     {
608:         if ($str[$i] != '[') {
609:             return false;
610:         }
611: 
612:         ++$i;
613: 
614:         for ($cnt = strlen($str); $i < $cnt; ++$i) {
615:             if ($str[$i] == ']') {
616:                 break;
617:             }
618: 
619:             if ($str[$i] == '[') {
620:                 return false;
621:             }
622:         }
623: 
624:         if ($i == ($cnt - 1)) {
625:             return false;
626:         }
627: 
628:         ++$i;
629: 
630:         if ($str[$i] == ' ') {
631:             ++$i;
632:         }
633: 
634:         return $i;
635:     }
636: 
637:     /**
638:      * Remove "[...]" text if it doesn't result in the subject becoming
639:      * empty.
640:      *
641:      * @param string &$str  The subject string.
642:      *
643:      * @return boolean  True if string was altered.
644:      */
645:     protected function _removeBlobWhenNonempty(&$str)
646:     {
647:         if ($str &&
648:             ($str[0] == '[') &&
649:             (($i = $this->_removeBlob($str, 0)) !== false) &&
650:             ($i != strlen($str))) {
651:             $str = substr($str, $i);
652:             return true;
653:         }
654: 
655:         return false;
656:     }
657: 
658:     /**
659:      * Remove a "[fwd: ... ]" string.
660:      *
661:      * @param string &$str  The subject string.
662:      *
663:      * @return boolean  True if string was altered.
664:      */
665:     protected function _removeSubjFwdHdr(&$str)
666:     {
667:         if ((stripos($str, '[fwd:') !== 0) || (substr($str, -1) != ']')) {
668:             return false;
669:         }
670: 
671:         $str = substr($str, 5, -1);
672:         return true;
673:     }
674: 
675: }
676: 
API documentation generated by ApiGen