Overview

Namespaces

  • Jyxo
    • Beholder
      • TestCase
    • Gettext
      • Parser
    • Input
      • Chain
      • Filter
      • Validator
    • Mail
      • Email
        • Attachment
      • Parser
      • Sender
    • Rpc
      • Json
      • Xml
    • Shell
    • Spl
    • Svn
    • Time
    • Webdav
  • PHP

Classes

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