Overview

Packages

  • Mime

Classes

  • Horde_Mime
  • Horde_Mime_Address
  • Horde_Mime_Exception
  • Horde_Mime_Headers
  • Horde_Mime_Magic
  • Horde_Mime_Mail
  • Horde_Mime_Mdn
  • Horde_Mime_Part
  • Horde_Mime_Translation
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * The Horde_Mime:: class provides methods for dealing with various MIME (see,
  4:  * e.g., RFC 2045-2049; 2183; 2231) standards.
  5:  *
  6:  * -----
  7:  *
  8:  * This file contains code adapted from PEAR's Mail_mimeDecode library (v1.5).
  9:  *
 10:  *   http://pear.php.net/package/Mail_mime
 11:  *
 12:  * This code appears in Horde_Mime::decodeParam().
 13:  *
 14:  * This code was originally released under this license:
 15:  *
 16:  * LICENSE: This LICENSE is in the BSD license style.
 17:  * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
 18:  * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
 19:  * All rights reserved.
 20:  *
 21:  * Redistribution and use in source and binary forms, with or
 22:  * without modification, are permitted provided that the following
 23:  * conditions are met:
 24:  *
 25:  * - Redistributions of source code must retain the above copyright
 26:  *   notice, this list of conditions and the following disclaimer.
 27:  * - Redistributions in binary form must reproduce the above copyright
 28:  *   notice, this list of conditions and the following disclaimer in the
 29:  *   documentation and/or other materials provided with the distribution.
 30:  * - Neither the name of the authors, nor the names of its contributors
 31:  *   may be used to endorse or promote products derived from this
 32:  *   software without specific prior written permission.
 33:  *
 34:  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 35:  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 36:  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 37:  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 38:  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 39:  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 40:  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 41:  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 42:  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 43:  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 44:  * THE POSSIBILITY OF SUCH DAMAGE.
 45:  *
 46:  * -----
 47:  *
 48:  * This file contains code adapted from PEAR's PHP_Compat library (v1.6.0a3).
 49:  *
 50:  *   http://pear.php.net/package/PHP_Compat
 51:  *
 52:  * This code appears in Horde_Mime::_uudecode().
 53:  *
 54:  * This code was originally released under the LGPL 2.1
 55:  *
 56:  * -----
 57:  *
 58:  * Copyright 1999-2012 Horde LLC (http://www.horde.org/)
 59:  *
 60:  * See the enclosed file COPYING for license information (LGPL). If you
 61:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 62:  *
 63:  * @author   Chuck Hagenbuch <chuck@horde.org>
 64:  * @author   Michael Slusarz <slusarz@horde.org>
 65:  * @category Horde
 66:  * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
 67:  * @package  Mime
 68:  */
 69: class Horde_Mime
 70: {
 71:     /**
 72:      * The RFC defined EOL string.
 73:      *
 74:      * @var string
 75:      */
 76:     const EOL = "\r\n";
 77: 
 78:     /**
 79:      * Attempt to work around non RFC 2231-compliant MUAs by generating both
 80:      * a RFC 2047-like parameter name and also the correct RFC 2231
 81:      * parameter.  See:
 82:      * http://lists.horde.org/archives/dev/Week-of-Mon-20040426/014240.html
 83:      *
 84:      * @var boolean
 85:      */
 86:     static public $brokenRFC2231 = false;
 87: 
 88:     /**
 89:      * Use windows-1252 charset when decoding ISO-8859-1 data?
 90:      *
 91:      * @var boolean
 92:      */
 93:     static public $decodeWindows1252 = false;
 94: 
 95:     /**
 96:      * Determines if a string contains 8-bit (non US-ASCII) characters.
 97:      *
 98:      * @param string $string   The string to check.
 99:      * @param string $charset  The charset of the string. Defaults to
100:      *                         US-ASCII.
101:      *
102:      * @return boolean  True if string contains non US-ASCII characters.
103:      */
104:     static public function is8bit($string, $charset = null)
105:     {
106:         if (empty($charset)) {
107:             $charset = 'us-ascii';
108:         }
109: 
110:         /* ISO-2022-JP is a 7bit charset, but it is an 8bit representation so
111:          * it needs to be entirely encoded. */
112:         return is_string($string) &&
113:                ((stristr('iso-2022-jp', $charset) &&
114:                 (strstr($string, "\x1b\$B"))) ||
115:                 preg_match('/[\x80-\xff]/', $string));
116:     }
117: 
118:     /**
119:      * Encodes a string containing non-ASCII characters according to RFC 2047.
120:      *
121:      * @param string $text     The text to encode.
122:      * @param string $charset  The character set of the text.
123:      *
124:      * @return string  The text, encoded only if it contains non-ASCII
125:      *                 characters.
126:      */
127:     static public function encode($text, $charset)
128:     {
129:         $charset = Horde_String::lower($charset);
130: 
131:         if (($charset == 'us-ascii') || !self::is8bit($text, $charset)) {
132:             return $text;
133:         }
134: 
135:         /* Get the list of elements in the string. */
136:         $size = preg_match_all('/([^\s]+)([\s]*)/', $text, $matches, PREG_SET_ORDER);
137: 
138:         $line = '';
139: 
140:         /* Return if nothing needs to be encoded. */
141:         foreach ($matches as $key => $val) {
142:             if (self::is8bit($val[1], $charset)) {
143:                 if ((($key + 1) < $size) &&
144:                     self::is8bit($matches[$key + 1][1], $charset)) {
145:                     $line .= self::_encode($val[1] . $val[2], $charset) . ' ';
146:                 } else {
147:                     $line .= self::_encode($val[1], $charset) . $val[2];
148:                 }
149:             } else {
150:                 $line .= $val[1] . $val[2];
151:             }
152:         }
153: 
154:         return rtrim($line);
155:     }
156: 
157:     /**
158:      * Internal recursive function to RFC 2047 encode a string.
159:      *
160:      * @param string $text     The text to encode.
161:      * @param string $charset  The character set of the text.
162:      *
163:      * @return string  The text, encoded only if it contains non-ASCII
164:      *                 characters.
165:      */
166:     static protected function _encode($text, $charset)
167:     {
168:         $encoded = trim(base64_encode($text));
169:         $c_size = strlen($charset) + 7;
170: 
171:         if ((strlen($encoded) + $c_size) > 75) {
172:             $parts = explode(self::EOL, rtrim(chunk_split($encoded, intval((75 - $c_size) / 4) * 4)));
173:         } else {
174:             $parts[] = $encoded;
175:         }
176: 
177:         $p_size = count($parts);
178:         $out = '';
179: 
180:         foreach ($parts as $key => $val) {
181:             $out .= '=?' . $charset . '?b?' . $val . '?=';
182:             if ($p_size > $key + 1) {
183:                 /* RFC 2047 [2]: no encoded word can be more than 75
184:                  * characters long. If longer, you must split the word with
185:                  * CRLF SPACE. */
186:                 $out .= self::EOL . ' ';
187:             }
188:         }
189: 
190:         return $out;
191:     }
192: 
193:     /**
194:      * Encodes a line via quoted-printable encoding.
195:      *
196:      * @param string $text   The text to encode.
197:      * @param string $eol    The EOL sequence to use.
198:      * @param integer $wrap  Wrap a line at this many characters.
199:      *
200:      * @return string  The quoted-printable encoded string.
201:      */
202:     static public function quotedPrintableEncode($text, $eol = self::EOL,
203:                                                  $wrap = 76)
204:     {
205:         $output = '';
206:         $curr_length = 0;
207: 
208:         /* We need to go character by character through the data. */
209:         for ($i = 0, $length = strlen($text); $i < $length; ++$i) {
210:             $char = $text[$i];
211: 
212:             /* If we have reached the end of the line, reset counters. */
213:             if ($char == "\n") {
214:                 $output .= $eol;
215:                 $curr_length = 0;
216:                 continue;
217:             } elseif ($char == "\r") {
218:                 continue;
219:             }
220: 
221:             /* Spaces or tabs at the end of the line are NOT allowed. Also,
222:              * ASCII characters below 32 or above 126 AND 61 must be
223:              * encoded. */
224:             $ascii = ord($char);
225:             if ((($ascii === 32) &&
226:                  ($i + 1 != $length) &&
227:                  (($text[$i + 1] == "\n") || ($text[$i + 1] == "\r"))) ||
228:                 (($ascii < 32) || ($ascii > 126) || ($ascii === 61))) {
229:                 $char_len = 3;
230:                 $char = '=' . Horde_String::upper(sprintf('%02s', dechex($ascii)));
231:             } else {
232:                 $char_len = 1;
233:             }
234: 
235:             /* Lines must be $wrap characters or less. */
236:             $curr_length += $char_len;
237:             if ($curr_length > $wrap) {
238:                 $output .= '=' . $eol;
239:                 $curr_length = $char_len;
240:             }
241:             $output .= $char;
242:         }
243: 
244:         return $output;
245:     }
246: 
247:     /**
248:      * Encodes a string containing email addresses according to RFC 2047.
249:      *
250:      * This differs from encode() because it keeps email addresses legal, only
251:      * encoding the personal information.
252:      *
253:      * @param mixed $addresses   The email addresses to encode (either a
254:      *                           string or an array of addresses).
255:      * @param string $charset    The character set of the text.
256:      * @param string $defserver  The default domain to append to mailboxes.
257:      *
258:      * @return string  The text, encoded only if it contains non-ASCII
259:      *                 characters.
260:      * @throws Horde_Mime_Exception
261:      */
262:     static public function encodeAddress($addresses, $charset,
263:                                          $defserver = null)
264:     {
265:         if (!is_array($addresses)) {
266:             $addresses = trim($addresses);
267:             $addresses = Horde_Mime_Address::parseAddressList($addresses, array(
268:                 'defserver' => $defserver,
269:                 'nestgroups' => true
270:             ));
271:         }
272: 
273:         $text = array();
274:         foreach ($addresses as $addr) {
275:             $addrobs = empty($addr['groupname'])
276:                 ? array($addr)
277:                 : $addr['addresses'];
278:             $addrlist = array();
279: 
280:             foreach ($addrobs as $val) {
281:                 if (empty($val['personal'])) {
282:                     $personal = '';
283:                 } else {
284:                     if (($val['personal'][0] == '"') &&
285:                         (substr($val['personal'], -1) == '"')) {
286:                         $val['personal'] = stripslashes(substr($val['personal'], 1, -1));
287:                     }
288:                     $personal = self::encode($val['personal'], $charset);
289:                 }
290:                 $addrlist[] = Horde_Mime_Address::writeAddress($val['mailbox'], $val['host'], $personal);
291:             }
292: 
293:             $text[] = empty($addr['groupname'])
294:                 ? reset($addrlist)
295:                 : Horde_Mime_Address::writeGroupAddress($addr['groupname'], $addrlist);
296:         }
297: 
298:         return implode(', ', $text);
299:     }
300: 
301:     /**
302:      * Decodes an RFC 2047-encoded string.
303:      *
304:      * @param string $string      The text to decode.
305:      * @param string $to_charset  The charset that the text should be decoded
306:      *                            to.
307:      *
308:      * @return string  The decoded text.
309:      */
310:     static public function decode($string, $to_charset)
311:     {
312:         /* Take out any spaces between multiple encoded words. */
313:         $string = preg_replace('|\?=\s+=\?|', '?==?', $string);
314: 
315:         $out = '';
316:         $old_pos = 0;
317: 
318:         while (($pos = strpos($string, '=?', $old_pos)) !== false) {
319:             /* Save any preceding text. */
320:             $out .= substr($string, $old_pos, $pos - $old_pos);
321: 
322:             /* Search for first delimiting question mark (charset). */
323:             if (($d1 = strpos($string, '?', $pos + 2)) === false) {
324:                 break;
325:             }
326: 
327:             $orig_charset = substr($string, $pos + 2, $d1 - $pos - 2);
328:             if (self::$decodeWindows1252 &&
329:                 (Horde_String::lower($orig_charset) == 'iso-8859-1')) {
330:                 $orig_charset = 'windows-1252';
331:             }
332: 
333:             /* Search for second delimiting question mark (encoding). */
334:             if (($d2 = strpos($string, '?', $d1 + 1)) === false) {
335:                 break;
336:             }
337: 
338:             $encoding = substr($string, $d1 + 1, $d2 - $d1 - 1);
339: 
340:             /* Search for end of encoded data. */
341:             if (($end = strpos($string, '?=', $d2 + 1)) === false) {
342:                 break;
343:             }
344: 
345:             $encoded_text = substr($string, $d2 + 1, $end - $d2 - 1);
346: 
347:             switch ($encoding) {
348:             case 'Q':
349:             case 'q':
350:                 $out .= Horde_String::convertCharset(
351:                     preg_replace('/=([0-9a-f]{2})/ie', 'chr(0x\1)', str_replace('_', ' ', $encoded_text)),
352:                     $orig_charset,
353:                     $to_charset
354:                 );
355:             break;
356: 
357:             case 'B':
358:             case 'b':
359:                 $out .= Horde_String::convertCharset(
360:                     base64_decode($encoded_text),
361:                     $orig_charset,
362:                     $to_charset
363:                 );
364:             break;
365: 
366:             default:
367:                 // Ignore unknown encoding.
368:                 break;
369:             }
370: 
371:             $old_pos = $end + 2;
372:         }
373: 
374:         return $out . substr($string, $old_pos);
375:     }
376: 
377:     /**
378:      * Decodes an RFC 2047-encoded address string.
379:      *
380:      * @param string $string      The text to decode.
381:      * @param string $to_charset  The charset that the text should be decoded
382:      *                            to.
383:      *
384:      * @return string  The decoded text.
385:      * @throws Horde_Mime_Exception
386:      */
387:     static public function decodeAddrString($string, $to_charset)
388:     {
389:         $addr_list = array();
390:         foreach (Horde_Mime_Address::parseAddressList($string) as $ob) {
391:             $ob['personal'] = isset($ob['personal'])
392:                 ? self::decode($ob['personal'], $to_charset)
393:                 : '';
394:             $addr_list[] = $ob;
395:         }
396: 
397:         return Horde_Mime_Address::addrArray2String($addr_list);
398:     }
399: 
400:     /**
401:      * Encodes a MIME parameter string pursuant to RFC 2183 & 2231
402:      * (Content-Type and Content-Disposition headers).
403:      *
404:      * @param string $name     The parameter name.
405:      * @param string $val      The parameter value.
406:      * @param string $charset  The charset the text should be encoded with.
407:      * @param array $opts      Additional options:
408:      * <pre>
409:      * 'escape' - (boolean) If true, escape param values as described in
410:      *            RFC 2045 [Appendix A].
411:      *            DEFAULT: false
412:      * 'lang' - (string) The language to use when encoding.
413:      *          DEFAULT: None specified
414:      * </pre>
415:      *
416:      * @return array  The encoded parameter string.
417:      */
418:     static public function encodeParam($name, $val, $charset, $opts = array())
419:     {
420:         $encode = $wrap = false;
421:         $output = array();
422:         $curr = 0;
423: 
424:         // 2 = '=', ';'
425:         $pre_len = strlen($name) + 2;
426: 
427:         if (self::is8bit($val, $charset)) {
428:             $string = Horde_String::lower($charset) . '\'' . (empty($opts['lang']) ? '' : Horde_String::lower($opts['lang'])) . '\'' . rawurlencode($val);
429:             $encode = true;
430:             /* Account for trailing '*'. */
431:             ++$pre_len;
432:         } else {
433:             $string = $val;
434:         }
435: 
436:         if (($pre_len + strlen($string)) > 75) {
437:             /* Account for continuation '*'. */
438:             ++$pre_len;
439:             $wrap = true;
440: 
441:             while ($string) {
442:                 $chunk = 75 - $pre_len - strlen($curr);
443:                 $pos = min($chunk, strlen($string) - 1);
444: 
445:                 /* Don't split in the middle of an encoded char. */
446:                 if (($chunk == $pos) && ($pos > 2)) {
447:                     for ($i = 0; $i <= 2; ++$i) {
448:                         if ($string[$pos - $i] == '%') {
449:                             $pos -= $i + 1;
450:                             break;
451:                         }
452:                     }
453:                 }
454: 
455:                 $lines[] = substr($string, 0, $pos + 1);
456:                 $string = substr($string, $pos + 1);
457:                 ++$curr;
458:             }
459:         } else {
460:             $lines = array($string);
461:         }
462: 
463:         foreach ($lines as $i => $line) {
464:             $output[$name . (($wrap) ? ('*' . $i) : '') . (($encode) ? '*' : '')] = $line;
465:         }
466: 
467:         if (self::$brokenRFC2231 && !isset($output[$name])) {
468:             $output = array_merge(array($name => self::encode($val, $charset)), $output);
469:         }
470: 
471:         /* Escape certain characters in params (See RFC 2045 [Appendix A]). */
472:         if (!empty($opts['escape'])) {
473:             foreach (array_keys($output) as $key) {
474:                 if (strcspn($output[$key], "\11\40\"(),/:;<=>?@[\\]") != strlen($output[$key])) {
475:                     $output[$key] = '"' . addcslashes($output[$key], '\\"') . '"';
476:                 }
477:             }
478:         }
479: 
480:         return $output;
481:     }
482: 
483:     /**
484:      * Decodes a MIME parameter string pursuant to RFC 2183 & 2231
485:      * (Content-Type and Content-Disposition headers).
486:      *
487:      * @param string $type     Either 'Content-Type' or 'Content-Disposition'
488:      *                         (case-insensitive).
489:      * @param mixed $data      The text of the header or an array of
490:      *                         param name => param values.
491:      * @param string $charset  The charset the text should be decoded to.
492:      *
493:      * @return array  An array with the following entries:
494:      * <pre>
495:      * 'params' - (array) The header's parameter values.
496:      * 'val' - (string) The header's "base" value.
497:      * </pre>
498:      */
499:     static public function decodeParam($type, $data, $charset)
500:     {
501:         $convert = array();
502:         $ret = array('params' => array(), 'val' => '');
503:         $splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/';
504:         $type = Horde_String::lower($type);
505: 
506:         if (is_array($data)) {
507:             // Use dummy base values
508:             $ret['val'] = ($type == 'content-type')
509:                 ? 'text/plain'
510:                 : 'attachment';
511:             $params = $data;
512:         } else {
513:             /* This code was adapted from PEAR's Mail_mimeDecode::. */
514:             if (($pos = strpos($data, ';')) === false) {
515:                 $ret['val'] = trim($data);
516:                 return $ret;
517:             }
518: 
519:             $ret['val'] = trim(substr($data, 0, $pos));
520:             $data = trim(substr($data, ++$pos));
521:             $params = $tmp = array();
522: 
523:             if (strlen($data) > 0) {
524:                 /* This splits on a semi-colon, if there's no preceeding
525:                  * backslash. */
526:                 preg_match_all($splitRegex, $data, $matches);
527: 
528:                 for ($i = 0, $cnt = count($matches[0]); $i < $cnt; ++$i) {
529:                     $param = $matches[0][$i];
530:                     while (substr($param, -2) == '\;') {
531:                         $param .= $matches[0][++$i];
532:                     }
533:                     $tmp[] = $param;
534:                 }
535: 
536:                 for ($i = 0, $cnt = count($tmp); $i < $cnt; ++$i) {
537:                     $pos = strpos($tmp[$i], '=');
538:                     $p_name = trim(substr($tmp[$i], 0, $pos), "'\";\t\\ ");
539:                     $p_val = trim(str_replace('\;', ';', substr($tmp[$i], $pos + 1)), "'\";\t\\ ");
540:                     if (strlen($p_val) && ($p_val[0] == '"')) {
541:                         $p_val = substr($p_val, 1, -1);
542:                     }
543: 
544:                     $params[$p_name] = $p_val;
545:                 }
546:             }
547:             /* End of code adapted from PEAR's Mail_mimeDecode::. */
548:         }
549: 
550:         /* Sort the params list. Prevents us from having to manually keep
551:          * track of continuation values below. */
552:         uksort($params, 'strnatcasecmp');
553: 
554:         foreach ($params as $name => $val) {
555:             /* Asterisk at end indicates encoded value. */
556:             if (substr($name, -1) == '*') {
557:                 $name = substr($name, 0, -1);
558:                 $encode = true;
559:             } else {
560:                 $encode = false;
561:             }
562: 
563:             /* This asterisk indicates continuation parameter. */
564:             if (($pos = strrpos($name, '*')) !== false) {
565:                 $name = substr($name, 0, $pos);
566:             }
567: 
568:             if (!isset($ret['params'][$name]) ||
569:                 ($encode && !isset($convert[$name]))) {
570:                 $ret['params'][$name] = '';
571:             }
572: 
573:             $ret['params'][$name] .= $val;
574: 
575:             if ($encode) {
576:                 $convert[$name] = true;
577:             }
578:         }
579: 
580:         foreach (array_keys($convert) as $name) {
581:             $val = $ret['params'][$name];
582:             $quote = strpos($val, "'");
583:             $orig_charset = substr($val, 0, $quote);
584:             if (self::$decodeWindows1252 &&
585:                 (Horde_String::lower($orig_charset) == 'iso-8859-1')) {
586:                 $orig_charset = 'windows-1252';
587:             }
588:             /* Ignore language. */
589:             $quote = strpos($val, "'", $quote + 1);
590:             substr($val, $quote + 1);
591:             $ret['params'][$name] = Horde_String::convertCharset(urldecode(substr($val, $quote + 1)), $orig_charset, $charset);
592:         }
593: 
594:         /* MIME parameters are supposed to be encoded via RFC 2231, but many
595:          * mailers do RFC 2045 encoding instead. However, if we see at least
596:          * one RFC 2231 encoding, then assume the sending mailer knew what
597:          * it was doing. */
598:         if (empty($convert)) {
599:             foreach (array_diff(array_keys($ret['params']), array_keys($convert)) as $name) {
600:                 $ret['params'][$name] = self::decode($ret['params'][$name], $charset);
601:             }
602:         }
603: 
604:         return $ret;
605:     }
606: 
607:     /**
608:      * Generates a Message-ID string conforming to RFC 2822 [3.6.4] and the
609:      * standards outlined in 'draft-ietf-usefor-message-id-01.txt'.
610:      *
611:      * @param string  A message ID string.
612:      */
613:     static public function generateMessageId()
614:     {
615:         return '<' . strval(new Horde_Support_Guid(array('prefix' => 'Horde'))) . '>';
616:     }
617: 
618:     /**
619:      * Performs MIME ID "arithmetic" on a given ID.
620:      *
621:      * @param string $id      The MIME ID string.
622:      * @param string $action  One of the following:
623:      * <pre>
624:      * 'down' - ID of child. Note: down will first traverse to "$id.0" if
625:      *          given an ID *NOT* of the form "$id.0". If given an ID of the
626:      *          form "$id.0", down will traverse to "$id.1". This behavior
627:      *          can be avoided if 'norfc822' option is set.
628:      * 'next' - ID of next sibling.
629:      * 'prev' - ID of previous sibling.
630:      * 'up' - ID of parent. Note: up will first traverse to "$id.0" if
631:      *        given an ID *NOT* of the form "$id.0". If given an ID of the
632:      *        form "$id.0", down will traverse to "$id". This behavior can be
633:      *        avoided if 'norfc822' option is set.
634:      * </pre>
635:      * @param array $options  Additional options:
636:      * <pre>
637:      * 'count' - (integer) How many levels to traverse.
638:      *           DEFAULT: 1
639:      * 'norfc822' - (boolean) Don't traverse rfc822 sub-levels
640:      *              DEFAULT: false
641:      * </pre>
642:      *
643:      * @return mixed  The resulting ID string, or null if that ID can not
644:      *                exist.
645:      */
646:     static public function mimeIdArithmetic($id, $action, $options = array())
647:     {
648:         $pos = strrpos($id, '.');
649:         $end = ($pos === false) ? $id : substr($id, $pos + 1);
650: 
651:         switch ($action) {
652:         case 'down':
653:             if ($end == '0') {
654:                 $id = ($pos === false) ? 1 : substr_replace($id, '1', $pos + 1);
655:             } else {
656:                 $id .= empty($options['norfc822']) ? '.0' : '.1';
657:             }
658:             break;
659: 
660:         case 'next':
661:             ++$end;
662:             $id = ($pos === false) ? $end : substr_replace($id, $end, $pos + 1);
663:             break;
664: 
665:         case 'prev':
666:             if (($end == '0') ||
667:                 (empty($options['norfc822']) && ($end == '1'))) {
668:                 $id = null;
669:             } elseif ($pos === false) {
670:                 $id = --$end;
671:             } else {
672:                 $id = substr_replace($id, --$end, $pos + 1);
673:             }
674:             break;
675: 
676:         case 'up':
677:             if ($pos === false) {
678:                 $id = ($end == '0') ? null : '0';
679:             } elseif (!empty($options['norfc822']) || ($end == '0')) {
680:                 $id = substr($id, 0, $pos);
681:             } else {
682:                 $id = substr_replace($id, '0', $pos + 1);
683:             }
684:             break;
685:         }
686: 
687:         return (!is_null($id) && !empty($options['count']) && --$options['count'])
688:             ? self::mimeIdArithmetic($id, $action, $options)
689:             : $id;
690:     }
691: 
692:     /**
693:      * Determines if a given MIME ID lives underneath a base ID.
694:      *
695:      * @param string $base  The base MIME ID.
696:      * @param string $id    The MIME ID to query.
697:      *
698:      * @return boolean  Whether $id lives underneath $base.
699:      */
700:     static public function isChild($base, $id)
701:     {
702:         $base = (substr($base, -2) == '.0')
703:             ? substr($base, 0, -1)
704:             : rtrim($base, '.') . '.';
705: 
706:         return ((($base == 0) && ($id != 0)) ||
707:                 (strpos(strval($id), strval($base)) === 0));
708:     }
709: 
710:     /**
711:      * Scans $input for uuencoded data and converts it to unencoded data.
712:      *
713:      * @param string $input  The input data
714:      *
715:      * @return array  A list of arrays, with each array corresponding to
716:      *                a file in the input and containing the following keys:
717:      * <pre>
718:      * 'data' - (string) Unencoded data.
719:      * 'name' - (string) Filename.
720:      * 'perms' - (string) Octal permissions.
721:      * </pre>
722:      */
723:     static public function uudecode($input)
724:     {
725:         $data = array();
726: 
727:         /* Find all uuencoded sections. */
728:         if (preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches, PREG_SET_ORDER)) {
729:             reset($matches);
730:             while (list(,$v) = each($matches)) {
731:                 $data[] = array(
732:                     'data' => self::_uudecode($v[3]),
733:                     'name' => $v[2],
734:                     'perm' => $v[1]
735:                 );
736:             }
737:         }
738: 
739:         return $data;
740:     }
741: 
742:     /**
743:      * PHP 5's built-in convert_uudecode() is broken. Need this wrapper.
744:      *
745:      * @param string $input  UUencoded input.
746:      *
747:      * @return string  Decoded string.
748:      */
749:     static protected function _uudecode($input)
750:     {
751:         $decoded = '';
752: 
753:         foreach (explode("\n", $input) as $line) {
754:             $c = count($bytes = unpack('c*', substr(trim($line,"\r\n\t"), 1)));
755: 
756:             while ($c % 4) {
757:                 $bytes[++$c] = 0;
758:             }
759: 
760:             foreach (array_chunk($bytes, 4) as $b) {
761:                 $b0 = ($b[0] == 0x60) ? 0 : $b[0] - 0x20;
762:                 $b1 = ($b[1] == 0x60) ? 0 : $b[1] - 0x20;
763:                 $b2 = ($b[2] == 0x60) ? 0 : $b[2] - 0x20;
764:                 $b3 = ($b[3] == 0x60) ? 0 : $b[3] - 0x20;
765: 
766:                 $b0 <<= 2;
767:                 $b0 |= ($b1 >> 4) & 0x03;
768:                 $b1 <<= 4;
769:                 $b1 |= ($b2 >> 2) & 0x0F;
770:                 $b2 <<= 6;
771:                 $b2 |= $b3 & 0x3F;
772: 
773:                 $decoded .= pack('c*', $b0, $b1, $b2);
774:             }
775:         }
776: 
777:         return rtrim($decoded, "\0");
778:     }
779: 
780: }
781: 
API documentation generated by ApiGen