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 337165 2015-07-15 09:42:08Z alan_k $ 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 */ 65require_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 */ 91class 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 * (set to UTF8 to iconv convert headers) 145 * @var mixed 146 * @access private 147 */ 148 var $_decode_headers; 149 150 151 /** 152 * Flag to determine whether to include attached messages 153 * as body in the returned object. Depends on $_include_bodies 154 * 155 * @var boolean 156 * @access private 157 */ 158 var $_rfc822_bodies; 159 160 /** 161 * Constructor. 162 * 163 * Sets up the object, initialise the variables, and splits and 164 * stores the header and body of the input. 165 * 166 * @param string The input to decode 167 * @access public 168 */ 169 function __construct($input) 170 { 171 list($header, $body) = $this->_splitBodyHeader($input); 172 173 $this->_input = $input; 174 $this->_header = $header; 175 $this->_body = $body; 176 $this->_decode_bodies = false; 177 $this->_include_bodies = true; 178 $this->_rfc822_bodies = false; 179 } 180 // BC 181 function Mail_mimeDecode($input) 182 { 183 $this->__construct($input); 184 } 185 186 187 /** 188 * Begins the decoding process. If called statically 189 * it will create an object and call the decode() method 190 * of it. 191 * 192 * @param array An array of various parameters that determine 193 * various things: 194 * include_bodies - Whether to include the body in the returned 195 * object. 196 * decode_bodies - Whether to decode the bodies 197 * of the parts. (Transfer encoding) 198 * decode_headers - Whether to decode headers, 199 * - use "UTF8//IGNORE" to convert charset. 200 * 201 * input - If called statically, this will be treated 202 * as the input 203 * @return object Decoded results 204 * @access public 205 */ 206 function decode($params = null) 207 { 208 // determine if this method has been called statically 209 $isStatic = empty($this) || !is_a($this, __CLASS__); 210 211 // Have we been called statically? 212 // If so, create an object and pass details to that. 213 if ($isStatic AND isset($params['input'])) { 214 215 $obj = new Mail_mimeDecode($params['input']); 216 $structure = $obj->decode($params); 217 218 // Called statically but no input 219 } elseif ($isStatic) { 220 return PEAR::raiseError('Called statically and no input given'); 221 222 // Called via an object 223 } else { 224 $this->_include_bodies = isset($params['include_bodies']) ? 225 $params['include_bodies'] : false; 226 $this->_decode_bodies = isset($params['decode_bodies']) ? 227 $params['decode_bodies'] : false; 228 $this->_decode_headers = isset($params['decode_headers']) ? 229 $params['decode_headers'] : false; 230 $this->_rfc822_bodies = isset($params['rfc_822bodies']) ? 231 $params['rfc_822bodies'] : false; 232 233 if (is_string($this->_decode_headers) && !function_exists('iconv')) { 234 PEAR::raiseError('header decode conversion requested, however iconv is missing'); 235 } 236 237 $structure = $this->_decode($this->_header, $this->_body); 238 if ($structure === false) { 239 $structure = $this->raiseError($this->_error); 240 } 241 } 242 243 return $structure; 244 } 245 246 /** 247 * Performs the decoding. Decodes the body string passed to it 248 * If it finds certain content-types it will call itself in a 249 * recursive fashion 250 * 251 * @param string Header section 252 * @param string Body section 253 * @return object Results of decoding process 254 * @access private 255 */ 256 function _decode($headers, $body, $default_ctype = 'text/plain') 257 { 258 $return = new stdClass; 259 $return->headers = array(); 260 $headers = $this->_parseHeaders($headers); 261 262 foreach ($headers as $value) { 263 $value['value'] = $this->_decodeHeader($value['value']); 264 if (isset($return->headers[strtolower($value['name'])]) AND !is_array($return->headers[strtolower($value['name'])])) { 265 $return->headers[strtolower($value['name'])] = array($return->headers[strtolower($value['name'])]); 266 $return->headers[strtolower($value['name'])][] = $value['value']; 267 268 } elseif (isset($return->headers[strtolower($value['name'])])) { 269 $return->headers[strtolower($value['name'])][] = $value['value']; 270 271 } else { 272 $return->headers[strtolower($value['name'])] = $value['value']; 273 } 274 } 275 276 277 foreach ($headers as $key => $value) { 278 $headers[$key]['name'] = strtolower($headers[$key]['name']); 279 switch ($headers[$key]['name']) { 280 281 case 'content-type': 282 $content_type = $this->_parseHeaderValue($headers[$key]['value']); 283 284 if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) { 285 $return->ctype_primary = $regs[1]; 286 $return->ctype_secondary = $regs[2]; 287 } 288 289 if (isset($content_type['other'])) { 290 foreach($content_type['other'] as $p_name => $p_value) { 291 $return->ctype_parameters[$p_name] = $p_value; 292 } 293 } 294 break; 295 296 case 'content-disposition': 297 $content_disposition = $this->_parseHeaderValue($headers[$key]['value']); 298 $return->disposition = $content_disposition['value']; 299 if (isset($content_disposition['other'])) { 300 foreach($content_disposition['other'] as $p_name => $p_value) { 301 $return->d_parameters[$p_name] = $p_value; 302 } 303 } 304 break; 305 306 case 'content-transfer-encoding': 307 $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']); 308 break; 309 } 310 } 311 312 if (isset($content_type)) { 313 switch (strtolower($content_type['value'])) { 314 case 'text/plain': 315 $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; 316 $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; 317 break; 318 319 case 'text/html': 320 $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; 321 $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null; 322 break; 323 324 case 'multipart/signed': // PGP 325 $parts = $this->_boundarySplit($body, $content_type['other']['boundary'], true); 326 $return->parts['msg_body'] = $parts[0]; 327 list($part_header, $part_body) = $this->_splitBodyHeader($parts[1]); 328 $return->parts['sig_hdr'] = $part_header; 329 $return->parts['sig_body'] = $part_body; 330 break; 331 332 case 'multipart/parallel': 333 case 'multipart/appledouble': // Appledouble mail 334 case 'multipart/report': // RFC1892 335 case 'multipart/signed': // PGP 336 case 'multipart/digest': 337 case 'multipart/alternative': 338 case 'multipart/related': 339 case 'multipart/relative': //#20431 - android 340 case 'multipart/mixed': 341 case 'application/vnd.wap.multipart.related': 342 if(!isset($content_type['other']['boundary'])){ 343 $this->_error = 'No boundary found for ' . $content_type['value'] . ' part'; 344 return false; 345 } 346 347 $default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain'; 348 349 $parts = $this->_boundarySplit($body, $content_type['other']['boundary']); 350 for ($i = 0; $i < count($parts); $i++) { 351 list($part_header, $part_body) = $this->_splitBodyHeader($parts[$i]); 352 $part = $this->_decode($part_header, $part_body, $default_ctype); 353 if($part === false) 354 $part = $this->raiseError($this->_error); 355 $return->parts[] = $part; 356 } 357 break; 358 359 case 'message/rfc822': 360 case 'message/delivery-status': // #bug #18693 361 if ($this->_rfc822_bodies) { 362 $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit'; 363 $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body); 364 } 365 $obj = new Mail_mimeDecode($body); 366 $return->parts[] = $obj->decode(array('include_bodies' => $this->_include_bodies, 367 'decode_bodies' => $this->_decode_bodies, 368 'decode_headers' => $this->_decode_headers)); 369 unset($obj); 370 break; 371 372 default: 373 if(!isset($content_transfer_encoding['value'])) 374 $content_transfer_encoding['value'] = '7bit'; 375 $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null; 376 break; 377 } 378 379 } else { 380 $ctype = explode('/', $default_ctype); 381 $return->ctype_primary = $ctype[0]; 382 $return->ctype_secondary = $ctype[1]; 383 $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null; 384 } 385 386 return $return; 387 } 388 389 /** 390 * Given the output of the above function, this will return an 391 * array of references to the parts, indexed by mime number. 392 * 393 * @param object $structure The structure to go through 394 * @param string $mime_number Internal use only. 395 * @return array Mime numbers 396 */ 397 function &getMimeNumbers(&$structure, $no_refs = false, $mime_number = '', $prepend = '') 398 { 399 $return = array(); 400 if (!empty($structure->parts)) { 401 if ($mime_number != '') { 402 $structure->mime_id = $prepend . $mime_number; 403 $return[$prepend . $mime_number] = &$structure; 404 } 405 for ($i = 0; $i < count($structure->parts); $i++) { 406 407 408 if (!empty($structure->headers['content-type']) AND substr(strtolower($structure->headers['content-type']), 0, 8) == 'message/') { 409 $prepend = $prepend . $mime_number . '.'; 410 $_mime_number = ''; 411 } else { 412 $_mime_number = ($mime_number == '' ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1)); 413 } 414 415 $arr = &Mail_mimeDecode::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend); 416 foreach ($arr as $key => $val) { 417 $no_refs ? $return[$key] = '' : $return[$key] = &$arr[$key]; 418 } 419 } 420 } else { 421 if ($mime_number == '') { 422 $mime_number = '1'; 423 } 424 $structure->mime_id = $prepend . $mime_number; 425 $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure; 426 } 427 428 return $return; 429 } 430 431 /** 432 * Given a string containing a header and body 433 * section, this function will split them (at the first 434 * blank line) and return them. 435 * 436 * @param string Input to split apart 437 * @return array Contains header and body section 438 * @access private 439 */ 440 function _splitBodyHeader($input) 441 { 442 if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) { 443 return array($match[1], $match[2]); 444 } 445 // bug #17325 - empty bodies are allowed. - we just check that at least one line 446 // of headers exist.. 447 if (count(explode("\n",$input))) { 448 return array($input, ''); 449 } 450 $this->_error = 'Could not split header and body'; 451 return false; 452 } 453 454 /** 455 * Parse headers given in $input and return 456 * as assoc array. 457 * 458 * @param string Headers to parse 459 * @return array Contains parsed headers 460 * @access private 461 */ 462 function _parseHeaders($input) 463 { 464 465 if ($input !== '') { 466 // Unfold the input 467 $input = preg_replace("/\r?\n/", "\r\n", $input); 468 //#7065 - wrapping.. with encoded stuff.. - probably not needed, 469 // wrapping space should only get removed if the trailing item on previous line is a 470 // encoded character 471 $input = preg_replace("/=\r\n(\t| )+/", '=', $input); 472 $input = preg_replace("/\r\n(\t| )+/", ' ', $input); 473 474 $headers = explode("\r\n", trim($input)); 475 $got_start = false; 476 foreach ($headers as $value) { 477 if (!$got_start) { 478 // munge headers for mbox style from 479 if ($value[0] == '>') { 480 $value = substring($value, 1); // remove mbox > 481 } 482 if (substr($value,0,5) == 'From ') { 483 $value = 'Return-Path: ' . substr($value, 5); 484 } else { 485 $got_start = true; 486 } 487 } 488 489 $hdr_name = substr($value, 0, $pos = strpos($value, ':')); 490 $hdr_value = substr($value, $pos+1); 491 if($hdr_value[0] == ' ') { 492 $hdr_value = substr($hdr_value, 1); 493 } 494 495 $return[] = array( 496 'name' => $hdr_name, 497 'value' => $hdr_value 498 ); 499 } 500 } else { 501 $return = array(); 502 } 503 504 return $return; 505 } 506 507 /** 508 * Function to parse a header value, 509 * extract first part, and any secondary 510 * parts (after ;) This function is not as 511 * robust as it could be. Eg. header comments 512 * in the wrong place will probably break it. 513 * 514 * Extra things this can handle 515 * filename*0=...... 516 * filename*1=...... 517 * 518 * This is where lines are broken in, and need merging. 519 * 520 * filename*0*=ENC'lang'urlencoded data. 521 * filename*1*=ENC'lang'urlencoded data. 522 * 523 * 524 * 525 * @param string Header value to parse 526 * @return array Contains parsed result 527 * @access private 528 */ 529 function _parseHeaderValue($input) 530 { 531 if (($pos = strpos($input, ';')) === false) { 532 $input = $this->_decodeHeader($input); 533 $return['value'] = trim($input); 534 return $return; 535 } 536 537 538 539 $value = substr($input, 0, $pos); 540 $value = $this->_decodeHeader($value); 541 $return['value'] = trim($value); 542 $input = trim(substr($input, $pos+1)); 543 544 if (!strlen($input) > 0) { 545 return $return; 546 } 547 // at this point input contains xxxx=".....";zzzz="...." 548 // since we are dealing with quoted strings, we need to handle this properly.. 549 $i = 0; 550 $l = strlen($input); 551 $key = ''; 552 $val = false; // our string - including quotes.. 553 $q = false; // in quote.. 554 $lq = ''; // last quote.. 555 556 while ($i < $l) { 557 558 $c = $input[$i]; 559 //var_dump(array('i'=>$i,'c'=>$c,'q'=>$q, 'lq'=>$lq, 'key'=>$key, 'val' =>$val)); 560 561 $escaped = false; 562 if ($c == '\\') { 563 $i++; 564 if ($i == $l-1) { // end of string. 565 break; 566 } 567 $escaped = true; 568 $c = $input[$i]; 569 } 570 571 572 // state - in key.. 573 if ($val === false) { 574 if (!$escaped && $c == '=') { 575 $val = ''; 576 $key = trim($key); 577 $i++; 578 continue; 579 } 580 if (!$escaped && $c == ';') { 581 if ($key) { // a key without a value.. 582 $key= trim($key); 583 $return['other'][$key] = ''; 584 } 585 $key = ''; 586 } 587 $key .= $c; 588 $i++; 589 continue; 590 } 591 592 // state - in value.. (as $val is set..) 593 594 if ($q === false) { 595 // not in quote yet. 596 if ((!strlen($val) || $lq !== false) && $c == ' ' || $c == "\t") { 597 $i++; 598 continue; // skip leading spaces after '=' or after '"' 599 } 600 601 // do not de-quote 'xxx*= itesm.. 602 $key_is_trans = $key[strlen($key)-1] == '*'; 603 604 if (!$key_is_trans && !$escaped && ($c == '"' || $c == "'")) { 605 // start quoted area.. 606 $q = $c; 607 // in theory should not happen raw text in value part.. 608 // but we will handle it as a merged part of the string.. 609 $val = !strlen(trim($val)) ? '' : trim($val); 610 $i++; 611 continue; 612 } 613 // got end.... 614 if (!$escaped && $c == ';') { 615 616 $return['other'][$key] = trim($val); 617 $val = false; 618 $key = ''; 619 $lq = false; 620 $i++; 621 continue; 622 } 623 624 $val .= $c; 625 $i++; 626 continue; 627 } 628 629 // state - in quote.. 630 if (!$escaped && $c == $q) { // potential exit state.. 631 632 // end of quoted string.. 633 $lq = $q; 634 $q = false; 635 $i++; 636 continue; 637 } 638 639 // normal char inside of quoted string.. 640 $val.= $c; 641 $i++; 642 } 643 644 // do we have anything left.. 645 if (strlen(trim($key)) || $val !== false) { 646 647 $val = trim($val); 648 649 $return['other'][$key] = $val; 650 } 651 652 653 $clean_others = array(); 654 // merge added values. eg. *1[*] 655 foreach($return['other'] as $key =>$val) { 656 if (preg_match('/\*[0-9]+\**$/', $key)) { 657 $key = preg_replace('/(.*)\*[0-9]+(\**)$/', '\1\2', $key); 658 if (isset($clean_others[$key])) { 659 $clean_others[$key] .= $val; 660 continue; 661 } 662 663 } 664 $clean_others[$key] = $val; 665 666 } 667 668 // handle language translation of '*' ending others. 669 foreach( $clean_others as $key =>$val) { 670 if ( $key[strlen($key)-1] != '*') { 671 $clean_others[strtolower($key)] = $val; 672 continue; 673 } 674 unset($clean_others[$key]); 675 $key = substr($key,0,-1); 676 //extended-initial-value := [charset] "'" [language] "'" 677 // extended-other-values 678 $match = array(); 679 $info = preg_match("/^([^']+)'([^']*)'(.*)$/", $val, $match); 680 681 $clean_others[$key] = urldecode($match[3]); 682 $clean_others[strtolower($key)] = $clean_others[$key]; 683 $clean_others[strtolower($key).'-charset'] = $match[1]; 684 $clean_others[strtolower($key).'-language'] = $match[2]; 685 686 687 } 688 689 690 $return['other'] = $clean_others; 691 692 // decode values. 693 foreach($return['other'] as $key =>$val) { 694 $charset = isset($return['other'][$key . '-charset']) ? 695 $return['other'][$key . '-charset'] : false; 696 697 $return['other'][$key] = $this->_decodeHeader($val, $charset); 698 } 699 700 return $return; 701 } 702 703 /** 704 * This function splits the input based 705 * on the given boundary 706 * 707 * @param string Input to parse 708 * @return array Contains array of resulting mime parts 709 * @access private 710 */ 711 function _boundarySplit($input, $boundary, $eatline = false) 712 { 713 $parts = array(); 714 715 $bs_possible = substr($boundary, 2, -2); 716 $bs_check = '\"' . $bs_possible . '\"'; 717 718 if ($boundary == $bs_check) { 719 $boundary = $bs_possible; 720 } 721 // eatline is used by multipart/signed. 722 $tmp = $eatline ? 723 preg_split("/\r?\n--".preg_quote($boundary, '/')."(|--)\n/", $input) : 724 preg_split("/--".preg_quote($boundary, '/')."((?=\s)|--)/", $input); 725 726 $len = count($tmp) -1; 727 for ($i = 1; $i < $len; $i++) { 728 if (strlen(trim($tmp[$i]))) { 729 $parts[] = $tmp[$i]; 730 } 731 } 732 733 // add the last part on if it does not end with the 'closing indicator' 734 if (!empty($tmp[$len]) && strlen(trim($tmp[$len])) && $tmp[$len][0] != '-') { 735 $parts[] = $tmp[$len]; 736 } 737 return $parts; 738 } 739 740 /** 741 * Given a header, this function will decode it 742 * according to RFC2047. Probably not *exactly* 743 * conformant, but it does pass all the given 744 * examples (in RFC2047). 745 * 746 * @param string Input header value to decode 747 * @return string Decoded header value 748 * @access private 749 */ 750 function _decodeHeader($input, $default_charset=false) 751 { 752 if (!$this->_decode_headers) { 753 return $input; 754 } 755 // Remove white space between encoded-words 756 $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input); 757 758 // For each encoded-word... 759 while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) { 760 761 $encoded = $matches[1]; 762 $charset = $matches[2]; 763 $encoding = $matches[3]; 764 $text = $matches[4]; 765 766 switch (strtolower($encoding)) { 767 case 'b': 768 $text = base64_decode($text); 769 break; 770 771 case 'q': 772 $text = str_replace('_', ' ', $text); 773 preg_match_all('/=([a-f0-9]{2})/i', $text, $matches); 774 foreach($matches[1] as $value) 775 $text = str_replace('='.$value, chr(hexdec($value)), $text); 776 break; 777 } 778 if (is_string($this->_decode_headers)) { 779 $conv = @iconv($charset, $this->_decode_headers, $text); 780 $text = ($conv === false) ? $text : $conv; 781 } 782 $input = str_replace($encoded, $text, $input); 783 } 784 785 if ($default_charset && is_string($this->_decode_headers)) { 786 $conv = @iconv($charset, $this->_decode_headers, $input); 787 $input = ($conv === false) ? $input : $conv; 788 } 789 790 return $input; 791 } 792 793 /** 794 * Given a body string and an encoding type, 795 * this function will decode and return it. 796 * 797 * @param string Input body to decode 798 * @param string Encoding type to use. 799 * @return string Decoded body 800 * @access private 801 */ 802 function _decodeBody($input, $encoding = '7bit') 803 { 804 switch (strtolower($encoding)) { 805 case '7bit': 806 return $input; 807 break; 808 809 case 'quoted-printable': 810 return $this->_quotedPrintableDecode($input); 811 break; 812 813 case 'base64': 814 return base64_decode($input); 815 break; 816 817 default: 818 return $input; 819 } 820 } 821 822 /** 823 * Given a quoted-printable string, this 824 * function will decode and return it. 825 * 826 * @param string Input body to decode 827 * @return string Decoded body 828 * @access private 829 */ 830 function _quotedPrintableDecode($input) 831 { 832 // Remove soft line breaks 833 $input = preg_replace("/=\r?\n/", '', $input); 834 835 // Replace encoded characters 836 837 $cb = create_function('$matches', ' return chr(hexdec($matches[0]));'); 838 839 $input = preg_replace_callback( '/=([a-f0-9]{2})/i', $cb, $input); 840 841 return $input; 842 } 843 844 /** 845 * Checks the input for uuencoded files and returns 846 * an array of them. Can be called statically, eg: 847 * 848 * $files =& Mail_mimeDecode::uudecode($some_text); 849 * 850 * It will check for the begin 666 ... end syntax 851 * however and won't just blindly decode whatever you 852 * pass it. 853 * 854 * @param string Input body to look for attahcments in 855 * @return array Decoded bodies, filenames and permissions 856 * @access public 857 * @author Unknown 858 */ 859 function &uudecode($input) 860 { 861 // Find all uuencoded sections 862 preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches); 863 864 for ($j = 0; $j < count($matches[3]); $j++) { 865 866 $str = $matches[3][$j]; 867 $filename = $matches[2][$j]; 868 $fileperm = $matches[1][$j]; 869 870 $file = ''; 871 $str = preg_split("/\r?\n/", trim($str)); 872 $strlen = count($str); 873 874 for ($i = 0; $i < $strlen; $i++) { 875 $pos = 1; 876 $d = 0; 877 $len=(int)(((ord(substr($str[$i],0,1)) -32) - ' ') & 077); 878 879 while (($d + 3 <= $len) AND ($pos + 4 <= strlen($str[$i]))) { 880 $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); 881 $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); 882 $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); 883 $c3 = (ord(substr($str[$i],$pos+3,1)) ^ 0x20); 884 $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); 885 886 $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); 887 888 $file .= chr(((($c2 - ' ') & 077) << 6) | (($c3 - ' ') & 077)); 889 890 $pos += 4; 891 $d += 3; 892 } 893 894 if (($d + 2 <= $len) && ($pos + 3 <= strlen($str[$i]))) { 895 $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); 896 $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); 897 $c2 = (ord(substr($str[$i],$pos+2,1)) ^ 0x20); 898 $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); 899 900 $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2)); 901 902 $pos += 3; 903 $d += 2; 904 } 905 906 if (($d + 1 <= $len) && ($pos + 2 <= strlen($str[$i]))) { 907 $c0 = (ord(substr($str[$i],$pos,1)) ^ 0x20); 908 $c1 = (ord(substr($str[$i],$pos+1,1)) ^ 0x20); 909 $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4)); 910 911 } 912 } 913 $files[] = array('filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file); 914 } 915 916 return $files; 917 } 918 919 /** 920 * getSendArray() returns the arguments required for Mail::send() 921 * used to build the arguments for a mail::send() call 922 * 923 * Usage: 924 * $mailtext = Full email (for example generated by a template) 925 * $decoder = new Mail_mimeDecode($mailtext); 926 * $parts = $decoder->getSendArray(); 927 * if (!PEAR::isError($parts) { 928 * list($recipents,$headers,$body) = $parts; 929 * $mail = Mail::factory('smtp'); 930 * $mail->send($recipents,$headers,$body); 931 * } else { 932 * echo $parts->message; 933 * } 934 * @return mixed array of recipeint, headers,body or Pear_Error 935 * @access public 936 * @author Alan Knowles <alan@akbkhome.com> 937 */ 938 function getSendArray() 939 { 940 // prevent warning if this is not set 941 $this->_decode_headers = FALSE; 942 $headerlist =$this->_parseHeaders($this->_header); 943 $to = ""; 944 if (!$headerlist) { 945 return $this->raiseError("Message did not contain headers"); 946 } 947 foreach($headerlist as $item) { 948 $header[$item['name']] = $item['value']; 949 switch (strtolower($item['name'])) { 950 case "to": 951 case "cc": 952 case "bcc": 953 $to .= ",".$item['value']; 954 default: 955 break; 956 } 957 } 958 if ($to == "") { 959 return $this->raiseError("Message did not contain any recipents"); 960 } 961 $to = substr($to,1); 962 return array($to,$header,$this->_body); 963 } 964 965 /** 966 * Returns a xml copy of the output of 967 * Mail_mimeDecode::decode. Pass the output in as the 968 * argument. This function can be called statically. Eg: 969 * 970 * $output = $obj->decode(); 971 * $xml = Mail_mimeDecode::getXML($output); 972 * 973 * The DTD used for this should have been in the package. Or 974 * alternatively you can get it from cvs, or here: 975 * http://www.phpguru.org/xmail/xmail.dtd. 976 * 977 * @param object Input to convert to xml. This should be the 978 * output of the Mail_mimeDecode::decode function 979 * @return string XML version of input 980 * @access public 981 */ 982 function getXML($input) 983 { 984 $crlf = "\r\n"; 985 $output = '<?xml version=\'1.0\'?>' . $crlf . 986 '<!DOCTYPE email SYSTEM "http://www.phpguru.org/xmail/xmail.dtd">' . $crlf . 987 '<email>' . $crlf . 988 Mail_mimeDecode::_getXML($input) . 989 '</email>'; 990 991 return $output; 992 } 993 994 /** 995 * Function that does the actual conversion to xml. Does a single 996 * mimepart at a time. 997 * 998 * @param object Input to convert to xml. This is a mimepart object. 999 * It may or may not contain subparts. 1000 * @param integer Number of tabs to indent 1001 * @return string XML version of input 1002 * @access private 1003 */ 1004 function _getXML($input, $indent = 1) 1005 { 1006 $htab = "\t"; 1007 $crlf = "\r\n"; 1008 $output = ''; 1009 $headers = @(array)$input->headers; 1010 1011 foreach ($headers as $hdr_name => $hdr_value) { 1012 1013 // Multiple headers with this name 1014 if (is_array($headers[$hdr_name])) { 1015 for ($i = 0; $i < count($hdr_value); $i++) { 1016 $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value[$i], $indent); 1017 } 1018 1019 // Only one header of this sort 1020 } else { 1021 $output .= Mail_mimeDecode::_getXML_helper($hdr_name, $hdr_value, $indent); 1022 } 1023 } 1024 1025 if (!empty($input->parts)) { 1026 for ($i = 0; $i < count($input->parts); $i++) { 1027 $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf . 1028 Mail_mimeDecode::_getXML($input->parts[$i], $indent+1) . 1029 str_repeat($htab, $indent) . '</mimepart>' . $crlf; 1030 } 1031 } elseif (isset($input->body)) { 1032 $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' . 1033 $input->body . ']]></body>' . $crlf; 1034 } 1035 1036 return $output; 1037 } 1038 1039 /** 1040 * Helper function to _getXML(). Returns xml of a header. 1041 * 1042 * @param string Name of header 1043 * @param string Value of header 1044 * @param integer Number of tabs to indent 1045 * @return string XML version of input 1046 * @access private 1047 */ 1048 function _getXML_helper($hdr_name, $hdr_value, $indent) 1049 { 1050 $htab = "\t"; 1051 $crlf = "\r\n"; 1052 $return = ''; 1053 1054 $new_hdr_value = ($hdr_name != 'received') ? Mail_mimeDecode::_parseHeaderValue($hdr_value) : array('value' => $hdr_value); 1055 $new_hdr_name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name))); 1056 1057 // Sort out any parameters 1058 if (!empty($new_hdr_value['other'])) { 1059 foreach ($new_hdr_value['other'] as $paramname => $paramvalue) { 1060 $params[] = str_repeat($htab, $indent) . $htab . '<parameter>' . $crlf . 1061 str_repeat($htab, $indent) . $htab . $htab . '<paramname>' . htmlspecialchars($paramname) . '</paramname>' . $crlf . 1062 str_repeat($htab, $indent) . $htab . $htab . '<paramvalue>' . htmlspecialchars($paramvalue) . '</paramvalue>' . $crlf . 1063 str_repeat($htab, $indent) . $htab . '</parameter>' . $crlf; 1064 } 1065 1066 $params = implode('', $params); 1067 } else { 1068 $params = ''; 1069 } 1070 1071 $return = str_repeat($htab, $indent) . '<header>' . $crlf . 1072 str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name) . '</headername>' . $crlf . 1073 str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars($new_hdr_value['value']) . '</headervalue>' . $crlf . 1074 $params . 1075 str_repeat($htab, $indent) . '</header>' . $crlf; 1076 1077 return $return; 1078 } 1079 1080} // End of class 1081