Mail_Mime
[ class tree: Mail_Mime ] [ index: Mail_Mime ] [ all elements ]

Source for file mimeDecode.php

Documentation is available at mimeDecode.php

  1. <?php
  2. /**
  3.  * The Mail_mimeDecode class is used to decode mail/mime messages
  4.  *
  5.  * This class will parse a raw mime email and return
  6.  * the structure. Returned structure is similar to
  7.  * that returned by imap_fetchstructure().
  8.  *
  9.  *  +----------------------------- IMPORTANT ------------------------------+
  10.  *  | Usage of this class compared to native php extensions such as        |
  11.  *  | mailparse or imap, is slow and may be feature deficient. If available|
  12.  *  | you are STRONGLY recommended to use the php extensions.              |
  13.  *  +----------------------------------------------------------------------+
  14.  *
  15.  * Compatible with PHP versions 4 and 5
  16.  *
  17.  * LICENSE: This LICENSE is in the BSD license style.
  18.  * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
  19.  * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
  20.  * All rights reserved.
  21.  *
  22.  * Redistribution and use in source and binary forms, with or
  23.  * without modification, are permitted provided that the following
  24.  * conditions are met:
  25.  *
  26.  * - Redistributions of source code must retain the above copyright
  27.  *   notice, this list of conditions and the following disclaimer.
  28.  * - Redistributions in binary form must reproduce the above copyright
  29.  *   notice, this list of conditions and the following disclaimer in the
  30.  *   documentation and/or other materials provided with the distribution.
  31.  * - Neither the name of the authors, nor the names of its contributors
  32.  *   may be used to endorse or promote products derived from this
  33.  *   software without specific prior written permission.
  34.  *
  35.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  36.  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  37.  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  38.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  39.  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  40.  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  41.  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  42.  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  43.  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  44.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
  45.  * THE POSSIBILITY OF SUCH DAMAGE.
  46.  *
  47.  * @category   Mail
  48.  * @package    Mail_Mime
  49.  * @author     Richard Heyes  <richard@phpguru.org>
  50.  * @author     George Schlossnagle <george@omniti.com>
  51.  * @author     Cipriano Groenendal <cipri@php.net>
  52.  * @author     Sean Coates <sean@php.net>
  53.  * @copyright  2003-2006 PEAR <pear-group@php.net>
  54.  * @license    http://www.opensource.org/licenses/bsd-license.php BSD License
  55.  * @version    CVS: $Id: mimeDecode.php,v 1.48 2006/12/03 13:43:33 cipri Exp $
  56.  * @link       http://pear.php.net/package/Mail_mime
  57.  */
  58.  
  59.  
  60. /**
  61.  * require PEAR
  62.  *
  63.  * This package depends on PEAR to raise errors.
  64.  */
  65. require_once 'PEAR.php';
  66.  
  67.  
  68. /**
  69.  * The Mail_mimeDecode class is used to decode mail/mime messages
  70.  *
  71.  * This class will parse a raw mime email and return the structure.
  72.  * Returned structure is similar to that returned by imap_fetchstructure().
  73.  *
  74.  *  +----------------------------- IMPORTANT ------------------------------+
  75.  *  | Usage of this class compared to native php extensions such as        |
  76.  *  | mailparse or imap, is slow and may be feature deficient. If available|
  77.  *  | you are STRONGLY recommended to use the php extensions.              |
  78.  *  +----------------------------------------------------------------------+
  79.  *
  80.  * @category   Mail
  81.  * @package    Mail_Mime
  82.  * @author     Richard Heyes  <richard@phpguru.org>
  83.  * @author     George Schlossnagle <george@omniti.com>
  84.  * @author     Cipriano Groenendal <cipri@php.net>
  85.  * @author     Sean Coates <sean@php.net>
  86.  * @copyright  2003-2006 PEAR <pear-group@php.net>
  87.  * @license    http://www.opensource.org/licenses/bsd-license.php BSD License
  88.  * @version    Release: @package_version@
  89.  * @link       http://pear.php.net/package/Mail_mime
  90.  */
  91. class Mail_mimeDecode extends PEAR
  92. {
  93.     /**
  94.      * The raw email to decode
  95.      *
  96.      * @var    string 
  97.      * @access private
  98.      */
  99.     var $_input;
  100.  
  101.     /**
  102.      * The header part of the input
  103.      *
  104.      * @var    string 
  105.      * @access private
  106.      */
  107.     var $_header;
  108.  
  109.     /**
  110.      * The body part of the input
  111.      *
  112.      * @var    string 
  113.      * @access private
  114.      */
  115.     var $_body;
  116.  
  117.     /**
  118.      * If an error occurs, this is used to store the message
  119.      *
  120.      * @var    string 
  121.      * @access private
  122.      */
  123.     var $_error;
  124.  
  125.     /**
  126.      * Flag to determine whether to include bodies in the
  127.      * returned object.
  128.      *
  129.      * @var    boolean 
  130.      * @access private
  131.      */
  132.     var $_include_bodies;
  133.  
  134.     /**
  135.      * Flag to determine whether to decode bodies
  136.      *
  137.      * @var    boolean 
  138.      * @access private
  139.      */
  140.     var $_decode_bodies;
  141.  
  142.     /**
  143.      * Flag to determine whether to decode headers
  144.      *
  145.      * @var    boolean 
  146.      * @access private
  147.      */
  148.     var $_decode_headers;
  149.  
  150.     /**
  151.      * Constructor.
  152.      *
  153.      * Sets up the object, initialise the variables, and splits and
  154.      * stores the header and body of the input.
  155.      *
  156.      * @param string The input to decode
  157.      * @access public
  158.      */
  159.     function Mail_mimeDecode($input)
  160.     {
  161.         list($header$body)   $this->_splitBodyHeader($input);
  162.  
  163.         $this->_input          $input;
  164.         $this->_header         $header;
  165.         $this->_body           $body;
  166.         $this->_decode_bodies  = false;
  167.         $this->_include_bodies = true;
  168.     }
  169.  
  170.     /**
  171.      * Begins the decoding process. If called statically
  172.      * it will create an object and call the decode() method
  173.      * of it.
  174.      *
  175.      * @param array An array of various parameters that determine
  176.      *               various things:
  177.      *               include_bodies - Whether to include the body in the returned
  178.      *                                object.
  179.      *               decode_bodies  - Whether to decode the bodies
  180.      *                                of the parts. (Transfer encoding)
  181.      *               decode_headers - Whether to decode headers
  182.      *               input          - If called statically, this will be treated
  183.      *                                as the input
  184.      * @return object Decoded results
  185.      * @access public
  186.      */
  187.     function decode($params = null)
  188.     {
  189.         // determine if this method has been called statically
  190.         $isStatic !(isset($this&& get_class($this== __CLASS__);
  191.  
  192.         // Have we been called statically?
  193.     // If so, create an object and pass details to that.
  194.         if ($isStatic AND isset($params['input'])) {
  195.  
  196.             $obj = new Mail_mimeDecode($params['input']);
  197.             $structure $obj->decode($params);
  198.  
  199.         // Called statically but no input
  200.         elseif ($isStatic{
  201.             return PEAR::raiseError('Called statically and no input given');
  202.  
  203.         // Called via an object
  204.         else {
  205.             $this->_include_bodies = isset($params['include_bodies']?
  206.                                  $params['include_bodies': false;
  207.             $this->_decode_bodies  = isset($params['decode_bodies']?
  208.                                  $params['decode_bodies']  : false;
  209.             $this->_decode_headers = isset($params['decode_headers']?
  210.                                  $params['decode_headers': false;
  211.  
  212.             $structure $this->_decode($this->_header$this->_body);
  213.             if ($structure === false{
  214.                 $structure $this->raiseError($this->_error);
  215.             }
  216.         }
  217.  
  218.         return $structure;
  219.     }
  220.  
  221.     /**
  222.      * Performs the decoding. Decodes the body string passed to it
  223.      * If it finds certain content-types it will call itself in a
  224.      * recursive fashion
  225.      *
  226.      * @param string Header section
  227.      * @param string Body section
  228.      * @return object Results of decoding process
  229.      * @access private
  230.      */
  231.     function _decode($headers$body$default_ctype 'text/plain')
  232.     {
  233.         $return = new stdClass;
  234.         $return->headers = array();
  235.         $headers $this->_parseHeaders($headers);
  236.  
  237.         foreach ($headers as $value{
  238.             if (isset($return->headers[strtolower($value['name'])]AND !is_array($return->headers[strtolower($value['name'])])) {
  239.                 $return->headers[strtolower($value['name'])]   = array($return->headers[strtolower($value['name'])]);
  240.                 $return->headers[strtolower($value['name'])][$value['value'];
  241.  
  242.             elseif (isset($return->headers[strtolower($value['name'])])) {
  243.                 $return->headers[strtolower($value['name'])][$value['value'];
  244.  
  245.             else {
  246.                 $return->headers[strtolower($value['name'])$value['value'];
  247.             }
  248.         }
  249.  
  250.         reset($headers);
  251.         while (list($key$valueeach($headers)) {
  252.             $headers[$key]['name'strtolower($headers[$key]['name']);
  253.             switch ($headers[$key]['name']{
  254.  
  255.                 case 'content-type':
  256.                     $content_type $this->_parseHeaderValue($headers[$key]['value']);
  257.  
  258.                     if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i'$content_type['value']$regs)) {
  259.                         $return->ctype_primary   = $regs[1];
  260.                         $return->ctype_secondary = $regs[2];
  261.                     }
  262.  
  263.                     if (isset($content_type['other'])) {
  264.                         while (list($p_name$p_valueeach($content_type['other'])) {
  265.                             $return->ctype_parameters[$p_name$p_value;
  266.                         }
  267.                     }
  268.                     break;
  269.  
  270.                 case 'content-disposition':
  271.                     $content_disposition $this->_parseHeaderValue($headers[$key]['value']);
  272.                     $return->disposition   = $content_disposition['value'];
  273.                     if (isset($content_disposition['other'])) {
  274.                         while (list($p_name$p_valueeach($content_disposition['other'])) {
  275.                             $return->d_parameters[$p_name$p_value;
  276.                         }
  277.                     }
  278.                     break;
  279.  
  280.                 case 'content-transfer-encoding':
  281.                     $content_transfer_encoding $this->_parseHeaderValue($headers[$key]['value']);
  282.                     break;
  283.             }
  284.         }
  285.  
  286.         if (isset($content_type)) {
  287.             switch (strtolower($content_type['value'])) {
  288.                 case 'text/plain':
  289.                     $encoding = isset($content_transfer_encoding$content_transfer_encoding['value''7bit';
  290.                     $this->_include_bodies $return->body = ($this->_decode_bodies $this->_decodeBody($body$encoding$body: null;
  291.                     break;
  292.  
  293.                 case 'text/html':
  294.                     $encoding = isset($content_transfer_encoding$content_transfer_encoding['value''7bit';
  295.                     $this->_include_bodies $return->body = ($this->_decode_bodies $this->_decodeBody($body$encoding$body: null;
  296.                     break;
  297.  
  298.                 case 'multipart/parallel':
  299.                 case 'multipart/appledouble'// Appledouble mail
  300.                 case 'multipart/report'// RFC1892
  301.                 case 'multipart/signed'// PGP
  302.                 case 'multipart/digest':
  303.                 case 'multipart/alternative':
  304.                 case 'multipart/related':
  305.                 case 'multipart/mixed':
  306.                     if(!isset($content_type['other']['boundary'])){
  307.                         $this->_error 'No boundary found for ' $content_type['value'' part';
  308.                         return false;
  309.                     }
  310.  
  311.                     $default_ctype (strtolower($content_type['value']=== 'multipart/digest''message/rfc822' 'text/plain';
  312.  
  313.                     $parts $this->_boundarySplit($body$content_type['other']['boundary']);
  314.                     for ($i = 0; $i count($parts)$i++{
  315.                         list($part_header$part_body$this->_splitBodyHeader($parts[$i]);
  316.                         $part $this->_decode($part_header$part_body$default_ctype);
  317.                         if($part === false)
  318.                             $part $this->raiseError($this->_error);
  319.                         $return->parts[$part;
  320.                     }
  321.                     break;
  322.  
  323.                 case 'message/rfc822':
  324.                     $obj &new Mail_mimeDecode($body);
  325.                     $return->parts[$obj->decode(array('include_bodies' => $this->_include_bodies,
  326.                                                           'decode_bodies'  => $this->_decode_bodies,
  327.                                                           'decode_headers' => $this->_decode_headers));
  328.                     unset($obj);
  329.                     break;
  330.  
  331.                 default:
  332.                     if(!isset($content_transfer_encoding['value']))
  333.                         $content_transfer_encoding['value''7bit';
  334.                     $this->_include_bodies $return->body = ($this->_decode_bodies $this->_decodeBody($body$content_transfer_encoding['value']$body: null;
  335.                     break;
  336.             }
  337.  
  338.         else {
  339.             $ctype explode('/'$default_ctype);
  340.             $return->ctype_primary   = $ctype[0];
  341.             $return->ctype_secondary = $ctype[1];
  342.             $this->_include_bodies $return->body = ($this->_decode_bodies $this->_decodeBody($body$body: null;
  343.         }
  344.  
  345.         return $return;
  346.     }
  347.  
  348.     /**
  349.      * Given the output of the above function, this will return an
  350.      * array of references to the parts, indexed by mime number.
  351.      *
  352.      * @param  object $structure   The structure to go through
  353.      * @param  string $mime_number Internal use only.
  354.      * @return array               Mime numbers
  355.      */
  356.     function &getMimeNumbers(&$structure$no_refs = false$mime_number ''$prepend '')
  357.     {
  358.         $return = array();
  359.         if (!empty($structure->parts)) {
  360.             if ($mime_number != ''{
  361.                 $structure->mime_id = $prepend $mime_number;
  362.                 $return[$prepend $mime_number&$structure;
  363.             }
  364.             for ($i = 0; $i count($structure->parts)$i++{
  365.  
  366.             
  367.                 if (!empty($structure->headers['content-type']AND substr(strtolower($structure->headers['content-type'])08== 'message/'{
  368.                     $prepend      $prepend $mime_number '.';
  369.                     $_mime_number '';
  370.                 else {
  371.                     $_mime_number ($mime_number == '' $i + 1 : sprintf('%s.%s'$mime_number$i + 1));
  372.                 }
  373.  
  374.                 $arr &Mail_mimeDecode::getMimeNumbers($structure->parts[$i]$no_refs$_mime_number$prepend);
  375.                 foreach ($arr as $key => $val{
  376.                     $no_refs $return[$key'' $return[$key&$arr[$key];
  377.                 }
  378.             }
  379.         else {
  380.             if ($mime_number == ''{
  381.                 $mime_number '1';
  382.             }
  383.             $structure->mime_id = $prepend $mime_number;
  384.             $no_refs $return[$prepend $mime_number'' $return[$prepend $mime_number&$structure;
  385.         }
  386.         
  387.         return $return;
  388.     }
  389.  
  390.     /**
  391.      * Given a string containing a header and body
  392.      * section, this function will split them (at the first
  393.      * blank line) and return them.
  394.      *
  395.      * @param string Input to split apart
  396.      * @return array Contains header and body section
  397.      * @access private
  398.      */
  399.     function _splitBodyHeader($input)
  400.     {
  401.         if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s"$input$match)) {
  402.             return array($match[1]$match[2]);
  403.         }
  404.         $this->_error 'Could not split header and body';
  405.         return false;
  406.     }
  407.  
  408.     /**
  409.      * Parse headers given in $input and return
  410.      * as assoc array.
  411.      *
  412.      * @param string Headers to parse
  413.      * @return array Contains parsed headers
  414.      * @access private
  415.      */
  416.     function _parseHeaders($input)
  417.     {
  418.  
  419.         if ($input !== ''{
  420.             // Unfold the input
  421.             $input   preg_replace("/\r?\n/""\r\n"$input);
  422.             $input   preg_replace("/\r\n(\t| )+/"' '$input);
  423.             $headers explode("\r\n"trim($input));
  424.  
  425.             foreach ($headers as $value{
  426.                 $hdr_name substr($value0$pos strpos($value':'));
  427.                 $hdr_value substr($value$pos+1);
  428.                 if($hdr_value[0== ' ')
  429.                     $hdr_value substr($hdr_value1);
  430.  
  431.                 $return[= array(
  432.                                   'name'  => $hdr_name,
  433.                                   'value' => $this->_decode_headers $this->_decodeHeader($hdr_value$hdr_value
  434.                                  );
  435.             }
  436.         else {
  437.             $return = array();
  438.         }
  439.  
  440.         return $return;
  441.     }
  442.  
  443.     /**
  444.      * Function to parse a header value,
  445.      * extract first part, and any secondary
  446.      * parts (after ;) This function is not as
  447.      * robust as it could be. Eg. header comments
  448.      * in the wrong place will probably break it.
  449.      *
  450.      * @param string Header value to parse
  451.      * @return array Contains parsed result
  452.      * @access private
  453.      */
  454.     function _parseHeaderValue($input)
  455.     {
  456.  
  457.         if (($pos strpos($input';')) !== false{
  458.  
  459.             $return['value'trim(substr($input0$pos));
  460.             $input trim(substr($input$pos+1));
  461.  
  462.             if (strlen($input> 0{
  463.  
  464.                 // This splits on a semi-colon, if there's no preceeding backslash
  465.                 // Now works with quoted values; had to glue the \; breaks in PHP
  466.                 // the regex is already bordering on incomprehensible
  467.                 $splitRegex '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/';
  468.                 preg_match_all($splitRegex$input$matches);
  469.                 $parameters = array();
  470.                 for ($i=0; $i<count($matches[0])$i++{
  471.                     $param $matches[0][$i];
  472.                     while (substr($param-2== '\;'{
  473.                         $param .= $matches[0][++$i];
  474.                     }
  475.                     $parameters[$param;
  476.                 }
  477.  
  478.                 for ($i = 0; $i count($parameters)$i++{
  479.                     $param_name  trim(substr($parameters[$i]0$pos strpos($parameters[$i]'='))"'\";\t\\ ");
  480.                     $param_value trim(str_replace('\;'';'substr($parameters[$i]$pos + 1))"'\";\t\\ ");
  481.                     if ($param_value[0== '"'{
  482.                         $param_value substr($param_value1-1);
  483.                     }
  484.                     $return['other'][$param_name$param_value;
  485.                     $return['other'][strtolower($param_name)$param_value;
  486.                 }
  487.             }
  488.         else {
  489.             $return['value'trim($input);
  490.         }
  491.  
  492.         return $return;
  493.     }
  494.  
  495.     /**
  496.      * This function splits the input based
  497.      * on the given boundary
  498.      *
  499.      * @param string Input to parse
  500.      * @return array Contains array of resulting mime parts
  501.      * @access private
  502.      */
  503.     function _boundarySplit($input$boundary)
  504.     {
  505.         $parts = array();
  506.  
  507.         $bs_possible substr($boundary2-2);
  508.         $bs_check '\"' $bs_possible '\"';
  509.  
  510.         if ($boundary == $bs_check{
  511.             $boundary $bs_possible;
  512.         }
  513.  
  514.         $tmp explode('--' $boundary$input);
  515.  
  516.         for ($i = 1; $i count($tmp- 1; $i++{
  517.             $parts[$tmp[$i];
  518.         }
  519.  
  520.         return $parts;
  521.     }
  522.  
  523.     /**
  524.      * Given a header, this function will decode it
  525.      * according to RFC2047. Probably not *exactly*
  526.      * conformant, but it does pass all the given
  527.      * examples (in RFC2047).
  528.      *
  529.      * @param string Input header value to decode
  530.      * @return string Decoded header value
  531.      * @access private
  532.      */
  533.     function _decodeHeader($input)
  534.     {
  535.         // Remove white space between encoded-words
  536.         $input preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i''\1=?'$input);
  537.  
  538.         // For each encoded-word...
  539.         while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i'$input$matches)) {
  540.  
  541.             $encoded  $matches[1];
  542.             $charset  $matches[2];
  543.             $encoding $matches[3];
  544.             $text     $matches[4];
  545.  
  546.             switch (strtolower($encoding)) {
  547.                 case 'b':
  548.                     $text base64_decode($text);
  549.                     break;
  550.  
  551.                 case 'q':
  552.                     $text str_replace('_'' '$text);
  553.                     preg_match_all('/=([a-f0-9]{2})/i'$text$matches);
  554.                     foreach($matches[1as $value)
  555.                         $text str_replace('='.$valuechr(hexdec($value))$text);
  556.                     break;
  557.             }
  558.  
  559.             $input str_replace($encoded$text$input);
  560.         }
  561.  
  562.         return $input;
  563.     }
  564.  
  565.     /**
  566.      * Given a body string and an encoding type,
  567.      * this function will decode and return it.
  568.      *
  569.      * @param  string Input body to decode
  570.      * </