Overview

Packages

  • Jyxo_Beholder
  • Jyxo_Charset
  • Jyxo_Color
  • Jyxo_Css
  • Jyxo_ErrorHandling
  • Jyxo_FirePhp
  • Jyxo_Gettext
    • Parser
  • Jyxo_Html
  • Jyxo_Input
    • Chain
    • Filter
    • Validator
  • Jyxo_Mail
    • Email
    • Parser
    • Sender
  • Jyxo_Rpc
    • Json
    • Xml
  • Jyxo_Shell
  • Jyxo_SpamFilter
  • Jyxo_Spl
  • Jyxo_String
  • Jyxo_Svn
  • Jyxo_Time
  • Jyxo_Timer
  • Jyxo_Webdav
  • Jyxo_XmlReader
  • PHP

Classes

  • Jyxo_Mail_Parser

Exceptions

  • Jyxo_Mail_Parser_DefaultPartIdNotExistException
  • Jyxo_Mail_Parser_EmailNotExistException
  • Jyxo_Mail_Parser_Exception
  • Jyxo_Mail_Parser_PartNotExistException
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
   1: <?php
   2: 
   3: /**
   4:  * Jyxo PHP Library
   5:  *
   6:  * LICENSE
   7:  *
   8:  * This source file is subject to the new BSD license that is bundled
   9:  * with this package in the file license.txt.
  10:  * It is also available through the world-wide-web at this URL:
  11:  * https://github.com/jyxo/php/blob/master/license.txt
  12:  */
  13: 
  14: /**
  15:  * Mail parsing class.
  16:  * Based on Mail_IMAPv2 class (c) Copyright 2004-2005 Richard York
  17:  *
  18:  * @category Jyxo
  19:  * @package Jyxo_Mail
  20:  * @subpackage Parser
  21:  * @copyright Copyright (c) 2005-2011 Jyxo, s.r.o.
  22:  * @license https://github.com/jyxo/php/blob/master/license.txt
  23:  * @author Jaroslav HanslĂ­k
  24:  */
  25: class Jyxo_Mail_Parser
  26: {
  27:     /**
  28:      * Retrieve message body.
  29:      * Search for possible alternatives.
  30:      *
  31:      * @var integer
  32:      * @see Jyxo_Mail_Parser::getBody()
  33:      */
  34:     const BODY = 0;
  35: 
  36:     /**
  37:      * Retrieve body info.
  38:      *
  39:      * @var integer
  40:      * @see Jyxo_Mail_Parser::getBody()
  41:      */
  42:     const BODY_INFO = 1;
  43: 
  44:     /**
  45:      * Retrieve raw message body.
  46:      *
  47:      * @var integer
  48:      * @see Jyxo_Mail_Parser::getBody()
  49:      */
  50:     const BODY_LITERAL = 2;
  51: 
  52:     /**
  53:      * Retrieve decoded message body.
  54:      *
  55:      * @var integer
  56:      * @see Jyxo_Mail_Parser::getBody()
  57:      */
  58:     const BODY_LITERAL_DECODE = 3;
  59: 
  60:     /**
  61:      * IMAP folder connection.
  62:      *
  63:      * @var resource
  64:      */
  65:     private $connection = null;
  66: 
  67:     /**
  68:      * Message Id.
  69:      *
  70:      * @var integer
  71:      */
  72:     private $uid = null;
  73: 
  74:     /**
  75:      * Message structure.
  76:      *
  77:      * @var array
  78:      */
  79:     private $structure = array();
  80: 
  81:     /**
  82:      * Default part Id.
  83:      *
  84:      * @var integer
  85:      */
  86:     private $defaultPid = null;
  87: 
  88:     /**
  89:      * Message parts (attachments and inline parts).
  90:      *
  91:      * @var array
  92:      */
  93:     private $parts = array();
  94: 
  95:     /**
  96:      * List of part types.
  97:      *
  98:      * @var array
  99:      */
 100:     private static $dataTypes = array(
 101:         TYPETEXT => 'text',
 102:         TYPEMULTIPART => 'multipart',
 103:         TYPEMESSAGE => 'message',
 104:         TYPEAPPLICATION => 'application',
 105:         TYPEAUDIO => 'audio',
 106:         TYPEIMAGE => 'image',
 107:         TYPEVIDEO => 'video',
 108:         TYPEMODEL => 'model',
 109:         TYPEOTHER => 'other'
 110:     );
 111: 
 112:     /**
 113:      * List of encodings.
 114:      *
 115:      * @var array
 116:      */
 117:     private static $encodingTypes = array(
 118:         ENC7BIT => '7bit',
 119:         ENC8BIT => '8bit',
 120:         ENCBINARY => 'binary',
 121:         ENCBASE64 => 'base64',
 122:         ENCQUOTEDPRINTABLE => 'quoted-printable',
 123:         ENCOTHER => 'other',
 124:         6 => 'other'
 125:     );
 126: 
 127:     /**
 128:      * Creates an instance.
 129:      *
 130:      * @param resource $connection IMAP folder connection.
 131:      * @param integer $uid Message Id
 132:      */
 133:     public function __construct($connection, $uid)
 134:     {
 135:         $this->connection = $connection;
 136:         $this->uid = (int) $uid;
 137:     }
 138: 
 139:     /**
 140:      * Parses a message if not already parsed.
 141:      *
 142:      * @throws Jyxo_Mail_Parser_EmailNotExistException If no such email exists
 143:      */
 144:     private function checkIfParsed()
 145:     {
 146:         try {
 147:             if (empty($this->structure)) {
 148:                 $this->setStructure();
 149:             }
 150: 
 151:             if (empty($this->defaultPid)) {
 152:                 $this->defaultPid = $this->getDefaultPid();
 153:             }
 154:         } catch (Jyxo_Mail_Parser_EmailNotExistException $e) {
 155:             throw $e;
 156:         }
 157:     }
 158: 
 159:     /**
 160:      * Creates message structure.
 161:      *
 162:      * @param array $subparts Subparts
 163:      * @param string $parentPartId Parent Id
 164:      * @param boolean $skipPart Skip parts
 165:      * @param boolean $lastWasSigned Was the pared signed
 166:      * @throws Jyxo_Mail_Parser_EmailNotExistException If no such email exists
 167:      */
 168:     private function setStructure(array $subparts = null, $parentPartId = null, $skipPart = false, $lastWasSigned = false)
 169:     {
 170:         // First call - an object returned by the imap_fetchstructure function is returned
 171:         if (null === $subparts) {
 172:             $this->structure['obj'] = imap_fetchstructure($this->connection, $this->uid, FT_UID);
 173:             if (!$this->structure['obj']) {
 174:                 throw new Jyxo_Mail_Parser_EmailNotExistException('Email does not exist');
 175:             }
 176:         }
 177: 
 178:         // Sometimes (especially in spams) the type is missing
 179:         if (empty($this->structure['obj']->type)) {
 180:             $this->structure['obj']->type = TYPETEXT;
 181:         }
 182: 
 183:         // For situations when the body is missing but we have attachments
 184:         if (($this->structure['obj']->type != TYPETEXT)
 185:                 && ($this->structure['obj']->type != TYPEMULTIPART)) {
 186:             $temp = $this->structure['obj'];
 187: 
 188:             // Don't add a body just create the multipart container because the body wouldn't have an Id
 189:             $this->structure['obj'] = new stdClass();
 190:             $this->structure['obj']->type = TYPEMULTIPART;
 191:             $this->structure['obj']->ifsubtype = 1;
 192:             $this->structure['obj']->subtype = 'MIXED';
 193:             $this->structure['obj']->ifdescription = 0;
 194:             $this->structure['obj']->ifid = 0;
 195:             $this->structure['obj']->bytes = isset($temp->bytes) ? $temp->bytes : 0;
 196:             $this->structure['obj']->ifdisposition = 1;
 197:             $this->structure['obj']->disposition = 'inline';
 198:             $this->structure['obj']->ifdparameters = 0;
 199:             $this->structure['obj']->dparameters = array();
 200:             $this->structure['obj']->ifparameters = 0;
 201:             $this->structure['obj']->parameters = array();
 202:             $this->structure['obj']->parts = array($temp);
 203:         }
 204: 
 205:         // Deals a multipart/alternative or multipart/report problem when they are as the first part
 206:         if ((null === $subparts) && (null === $parentPartId)) {
 207:             $ftype = empty($this->structure['obj']->type)
 208:                 ? $this->getMajorMimeType(0) . '/' . strtolower($this->structure['obj']->subtype)
 209:                 : $this->getMajorMimeType($this->structure['obj']->type) . '/' . strtolower($this->structure['obj']->subtype);
 210: 
 211:             // As first they do not have any actual Id, assign a fake one 0
 212:             // if (('multipart/alternative' == $ftype) || ('multipart/report' == $ftype)) {
 213:                 $this->structure['pid'][0] = 0;
 214:                 $this->structure['ftype'][0] = $ftype;
 215:                 $this->structure['encoding'][0] = !empty($this->structure['obj']->encoding) ? self::$encodingTypes[$this->structure['obj']->encoding] : self::$encodingTypes[0];
 216:                 $this->structure['fsize'][0] = !empty($this->structure['obj']->bytes) ? $this->structure['obj']->bytes : 0;
 217:                 $this->structure['disposition'][0] = 'inline';
 218:             // }
 219:         }
 220: 
 221:         // Subparts
 222:         if (isset($this->structure['obj']->parts) || is_array($subparts)) {
 223:             if (is_array($subparts)) {
 224:                 $parts = $subparts;
 225:             } else {
 226:                 $parts = $this->structure['obj']->parts;
 227:             }
 228: 
 229:             $count = 1;
 230:             foreach ($parts as $part) {
 231:                 // Skips multipart/mixed, following multipart/alternative or multipart/report (if this part is message/rfc822), multipart/related
 232:                 // There are more problematic parts but we haven't tested them yet
 233:                 $ftype = empty($part->type)
 234:                     ? $this->getMajorMimeType(0) . '/' . strtolower($part->subtype)
 235:                     : $this->getMajorMimeType($part->type) . '/' . strtolower($part->subtype);
 236: 
 237:                 $thisIsSigned = ('multipart/signed' == $ftype);
 238:                 $skipNext = ('message/rfc822' == $ftype);
 239: 
 240:                 $no = isset($this->structure['pid']) ? count($this->structure['pid']) : 0;
 241: 
 242:                 // Skip parts fulfilling certain conditions
 243:                 if ((('multipart/mixed' == $ftype) && ($lastWasSigned || $skipPart))
 244:                         || ('multipart/signed' == $ftype)
 245:                         || ($skipPart && ('multipart/alternative' == $ftype))
 246:                         || ($skipPart && ('multipart/report' == $ftype))
 247:                         || (('multipart/related' == $ftype) && (1 === count($parts)))) {
 248:                     $skipped = true;
 249: 
 250:                     // Although this part is skipped, save is for later use (as Id we use the parent Id)
 251:                     $this->structure['pid'][$no] = $parentPartId;
 252:                     $this->structure['ftype'][$no] = $ftype;
 253:                     $this->structure['encoding'][$no] = !empty($this->structure['obj']->encoding) ? self::$encodingTypes[$this->structure['obj']->encoding] : self::$encodingTypes[0];
 254:                     $this->structure['fsize'][$no] = !empty($this->structure['obj']->bytes) ? $this->structure['obj']->bytes : 0;
 255:                     $this->structure['disposition'][$no] = 'inline';
 256:                 } else {
 257:                     $skipped = false;
 258: 
 259:                     $this->structure['pid'][$no] = (!is_array($subparts)) ? strval($count) : $parentPartId . '.' . $count;
 260:                     $this->structure['ftype'][$no] = $ftype;
 261:                     $this->structure['encoding'][$no] = !empty($part->encoding) ? self::$encodingTypes[$part->encoding] : self::$encodingTypes[0];
 262:                     $this->structure['fsize'][$no] = !empty($part->bytes) ? $part->bytes : 0;
 263: 
 264:                     // Loads parameters
 265:                     if ($part->ifdparameters) {
 266:                         foreach ($part->dparameters as $param) {
 267:                             $this->structure[strtolower($param->attribute)][$no] = strtolower($param->value);
 268:                         }
 269:                     }
 270:                     if ($part->ifparameters) {
 271:                         foreach ($part->parameters as $param) {
 272:                             $this->structure[strtolower($param->attribute)][$no] = strtolower($param->value);
 273:                         }
 274:                     }
 275: 
 276:                     // Builds a part name (can be split into multiple lines)
 277:                     if ($part->ifparameters) {
 278:                         foreach ($part->parameters as $param) {
 279:                             if (0 === stripos($param->attribute, 'name')) {
 280:                                 if (!isset($this->structure['fname'][$no])) {
 281:                                     $this->structure['fname'][$no] = $param->value;
 282:                                 } else {
 283:                                     $this->structure['fname'][$no] .= $param->value;
 284:                                 }
 285:                             }
 286:                         }
 287:                     }
 288:                     if (($part->ifdparameters)
 289:                             && ((!isset($this->structure['fname'][$no])) || (empty($this->structure['fname'][$no])))) {
 290:                         foreach ($part->dparameters as $param) {
 291:                             if (0 === stripos($param->attribute, 'filename')) {
 292:                                 if (!isset($this->structure['fname'][$no])) {
 293:                                     $this->structure['fname'][$no] = $param->value;
 294:                                 } else {
 295:                                     $this->structure['fname'][$no] .= $param->value;
 296:                                 }
 297:                             }
 298:                         }
 299:                     }
 300:                     // If a name exists, decode it
 301:                     if (isset($this->structure['fname'][$no])) {
 302:                         $this->structure['fname'][$no] = $this->decodeFilename($this->structure['fname'][$no]);
 303:                     }
 304: 
 305:                     // If the given part is message/rfc822, load its headers and use the subject as its name
 306:                     if ('message/rfc822' == $ftype) {
 307:                         $rfcHeader = $this->getHeaders($this->structure['pid'][$no]);
 308:                         $this->structure['fname'][$no] = !empty($rfcHeader['subject']) ? $rfcHeader['subject'] . '.eml' : '';
 309:                     }
 310: 
 311:                     // Part Id
 312:                     if ($part->ifid) {
 313:                         $this->structure['cid'][$no] = substr($part->id, 1, -1);
 314:                     }
 315: 
 316:                     // Attachment or inline part (sometimes we do not get the required information from the message or it's nonsense)
 317:                     list($type, $subtype) = explode('/', $ftype);
 318:                     if (($part->ifdisposition) && ('attachment' == strtolower($part->disposition))) {
 319:                         $this->structure['disposition'][$no] = 'attachment';
 320:                     } elseif ((isset($this->structure['cid'][$no])) && ('image' == $type)) {
 321:                         $this->structure['disposition'][$no] = 'inline';
 322:                     } elseif (('message' == $type) || ('application' == $type) || ('image' == $type) || ('audio' == $type)
 323:                             || ('video' == $type) || ('model' == $type) || ('other' == $type)) {
 324:                         $this->structure['disposition'][$no] = 'attachment';
 325:                     } elseif (('text' == $type) && (('html' != $subtype) && ('plain' != $subtype))) {
 326:                         $this->structure['disposition'][$no] = 'attachment';
 327:                     } elseif (('text' == $type) && (isset($this->structure['fname'][$no]))) {
 328:                         $this->structure['disposition'][$no] = 'attachment';
 329:                     } else {
 330:                         $this->structure['disposition'][$no] = 'inline';
 331:                     }
 332:                 }
 333: 
 334:                 if ((isset($part->parts)) && (is_array($part->parts))) {
 335:                     if (!$skipped) {
 336:                         $this->structure['hasAttach'][$no] = true;
 337:                     }
 338: 
 339:                     $this->setStructure($part->parts, end($this->structure['pid']), $skipNext, $thisIsSigned);
 340:                 } elseif (!$skipped) {
 341:                     $this->structure['hasAttach'][$no] = false;
 342:                 }
 343: 
 344:                 $count++;
 345:             }
 346:         } else {
 347:             // No subparts
 348: 
 349:             $this->structure['pid'][0] = 1;
 350: 
 351:             $this->structure['ftype'][0] = $this->getMajorMimeType($this->structure['obj']->type) . '/' . strtolower($this->structure['obj']->subtype);
 352: 
 353:             // If the message has only one part it should be text/plain or text/html
 354:             if (($this->structure['ftype'][0] != 'text/plain') && ($this->structure['ftype'][0] != 'text/html')) {
 355:                 $this->structure['ftype'][0] = 'text/plain';
 356:             }
 357: 
 358:             if (empty($this->structure['obj']->encoding)) {
 359:                 $this->structure['obj']->encoding = 0;
 360:             }
 361: 
 362:             $this->structure['encoding'][0] = self::$encodingTypes[$this->structure['obj']->encoding];
 363: 
 364:             if (isset($this->structure['obj']->bytes)) {
 365:                 $this->structure['fsize'][0] = $this->structure['obj']->bytes;
 366:             }
 367: 
 368:             $this->structure['disposition'][0] = 'inline';
 369:             $this->structure['hasAttach'][0] = false;
 370: 
 371:             // Walks through next parameters
 372:             if ((isset($this->structure['obj']->ifparameters)) && ($this->structure['obj']->ifparameters)) {
 373:                 foreach ($this->structure['obj']->parameters as $param) {
 374:                     $this->structure[strtolower($param->attribute)][0] = $param->value;
 375:                 }
 376:             }
 377:         }
 378:     }
 379: 
 380:     /**
 381:      * Returns default part's Id.
 382:      *
 383:      * @param string $mimeType Mime-type
 384:      * @param integer $attempt Number of retries
 385:      * @return integer
 386:      */
 387:     private function getDefaultPid($mimeType = 'text/html', $attempt = 1)
 388:     {
 389:         $mimeCheck = ('text/html' == $mimeType) ? array('text/html', 'text/plain') : array('text/plain', 'text/html');
 390: 
 391:         // Tries to find text/html or text/plain in main parts
 392:         foreach ($mimeCheck as $mime) {
 393:             $parts = array_keys($this->structure['ftype'], $mime);
 394:             foreach ($parts as $part) {
 395:                 if (('inline' == $this->structure['disposition'][$part])
 396:                         && (false === strpos($this->structure['pid'][$part], '.'))) {
 397:                     return $this->structure['pid'][$part];
 398:                 }
 399:             }
 400:         }
 401: 
 402:         // There was nothing found in the main parts, try multipart/alternative or multipart/report
 403:         $partLevel = 1;
 404:         $pidLength = 1;
 405:         foreach ($this->structure['pid'] as $partNo => $pid) {
 406:             $level = count(explode('.', $pid));
 407: 
 408:             if (!isset($multipartPid)) {
 409:                 if ((1 === $level) && (isset($this->structure['ftype'][$partNo])) && ($this->isPartMultipart($partNo, 'related'))) {
 410:                     $partLevel = 2;
 411:                     $pidLength = 3;
 412:                     continue;
 413:                 }
 414: 
 415:                 if (($level == $partLevel) && (isset($this->structure['ftype'][$partNo]))
 416:                         && (($this->isPartMultipart($partNo, 'alternative'))
 417:                         || ($this->isPartMultipart($partNo, 'report')) || ($this->isPartMultipart($partNo, 'mixed')))) {
 418:                     $multipartPid = $pid;
 419:                     continue;
 420:                 }
 421:             }
 422: 
 423:             if ((isset($multipartPid)) && ($level == ($partLevel + 1))
 424:                     && ($this->structure['ftype'][$partNo] == $mimeType) && ($multipartPid == substr($pid, 0, $pidLength))) {
 425:                 return $pid;
 426:             }
 427:         }
 428: 
 429:         // Nothing was found, try next possible type
 430:         if (1 === $attempt) {
 431:             if ('text/html' == $mimeType) {
 432:                 return $this->getDefaultPid('text/plain', 2);
 433:             } else {
 434:                 return $this->getDefaultPid('text/html', 2);
 435:             }
 436:         } else {
 437:             // There should be a default part found in every mail; this is because of spams that are often in wrong format
 438:             return 1;
 439:             // throw new Jyxo_Mail_Parser_DefaultPartIdNotExistException('Default part Id does no exist.');
 440:         }
 441:     }
 442: 
 443:     /**
 444:      * Returns headers.
 445:      *
 446:      * @param string $pid Part Id
 447:      * @return array
 448:      * @throws Jyxo_Mail_Parser_EmailNotExistException If no such email exists
 449:      */
 450:     public function getHeaders($pid = null)
 451:     {
 452:         // Parses headers
 453:         $rawHeaders = $this->getRawHeaders($pid);
 454:         if (null === $pid) {
 455:             $msgno = imap_msgno($this->connection, $this->uid);
 456:             if (0 === $msgno) {
 457:                 throw new Jyxo_Mail_Parser_EmailNotExistException('Email does not exist');
 458:             }
 459:             $headerInfo = imap_headerinfo($this->connection, $msgno);
 460:         } else {
 461:             $headerInfo = imap_rfc822_parse_headers($rawHeaders);
 462:         }
 463: 
 464:         // Adds a header that the IMAP extension does not support
 465:         if (preg_match("~Disposition-Notification-To:(.+?)(?=\r?\n(?:\S|\r?\n))~is", $rawHeaders, $matches)) {
 466:             $addressList = imap_rfc822_parse_adrlist($matches[1], '');
 467:             // {''} is used because of CS rules
 468:             $headerInfo->{'disposition_notification_toaddress'} = substr(trim($matches[1]), 0, 1024);
 469:             $headerInfo->{'disposition_notification_to'} = array($addressList[0]);
 470:         }
 471: 
 472:         $headers = array();
 473:         static $mimeHeaders = array(
 474:             'toaddress', 'ccaddress', 'bccaddress', 'fromaddress', 'reply_toaddress', 'senderaddress',
 475:             'return_pathaddress', 'subject', 'fetchfrom', 'fetchsubject', 'disposition_notification_toaddress'
 476:         );
 477:         foreach ($headerInfo as $key => $value) {
 478:             if ((!is_object($value)) && (!is_array($value))) {
 479:                 if (in_array($key, $mimeHeaders)) {
 480:                     $headers[$key] = $this->decodeMimeHeader($value);
 481:                 } else {
 482:                     $headers[$key] = $this->convertToUtf8($value);
 483:                 }
 484:             }
 485:         }
 486: 
 487:         // Adds "udate" if missing
 488:         if (!empty($headerInfo->udate)) {
 489:             $headers['udate'] = $headerInfo->udate;
 490:         } elseif (!empty($headerInfo->date)) {
 491:             $headers['udate'] = strtotime($headerInfo->date);
 492:         } else {
 493:             $headers['udate'] = time();
 494:         }
 495: 
 496:         // Parses references
 497:         $headers['references'] = explode('> <', trim($headers['references'], '<>'));
 498: 
 499:         static $types = array('to', 'cc', 'bcc', 'from', 'reply_to', 'sender', 'return_path', 'disposition_notification_to');
 500:         for ($i = 0; $i < count($types); $i++) {
 501:             $type = $types[$i];
 502:             $headers[$type] = array();
 503:             if (isset($headerInfo->$type)) {
 504:                 foreach ($headerInfo->$type as $object) {
 505:                     $newHeader = array();
 506:                     foreach ($object as $attributeName => $attributeValue) {
 507:                         if (!empty($attributeValue)) {
 508:                             $newHeader[$attributeName] = ('personal' === $attributeName)
 509:                                 ? $this->decodeMimeHeader($attributeValue)
 510:                                 : $this->convertToUtf8($attributeValue);
 511:                         }
 512:                     }
 513: 
 514:                     if (!empty($newHeader)) {
 515:                         if (isset($newHeader['mailbox'], $newHeader['host'])) {
 516:                             $newHeader['email'] = $newHeader['mailbox'] . '@' . $newHeader['host'];
 517:                         } elseif (isset($newHeader['mailbox'])) {
 518:                             $newHeader['email'] = $newHeader['mailbox'];
 519:                         } else {
 520:                             $newHeader['email'] = 'undisclosed-recipients';
 521:                         }
 522: 
 523:                         $headers[$type][] = $newHeader;
 524:                     }
 525:                 }
 526:             }
 527:         }
 528: 
 529:         // Adds X-headers
 530:         if (preg_match_all("~(X(?:[-]\w+)+):(.+?)(?=\r?\n(?:\S|\r?\n))~is", $rawHeaders, $matches) > 0) {
 531:             for ($i = 0; $i < count($matches[0]); $i++) {
 532:                 // Converts to the format used by imap_headerinfo()
 533:                 $key = str_replace('-', '_', strtolower($matches[1][$i]));
 534:                 // Removes line endings
 535:                 $value = strtr(trim($matches[2][$i]), array("\r" => '', "\n" => '', "\t" => ' '));
 536:                 $headers[$key] = $value;
 537:             }
 538:         }
 539: 
 540:         return $headers;
 541:     }
 542: 
 543:     /**
 544:      * Returns raw headers.
 545:      *
 546:      * @param string $pid Part Id
 547:      * @return string
 548:      * @throws Jyxo_Mail_Parser_PartNotExistException If there is no such
 549:      */
 550:     private function getRawHeaders($pid = null)
 551:     {
 552:         if (null === $pid) {
 553:             return imap_fetchheader($this->connection, $this->uid, FT_UID);
 554:         }
 555: 
 556:         $rawHeaders = imap_fetchbody($this->connection, $this->uid, $pid, FT_UID);
 557:         if (empty($rawHeaders)) {
 558:             throw new Jyxo_Mail_Parser_PartNotExistException('No such part exists.');
 559:         }
 560: 
 561:         $headersEnd = (false !== strpos($rawHeaders, "\n\n"))
 562:             ? strpos($rawHeaders, "\n\n")
 563:             : strpos($rawHeaders, "\n\r\n");
 564: 
 565:         return substr($rawHeaders, 0, $headersEnd);
 566:     }
 567: 
 568:     /**
 569:      * Parses message body.
 570:      *
 571:      * @param string $pid Part Id
 572:      * @param string $mimeType Default mime-type
 573:      * @param boolean $alternative Should the alternative part be used as well
 574:      * @param boolean $all Should all parts get parsed
 575:      * @throws Jyxo_Mail_Parser_EmailNotExistException If no such email exists
 576:      */
 577:     public function parseBody($pid = null, $mimeType = 'text/html', $alternative = true, $all = false)
 578:     {
 579:         try {
 580:             $this->checkIfParsed();
 581:         } catch (Jyxo_Mail_Parser_EmailNotExistException $e) {
 582:             throw $e;
 583:         }
 584: 
 585:         if (null === $pid) {
 586:             $pid = $this->defaultPid;
 587:         }
 588: 
 589:         // If only one part exists, it is already parsed
 590:         if (count($this->structure['pid']) > 1) {
 591:             $key = array_search($pid, $this->structure['pid']);
 592:             if (false !== $key) {
 593:                 if ($all) {
 594:                     $this->parseMultiparts($pid, $mimeType, 'all', 2, $alternative);
 595:                 } else {
 596:                     if ($pid == $this->defaultPid) {
 597:                         $this->parseMultiparts($pid, $mimeType, 'top', 2, $alternative);
 598:                     } elseif ('message/rfc822' == $this->structure['ftype'][$i]) {
 599:                         $this->parseMultiparts($pid, $mimeType, 'subparts', 1, $alternative);
 600:                     }
 601:                 }
 602:             }
 603:         }
 604:     }
 605: 
 606:     /**
 607:      * Parses multiple parts.
 608:      *
 609:      * @param string $pid Part Id
 610:      * @param string $mimeType Default mime-type
 611:      * @param string $lookFor What parts to look for
 612:      * @param integer $pidAdd The level of nesting
 613:      * @param boolean $getAlternative Should the alternative part be used as well
 614:      */
 615:     private function parseMultiparts($pid, $mimeType, $lookFor = 'all', $pidAdd = 1, $getAlternative = true)
 616:     {
 617:         // If the type is message/rfc822, gathers subparts that begin with the same Id
 618:         // Skips multipart/alternative or multipart/report
 619:         $excludeMime = $mimeType;
 620:         $mimeType = ('text/plain' == $excludeMime) ? 'text/html' : 'text/plain';
 621: 
 622:         $partLevel = count(explode('.', $pid));
 623:         $pidLength = strlen($pid);
 624:         foreach ($this->structure['pid'] as $partNo => $id) {
 625:             $level = count(explode('.', $this->structure['pid'][$partNo]));
 626: 
 627:             switch ($lookFor) {
 628:                 case 'all':
 629:                     $condition = true;
 630:                     break;
 631:                 case 'subparts':
 632:                     $condition = (($level == ($partLevel + 1)) &&  ($pid == substr($this->structure['pid'][$partNo], 0, $pidLength)));
 633:                     break;
 634:                 case 'top':
 635:                     // Break missing intentionally
 636:                 default:
 637:                     if ($this->isMultipart('related') || $this->isMultipart('mixed')) {
 638:                         // Top level and second level, but the same parent
 639:                         $condition = ((false === strpos($this->structure['pid'][$partNo], '.'))
 640:                             || ((2 == $level) && substr($this->defaultPid, 0, 1) == substr($this->structure['pid'][$partNo], 0, 1)));
 641:                     } else {
 642:                         // Top level
 643:                         $condition = (false === strpos($this->structure['pid'][$partNo], '.'));
 644:                     }
 645:                     break;
 646:             }
 647: 
 648:             if ($condition) {
 649:                 if (($this->isPartMultipart($partNo, 'alternative')) || ($this->isPartMultipart($partNo, 'report')) || ($this->isPartMultipart($partNo, 'mixed'))) {
 650:                     $subLevel = count(explode('.', $this->structure['pid'][$partNo]));
 651:                     foreach ($this->structure['pid'] as $multipartNo => $multipartPid) {
 652:                         // Part must begin with the last tested Id and be in the next level
 653:                         if (($this->structure['ftype'][$multipartNo] == $mimeType) && $getAlternative && ($subLevel == ($partLevel + $pidAdd))
 654:                                 && ($pid == substr($multipartPid, 0, strlen($this->structure['pid'][$partNo])))) {
 655:                             $this->addPart($partNo, 'inline');
 656:                             break;
 657:                         }
 658:                     }
 659:                 } elseif (('inline' == $this->structure['disposition'][$partNo])
 660:                         && (!$this->isPartMultipart($partNo, 'related')) && (!$this->isPartMultipart($partNo, 'mixed'))) {
 661:                     // It is inline, but not related or mixed type
 662: 
 663:                     if ((($this->structure['ftype'][$partNo] != $excludeMime) && ($pid != $this->structure['pid'][$partNo]) && ($getAlternative || !$this->isParentAlternative($partNo)))
 664:                             || (($this->structure['ftype'][$partNo] == $excludeMime) && (isset($this->structure['fname'][$partNo])) && ($pid != $this->structure['pid'][$partNo]))) {
 665:                         $this->addPart($partNo, 'inline');
 666:                     }
 667:                 } elseif ('attachment' == $this->structure['disposition'][$partNo]) {
 668:                     // It is an attachment; add to the attachment list
 669: 
 670:                     $this->addPart($partNo, 'attach');
 671:                 }
 672:             }
 673:         }
 674:     }
 675: 
 676:     /**
 677:      * Returns if the parent is multipart/alternative type.
 678:      *
 679:      * @param integer $partNo Part Id
 680:      * @return boolean
 681:      */
 682:     private function isParentAlternative($partNo)
 683:     {
 684:         // Multipart/alternative can be a child of only two types
 685:         if (($this->structure['ftype'][$partNo] != 'text/plain') && ($this->structure['ftype'][$partNo] != 'text/plain')) {
 686:             return false;
 687:         }
 688: 
 689:         $partId = $this->structure['pid'][$partNo];
 690:         $partLevel = count(explode('.', $partId));
 691:         if (1 === $partLevel) {
 692:             return $this->isPartMultipart(0, 'alternative');
 693:         }
 694: 
 695:         $parentId = substr($partId, 0, strrpos($partId, '.'));
 696:         for ($i = 0; $i < count($this->structure['pid']); $i++) {
 697:             // There can be multiple parts with the same Id (because we assign parent Id to parts without an own Id)
 698:             if (($parentId == $this->structure['pid'][$i]) && ($this->isPartMultipart($i, 'alternative'))) {
 699:                 return true;
 700:             }
 701:         }
 702: 
 703:         return false;
 704:     }
 705: 
 706:     /**
 707:      * Returns if the message is multipart/subtype.
 708:      *
 709:      * @param string $subtype Subtype
 710:      * @return boolean
 711:      */
 712:     private function isMultipart($subtype)
 713:     {
 714:         return (count($this->getMime(array('multipart/' . $subtype))) > 0);
 715:     }
 716: 
 717:     /**
 718:      * Returns if the given part is is multipart/subtype.
 719:      *
 720:      * @param integer $partNo Part Id
 721:      * @param string $subtype Subtype
 722:      * @return boolean
 723:      */
 724:     private function isPartMultipart($partNo, $subtype)
 725:     {
 726:         return ($this->structure['ftype'][$partNo] == ('multipart/' . $subtype));
 727:     }
 728: 
 729:     /**
 730:      * Adds a part to the list.
 731:      *
 732:      * @param integer $structureNo Part Id in the structure
 733:      * @param string $partType Part type
 734:      */
 735:     private function addPart($structureNo, $partType)
 736:     {
 737:         $fields = array('fname', 'pid', 'ftype', 'fsize', 'hasAttach', 'charset');
 738: 
 739:         $no = isset($this->parts[$partType]['pid']) ? count($this->parts[$partType]['pid']) : 0;
 740:         foreach ($fields as $field) {
 741:             if (!empty($this->structure[$field][$structureNo])) {
 742:                 $this->parts[$partType][$field][$no] = $this->structure[$field][$structureNo];
 743:             }
 744:         }
 745:     }
 746: 
 747:     /**
 748:      * Returns a part Id.
 749:      *
 750:      * @param string $pid Parent Id
 751:      * @param string $mimeType Requested mime-type
 752:      * @param string $lookFor What to look for
 753:      * @return string
 754:      */
 755:     private function getMultipartPid($pid, $mimeType, $lookFor)
 756:     {
 757:         $partLevel = count(explode('.', $pid));
 758:         $pidLength = strlen($pid);
 759:         $pidAdd = 1;
 760:         foreach ($this->structure['pid'] as $partNo => $id) {
 761:             $level = count(explode('.', $this->structure['pid'][$partNo]));
 762: 
 763:             switch ($lookFor) {
 764:                 case 'subparts':
 765:                     $condition = (($level == ($partLevel + 1)) &&  ($pid == substr($this->structure['pid'][$partNo], 0, $pidLength)));
 766:                     break;
 767:                 case 'multipart':
 768:                     $condition = (($level == ($partLevel + 1)) && ($pid == substr($this->structure['pid'][$partNo])));
 769:                     break;
 770:                 default:
 771:                     $condition = false;
 772:                     break;
 773:             }
 774: 
 775:             if ($condition) {
 776:                 if (($this->isPartMultipart($partNo, 'alternative')) || ($this->isPartMultipart($partNo, 'report')) || ($this->isPartMultipart($partNo, 'mixed'))) {
 777:                     foreach ($this->structure['pid'] as $multipartNo => $multipartPid) {
 778:                         // Part has to begin with the last tested Id and has to be in the next level
 779:                         $subLevel = count(explode('.', $this->structure['pid'][$partNo]));
 780: 
 781:                         if (($this->structure['ftype'][$multipartNo] == $mimeType) && ($subLevel == ($partLevel + $pidAdd))
 782:                                 && ($pid == substr($multipartPid, 0, strlen($this->structure['pid'][$partNo])))) {
 783:                             if (empty($this->structure['fname'][$multipartNo])) {
 784:                                 return $this->structure['pid'][$multipartNo];
 785:                             }
 786:                         } elseif (($this->isPartMultipart($multipartNo, 'alternative')) || ($this->isPartMultipart($multipartNo, 'report'))) {
 787:                             // Need to match this PID to next level in
 788:                             $pid = $this->structure['pid'][$multipartNo];
 789:                             $pidLength = strlen($pid);
 790:                             $partLevel = count(explode('.', $pid));
 791:                             $pidAdd = 2;
 792:                             continue;
 793:                         }
 794:                     }
 795:                 } elseif (('inline' == $this->structure['disposition'][$partNo])
 796:                         && (!$this->isPartMultipart($multipartNo, 'related')) && (!$this->isPartMultipart($multipartNo, 'mixed'))) {
 797:                     // It is inline, but not related or mixed type
 798: 
 799:                     if (($this->structure['ftype'][$partNo] == $mimeType) && (!isset($this->structure['fname'][$partNo]))) {
 800:                         return $this->structure['pid'][$partNo];
 801:                     }
 802:                 }
 803:             }
 804:         }
 805:     }
 806: 
 807:     /**
 808:      * Returns a list of attachments.
 809:      *
 810:      * @return array
 811:      */
 812:     public function getAttachments()
 813:     {
 814:         return isset($this->parts['attach']['pid']) ? $this->parts['attach']['pid'] : array();
 815:     }
 816: 
 817:     /**
 818:      * Returns a list of part Ids of inline parts.
 819:      *
 820:      * @return array
 821:      */
 822:     public function getInlines()
 823:     {
 824:         return isset($this->parts['inline']['pid']) ? $this->parts['inline']['pid'] : array();
 825:     }
 826: 
 827:     /**
 828:      * Returns related parts.
 829:      *
 830:      * @param string $pid Part Id
 831:      * @param array $types List of types to search for
 832:      * @param boolean $all Return all types
 833:      * @return array
 834:      * @throws Jyxo_Mail_Parser_EmailNotExistException If no such email exists
 835:      */
 836:     public function getRelatedParts($pid, array $types, $all = false)
 837:     {
 838:         try {
 839:             $this->checkIfParsed();
 840:         } catch (Jyxo_Mail_Parser_EmailNotExistException $e) {
 841:             throw $e;
 842:         }
 843: 
 844:         $related = array();
 845:         if (!empty($this->structure['pid'])) {
 846:             // Deals a problem with multipart/alternative and multipart/report, when they are as the first part and don't have any real Ids (they have a fake Id 0 assigned then)
 847:             if (0 === $pid) {
 848:                 for ($i = 1; $i < count($this->structure['pid']); $i++) {
 849:                     // Subparts do not contain a dot because they are in the first level
 850:                     if ((false === strpos($this->structure['pid'][$i], '.'))
 851:                             && (($all) || (in_array($this->structure['ftype'][$i], $types)))) {
 852:                         $related['pid'][] = $this->structure['pid'][$i];
 853:                         $related['ftype'][] = $this->structure['ftype'][$i];
 854:                     }
 855:                 }
 856:             } else {
 857:                 $level = count(explode('.', $pid));
 858:                 foreach ($this->structure['pid'] as $i => $rpid) {
 859:                     // Part is one level deeper and the first number equals to the parent
 860:                     if ((count(explode('.', $rpid)) == ($level + 1)) && ($pid == substr($rpid, 0, strrpos($rpid, '.')))) {
 861:                         if (($all) || (in_array($this->structure['ftype'][$i], $types))) {
 862:                             $related['pid'][] = $this->structure['pid'][$i];
 863:                             $related['ftype'][] = $this->structure['ftype'][$i];
 864:                         }
 865:                     }
 866:                 }
 867:             }
 868:         }
 869: 
 870:         return $related;
 871:     }
 872: 
 873:     /**
 874:      * Returns all related parts.
 875:      *
 876:      * @param string $pid Part Id
 877:      * @return array
 878:      * @throws Jyxo_Mail_Parser_EmailNotExistException If no such email exists
 879:      */
 880:     public function getAllRelatedParts($pid)
 881:     {
 882:         try {
 883:             return $this->getRelatedParts($pid, array(), true);
 884:         } catch (Jyxo_Mail_Parser_EmailNotExistException $e) {
 885:             throw $e;
 886:         }
 887:     }
 888: 
 889:     /**
 890:      * Returns body of the given part.
 891:      *
 892:      * @param string $pid Part Id
 893:      * @param integer $mode Body return mode
 894:      * @param string $mimeType Requested mime-type
 895:      * @param integer $attempt Number of retries
 896:      * @return array
 897:      * @throws Jyxo_Mail_Parser_EmailNotExistException If no such email exists
 898:      * @throws Jyxo_Mail_Parser_PartNotExistException If no such part exists
 899:      */
 900:     public function getBody($pid = '1', $mode = self::BODY, $mimeType = 'text/html', $attempt = 1)
 901:     {
 902:         try {
 903:             $this->checkIfParsed();
 904:         } catch (Jyxo_Mail_Parser_EmailNotExistException $e) {
 905:             throw $e;
 906:         }
 907: 
 908:         $key = array_search($pid, $this->structure['pid']);
 909:         if (false === $key) {
 910:             throw new Jyxo_Mail_Parser_PartNotExistException('Requested part does not exist');
 911:         }
 912: 
 913:         $output['encoding'] = $this->structure['encoding'][$key];
 914:         $output['type'] = $this->structure['ftype'][$key];
 915:         $output['size'] = $this->structure['fsize'][$key];
 916: 
 917:         if (isset($this->structure['fname'][$key])) {
 918:             $output['filename'] = $this->structure['fname'][$key];
 919:         }
 920:         if (isset($this->structure['charset'][$key])) {
 921:             $output['charset'] = $this->structure['charset'][$key];
 922:         }
 923:         if (isset($this->structure['cid'][$key])) {
 924:             $output['cid'] = $this->structure['cid'][$key];
 925:         }
 926: 
 927:         if (self::BODY_INFO === $mode) {
 928:             return $output;
 929:         }
 930: 
 931:         if (self::BODY_LITERAL === $mode) {
 932:             $output['content'] = imap_fetchbody($this->connection, $this->uid, $pid, FT_UID);
 933:             return $output;
 934:         }
 935: 
 936:         if (self::BODY_LITERAL_DECODE === $mode) {
 937:             $output['content'] = self::decodeBody(imap_fetchbody($this->connection, $this->uid, $pid, FT_UID), $output['encoding']);
 938: 
 939:             // Textual types are converted to UTF-8
 940:             if ((0 === strpos($output['type'], 'text/')) || (0 === strpos($output['type'], 'message/'))) {
 941:                 $output['content'] = $this->convertToUtf8($output['content'], isset($output['charset']) ? $output['charset'] : '');
 942:             }
 943: 
 944:             return $output;
 945:         }
 946: 
 947:         // Get a new part number
 948:         if (('message/rfc822' == $this->structure['ftype'][$key]) || ($this->isPartMultipart($key, 'related'))
 949:                 || ($this->isPartMultipart($key, 'alternative')) || ($this->isPartMultipart($key, 'report'))) {
 950: 
 951:             $newPid = ('message/rfc822' == $this->structure['ftype'][$key])
 952:                     || ($this->isPartMultipart($key, 'related'))
 953:                     || ($this->isPartMultipart($key, 'alternative'))
 954:                     || ($this->isPartMultipart($key, 'report'))
 955:                 ? $this->getMultipartPid($pid, $mimeType, 'subparts')
 956:                 : $this->getMultipartPid($pid, $mimeType, 'multipart');
 957: 
 958:             // If no type was found, try again
 959:             if (!empty($newPid)) {
 960:                 $pid = $newPid;
 961:             } elseif (empty($newPid) && ('text/html' == $mimeType)) {
 962:                 if (1 === $attempt) {
 963:                     return $this->getBody($pid, $mode, 'text/plain', 2);
 964:                 }
 965:             } elseif (empty($newPid) && ('text/plain' == $mimeType)) {
 966:                 if (1 === $attempt) {
 967:                     return $this->getBody($pid, $mode, 'text/html', 2);
 968:                 }
 969:             }
 970:         }
 971: 
 972:         if (!empty($newPid)) {
 973:             $key = array_search($pid, $this->structure['pid']);
 974:             if (false === $key) {
 975:                 throw new Jyxo_Mail_Parser_PartNotExistException('Requested part does not exist');
 976:             }
 977:         }
 978: 
 979:         $output['encoding'] = $this->structure['encoding'][$key];
 980:         $output['type'] = $this->structure['ftype'][$key];
 981:         $output['size'] = $this->structure['fsize'][$key];
 982: 
 983:         if (isset($this->structure['fname'][$key])) {
 984:             $output['filename'] = $this->structure['fname'][$key];
 985:         }
 986:         if (isset($this->structure['charset'][$key])) {
 987:             $output['charset'] = $this->structure['charset'][$key];
 988:         }
 989: 
 990:         $output['content']  = self::decodeBody(imap_fetchbody($this->connection, $this->uid, $pid, FT_UID), $output['encoding']);
 991: 
 992:         // Textual types are converted to UTF-8
 993:         if ((0 === strpos($output['type'], 'text/')) || (0 === strpos($output['type'], 'message/'))) {
 994:             $output['content'] = $this->convertToUtf8($output['content'], isset($output['charset']) ? $output['charset'] : '');
 995:         }
 996: 
 997:         return $output;
 998:     }
 999: 
1000:     /**
1001:      * Decodes body.
1002:      *
1003:      * @param string $body Body
1004:      * @param string $encoding Body encoding
1005:      * @return string
1006:      */
1007:     public static function decodeBody($body, $encoding)
1008:     {
1009:         switch ($encoding) {
1010:             case 'quoted-printable':
1011:                 return quoted_printable_decode($body);
1012:             case 'base64':
1013:                 return imap_base64($body);
1014:             default:
1015:                 return $body;
1016:         }
1017:     }
1018: 
1019:     /**
1020:      * Returns a list of part Ids of given types.
1021:      *
1022:      * @param array $types Part types
1023:      * @return array
1024:      * @throws Jyxo_Mail_Parser_EmailNotExistException If no such email exists
1025:      */
1026:     public function getMime(array $types)
1027:     {
1028:         try {
1029:             $this->checkIfParsed();
1030:         } catch (Jyxo_Mail_Parser_EmailNotExistException $e) {
1031:             throw $e;
1032:         }
1033: 
1034:         $parts = array();
1035:         if (is_array($this->structure['ftype'])) {
1036:             foreach ($types as $type) {
1037:                 foreach (array_keys($this->structure['ftype'], $type) as $key) {
1038:                     $parts[] = $this->structure['pid'][$key];
1039:                 }
1040:             }
1041:         }
1042: 
1043:         return $parts;
1044:     }
1045: 
1046:     /**
1047:      * Returns a list of part Ids of all parts except for the given types.
1048:      *
1049:      * @param array $exceptTypes Ignored part types
1050:      * @return array
1051:      * @throws Jyxo_Mail_Parser_EmailNotExistException If no such email exists
1052:      */
1053:     public function getMimeExcept(array $exceptTypes)
1054:     {
1055:         try {
1056:             $this->checkIfParsed();
1057:         } catch (Jyxo_Mail_Parser_EmailNotExistException $e) {
1058:             throw $e;
1059:         }
1060: 
1061:         $parts = array();
1062:         if (is_array($this->structure['ftype'])) {
1063:             $allExcept = array_diff($this->structure['ftype'], $types);
1064:             foreach (array_keys($allExcept) as $key) {
1065:                 $parts[] = $this->structure['pid'][$key];
1066:             }
1067:         }
1068: 
1069:         return $parts;
1070:     }
1071: 
1072:     /**
1073:      * Returns textual representation of the major mime-type.
1074:      *
1075:      * @param integer $mimetypeNo Mime-type number
1076:      * @return string
1077:      */
1078:     private function getMajorMimeType($mimetypeNo)
1079:     {
1080:         if (isset(self::$dataTypes[$mimetypeNo])) {
1081:             return self::$dataTypes[$mimetypeNo];
1082:         }
1083: 
1084:         // Type other
1085:         return self::$dataTypes[max(array_keys(self::$dataTypes))];
1086:     }
1087: 
1088:     /**
1089:      * Decodes given header.
1090:      *
1091:      * @param string $header Header contents
1092:      * @return string
1093:      */
1094:     private function decodeMimeHeader($header)
1095:     {
1096:         $headerDecoded = imap_mime_header_decode($header);
1097:         // Decode failed
1098:         if (false === $headerDecoded) {
1099:             return trim($header);
1100:         }
1101: 
1102:         $header = '';
1103:         for ($i = 0; $i < count($headerDecoded); $i++) {
1104:             $header .= $this->convertToUtf8($headerDecoded[$i]->text, $headerDecoded[$i]->charset);
1105:         }
1106:         return trim($header);
1107:     }
1108: 
1109:     /**
1110:      * Decodes attachment's name.
1111:      *
1112:      * @param string $filename Filename
1113:      * @return string
1114:      */
1115:     private function decodeFilename($filename)
1116:     {
1117:         if (preg_match('~(?P<charset>[^\']+)\'(?P<lang>[^\']*)\'(?P<filename>.+)~i', $filename, $parts)) {
1118:             $filename = $this->convertToUtf8(rawurldecode($parts['filename']), $parts['charset']);
1119:         } elseif (0 === strpos($filename, '=?')) {
1120:             $filename = $this->decodeMimeHeader($filename);
1121:         }
1122:         return $filename;
1123:     }
1124: 
1125:     /**
1126:      * Converts a string from various encodings to UTF-8.
1127:      *
1128:      * @param string $string Input string
1129:      * @param string $charset String charset
1130:      * @return string
1131:      */
1132:     private function convertToUtf8($string, $charset = '')
1133:     {
1134:         // Imap_mime_header_decode returns "default" in case of ASCII, but we make a detection for sure
1135:         if ('default' === $charset || 'us-ascii' === $charset || empty($charset)) {
1136:             $charset = Jyxo_Charset::detect($string);
1137:         }
1138: 
1139:         return Jyxo_Charset::convert2utf($string, $charset);
1140:     }
1141: }
1142: 
Jyxo PHP Library API documentation generated by ApiGen 2.3.0