[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * This class contains functions related to handling the headers of MIME data. 4 * 5 * Copyright 2002-2014 Horde LLC (http://www.horde.org/) 6 * 7 * See the enclosed file COPYING for license information (LGPL). If you 8 * did not receive this file, see http://www.horde.org/licenses/lgpl21. 9 * 10 * @author Michael Slusarz <slusarz@horde.org> 11 * @category Horde 12 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 13 * @package Mime 14 */ 15 class Horde_Mime_Headers implements Serializable 16 { 17 /* Serialized version. */ 18 const VERSION = 2; 19 20 /* Constants for getValue(). */ 21 const VALUE_STRING = 1; 22 const VALUE_BASE = 2; 23 const VALUE_PARAMS = 3; 24 25 /** 26 * The default charset to use when parsing text parts with no charset 27 * information. 28 * 29 * @var string 30 */ 31 static public $defaultCharset = 'us-ascii'; 32 33 /** 34 * The internal headers array. 35 * 36 * Keys are the lowercase header name. 37 * Values are: 38 * - h: The case-sensitive header name. 39 * - p: Parameters for this header. 40 * - v: The value of the header. Values are stored in UTF-8. 41 * 42 * @var array 43 */ 44 protected $_headers = array(); 45 46 /** 47 * The sequence to use as EOL for the headers. 48 * The default is currently to output the EOL sequence internally as 49 * just "\n" instead of the canonical "\r\n" required in RFC 822 & 2045. 50 * To be RFC complaint, the full <CR><LF> EOL combination should be used 51 * when sending a message. 52 * 53 * @var string 54 */ 55 protected $_eol = "\n"; 56 57 /** 58 * The User-Agent string to use. 59 * 60 * @var string 61 */ 62 protected $_agent = null; 63 64 /** 65 * List of single header fields. 66 * 67 * @var array 68 */ 69 protected $_singleFields = array( 70 // Mail: RFC 5322 71 'to', 'from', 'cc', 'bcc', 'date', 'sender', 'reply-to', 72 'message-id', 'in-reply-to', 'references', 'subject', 73 // MIME: RFC 1864 74 'content-md5', 75 // MIME: RFC 2045 76 'mime-version', 'content-type', 'content-transfer-encoding', 77 'content-id', 'content-description', 78 // MIME: RFC 2110 79 'content-base', 80 // MIME: RFC 2183 81 'content-disposition', 82 // MIME: RFC 2424 83 'content-duration', 84 // MIME: RFC 2557 85 'content-location', 86 // MIME: RFC 2912 [3] 87 'content-features', 88 // MIME: RFC 3282 89 'content-language', 90 // MIME: RFC 3297 91 'content-alternative', 92 // Importance: See, e.g., RFC 4356 [2.1.3.3.1] 93 'importance', 94 // OTHER: X-Priority 95 // See: http://kb.mozillazine.org/Emulate_Microsoft_email_clients 96 'x-priority' 97 ); 98 99 /** 100 * Returns the internal header array in array format. 101 * 102 * @param array $opts Optional parameters: 103 * - canonical: (boolean) Use canonical (RFC 822/2045) line endings? 104 * DEFAULT: Uses $this->_eol 105 * - charset: (string) Encodes the headers using this charset. If empty, 106 * encodes using internal charset (UTF-8). 107 * DEFAULT: No encoding. 108 * - defserver: (string) The default domain to append to mailboxes. 109 * DEFAULT: No default name. 110 * - nowrap: (integer) Don't wrap the headers. 111 * DEFAULT: Headers are wrapped. 112 * 113 * @return array The headers in array format. 114 */ 115 public function toArray(array $opts = array()) 116 { 117 $address_keys = $this->addressFields(); 118 $charset = array_key_exists('charset', $opts) 119 ? (empty($opts['charset']) ? 'UTF-8' : $opts['charset']) 120 : null; 121 $eol = empty($opts['canonical']) 122 ? $this->_eol 123 : "\r\n"; 124 $mime = $this->mimeParamFields(); 125 $ret = array(); 126 127 foreach ($this->_headers as $header => $ob) { 128 $val = is_array($ob['v']) ? $ob['v'] : array($ob['v']); 129 130 foreach (array_keys($val) as $key) { 131 if (in_array($header, $address_keys) ) { 132 /* Address encoded headers. */ 133 $rfc822 = new Horde_Mail_Rfc822(); 134 $text = $rfc822->parseAddressList($val[$key], array( 135 'default_domain' => empty($opts['defserver']) ? null : $opts['defserver'] 136 ))->writeAddress(array( 137 'encode' => $charset, 138 'idn' => true 139 )); 140 } elseif (in_array($header, $mime) && !empty($ob['p'])) { 141 /* MIME encoded headers (RFC 2231). */ 142 $text = $val[$key]; 143 foreach ($ob['p'] as $name => $param) { 144 foreach (Horde_Mime::encodeParam($name, $param, array('charset' => $charset, 'escape' => true)) as $name2 => $param2) { 145 $text .= '; ' . $name2 . '=' . $param2; 146 } 147 } 148 } else { 149 $text = is_null($charset) 150 ? $val[$key] 151 : Horde_Mime::encode($val[$key], $charset); 152 } 153 154 if (empty($opts['nowrap'])) { 155 /* Remove any existing linebreaks and wrap the line. */ 156 $header_text = $ob['h'] . ': '; 157 $text = ltrim(substr(wordwrap($header_text . strtr(trim($text), array("\r" => '', "\n" => '')), 76, $eol . ' '), strlen($header_text))); 158 } 159 160 $val[$key] = $text; 161 } 162 163 $ret[$ob['h']] = (count($val) == 1) ? reset($val) : $val; 164 } 165 166 return $ret; 167 } 168 169 /** 170 * Returns the internal header array in string format. 171 * 172 * @param array $opts Optional parameters: 173 * - canonical: (boolean) Use canonical (RFC 822/2045) line endings? 174 * DEFAULT: Uses $this->_eol 175 * - charset: (string) Encodes the headers using this charset. 176 * DEFAULT: No encoding. 177 * - defserver: (string) The default domain to append to mailboxes. 178 * DEFAULT: No default name. 179 * - nowrap: (integer) Don't wrap the headers. 180 * DEFAULT: Headers are wrapped. 181 * 182 * @return string The headers in string format. 183 */ 184 public function toString(array $opts = array()) 185 { 186 $eol = empty($opts['canonical']) 187 ? $this->_eol 188 : "\r\n"; 189 $text = ''; 190 191 foreach ($this->toArray($opts) as $key => $val) { 192 if (!is_array($val)) { 193 $val = array($val); 194 } 195 foreach ($val as $entry) { 196 $text .= $key . ': ' . $entry . $eol; 197 } 198 } 199 200 return $text . $eol; 201 } 202 203 /** 204 * Generate the 'Received' header for the Web browser->Horde hop 205 * (attempts to conform to guidelines in RFC 5321 [4.4]). 206 * 207 * @param array $opts Additional opts: 208 * - dns: (Net_DNS2_Resolver) Use the DNS resolver object to lookup 209 * hostnames. 210 * DEFAULT: Use gethostbyaddr() function. 211 * - server: (string) Use this server name. 212 * DEFAULT: Auto-detect using current PHP values. 213 */ 214 public function addReceivedHeader(array $opts = array()) 215 { 216 $old_error = error_reporting(0); 217 if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { 218 /* This indicates the user is connecting through a proxy. */ 219 $remote_path = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); 220 $remote_addr = $remote_path[0]; 221 if (!empty($opts['dns'])) { 222 $remote = $remote_addr; 223 try { 224 if ($response = $opts['dns']->query($remote_addr, 'PTR')) { 225 foreach ($response->answer as $val) { 226 if (isset($val->ptrdname)) { 227 $remote = $val->ptrdname; 228 break; 229 } 230 } 231 } 232 } catch (Net_DNS2_Exception $e) {} 233 } else { 234 $remote = gethostbyaddr($remote_addr); 235 } 236 } else { 237 $remote_addr = $_SERVER['REMOTE_ADDR']; 238 if (empty($_SERVER['REMOTE_HOST'])) { 239 if (!empty($opts['dns'])) { 240 $remote = $remote_addr; 241 try { 242 if ($response = $opts['dns']->query($remote_addr, 'PTR')) { 243 foreach ($response->answer as $val) { 244 if (isset($val->ptrdname)) { 245 $remote = $val->ptrdname; 246 break; 247 } 248 } 249 } 250 } catch (Net_DNS2_Exception $e) {} 251 } else { 252 $remote = gethostbyaddr($remote_addr); 253 } 254 } else { 255 $remote = $_SERVER['REMOTE_HOST']; 256 } 257 } 258 error_reporting($old_error); 259 260 if (!empty($_SERVER['REMOTE_IDENT'])) { 261 $remote_ident = $_SERVER['REMOTE_IDENT'] . '@' . $remote . ' '; 262 } elseif ($remote != $_SERVER['REMOTE_ADDR']) { 263 $remote_ident = $remote . ' '; 264 } else { 265 $remote_ident = ''; 266 } 267 268 if (!empty($opts['server'])) { 269 $server_name = $opts['server']; 270 } elseif (!empty($_SERVER['SERVER_NAME'])) { 271 $server_name = $_SERVER['SERVER_NAME']; 272 } elseif (!empty($_SERVER['HTTP_HOST'])) { 273 $server_name = $_SERVER['HTTP_HOST']; 274 } else { 275 $server_name = 'unknown'; 276 } 277 278 $received = 'from ' . $remote . ' (' . $remote_ident . 279 '[' . $remote_addr . ']) ' . 280 'by ' . $server_name . ' (Horde Framework) with HTTP; ' . 281 date('r'); 282 283 $this->addHeader('Received', $received); 284 } 285 286 /** 287 * Generate the 'Message-ID' header. 288 */ 289 public function addMessageIdHeader() 290 { 291 $this->addHeader('Message-ID', Horde_Mime::generateMessageId()); 292 } 293 294 /** 295 * Generate the user agent description header. 296 */ 297 public function addUserAgentHeader() 298 { 299 $this->addHeader('User-Agent', $this->getUserAgent()); 300 } 301 302 /** 303 * Returns the user agent description header. 304 * 305 * @return string The user agent header. 306 */ 307 public function getUserAgent() 308 { 309 if (is_null($this->_agent)) { 310 $this->_agent = 'Horde Application Framework 5'; 311 } 312 return $this->_agent; 313 } 314 315 /** 316 * Explicitly sets the User-Agent string. 317 * 318 * @param string $agent The User-Agent string to use. 319 */ 320 public function setUserAgent($agent) 321 { 322 $this->_agent = $agent; 323 } 324 325 /** 326 * Add a header to the header array. 327 * 328 * @param string $header The header name. 329 * @param string $value The header value (UTF-8). 330 * @param array $opts Additional options: 331 * - params: (array) MIME parameters for Content-Type or 332 * Content-Disposition. 333 * DEFAULT: None 334 * - sanity_check: (boolean) Do sanity-checking on header value? 335 * DEFAULT: false 336 */ 337 public function addHeader($header, $value, array $opts = array()) 338 { 339 $header = trim($header); 340 $lcHeader = Horde_String::lower($header); 341 342 if (!isset($this->_headers[$lcHeader])) { 343 $this->_headers[$lcHeader] = array( 344 'h' => $header 345 ); 346 } 347 $ptr = &$this->_headers[$lcHeader]; 348 349 if (!empty($opts['sanity_check'])) { 350 $value = $this->_sanityCheck($value); 351 } 352 353 // Fields defined in RFC 2822 that contain address information 354 if (in_array($lcHeader, $this->addressFields())) { 355 $rfc822 = new Horde_Mail_Rfc822(); 356 $addr_list = $rfc822->parseAddressList($value); 357 358 switch ($lcHeader) { 359 case 'bcc': 360 case 'cc': 361 case 'from': 362 case 'to': 363 /* Catch malformed undisclosed-recipients entries. */ 364 if ((count($addr_list) == 1) && 365 preg_match("/^\s*undisclosed-recipients:?\s*$/i", $addr_list[0]->bare_address)) { 366 $addr_list = new Horde_Mail_Rfc822_List('undisclosed-recipients:;'); 367 } 368 break; 369 } 370 $value = strval($addr_list); 371 } else { 372 $value = Horde_Mime::decode($value); 373 } 374 375 if (isset($ptr['v'])) { 376 if (!is_array($ptr['v'])) { 377 $ptr['v'] = array($ptr['v']); 378 } 379 $ptr['v'][] = $value; 380 } else { 381 $ptr['v'] = $value; 382 } 383 384 if (!empty($opts['params'])) { 385 $ptr['p'] = $opts['params']; 386 } 387 } 388 389 /** 390 * Remove a header from the header array. 391 * 392 * @param string $header The header name. 393 */ 394 public function removeHeader($header) 395 { 396 unset($this->_headers[Horde_String::lower(trim($header))]); 397 } 398 399 /** 400 * Replace a value of a header. 401 * 402 * @param string $header The header name. 403 * @param string $value The header value. 404 * @param array $opts Additional options: 405 * - params: (array) MIME parameters for Content-Type or 406 * Content-Disposition. 407 * DEFAULT: None 408 * - sanity_check: (boolean) Do sanity-checking on header value? 409 * DEFAULT: false 410 */ 411 public function replaceHeader($header, $value, array $opts = array()) 412 { 413 $this->removeHeader($header); 414 $this->addHeader($header, $value, $opts); 415 } 416 417 /** 418 * Attempts to return the header in the correct case. 419 * 420 * @param string $header The header to search for. 421 * 422 * @return string The value for the given header. 423 * If the header is not found, returns null. 424 */ 425 public function getString($header) 426 { 427 $lcHeader = Horde_String::lower($header); 428 return (isset($this->_headers[$lcHeader])) 429 ? $this->_headers[$lcHeader]['h'] 430 : null; 431 } 432 433 /** 434 * Attempt to return the value for a given header. 435 * The following header fields can only have 1 entry, so if duplicate 436 * entries exist, the first value will be used: 437 * * To, From, Cc, Bcc, Date, Sender, Reply-to, Message-ID, In-Reply-To, 438 * References, Subject (RFC 2822 [3.6]) 439 * * All List Headers (RFC 2369 [3]) 440 * The values are not MIME encoded. 441 * 442 * @param string $header The header to search for. 443 * @param integer $type The type of return: 444 * - VALUE_STRING: Returns a string representation of the entire header. 445 * - VALUE_BASE: Returns a string representation of the base value of 446 * the header. If this is not a header that allows 447 * parameters, this will be equivalent to VALUE_STRING. 448 * - VALUE_PARAMS: Returns the list of parameters for this header. If 449 * this is not a header that allows parameters, this 450 * will be an empty array. 451 * 452 * @return mixed The value for the given header. 453 * If the header is not found, returns null. 454 */ 455 public function getValue($header, $type = self::VALUE_STRING) 456 { 457 $header = Horde_String::lower($header); 458 459 if (!isset($this->_headers[$header])) { 460 return null; 461 } 462 463 $ptr = &$this->_headers[$header]; 464 if (is_array($ptr['v']) && 465 in_array($header, $this->singleFields(true))) { 466 if (in_array($header, $this->addressFields())) { 467 $base = str_replace(';,', ';', implode(', ', $ptr['v'])); 468 } else { 469 $base = $ptr['v'][0]; 470 } 471 } else { 472 $base = $ptr['v']; 473 } 474 $params = isset($ptr['p']) ? $ptr['p'] : array(); 475 476 switch ($type) { 477 case self::VALUE_BASE: 478 return $base; 479 480 case self::VALUE_PARAMS: 481 return $params; 482 483 case self::VALUE_STRING: 484 foreach ($params as $key => $val) { 485 $base .= '; ' . $key . '=' . $val; 486 } 487 return $base; 488 } 489 } 490 491 /** 492 * Returns the list of RFC defined header fields that contain address 493 * info. 494 * 495 * @return array The list of headers, in lowercase. 496 */ 497 static public function addressFields() 498 { 499 return array( 500 'from', 'to', 'cc', 'bcc', 'reply-to', 'resent-to', 'resent-cc', 501 'resent-bcc', 'resent-from', 'sender' 502 ); 503 } 504 505 /** 506 * Returns the list of RFC defined header fields that can only contain 507 * a single value. 508 * 509 * @param boolean $list Return list-related headers also? 510 * 511 * @return array The list of headers, in lowercase. 512 */ 513 public function singleFields($list = true) 514 { 515 return $list 516 ? array_merge($this->_singleFields, array_keys($this->listHeaders())) 517 : $this->_singleFields; 518 } 519 520 /** 521 * Returns the list of RFC defined MIME header fields that may contain 522 * parameter info. 523 * 524 * @return array The list of headers, in lowercase. 525 */ 526 static public function mimeParamFields() 527 { 528 return array('content-type', 'content-disposition'); 529 } 530 531 /** 532 * Returns the list of valid mailing list headers. 533 * 534 * @deprecated Use Horde_ListHeaders#headers() instead. 535 * 536 * @return array The list of valid mailing list headers. 537 */ 538 static public function listHeaders() 539 { 540 return array( 541 /* RFC 2369 */ 542 'list-help' => Horde_Mime_Translation::t("List-Help"), 543 'list-unsubscribe' => Horde_Mime_Translation::t("List-Unsubscribe"), 544 'list-subscribe' => Horde_Mime_Translation::t("List-Subscribe"), 545 'list-owner' => Horde_Mime_Translation::t("List-Owner"), 546 'list-post' => Horde_Mime_Translation::t("List-Post"), 547 'list-archive' => Horde_Mime_Translation::t("List-Archive"), 548 /* RFC 2919 */ 549 'list-id' => Horde_Mime_Translation::t("List-Id") 550 ); 551 } 552 553 /** 554 * Do any mailing list headers exist? 555 * 556 * @return boolean True if any mailing list headers exist. 557 */ 558 public function listHeadersExist() 559 { 560 return (bool)count(array_intersect(array_keys($this->listHeaders()), array_keys($this->_headers))); 561 } 562 563 /** 564 * Sets a new string to use for EOLs. 565 * 566 * @param string $eol The string to use for EOLs. 567 */ 568 public function setEOL($eol) 569 { 570 $this->_eol = $eol; 571 } 572 573 /** 574 * Get the string to use for EOLs. 575 * 576 * @return string The string to use for EOLs. 577 */ 578 public function getEOL() 579 { 580 return $this->_eol; 581 } 582 583 /** 584 * Returns an address object for a header. 585 * 586 * @param string $field The header to return as an object. 587 * 588 * @return Horde_Mail_Rfc822_List The object for the requested field. 589 * Returns null if field doesn't exist. 590 */ 591 public function getOb($field) 592 { 593 if (($value = $this->getValue($field)) === null) { 594 return null; 595 } 596 597 $rfc822 = new Horde_Mail_Rfc822(); 598 return $rfc822->parseAddressList($value); 599 } 600 601 /** 602 * Perform sanity checking on a raw header (e.g. handle 8-bit characters). 603 * 604 * @param string $data The header data. 605 * 606 * @return string The cleaned header data. 607 */ 608 protected function _sanityCheck($data) 609 { 610 $charset_test = array( 611 'windows-1252', 612 self::$defaultCharset 613 ); 614 615 if (!Horde_String::validUtf8($data)) { 616 /* Appears to be a PHP error with the internal String structure 617 * which prevents accurate manipulation of the string. Copying 618 * the data to a new variable fixes things. */ 619 $data = substr($data, 0); 620 621 /* Assumption: broken charset in headers is generally either 622 * UTF-8 or ISO-8859-1/Windows-1252. Test these charsets 623 * first before using default charset. This may be a 624 * Western-centric approach, but it's better than nothing. */ 625 foreach ($charset_test as $charset) { 626 $tmp = Horde_String::convertCharset($data, $charset, 'UTF-8'); 627 if (Horde_String::validUtf8($tmp)) { 628 return $tmp; 629 } 630 } 631 } 632 633 return $data; 634 } 635 636 /* Static methods. */ 637 638 /** 639 * Builds a Horde_Mime_Headers object from header text. 640 * 641 * @param mixed $text A text string (or, as of 2.3.0, a Horde_Stream 642 * object or stream resource) containing the headers. 643 * 644 * @return Horde_Mime_Headers A new Horde_Mime_Headers object. 645 */ 646 static public function parseHeaders($text) 647 { 648 $currheader = $currtext = null; 649 $mime = self::mimeParamFields(); 650 $to_process = array(); 651 652 if ($text instanceof Horde_Stream) { 653 $stream = $text; 654 $stream->rewind(); 655 } else { 656 $stream = new Horde_Stream_Temp(); 657 $stream->add($text, true); 658 } 659 660 while (!$stream->eof()) { 661 if (!($val = rtrim($stream->getToChar("\n", false), "\r"))) { 662 break; 663 } 664 665 if (($val[0] == ' ') || ($val[0] == "\t")) { 666 $currtext .= ' ' . ltrim($val); 667 } else { 668 if (!is_null($currheader)) { 669 $to_process[] = array($currheader, rtrim($currtext)); 670 } 671 672 $pos = strpos($val, ':'); 673 $currheader = substr($val, 0, $pos); 674 $currtext = ltrim(substr($val, $pos + 1)); 675 } 676 } 677 678 if (!is_null($currheader)) { 679 $to_process[] = array($currheader, $currtext); 680 } 681 682 $headers = new Horde_Mime_Headers(); 683 684 reset($to_process); 685 while (list(,$val) = each($to_process)) { 686 /* Ignore empty headers. */ 687 if (!strlen($val[1])) { 688 continue; 689 } 690 691 if (in_array(Horde_String::lower($val[0]), $mime)) { 692 $res = Horde_Mime::decodeParam($val[0], $val[1]); 693 $headers->addHeader($val[0], $res['val'], array( 694 'params' => $res['params'], 695 'sanity_check' => true 696 )); 697 } else { 698 $headers->addHeader($val[0], $val[1], array( 699 'sanity_check' => true 700 )); 701 } 702 } 703 704 return $headers; 705 } 706 707 /* Serializable methods. */ 708 709 /** 710 * Serialization. 711 * 712 * @return string Serialized data. 713 */ 714 public function serialize() 715 { 716 $data = array( 717 // Serialized data ID. 718 self::VERSION, 719 $this->_headers, 720 $this->_eol 721 ); 722 723 if (!is_null($this->_agent)) { 724 $data[] = $this->_agent; 725 } 726 727 return serialize($data); 728 } 729 730 /** 731 * Unserialization. 732 * 733 * @param string $data Serialized data. 734 * 735 * @throws Exception 736 */ 737 public function unserialize($data) 738 { 739 $data = @unserialize($data); 740 if (!is_array($data) || 741 !isset($data[0]) || 742 ($data[0] != self::VERSION)) { 743 throw new Horde_Mime_Exception('Cache version change'); 744 } 745 746 $this->_headers = $data[1]; 747 $this->_eol = $data[2]; 748 if (isset($data[3])) { 749 $this->_agent = $data[3]; 750 } 751 } 752 753 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |