1<?php 2/** 3 * The Mail_mimePart class is used to create MIME E-mail messages 4 * 5 * This class enables you to manipulate and build a mime email 6 * from the ground up. The Mail_Mime class is a userfriendly api 7 * to this class for people who aren't interested in the internals 8 * of mime mail. 9 * This class however allows full control over the email. 10 * 11 * Compatible with PHP versions 4 and 5 12 * 13 * LICENSE: This LICENSE is in the BSD license style. 14 * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org> 15 * Copyright (c) 2003-2006, PEAR <pear-group@php.net> 16 * All rights reserved. 17 * 18 * Redistribution and use in source and binary forms, with or 19 * without modification, are permitted provided that the following 20 * conditions are met: 21 * 22 * - Redistributions of source code must retain the above copyright 23 * notice, this list of conditions and the following disclaimer. 24 * - Redistributions in binary form must reproduce the above copyright 25 * notice, this list of conditions and the following disclaimer in the 26 * documentation and/or other materials provided with the distribution. 27 * - Neither the name of the authors, nor the names of its contributors 28 * may be used to endorse or promote products derived from this 29 * software without specific prior written permission. 30 * 31 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 32 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 33 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 34 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 35 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 36 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 37 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 38 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 39 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 40 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 41 * THE POSSIBILITY OF SUCH DAMAGE. 42 * 43 * @category Mail 44 * @package Mail_Mime 45 * @author Richard Heyes <richard@phpguru.org> 46 * @author Cipriano Groenendal <cipri@php.net> 47 * @author Sean Coates <sean@php.net> 48 * @author Aleksander Machniak <alec@php.net> 49 * @copyright 2003-2006 PEAR <pear-group@php.net> 50 * @license http://www.opensource.org/licenses/bsd-license.php BSD License 51 * @version CVS: $Id$ 52 * @link http://pear.php.net/package/Mail_mime 53 */ 54 55 56/** 57 * The Mail_mimePart class is used to create MIME E-mail messages 58 * 59 * This class enables you to manipulate and build a mime email 60 * from the ground up. The Mail_Mime class is a userfriendly api 61 * to this class for people who aren't interested in the internals 62 * of mime mail. 63 * This class however allows full control over the email. 64 * 65 * @category Mail 66 * @package Mail_Mime 67 * @author Richard Heyes <richard@phpguru.org> 68 * @author Cipriano Groenendal <cipri@php.net> 69 * @author Sean Coates <sean@php.net> 70 * @author Aleksander Machniak <alec@php.net> 71 * @copyright 2003-2006 PEAR <pear-group@php.net> 72 * @license http://www.opensource.org/licenses/bsd-license.php BSD License 73 * @version Release: @package_version@ 74 * @link http://pear.php.net/package/Mail_mime 75 */ 76class Mail_mimePart 77{ 78 /** 79 * The encoding type of this part 80 * 81 * @var string 82 * @access private 83 */ 84 var $_encoding; 85 86 /** 87 * An array of subparts 88 * 89 * @var array 90 * @access private 91 */ 92 var $_subparts; 93 94 /** 95 * The output of this part after being built 96 * 97 * @var string 98 * @access private 99 */ 100 var $_encoded; 101 102 /** 103 * Headers for this part 104 * 105 * @var array 106 * @access private 107 */ 108 var $_headers; 109 110 /** 111 * The body of this part (not encoded) 112 * 113 * @var string 114 * @access private 115 */ 116 var $_body; 117 118 /** 119 * The location of file with body of this part (not encoded) 120 * 121 * @var string 122 * @access private 123 */ 124 var $_body_file; 125 126 /** 127 * The end-of-line sequence 128 * 129 * @var string 130 * @access private 131 */ 132 var $_eol = "\r\n"; 133 134 135 /** 136 * Constructor. 137 * 138 * Sets up the object. 139 * 140 * @param string $body The body of the mime part if any. 141 * @param array $params An associative array of optional parameters: 142 * content_type - The content type for this part eg multipart/mixed 143 * encoding - The encoding to use, 7bit, 8bit, 144 * base64, or quoted-printable 145 * charset - Content character set 146 * cid - Content ID to apply 147 * disposition - Content disposition, inline or attachment 148 * filename - Filename parameter for content disposition 149 * description - Content description 150 * name_encoding - Encoding of the attachment name (Content-Type) 151 * By default filenames are encoded using RFC2231 152 * Here you can set RFC2047 encoding (quoted-printable 153 * or base64) instead 154 * filename_encoding - Encoding of the attachment filename (Content-Disposition) 155 * See 'name_encoding' 156 * headers_charset - Charset of the headers e.g. filename, description. 157 * If not set, 'charset' will be used 158 * eol - End of line sequence. Default: "\r\n" 159 * headers - Hash array with additional part headers. Array keys can be 160 * in form of <header_name>:<parameter_name> 161 * body_file - Location of file with part's body (instead of $body) 162 * 163 * @access public 164 */ 165 function Mail_mimePart($body = '', $params = array()) 166 { 167 if (!empty($params['eol'])) { 168 $this->_eol = $params['eol']; 169 } else if (defined('MAIL_MIMEPART_CRLF')) { // backward-copat. 170 $this->_eol = MAIL_MIMEPART_CRLF; 171 } 172 173 // Additional part headers 174 if (!empty($params['headers']) && is_array($params['headers'])) { 175 $headers = $params['headers']; 176 } 177 178 foreach ($params as $key => $value) { 179 switch ($key) { 180 case 'encoding': 181 $this->_encoding = $value; 182 $headers['Content-Transfer-Encoding'] = $value; 183 break; 184 185 case 'cid': 186 $headers['Content-ID'] = '<' . $value . '>'; 187 break; 188 189 case 'location': 190 $headers['Content-Location'] = $value; 191 break; 192 193 case 'body_file': 194 $this->_body_file = $value; 195 break; 196 197 // for backward compatibility 198 case 'dfilename': 199 $params['filename'] = $value; 200 break; 201 } 202 } 203 204 // Default content-type 205 if (empty($params['content_type'])) { 206 $params['content_type'] = 'text/plain'; 207 } 208 209 // Content-Type 210 $headers['Content-Type'] = $params['content_type']; 211 if (!empty($params['charset'])) { 212 $charset = "charset={$params['charset']}"; 213 // place charset parameter in the same line, if possible 214 if ((strlen($headers['Content-Type']) + strlen($charset) + 16) <= 76) { 215 $headers['Content-Type'] .= '; '; 216 } else { 217 $headers['Content-Type'] .= ';' . $this->_eol . ' '; 218 } 219 $headers['Content-Type'] .= $charset; 220 221 // Default headers charset 222 if (!isset($params['headers_charset'])) { 223 $params['headers_charset'] = $params['charset']; 224 } 225 } 226 227 // header values encoding parameters 228 $h_charset = !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII'; 229 $h_language = !empty($params['language']) ? $params['language'] : null; 230 $h_encoding = !empty($params['name_encoding']) ? $params['name_encoding'] : null; 231 232 233 if (!empty($params['filename'])) { 234 $headers['Content-Type'] .= ';' . $this->_eol; 235 $headers['Content-Type'] .= $this->_buildHeaderParam( 236 'name', $params['filename'], $h_charset, $h_language, $h_encoding 237 ); 238 } 239 240 // Content-Disposition 241 if (!empty($params['disposition'])) { 242 $headers['Content-Disposition'] = $params['disposition']; 243 if (!empty($params['filename'])) { 244 $headers['Content-Disposition'] .= ';' . $this->_eol; 245 $headers['Content-Disposition'] .= $this->_buildHeaderParam( 246 'filename', $params['filename'], $h_charset, $h_language, 247 !empty($params['filename_encoding']) ? $params['filename_encoding'] : null 248 ); 249 } 250 251 // add attachment size 252 $size = $this->_body_file ? filesize($this->_body_file) : strlen($body); 253 if ($size) { 254 $headers['Content-Disposition'] .= ';' . $this->_eol . ' size=' . $size; 255 } 256 } 257 258 if (!empty($params['description'])) { 259 $headers['Content-Description'] = $this->encodeHeader( 260 'Content-Description', $params['description'], $h_charset, $h_encoding, 261 $this->_eol 262 ); 263 } 264 265 // Search and add existing headers' parameters 266 foreach ($headers as $key => $value) { 267 $items = explode(':', $key); 268 if (count($items) == 2) { 269 $header = $items[0]; 270 $param = $items[1]; 271 if (isset($headers[$header])) { 272 $headers[$header] .= ';' . $this->_eol; 273 } 274 $headers[$header] .= $this->_buildHeaderParam( 275 $param, $value, $h_charset, $h_language, $h_encoding 276 ); 277 unset($headers[$key]); 278 } 279 } 280 281 // Default encoding 282 if (!isset($this->_encoding)) { 283 $this->_encoding = '7bit'; 284 } 285 286 // Assign stuff to member variables 287 $this->_encoded = array(); 288 $this->_headers = $headers; 289 $this->_body = $body; 290 } 291 292 /** 293 * Encodes and returns the email. Also stores 294 * it in the encoded member variable 295 * 296 * @param string $boundary Pre-defined boundary string 297 * 298 * @return An associative array containing two elements, 299 * body and headers. The headers element is itself 300 * an indexed array. On error returns PEAR error object. 301 * @access public 302 */ 303 function encode($boundary=null) 304 { 305 $encoded =& $this->_encoded; 306 307 if (count($this->_subparts)) { 308 $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); 309 $eol = $this->_eol; 310 311 $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; 312 313 $encoded['body'] = ''; 314 315 for ($i = 0; $i < count($this->_subparts); $i++) { 316 $encoded['body'] .= '--' . $boundary . $eol; 317 $tmp = $this->_subparts[$i]->encode(); 318 if ($this->_isError($tmp)) { 319 return $tmp; 320 } 321 foreach ($tmp['headers'] as $key => $value) { 322 $encoded['body'] .= $key . ': ' . $value . $eol; 323 } 324 $encoded['body'] .= $eol . $tmp['body'] . $eol; 325 } 326 327 $encoded['body'] .= '--' . $boundary . '--' . $eol; 328 329 } else if ($this->_body) { 330 $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding); 331 } else if ($this->_body_file) { 332 // Temporarily reset magic_quotes_runtime for file reads and writes 333 if ($magic_quote_setting = get_magic_quotes_runtime()) { 334 @ini_set('magic_quotes_runtime', 0); 335 } 336 $body = $this->_getEncodedDataFromFile($this->_body_file, $this->_encoding); 337 if ($magic_quote_setting) { 338 @ini_set('magic_quotes_runtime', $magic_quote_setting); 339 } 340 341 if ($this->_isError($body)) { 342 return $body; 343 } 344 $encoded['body'] = $body; 345 } else { 346 $encoded['body'] = ''; 347 } 348 349 // Add headers to $encoded 350 $encoded['headers'] =& $this->_headers; 351 352 return $encoded; 353 } 354 355 /** 356 * Encodes and saves the email into file. File must exist. 357 * Data will be appended to the file. 358 * 359 * @param string $filename Output file location 360 * @param string $boundary Pre-defined boundary string 361 * @param boolean $skip_head True if you don't want to save headers 362 * 363 * @return array An associative array containing message headers 364 * or PEAR error object 365 * @access public 366 * @since 1.6.0 367 */ 368 function encodeToFile($filename, $boundary=null, $skip_head=false) 369 { 370 if (file_exists($filename) && !is_writable($filename)) { 371 $err = $this->_raiseError('File is not writeable: ' . $filename); 372 return $err; 373 } 374 375 if (!($fh = fopen($filename, 'ab'))) { 376 $err = $this->_raiseError('Unable to open file: ' . $filename); 377 return $err; 378 } 379 380 // Temporarily reset magic_quotes_runtime for file reads and writes 381 if ($magic_quote_setting = get_magic_quotes_runtime()) { 382 @ini_set('magic_quotes_runtime', 0); 383 } 384 385 $res = $this->_encodePartToFile($fh, $boundary, $skip_head); 386 387 fclose($fh); 388 389 if ($magic_quote_setting) { 390 @ini_set('magic_quotes_runtime', $magic_quote_setting); 391 } 392 393 return $this->_isError($res) ? $res : $this->_headers; 394 } 395 396 /** 397 * Encodes given email part into file 398 * 399 * @param string $fh Output file handle 400 * @param string $boundary Pre-defined boundary string 401 * @param boolean $skip_head True if you don't want to save headers 402 * 403 * @return array True on sucess or PEAR error object 404 * @access private 405 */ 406 function _encodePartToFile($fh, $boundary=null, $skip_head=false) 407 { 408 $eol = $this->_eol; 409 410 if (count($this->_subparts)) { 411 $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime()); 412 $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\""; 413 } 414 415 if (!$skip_head) { 416 foreach ($this->_headers as $key => $value) { 417 fwrite($fh, $key . ': ' . $value . $eol); 418 } 419 $f_eol = $eol; 420 } else { 421 $f_eol = ''; 422 } 423 424 if (count($this->_subparts)) { 425 for ($i = 0; $i < count($this->_subparts); $i++) { 426 fwrite($fh, $f_eol . '--' . $boundary . $eol); 427 $res = $this->_subparts[$i]->_encodePartToFile($fh); 428 if ($this->_isError($res)) { 429 return $res; 430 } 431 $f_eol = $eol; 432 } 433 434 fwrite($fh, $eol . '--' . $boundary . '--' . $eol); 435 436 } else if ($this->_body) { 437 fwrite($fh, $f_eol . $this->_getEncodedData($this->_body, $this->_encoding)); 438 } else if ($this->_body_file) { 439 fwrite($fh, $f_eol); 440 $res = $this->_getEncodedDataFromFile( 441 $this->_body_file, $this->_encoding, $fh 442 ); 443 if ($this->_isError($res)) { 444 return $res; 445 } 446 } 447 448 return true; 449 } 450 451 /** 452 * Adds a subpart to current mime part and returns 453 * a reference to it 454 * 455 * @param string $body The body of the subpart, if any. 456 * @param array $params The parameters for the subpart, same 457 * as the $params argument for constructor. 458 * 459 * @return Mail_mimePart A reference to the part you just added. In PHP4, it is 460 * crucial if using multipart/* in your subparts that 461 * you use =& in your script when calling this function, 462 * otherwise you will not be able to add further subparts. 463 * @access public 464 */ 465 function &addSubpart($body, $params) 466 { 467 $this->_subparts[] = $part = new Mail_mimePart($body, $params); 468 return $part; 469 } 470 471 /** 472 * Returns encoded data based upon encoding passed to it 473 * 474 * @param string $data The data to encode. 475 * @param string $encoding The encoding type to use, 7bit, base64, 476 * or quoted-printable. 477 * 478 * @return string 479 * @access private 480 */ 481 function _getEncodedData($data, $encoding) 482 { 483 switch ($encoding) { 484 case 'quoted-printable': 485 return $this->_quotedPrintableEncode($data); 486 break; 487 488 case 'base64': 489 return rtrim(chunk_split(base64_encode($data), 76, $this->_eol)); 490 break; 491 492 case '8bit': 493 case '7bit': 494 default: 495 return $data; 496 } 497 } 498 499 /** 500 * Returns encoded data based upon encoding passed to it 501 * 502 * @param string $filename Data file location 503 * @param string $encoding The encoding type to use, 7bit, base64, 504 * or quoted-printable. 505 * @param resource $fh Output file handle. If set, data will be 506 * stored into it instead of returning it 507 * 508 * @return string Encoded data or PEAR error object 509 * @access private 510 */ 511 function _getEncodedDataFromFile($filename, $encoding, $fh=null) 512 { 513 if (!is_readable($filename)) { 514 $err = $this->_raiseError('Unable to read file: ' . $filename); 515 return $err; 516 } 517 518 if (!($fd = fopen($filename, 'rb'))) { 519 $err = $this->_raiseError('Could not open file: ' . $filename); 520 return $err; 521 } 522 523 $data = ''; 524 525 switch ($encoding) { 526 case 'quoted-printable': 527 while (!feof($fd)) { 528 $buffer = $this->_quotedPrintableEncode(fgets($fd)); 529 if ($fh) { 530 fwrite($fh, $buffer); 531 } else { 532 $data .= $buffer; 533 } 534 } 535 break; 536 537 case 'base64': 538 while (!feof($fd)) { 539 // Should read in a multiple of 57 bytes so that 540 // the output is 76 bytes per line. Don't use big chunks 541 // because base64 encoding is memory expensive 542 $buffer = fread($fd, 57 * 9198); // ca. 0.5 MB 543 $buffer = base64_encode($buffer); 544 $buffer = chunk_split($buffer, 76, $this->_eol); 545 if (feof($fd)) { 546 $buffer = rtrim($buffer); 547 } 548 549 if ($fh) { 550 fwrite($fh, $buffer); 551 } else { 552 $data .= $buffer; 553 } 554 } 555 break; 556 557 case '8bit': 558 case '7bit': 559 default: 560 while (!feof($fd)) { 561 $buffer = fread($fd, 1048576); // 1 MB 562 if ($fh) { 563 fwrite($fh, $buffer); 564 } else { 565 $data .= $buffer; 566 } 567 } 568 } 569 570 fclose($fd); 571 572 if (!$fh) { 573 return $data; 574 } 575 } 576 577 /** 578 * Encodes data to quoted-printable standard. 579 * 580 * @param string $input The data to encode 581 * @param int $line_max Optional max line length. Should 582 * not be more than 76 chars 583 * 584 * @return string Encoded data 585 * 586 * @access private 587 */ 588 function _quotedPrintableEncode($input , $line_max = 76) 589 { 590 $eol = $this->_eol; 591 /* 592 // imap_8bit() is extremely fast, but doesn't handle properly some characters 593 if (function_exists('imap_8bit') && $line_max == 76) { 594 $input = preg_replace('/\r?\n/', "\r\n", $input); 595 $input = imap_8bit($input); 596 if ($eol != "\r\n") { 597 $input = str_replace("\r\n", $eol, $input); 598 } 599 return $input; 600 } 601 */ 602 $lines = preg_split("/\r?\n/", $input); 603 $escape = '='; 604 $output = ''; 605 606 while (list($idx, $line) = each($lines)) { 607 $newline = ''; 608 $i = 0; 609 610 while (isset($line[$i])) { 611 $char = $line[$i]; 612 $dec = ord($char); 613 $i++; 614 615 if (($dec == 32) && (!isset($line[$i]))) { 616 // convert space at eol only 617 $char = '=20'; 618 } elseif ($dec == 9 && isset($line[$i])) { 619 ; // Do nothing if a TAB is not on eol 620 } elseif (($dec == 61) || ($dec < 32) || ($dec > 126)) { 621 $char = $escape . sprintf('%02X', $dec); 622 } elseif (($dec == 46) && (($newline == '') 623 || ((strlen($newline) + strlen("=2E")) >= $line_max)) 624 ) { 625 // Bug #9722: convert full-stop at bol, 626 // some Windows servers need this, won't break anything (cipri) 627 // Bug #11731: full-stop at bol also needs to be encoded 628 // if this line would push us over the line_max limit. 629 $char = '=2E'; 630 } 631 632 // Note, when changing this line, also change the ($dec == 46) 633 // check line, as it mimics this line due to Bug #11731 634 // EOL is not counted 635 if ((strlen($newline) + strlen($char)) >= $line_max) { 636 // soft line break; " =\r\n" is okay 637 $output .= $newline . $escape . $eol; 638 $newline = ''; 639 } 640 $newline .= $char; 641 } // end of for 642 $output .= $newline . $eol; 643 unset($lines[$idx]); 644 } 645 // Don't want last crlf 646 $output = substr($output, 0, -1 * strlen($eol)); 647 return $output; 648 } 649 650 /** 651 * Encodes the parameter of a header. 652 * 653 * @param string $name The name of the header-parameter 654 * @param string $value The value of the paramter 655 * @param string $charset The characterset of $value 656 * @param string $language The language used in $value 657 * @param string $encoding Parameter encoding. If not set, parameter value 658 * is encoded according to RFC2231 659 * @param int $maxLength The maximum length of a line. Defauls to 75 660 * 661 * @return string 662 * 663 * @access private 664 */ 665 function _buildHeaderParam($name, $value, $charset=null, $language=null, 666 $encoding=null, $maxLength=75 667 ) { 668 // RFC 2045: 669 // value needs encoding if contains non-ASCII chars or is longer than 78 chars 670 if (!preg_match('#[^\x20-\x7E]#', $value)) { 671 $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D' 672 . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#'; 673 if (!preg_match($token_regexp, $value)) { 674 // token 675 if (strlen($name) + strlen($value) + 3 <= $maxLength) { 676 return " {$name}={$value}"; 677 } 678 } else { 679 // quoted-string 680 $quoted = addcslashes($value, '\\"'); 681 if (strlen($name) + strlen($quoted) + 5 <= $maxLength) { 682 return " {$name}=\"{$quoted}\""; 683 } 684 } 685 } 686 687 // RFC2047: use quoted-printable/base64 encoding 688 if ($encoding == 'quoted-printable' || $encoding == 'base64') { 689 return $this->_buildRFC2047Param($name, $value, $charset, $encoding); 690 } 691 692 // RFC2231: 693 $encValue = preg_replace_callback( 694 '/([^\x21\x23\x24\x26\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E])/', 695 array($this, '_encodeReplaceCallback'), $value 696 ); 697 $value = "$charset'$language'$encValue"; 698 699 $header = " {$name}*={$value}"; 700 if (strlen($header) <= $maxLength) { 701 return $header; 702 } 703 704 $preLength = strlen(" {$name}*0*="); 705 $maxLength = max(16, $maxLength - $preLength - 3); 706 $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|"; 707 708 $headers = array(); 709 $headCount = 0; 710 while ($value) { 711 $matches = array(); 712 $found = preg_match($maxLengthReg, $value, $matches); 713 if ($found) { 714 $headers[] = " {$name}*{$headCount}*={$matches[0]}"; 715 $value = substr($value, strlen($matches[0])); 716 } else { 717 $headers[] = " {$name}*{$headCount}*={$value}"; 718 $value = ''; 719 } 720 $headCount++; 721 } 722 723 $headers = implode(';' . $this->_eol, $headers); 724 return $headers; 725 } 726 727 /** 728 * Encodes header parameter as per RFC2047 if needed 729 * 730 * @param string $name The parameter name 731 * @param string $value The parameter value 732 * @param string $charset The parameter charset 733 * @param string $encoding Encoding type (quoted-printable or base64) 734 * @param int $maxLength Encoded parameter max length. Default: 76 735 * 736 * @return string Parameter line 737 * @access private 738 */ 739 function _buildRFC2047Param($name, $value, $charset, 740 $encoding='quoted-printable', $maxLength=76 741 ) { 742 // WARNING: RFC 2047 says: "An 'encoded-word' MUST NOT be used in 743 // parameter of a MIME Content-Type or Content-Disposition field", 744 // but... it's supported by many clients/servers 745 $quoted = ''; 746 747 if ($encoding == 'base64') { 748 $value = base64_encode($value); 749 $prefix = '=?' . $charset . '?B?'; 750 $suffix = '?='; 751 752 // 2 x SPACE, 2 x '"', '=', ';' 753 $add_len = strlen($prefix . $suffix) + strlen($name) + 6; 754 $len = $add_len + strlen($value); 755 756 while ($len > $maxLength) { 757 // We can cut base64-encoded string every 4 characters 758 $real_len = floor(($maxLength - $add_len) / 4) * 4; 759 $_quote = substr($value, 0, $real_len); 760 $value = substr($value, $real_len); 761 762 $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; 763 $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' 764 $len = strlen($value) + $add_len; 765 } 766 $quoted .= $prefix . $value . $suffix; 767 768 } else { 769 // quoted-printable 770 $value = $this->encodeQP($value); 771 $prefix = '=?' . $charset . '?Q?'; 772 $suffix = '?='; 773 774 // 2 x SPACE, 2 x '"', '=', ';' 775 $add_len = strlen($prefix . $suffix) + strlen($name) + 6; 776 $len = $add_len + strlen($value); 777 778 while ($len > $maxLength) { 779 $length = $maxLength - $add_len; 780 // don't break any encoded letters 781 if (preg_match("/^(.{0,$length}[^\=][^\=])/", $value, $matches)) { 782 $_quote = $matches[1]; 783 } 784 785 $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' '; 786 $value = substr($value, strlen($_quote)); 787 $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';' 788 $len = strlen($value) + $add_len; 789 } 790 791 $quoted .= $prefix . $value . $suffix; 792 } 793 794 return " {$name}=\"{$quoted}\""; 795 } 796 797 /** 798 * Encodes a header as per RFC2047 799 * 800 * @param string $name The header name 801 * @param string $value The header data to encode 802 * @param string $charset Character set name 803 * @param string $encoding Encoding name (base64 or quoted-printable) 804 * @param string $eol End-of-line sequence. Default: "\r\n" 805 * 806 * @return string Encoded header data (without a name) 807 * @access public 808 * @since 1.6.1 809 */ 810 function encodeHeader($name, $value, $charset='ISO-8859-1', 811 $encoding='quoted-printable', $eol="\r\n" 812 ) { 813 // Structured headers 814 $comma_headers = array( 815 'from', 'to', 'cc', 'bcc', 'sender', 'reply-to', 816 'resent-from', 'resent-to', 'resent-cc', 'resent-bcc', 817 'resent-sender', 'resent-reply-to', 818 'mail-reply-to', 'mail-followup-to', 819 'return-receipt-to', 'disposition-notification-to', 820 ); 821 $other_headers = array( 822 'references', 'in-reply-to', 'message-id', 'resent-message-id', 823 ); 824 825 $name = strtolower($name); 826 827 if (in_array($name, $comma_headers)) { 828 $separator = ','; 829 } else if (in_array($name, $other_headers)) { 830 $separator = ' '; 831 } 832 833 if (!$charset) { 834 $charset = 'ISO-8859-1'; 835 } 836 837 // Structured header (make sure addr-spec inside is not encoded) 838 if (!empty($separator)) { 839 // Simple e-mail address regexp 840 $email_regexp = '([^\s<]+|("[^\r\n"]+"))@\S+'; 841 842 $parts = Mail_mimePart::_explodeQuotedString("[\t$separator]", $value); 843 $value = ''; 844 845 foreach ($parts as $part) { 846 $part = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $part); 847 $part = trim($part); 848 849 if (!$part) { 850 continue; 851 } 852 if ($value) { 853 $value .= $separator == ',' ? $separator . ' ' : ' '; 854 } else { 855 $value = $name . ': '; 856 } 857 858 // let's find phrase (name) and/or addr-spec 859 if (preg_match('/^<' . $email_regexp . '>$/', $part)) { 860 $value .= $part; 861 } else if (preg_match('/^' . $email_regexp . '$/', $part)) { 862 // address without brackets and without name 863 $value .= $part; 864 } else if (preg_match('/<*' . $email_regexp . '>*$/', $part, $matches)) { 865 // address with name (handle name) 866 $address = $matches[0]; 867 $word = str_replace($address, '', $part); 868 $word = trim($word); 869 // check if phrase requires quoting 870 if ($word) { 871 // non-ASCII: require encoding 872 if (preg_match('#([^\s\x21-\x7E]){1}#', $word)) { 873 if ($word[0] == '"' && $word[strlen($word)-1] == '"') { 874 // de-quote quoted-string, encoding changes 875 // string to atom 876 $search = array("\\\"", "\\\\"); 877 $replace = array("\"", "\\"); 878 $word = str_replace($search, $replace, $word); 879 $word = substr($word, 1, -1); 880 } 881 // find length of last line 882 if (($pos = strrpos($value, $eol)) !== false) { 883 $last_len = strlen($value) - $pos; 884 } else { 885 $last_len = strlen($value); 886 } 887 $word = Mail_mimePart::encodeHeaderValue( 888 $word, $charset, $encoding, $last_len, $eol 889 ); 890 } else if (($word[0] != '"' || $word[strlen($word)-1] != '"') 891 && preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $word) 892 ) { 893 // ASCII: quote string if needed 894 $word = '"'.addcslashes($word, '\\"').'"'; 895 } 896 } 897 $value .= $word.' '.$address; 898 } else { 899 // addr-spec not found, don't encode (?) 900 $value .= $part; 901 } 902 903 // RFC2822 recommends 78 characters limit, use 76 from RFC2047 904 $value = wordwrap($value, 76, $eol . ' '); 905 } 906 907 // remove header name prefix (there could be EOL too) 908 $value = preg_replace( 909 '/^'.$name.':('.preg_quote($eol, '/').')* /', '', $value 910 ); 911 } else { 912 // Unstructured header 913 // non-ASCII: require encoding 914 if (preg_match('#([^\s\x21-\x7E]){1}#', $value)) { 915 if ($value[0] == '"' && $value[strlen($value)-1] == '"') { 916 // de-quote quoted-string, encoding changes 917 // string to atom 918 $search = array("\\\"", "\\\\"); 919 $replace = array("\"", "\\"); 920 $value = str_replace($search, $replace, $value); 921 $value = substr($value, 1, -1); 922 } 923 $value = Mail_mimePart::encodeHeaderValue( 924 $value, $charset, $encoding, strlen($name) + 2, $eol 925 ); 926 } else if (strlen($name.': '.$value) > 78) { 927 // ASCII: check if header line isn't too long and use folding 928 $value = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $value); 929 $tmp = wordwrap($name.': '.$value, 78, $eol . ' '); 930 $value = preg_replace('/^'.$name.':\s*/', '', $tmp); 931 // hard limit 998 (RFC2822) 932 $value = wordwrap($value, 998, $eol . ' ', true); 933 } 934 } 935 936 return $value; 937 } 938 939 /** 940 * Explode quoted string 941 * 942 * @param string $delimiter Delimiter expression string for preg_match() 943 * @param string $string Input string 944 * 945 * @return array String tokens array 946 * @access private 947 */ 948 function _explodeQuotedString($delimiter, $string) 949 { 950 $result = array(); 951 $strlen = strlen($string); 952 953 for ($q=$p=$i=0; $i < $strlen; $i++) { 954 if ($string[$i] == "\"" 955 && (empty($string[$i-1]) || $string[$i-1] != "\\") 956 ) { 957 $q = $q ? false : true; 958 } else if (!$q && preg_match("/$delimiter/", $string[$i])) { 959 $result[] = substr($string, $p, $i - $p); 960 $p = $i + 1; 961 } 962 } 963 964 $result[] = substr($string, $p); 965 return $result; 966 } 967 968 /** 969 * Encodes a header value as per RFC2047 970 * 971 * @param string $value The header data to encode 972 * @param string $charset Character set name 973 * @param string $encoding Encoding name (base64 or quoted-printable) 974 * @param int $prefix_len Prefix length. Default: 0 975 * @param string $eol End-of-line sequence. Default: "\r\n" 976 * 977 * @return string Encoded header data 978 * @access public 979 * @since 1.6.1 980 */ 981 function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $eol="\r\n") 982 { 983 // #17311: Use multibyte aware method (requires mbstring extension) 984 if ($result = Mail_mimePart::encodeMB($value, $charset, $encoding, $prefix_len, $eol)) { 985 return $result; 986 } 987 988 // Generate the header using the specified params and dynamicly 989 // determine the maximum length of such strings. 990 // 75 is the value specified in the RFC. 991 $encoding = $encoding == 'base64' ? 'B' : 'Q'; 992 $prefix = '=?' . $charset . '?' . $encoding .'?'; 993 $suffix = '?='; 994 $maxLength = 75 - strlen($prefix . $suffix); 995 $maxLength1stLine = $maxLength - $prefix_len; 996 997 if ($encoding == 'B') { 998 // Base64 encode the entire string 999 $value = base64_encode($value); 1000 1001 // We can cut base64 every 4 characters, so the real max 1002 // we can get must be rounded down. 1003 $maxLength = $maxLength - ($maxLength % 4); 1004 $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4); 1005 1006 $cutpoint = $maxLength1stLine; 1007 $output = ''; 1008 1009 while ($value) { 1010 // Split translated string at every $maxLength 1011 $part = substr($value, 0, $cutpoint); 1012 $value = substr($value, $cutpoint); 1013 $cutpoint = $maxLength; 1014 // RFC 2047 specifies that any split header should 1015 // be separated by a CRLF SPACE. 1016 if ($output) { 1017 $output .= $eol . ' '; 1018 } 1019 $output .= $prefix . $part . $suffix; 1020 } 1021 $value = $output; 1022 } else { 1023 // quoted-printable encoding has been selected 1024 $value = Mail_mimePart::encodeQP($value); 1025 1026 // This regexp will break QP-encoded text at every $maxLength 1027 // but will not break any encoded letters. 1028 $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|"; 1029 $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|"; 1030 1031 if (strlen($value) > $maxLength1stLine) { 1032 // Begin with the regexp for the first line. 1033 $reg = $reg1st; 1034 $output = ''; 1035 while ($value) { 1036 // Split translated string at every $maxLength 1037 // But make sure not to break any translated chars. 1038 $found = preg_match($reg, $value, $matches); 1039 1040 // After this first line, we need to use a different 1041 // regexp for the first line. 1042 $reg = $reg2nd; 1043 1044 // Save the found part and encapsulate it in the 1045 // prefix & suffix. Then remove the part from the 1046 // $value_out variable. 1047 if ($found) { 1048 $part = $matches[0]; 1049 $len = strlen($matches[0]); 1050 $value = substr($value, $len); 1051 } else { 1052 $part = $value; 1053 $value = ''; 1054 } 1055 1056 // RFC 2047 specifies that any split header should 1057 // be separated by a CRLF SPACE 1058 if ($output) { 1059 $output .= $eol . ' '; 1060 } 1061 $output .= $prefix . $part . $suffix; 1062 } 1063 $value = $output; 1064 } else { 1065 $value = $prefix . $value . $suffix; 1066 } 1067 } 1068 1069 return $value; 1070 } 1071 1072 /** 1073 * Encodes the given string using quoted-printable 1074 * 1075 * @param string $str String to encode 1076 * 1077 * @return string Encoded string 1078 * @access public 1079 * @since 1.6.0 1080 */ 1081 function encodeQP($str) 1082 { 1083 // Bug #17226 RFC 2047 restricts some characters 1084 // if the word is inside a phrase, permitted chars are only: 1085 // ASCII letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_" 1086 1087 // "=", "_", "?" must be encoded 1088 $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/'; 1089 $str = preg_replace_callback( 1090 $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $str 1091 ); 1092 1093 return str_replace(' ', '_', $str); 1094 } 1095 1096 /** 1097 * Encodes the given string using base64 or quoted-printable. 1098 * This method makes sure that encoded-word represents an integral 1099 * number of characters as per RFC2047. 1100 * 1101 * @param string $str String to encode 1102 * @param string $charset Character set name 1103 * @param string $encoding Encoding name (base64 or quoted-printable) 1104 * @param int $prefix_len Prefix length. Default: 0 1105 * @param string $eol End-of-line sequence. Default: "\r\n" 1106 * 1107 * @return string Encoded string 1108 * @access public 1109 * @since 1.8.0 1110 */ 1111 function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n") 1112 { 1113 if (!function_exists('mb_substr') || !function_exists('mb_strlen')) { 1114 return; 1115 } 1116 1117 $encoding = $encoding == 'base64' ? 'B' : 'Q'; 1118 // 75 is the value specified in the RFC 1119 $prefix = '=?' . $charset . '?'.$encoding.'?'; 1120 $suffix = '?='; 1121 $maxLength = 75 - strlen($prefix . $suffix); 1122 1123 // A multi-octet character may not be split across adjacent encoded-words 1124 // So, we'll loop over each character 1125 // mb_stlen() with wrong charset will generate a warning here and return null 1126 $length = mb_strlen($str, $charset); 1127 $result = ''; 1128 $line_length = $prefix_len; 1129 1130 if ($encoding == 'B') { 1131 // base64 1132 $start = 0; 1133 $prev = ''; 1134 1135 for ($i=1; $i<=$length; $i++) { 1136 // See #17311 1137 $chunk = mb_substr($str, $start, $i-$start, $charset); 1138 $chunk = base64_encode($chunk); 1139 $chunk_len = strlen($chunk); 1140 1141 if ($line_length + $chunk_len == $maxLength || $i == $length) { 1142 if ($result) { 1143 $result .= "\n"; 1144 } 1145 $result .= $chunk; 1146 $line_length = 0; 1147 $start = $i; 1148 } else if ($line_length + $chunk_len > $maxLength) { 1149 if ($result) { 1150 $result .= "\n"; 1151 } 1152 if ($prev) { 1153 $result .= $prev; 1154 } 1155 $line_length = 0; 1156 $start = $i - 1; 1157 } else { 1158 $prev = $chunk; 1159 } 1160 } 1161 } else { 1162 // quoted-printable 1163 // see encodeQP() 1164 $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/'; 1165 1166 for ($i=0; $i<=$length; $i++) { 1167 $char = mb_substr($str, $i, 1, $charset); 1168 // RFC recommends underline (instead of =20) in place of the space 1169 // that's one of the reasons why we're not using iconv_mime_encode() 1170 if ($char == ' ') { 1171 $char = '_'; 1172 $char_len = 1; 1173 } else { 1174 $char = preg_replace_callback( 1175 $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $char 1176 ); 1177 $char_len = strlen($char); 1178 } 1179 1180 if ($line_length + $char_len > $maxLength) { 1181 if ($result) { 1182 $result .= "\n"; 1183 } 1184 $line_length = 0; 1185 } 1186 1187 $result .= $char; 1188 $line_length += $char_len; 1189 } 1190 } 1191 1192 if ($result) { 1193 $result = $prefix 1194 .str_replace("\n", $suffix.$eol.' '.$prefix, $result).$suffix; 1195 } 1196 1197 return $result; 1198 } 1199 1200 /** 1201 * Callback function to replace extended characters (\x80-xFF) with their 1202 * ASCII values (RFC2047: quoted-printable) 1203 * 1204 * @param array $matches Preg_replace's matches array 1205 * 1206 * @return string Encoded character string 1207 * @access private 1208 */ 1209 function _qpReplaceCallback($matches) 1210 { 1211 return sprintf('=%02X', ord($matches[1])); 1212 } 1213 1214 /** 1215 * Callback function to replace extended characters (\x80-xFF) with their 1216 * ASCII values (RFC2231) 1217 * 1218 * @param array $matches Preg_replace's matches array 1219 * 1220 * @return string Encoded character string 1221 * @access private 1222 */ 1223 function _encodeReplaceCallback($matches) 1224 { 1225 return sprintf('%%%02X', ord($matches[1])); 1226 } 1227 1228 /** 1229 * PEAR::isError implementation 1230 * 1231 * @param mixed $data Object 1232 * 1233 * @return bool True if object is an instance of PEAR_Error 1234 * @access private 1235 */ 1236 function _isError($data) 1237 { 1238 // PEAR::isError() is not PHP 5.4 compatible (see Bug #19473) 1239 if (is_object($data) && is_a($data, 'PEAR_Error')) { 1240 return true; 1241 } 1242 1243 return false; 1244 } 1245 1246 /** 1247 * PEAR::raiseError implementation 1248 * 1249 * @param $message A text error message 1250 * 1251 * @return PEAR_Error Instance of PEAR_Error 1252 * @access private 1253 */ 1254 function _raiseError($message) 1255 { 1256 // PEAR::raiseError() is not PHP 5.4 compatible 1257 return new PEAR_Error($message); 1258 } 1259 1260} // End of class 1261