1<?php 2 3/** 4 * Swift Mailer MIME Library Headers component 5 * Please read the LICENSE file 6 * @copyright Chris Corbyn <chris@w3style.co.uk> 7 * @author Chris Corbyn <chris@w3style.co.uk> 8 * @package Swift_Message 9 * @license GNU Lesser General Public License 10 */ 11 12require_once dirname(__FILE__) . "/../ClassLoader.php"; 13 14/** 15 * Contains and constructs the headers for a MIME document 16 * @package Swift_Message 17 * @author Chris Corbyn <chris@w3style.co.uk> 18 */ 19class Swift_Message_Headers 20{ 21 /** 22 * Headers which may contain email addresses, and therefore should take notice when encoding 23 * @var array headers 24 */ 25 protected $emailContainingHeaders = array( 26 "To", "From", "Reply-To", "Cc", "Bcc", "Return-Path", "Sender"); 27 /** 28 * The encoding format used for the body of the document 29 * @var string format 30 */ 31 protected $encoding = "B"; 32 /** 33 * The charset used in the headers 34 * @var string 35 */ 36 protected $charset = false; 37 /** 38 * A collection of headers 39 * @var array headers 40 */ 41 protected $headers = array(); 42 /** 43 * A container of references to the headers 44 * @var array 45 */ 46 protected $lowerHeaders = array(); 47 /** 48 * Attributes appended to headers 49 * @var array 50 */ 51 protected $attributes = array(); 52 /** 53 * If QP or Base64 encoding should be forced 54 * @var boolean 55 */ 56 protected $forceEncoding = false; 57 /** 58 * The language used in the headers (doesn't really matter much) 59 * @var string 60 */ 61 protected $language = "en-us"; 62 /** 63 * Cached, pre-built headers 64 * @var string 65 */ 66 protected $cached = array(); 67 /** 68 * The line ending used in the headers 69 * @var string 70 */ 71 protected $LE = "\r\n"; 72 73 /** 74 * Set the line ending character to use 75 * @param string The line ending sequence 76 * @return boolean 77 */ 78 public function setLE($le) 79 { 80 if (in_array($le, array("\r", "\n", "\r\n"))) 81 { 82 foreach (array_keys($this->cached) as $k) $this->cached[$k] = null; 83 $this->LE = $le; 84 return true; 85 } 86 else return false; 87 } 88 /** 89 * Get the line ending sequence 90 * @return string 91 */ 92 public function getLE() 93 { 94 return $this->LE; 95 } 96 /** 97 * Reset the cache state in these headers 98 */ 99 public function uncacheAll() 100 { 101 foreach (array_keys($this->cached) as $k) 102 { 103 $this->cached[$k] = null; 104 } 105 } 106 /** 107 * Add a header or change an existing header value 108 * @param string The header name, for example "From" or "Subject" 109 * @param string The value to be inserted into the header. This is safe from header injection. 110 */ 111 public function set($name, $value) 112 { 113 $lname = strtolower($name); 114 if (!isset($this->lowerHeaders[$lname])) 115 { 116 $this->headers[$name] = null; 117 $this->lowerHeaders[$lname] =& $this->headers[$name]; 118 } 119 $this->cached[$lname] = null; 120 Swift_ClassLoader::load("Swift_Message_Encoder"); 121 if (is_array($value)) 122 { 123 foreach ($value as $v) 124 { 125 if (!$this->getCharset() && Swift_Message_Encoder::instance()->isUTF8($v)) 126 { 127 $this->setCharset("utf-8"); 128 break; 129 } 130 } 131 } 132 elseif ($value !== null) 133 { 134 if (!$this->getCharset() && Swift_Message_Encoder::instance()->isUTF8($value)) 135 { 136 $this->setCharset("utf-8"); 137 } 138 } 139 if (!is_array($value) && $value !== null) $this->lowerHeaders[$lname] = (string) $value; 140 else $this->lowerHeaders[$lname] = $value; 141 } 142 /** 143 * Get the value at a given header 144 * @param string The name of the header, for example "From" or "Subject" 145 * @return string 146 * @throws Swift_Message_MimeException If no such header exists 147 * @see hasHeader 148 */ 149 public function get($name) 150 { 151 $lname = strtolower($name); 152 if ($this->has($name)) 153 { 154 return $this->lowerHeaders[$lname]; 155 } 156 } 157 /** 158 * Remove a header from the list 159 * @param string The name of the header 160 */ 161 public function remove($name) 162 { 163 $lname = strtolower($name); 164 if ($this->has($name)) 165 { 166 unset($this->headers[$name]); 167 unset($this->lowerHeaders[$lname]); 168 unset($this->cached[$lname]); 169 if (isset($this->attributes[$lname])) unset($this->attributes[$lname]); 170 } 171 } 172 /** 173 * Just fetch the array containing the headers 174 * @return array 175 */ 176 public function getList() 177 { 178 return $this->headers; 179 } 180 /** 181 * Check if a header has been set or not 182 * @param string The name of the header, for example "From" or "Subject" 183 * @return boolean 184 */ 185 public function has($name) 186 { 187 $lname = strtolower($name); 188 return (array_key_exists($lname, $this->lowerHeaders) && $this->lowerHeaders[$lname] !== null); 189 } 190 /** 191 * Set the language used in the headers to $lang (e.g. en-us, en-gb, sv etc) 192 * @param string The language to use 193 */ 194 public function setLanguage($lang) 195 { 196 $this->language = (string) $lang; 197 } 198 /** 199 * Get the language used in the headers to $lang (e.g. en-us, en-gb, sv etc) 200 * @return string 201 */ 202 public function getLanguage() 203 { 204 return $this->language; 205 } 206 /** 207 * Set the charset used in the headers 208 * @param string The charset name 209 */ 210 public function setCharset($charset) 211 { 212 $this->charset = (string) $charset; 213 } 214 /** 215 * Get the current charset used 216 * @return string 217 */ 218 public function getCharset() 219 { 220 return $this->charset; 221 } 222 /** 223 * Specify the encoding to use for the headers if characters outside the 7-bit-printable ascii range are found 224 * This encoding will never be used if only 7-bit-printable characters are found in the headers. 225 * Possible values are: 226 * - QP 227 * - Q 228 * - Quoted-Printable 229 * - B 230 * - Base64 231 * NOTE: Q, QP, Quoted-Printable are all the same; as are B and Base64 232 * @param string The encoding format to use 233 * @return boolean 234 */ 235 public function setEncoding($encoding) 236 { 237 switch (strtolower($encoding)) 238 { 239 case "qp": case "q": case "quoted-printable": 240 $this->encoding = "Q"; 241 return true; 242 case "base64": case "b": 243 $this->encoding = "B"; 244 return true; 245 default: return false; 246 } 247 } 248 /** 249 * Get the encoding format used in this document 250 * @return string 251 */ 252 public function getEncoding() 253 { 254 return $this->encoding; 255 } 256 /** 257 * Turn on or off forced header encoding 258 * @param boolean On/Off 259 */ 260 public function forceEncoding($force=true) 261 { 262 $this->forceEncoding = (boolean) $force; 263 } 264 /** 265 * Set an attribute in a major header 266 * For example $headers->setAttribute("Content-Type", "format", "flowed") 267 * @param string The main header these values exist in 268 * @param string The name for this value 269 * @param string The value to set 270 * @throws Swift_Message_MimeException If no such header exists 271 */ 272 public function setAttribute($header, $name, $value) 273 { 274 $name = strtolower($name); 275 $lheader = strtolower($header); 276 $this->cached[$lheader] = null; 277 if (!$this->has($header)) 278 { 279 throw new Swift_Message_MimeException( 280 "Cannot set attribute '" . $name . "' for header '" . $header . "' as the header does not exist. " . 281 "Consider using Swift_Message_Headers->has() to check."); 282 } 283 else 284 { 285 Swift_ClassLoader::load("Swift_Message_Encoder"); 286 if (!$this->getCharset() && Swift_Message_Encoder::instance()->isUTF8($value)) $this->setCharset("utf-8"); 287 if (!isset($this->attributes[$lheader])) $this->attributes[$lheader] = array(); 288 if ($value !== null) $this->attributes[$lheader][$name] = (string) $value; 289 else $this->attributes[$lheader][$name] = $value; 290 } 291 } 292 /** 293 * Check if a header has a given attribute applied to it 294 * @param string The name of the main header 295 * @param string The name of the attribute 296 * @return boolean 297 */ 298 public function hasAttribute($header, $name) 299 { 300 $name = strtolower($name); 301 $lheader = strtolower($header); 302 if (!$this->has($header)) 303 { 304 return false; 305 } 306 else 307 { 308 return (isset($this->attributes[$lheader]) && isset($this->attributes[$lheader][$name]) && ($this->attributes[$lheader][$name] !== null)); 309 } 310 } 311 /** 312 * Get the value for a given attribute on a given header 313 * @param string The name of the main header 314 * @param string The name of the attribute 315 * @return string 316 * @throws Swift_Message_MimeException If no header is set 317 */ 318 public function getAttribute($header, $name) 319 { 320 if (!$this->has($header)) 321 { 322 throw new Swift_Message_MimeException( 323 "Cannot locate attribute '" . $name . "' for header '" . $header . "' as the header does not exist. " . 324 "Consider using Swift_Message_Headers->has() to check."); 325 } 326 327 $name = strtolower($name); 328 $lheader = strtolower($header); 329 330 if ($this->hasAttribute($header, $name)) 331 { 332 return $this->attributes[$lheader][$name]; 333 } 334 } 335 /** 336 * Remove an attribute from a header 337 * @param string The name of the header to remove the attribute from 338 * @param string The name of the attribute to remove 339 */ 340 public function removeAttribute($header, $name) 341 { 342 $name = strtolower($name); 343 $lheader = strtolower($header); 344 if ($this->has($header)) 345 { 346 unset($this->attributes[$lheader][$name]); 347 } 348 } 349 /** 350 * Get a list of all the attributes in the given header. 351 * @param string The name of the header 352 * @return array 353 */ 354 public function listAttributes($header) 355 { 356 $header = strtolower($header); 357 if (array_key_exists($header, $this->attributes)) 358 { 359 return $this->attributes[$header]; 360 } 361 else return array(); 362 } 363 /** 364 * Get the header in it's compliant, encoded form 365 * @param string The name of the header 366 * @return string 367 * @throws Swift_Message_MimeException If the header doesn't exist 368 */ 369 public function getEncoded($name) 370 { 371 if (!$this->getCharset()) $this->setCharset("iso-8859-1"); 372 Swift_ClassLoader::load("Swift_Message_Encoder"); 373 //I'll try as best I can to walk through this... 374 375 $lname = strtolower($name); 376 377 if ($this->cached[$lname] !== null) return $this->cached[$lname]; 378 379 $value = $this->get($name); 380 381 $is_email = in_array($name, $this->emailContainingHeaders); 382 383 $encoded_value = (array) $value; //Turn strings into arrays (just to make the following logic simpler) 384 385 //Look at each value in this header 386 // There will only be 1 value if it was a string to begin with, and usually only address lists will be multiple 387 foreach ($encoded_value as $key => $row) 388 { 389 $spec = ""; //The bit which specifies the encoding of the header (if any) 390 $end = ""; //The end delimiter for an encoded header 391 392 //If the header is 7-bit printable it's at no risk of injection 393 if (Swift_Message_Encoder::instance()->isHeaderSafe($row) && !$this->forceEncoding) 394 { 395 //Keeps the total line length at less than 76 chars, taking into account the Header name length 396 $encoded_value[$key] = Swift_Message_Encoder::instance()->header7BitEncode( 397 $row, 72, ($key > 0 ? 0 : (75-(strlen($name)+5))), $this->LE); 398 } 399 elseif ($this->encoding == "Q") //QP encode required 400 { 401 $spec = "=?" . $this->getCharset() . "?Q?"; //e.g. =?iso-8859-1?Q? 402 $end = "?="; 403 //Calculate the length of, for example: "From: =?iso-8859-1?Q??=" 404 $used_length = strlen($name) + 2 + strlen($spec) + 2; 405 406 //Encode to QP, excluding the specification for now but keeping the lines short enough to be compliant 407 $encoded_value[$key] = str_replace(" ", "_", Swift_Message_Encoder::instance()->QPEncode( 408 $row, (75-(strlen($spec)+6)), ($key > 0 ? 0 : (75-$used_length)), true, $this->LE)); 409 410 } 411 elseif ($this->encoding == "B") //Need to Base64 encode 412 { 413 //See the comments in the elseif() above since the logic is the same (refactor?) 414 $spec = "=?" . $this->getCharset() . "?B?"; 415 $end = "?="; 416 $used_length = strlen($name) + 2 + strlen($spec) + 2; 417 $encoded_value[$key] = Swift_Message_Encoder::instance()->base64Encode( 418 $row, (75-(strlen($spec)+5)), ($key > 0 ? 0 : (76-($used_length+3))), true, $this->LE); 419 } 420 421 if (false !== $p = strpos($encoded_value[$key], $this->LE)) 422 { 423 $cb = 'str_replace("' . $this->LE . '", "", "<$1>");'; 424 $encoded_value[$key] = preg_replace("/<([^>]+)>/e", $cb, $encoded_value[$key]); 425 } 426 427 //Turn our header into an array of lines ready for wrapping around the encoding specification 428 $lines = explode($this->LE, $encoded_value[$key]); 429 430 for ($i = 0, $len = count($lines); $i < $len; $i++) 431 { 432 //Don't allow commas in address fields without quotes unless they're encoded 433 if (empty($spec) && $is_email && (false !== $p = strpos($lines[$i], ","))) 434 { 435 $s = strpos($lines[$i], " <"); 436 $e = strpos($lines[$i], ">"); 437 if ($s < $e) 438 { 439 $addr = substr($lines[$i], $s); 440 $lines[$i] = "\"" . substr($lines[$i], 0, $s) . "\"" . $addr; 441 } 442 else 443 { 444 $lines[$i] = "\"" . $lines[$i] . "\""; 445 } 446 } 447 448 if ($this->encoding == "Q") $lines[$i] = rtrim($lines[$i], "="); 449 450 if ($lines[$i] == "" && $i > 0) 451 { 452 unset($lines[$i]); //Empty line, we'd rather not have these in the headers thank you! 453 continue; 454 } 455 if ($i > 0) 456 { 457 //Don't stick the specification part around the line if it's an address 458 if (substr($lines[$i], 0, 1) == '<' && substr($lines[$i], -1) == '>') $lines[$i] = " " . $lines[$i]; 459 else $lines[$i] = " " . $spec . $lines[$i] . $end; 460 } 461 else 462 { 463 if (substr($lines[$i], 0, 1) != '<' || substr($lines[$i], -1) != '>') $lines[$i] = $spec . $lines[$i] . $end; 464 } 465 } 466 //Build back into a string, now includes the specification 467 $encoded_value[$key] = implode($this->LE, $lines); 468 $lines = null; 469 } 470 471 //If there are multiple values in this header, put them on separate lines, cleared by commas 472 $this->cached[$lname] = implode("," . $this->LE . " ", $encoded_value); 473 474 //Append attributes if there are any 475 if (!empty($this->attributes[$lname])) $this->cached[$lname] .= $this->buildAttributes($this->cached[$lname], $lname); 476 477 return $this->cached[$lname]; 478 } 479 /** 480 * Build the list of attributes for appending to the given header 481 * This is RFC 2231 & 2047 compliant. 482 * A HUGE thanks to Joaquim Homrighausen for heaps of help, advice 483 * and testing to get this working rock solid. 484 * @param string The header built without attributes 485 * @param string The lowercase name of the header 486 * @return string 487 * @throws Swift_Message_MimeException If no such header exists or there are no attributes 488 */ 489 protected function buildAttributes($header_line, $header_name) 490 { 491 Swift_ClassLoader::load("Swift_Message_Encoder"); 492 $lines = explode($this->LE, $header_line); 493 $used_len = strlen($lines[count($lines)-1]); 494 $lines= null; 495 $ret = ""; 496 foreach ($this->attributes[$header_name] as $attribute => $att_value) 497 { 498 if ($att_value === null) continue; 499 // 70 to account for LWSP, CRLF, quotes and a semi-colon 500 // + length of attribute 501 // + 4 for a 2 digit number and 2 asterisks 502 $avail_len = 70 - (strlen($attribute) + 4); 503 $encoded = Swift_Message_Encoder::instance()->rfc2047Encode($att_value, $this->charset, $this->language, $avail_len, $this->LE); 504 $lines = explode($this->LE, $encoded); 505 foreach ($lines as $i => $line) 506 { 507 //Add quotes if needed (RFC 2045) 508 if (preg_match("~[\\s\";,<>\\(\\)@:\\\\/\\[\\]\\?=]~", $line)) $lines[$i] = '"' . $line . '"'; 509 } 510 $encoded = implode($this->LE, $lines); 511 512 //If we can fit this entire attribute onto the same line as the header then do it! 513 if ((strlen($encoded) + $used_len + strlen($attribute) + 4) < 74) 514 { 515 if (strpos($encoded, "'") !== false) $attribute .= "*"; 516 $append = "; " . $attribute . "=" . $encoded; 517 $ret .= $append; 518 $used_len += strlen($append); 519 } 520 else //... otherwise list of underneath 521 { 522 $ret .= ";"; 523 if (count($lines) > 1) 524 { 525 $loop = false; 526 $add_asterisk = false; 527 foreach ($lines as $i => $line) 528 { 529 $att_copy = $attribute; //Because it's multi-line it needs asterisks with decimal indices 530 $att_copy .= "*" . $i; 531 if ($add_asterisk || strpos($encoded, "'") !== false) 532 { 533 $att_copy .= "*"; //And if it's got a ' then it needs another asterisk 534 $add_asterisk = true; 535 } 536 $append = ""; 537 if ($loop) $append .= ";"; 538 $append .= $this->LE . " " . $att_copy . "=" . $line; 539 $ret .= $append; 540 $used_len = strlen($append)+1; 541 $loop = true; 542 } 543 } 544 else 545 { 546 if (strpos($encoded, "'") !== false) $attribute .= "*"; 547 $append = $this->LE . " " . $attribute . "=" . $encoded; 548 $used_len = strlen($append)+1; 549 $ret .= $append; 550 } 551 } 552 $lines= null; 553 } 554 return $ret; 555 } 556 /** 557 * Compile the list of headers which have been set and return an ascii string 558 * The return value should always be 7-bit ascii and will have been cleaned for header injection 559 * If this looks complicated it's probably because it is!! Keeping everything compliant is not easy. 560 * This is RFC 2822 compliant 561 * @return string 562 */ 563 public function build() 564 { 565 $ret = ""; 566 foreach ($this->headers as $name => $value) //Look at each header 567 { 568 if ($value === null) continue; 569 $ret .= ltrim($name, ".") . ": " . $this->getEncoded($name) . $this->LE; 570 } 571 return trim($ret); 572 } 573} 574