1<?php 2 3/** 4 * Swift Mailer MIME Library central 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"; 13Swift_ClassLoader::load("Swift_File"); 14Swift_ClassLoader::load("Swift_Message_MimeException"); 15 16/** 17 * Mime is the underbelly for Messages, Attachments, Parts, Embedded Images, Forwarded Mail, etc 18 * In fact, every single component of the composed email is simply a new Mime document nested inside another 19 * When you piece an email together in this way you see just how straight-forward it really is 20 * @package Swift_Message 21 * @author Chris Corbyn <chris@w3style.co.uk> 22 */ 23abstract class Swift_Message_Mime 24{ 25 /** 26 * Constant for plain-text emails 27 */ 28 const PLAIN = "text/plain"; 29 /** 30 * Constant for HTML emails 31 */ 32 const HTML = "text/html"; 33 /** 34 * Constant for miscellaneous mime type 35 */ 36 const MISC = "application/octet-stream"; 37 /** 38 * Constant for MIME sections which must appear in the multipart/alternative section. 39 */ 40 const LEVEL_ALTERNATIVE = "alternative"; 41 /** 42 * Constant for MIME sections which must appear in the multipart/related section. 43 */ 44 const LEVEL_RELATED = "related"; 45 /** 46 * Constant for MIME sections which must appear in the multipart/mixed section. 47 */ 48 const LEVEL_MIXED = "mixed"; 49 /** 50 * Constant for MIME sections which must appear in the multipart/mixed section. 51 */ 52 const LEVEL_TOP = "top"; 53 /** 54 * Constant for safe line length in almost all places 55 */ 56 const SAFE_LENGTH = 1000; //RFC 2822 57 /** 58 * Constant for really safe line length 59 */ 60 const VERY_SAFE_LENGTH = 76; //For command line mail clients such as pine 61 /** 62 * The header part of this MIME document 63 * @var Swift_Message_Headers 64 */ 65 public $headers = null; 66 /** 67 * The body of the documented (unencoded) 68 * @var string data 69 */ 70 protected $data = ""; 71 /** 72 * Maximum line length 73 * @var int 74 */ 75 protected $wrap = 1000; //RFC 2822 76 /** 77 * Nested mime parts 78 * @var array 79 */ 80 protected $children = array(); 81 /** 82 * The boundary used to separate mime parts 83 * @var string 84 */ 85 protected $boundary = null; 86 /** 87 * The line ending characters needed 88 * @var string 89 */ 90 protected $LE = "\r\n"; 91 /** 92 * An instance of Swift_Cache 93 * @var Swift_Cache 94 */ 95 protected $cache; 96 /** 97 * A list of used MIME boundaries after they're generated. 98 * @var array 99 */ 100 protected static $usedBoundaries = array(); 101 102 /** 103 * Constructor 104 */ 105 public function __construct() 106 { 107 Swift_ClassLoader::load("Swift_Message_Headers"); 108 $this->setHeaders(new Swift_Message_Headers()); 109 Swift_ClassLoader::load("Swift_CacheFactory"); 110 $this->cache = Swift_CacheFactory::getCache(); 111 } 112 /** 113 * Compute a unique boundary 114 * @return string 115 */ 116 public static function generateBoundary() 117 { 118 do 119 { 120 $boundary = uniqid(rand(), true); 121 } while (in_array($boundary, self::$usedBoundaries)); 122 self::$usedBoundaries[] = $boundary; 123 return "_=_swift-" . $boundary . "_=_"; 124 } 125 /** 126 * Replace the current headers with new ones 127 * DO NOT DO THIS UNLESS YOU KNOW WHAT YOU'RE DOING! 128 * @param Swift_Message_Headers The headers to use 129 */ 130 public function setHeaders($headers) 131 { 132 $this->headers = $headers; 133 } 134 /** 135 * Set the line ending character to use 136 * @param string The line ending sequence 137 * @return boolean 138 */ 139 public function setLE($le) 140 { 141 if (in_array($le, array("\r", "\n", "\r\n"))) 142 { 143 $this->cache->clear("body"); 144 $this->LE = $le; 145 //This change should be recursive 146 $this->headers->setLE($le); 147 foreach ($this->children as $id => $child) 148 { 149 $this->children[$id]->setLE($le); 150 } 151 152 return true; 153 } 154 else return false; 155 } 156 /** 157 * Get the line ending sequence 158 * @return string 159 */ 160 public function getLE() 161 { 162 return $this->LE; 163 } 164 /** 165 * Reset the entire cache state from this branch of the tree and traversing down through the children 166 */ 167 public function uncacheAll() 168 { 169 $this->cache->clear("body"); 170 $this->cache->clear("append"); 171 $this->cache->clear("headers"); 172 $this->cache->clear("dbl_le"); 173 $this->headers->uncacheAll(); 174 foreach ($this->children as $id => $child) 175 { 176 $this->children[$id]->uncacheAll(); 177 } 178 } 179 /** 180 * Set the content type of this MIME document 181 * @param string The content type to use in the same format as MIME 1.0 expects 182 */ 183 public function setContentType($type) 184 { 185 $this->headers->set("Content-Type", $type); 186 } 187 /** 188 * Get the content type which has been set 189 * The MIME 1.0 Content-Type is provided as a string 190 * @return string 191 */ 192 public function getContentType() 193 { 194 try { 195 return $this->headers->get("Content-Type"); 196 } catch (Swift_Message_MimeException $e) { 197 return false; 198 } 199 } 200 /** 201 * Set the encoding format to be used on the body of the document 202 * @param string The encoding type used 203 * @param boolean If this encoding format should be used recursively. Note, this only takes effect if no encoding is set in the children. 204 * @param boolean If the encoding should only be applied when the string is not ascii. 205 */ 206 public function setEncoding($encoding, $recursive=false, $non_ascii=false) 207 { 208 $this->cache->clear("body"); 209 switch (strtolower($encoding)) 210 { 211 case "q": case "qp": case "quoted-printable": 212 $encoding = "quoted-printable"; 213 break; 214 case "b": case "base64": 215 $encoding = "base64"; 216 break; 217 case "7bit": case "8bit": case "binary": 218 $encoding = strtolower($encoding); 219 break; 220 } 221 222 $data = $this->getData(); 223 Swift_ClassLoader::load("Swift_Message_Encoder"); 224 if ($non_ascii && is_string($data) && strlen($data) > 0 && !Swift_Message_Encoder::instance()->is7BitAscii($data)) 225 { 226 $this->headers->set("Content-Transfer-Encoding", $encoding); 227 } 228 elseif (!$non_ascii || !is_string($data)) 229 { 230 $this->headers->set("Content-Transfer-Encoding", $encoding); 231 } 232 233 if ($recursive) 234 { 235 foreach ($this->children as $id => $child) 236 { 237 if (!$child->getEncoding()) $this->children[$id]->setEncoding($encoding, $recursive, $non_ascii); 238 } 239 } 240 } 241 /** 242 * Get the encoding format used in this document 243 * @return string 244 */ 245 public function getEncoding() 246 { 247 try { 248 return $this->headers->get("Content-Transfer-Encoding"); 249 } catch (Swift_Message_MimeException $e) { 250 return false; 251 } 252 } 253 /** 254 * Specify the string which makes up the body of this message 255 * HINT: You can always nest another MIME document here if you call it's build() method. 256 * $data can be an object of Swift_File or a string 257 * @param mixed The body of the document 258 */ 259 public function setData($data) 260 { 261 $this->cache->clear("body"); 262 if ($data instanceof Swift_File) $this->data = $data; 263 else $this->data = (string) $data; 264 } 265 /** 266 * Return the string which makes up the body of this MIME document 267 * @return string,Swift_File 268 */ 269 public function getData() 270 { 271 return $this->data; 272 } 273 /** 274 * Get the data in the format suitable for sending 275 * @return Swift_Cache_OutputStream 276 * @throws Swift_FileException If the file stream given cannot be read 277 * @throws Swift_Message_MimeException If some required headers have been forcefully removed 278 */ 279 public function buildData() 280 { 281 Swift_ClassLoader::load("Swift_Message_Encoder"); 282 Swift_ClassLoader::load("Swift_Cache_JointOutputStream"); 283 if (!empty($this->children)) //If we've got some mime parts we need to stick them onto the end of the message 284 { 285 if ($this->boundary === null) $this->boundary = self::generateBoundary(); 286 $this->headers->setAttribute("Content-Type", "boundary", $this->boundary); 287 288 $this->cache->clear("append"); 289 foreach ($this->children as $part) 290 { 291 $this->cache->write("append", $this->LE . "--" . $this->boundary . $this->LE); 292 $part_stream = $part->build(); 293 while (false !== $bytes = $part_stream->read()) $this->cache->write("append", $bytes); 294 } 295 $this->cache->write("append", $this->LE . "--" . $this->boundary . "--" . $this->LE); 296 } 297 298 $joint_os = new Swift_Cache_JointOutputStream(); 299 300 //Try using a cached version to save some cycles (at the expense of memory) 301 //if ($this->cache !== null) return $this->cache . $append; 302 if ($this->cache->has("body")) 303 { 304 $joint_os->addStream($this->cache->getOutputStream("body")); 305 $joint_os->addStream($this->cache->getOutputStream("append")); 306 return $joint_os; 307 } 308 309 $is_file = ($this->getData() instanceof Swift_File); 310 switch ($this->getEncoding()) 311 { 312 case "quoted-printable": 313 if ($is_file) 314 { 315 $qp_os = Swift_Message_Encoder::instance()->QPEncodeFile($this->getData(), 76, $this->LE); 316 while (false !== $bytes = $qp_os->read()) 317 $this->cache->write("body", $bytes); 318 } 319 else 320 { 321 $this->cache->write("body", Swift_Message_Encoder::instance()->QPEncode($this->getData(), 76, 0, false, $this->LE)); 322 } 323 break; 324 case "base64": 325 if ($is_file) 326 { 327 $b64_os = Swift_Message_Encoder::instance()->base64EncodeFile($this->getData(), 76, $this->LE); 328 while (false !== $bytes = $b64_os->read()) 329 $this->cache->write("body", $bytes); 330 } 331 else 332 { 333 $this->cache->write("body", Swift_Message_Encoder::instance()->base64Encode($this->getData(), 76, 0, false, $this->LE)); 334 } 335 break; 336 case "binary": 337 if ($is_file) 338 { 339 $data = $this->getData(); 340 while (false !== $bytes = $data->read(8192)) 341 $this->cache->write("body", $bytes); 342 } 343 else 344 { 345 $this->cache->write("body", $this->getData()); 346 } 347 break; 348 case "7bit": 349 if ($is_file) 350 { 351 $os = Swift_Message_Encoder::instance()->encode7BitFile($this->getData(), $this->wrap, $this->LE); 352 while (false !== $bytes = $os->read()) 353 $this->cache->write("body", $bytes); 354 } 355 else 356 { 357 $this->cache->write("body", Swift_Message_Encoder::instance()->encode7Bit($this->getData(), $this->wrap, $this->LE)); 358 } 359 break; 360 case "8bit": default: 361 if ($is_file) 362 { 363 $os = Swift_Message_Encoder::instance()->encode8BitFile($this->getData(), $this->wrap, $this->LE); 364 while (false !== $bytes = $os->read()) 365 $this->cache->write("body", $bytes); 366 } 367 else 368 { 369 $this->cache->write("body", Swift_Message_Encoder::instance()->encode8Bit($this->getData(), $this->wrap, $this->LE)); 370 } 371 break; 372 } 373 $joint_os->addStream($this->cache->getOutputStream("body")); 374 $joint_os->addStream($this->cache->getOutputStream("append")); 375 return $joint_os; 376 } 377 /** 378 * Set the size at which lines wrap around (includes the CRLF) 379 * @param int The length of a line 380 */ 381 public function setLineWrap($len) 382 { 383 $this->cache->clear("body"); 384 $this->wrap = (int) $len; 385 } 386 /** 387 * Nest a child mime part in this document 388 * @param Swift_Message_Mime 389 * @param string The identifier to use, optional 390 * @param int Add the part before (-1) or after (+1) the other parts 391 * @return string The identifier for this part 392 */ 393 public function addChild(Swift_Message_Mime $mime, $id=null, $after=1) 394 { 395 if (empty($id)) 396 { 397 do 398 { 399 $id = uniqid(); 400 } while (array_key_exists($id, $this->children)); 401 } 402 $id = (string) $id; 403 if ($after == -1) $this->children = array_merge(array($id => $mime), $this->children); 404 else $this->children[$id] = $mime; 405 406 return $id; 407 } 408 /** 409 * Check if a child exists identified by $id 410 * @param string Identifier to look for 411 * @return boolean 412 */ 413 public function hasChild($id) 414 { 415 return array_key_exists($id, $this->children); 416 } 417 /** 418 * Get a child document, identified by $id 419 * @param string The identifier for this child 420 * @return Swift_Message_Mime The child document 421 * @throws Swift_Message_MimeException If no such child exists 422 */ 423 public function getChild($id) 424 { 425 if ($this->hasChild($id)) 426 { 427 return $this->children[$id]; 428 } 429 else 430 { 431 throw new Swift_Message_MimeException( 432 "Cannot retrieve child part identified by '" . $id . "' as it does not exist. Consider using hasChild() to check."); 433 } 434 } 435 /** 436 * Remove a part from the document 437 * @param string The identifier of the child 438 * @throws Swift_Message_MimeException If no such part exists 439 */ 440 public function removeChild($id) 441 { 442 $id = (string) $id; 443 if (!$this->hasChild($id)) 444 { 445 throw new Swift_Message_MimeException( 446 "Cannot remove child part identified by '" . $id . "' as it does not exist. Consider using hasChild() to check."); 447 } 448 else 449 { 450 $this->children[$id] = null; 451 unset($this->children[$id]); 452 } 453 } 454 /** 455 * List the IDs of all children in this document 456 * @return array 457 */ 458 public function listChildren() 459 { 460 return array_keys($this->children); 461 } 462 /** 463 * Get the total number of children present in this document 464 * @return int 465 */ 466 public function numChildren() 467 { 468 return count($this->children); 469 } 470 /** 471 * Get the level at which this mime part would appear in a document 472 * One of "mixed", "alternative" or "related" 473 * @return string 474 */ 475 abstract public function getLevel(); 476 /** 477 * Compile the entire MIME document into a string 478 * The returned string may be used in other documents if needed. 479 * @return Swift_Cache_OutputStream 480 */ 481 public function build() 482 { 483 $this->preBuild(); 484 $data = $this->buildData(); 485 $joint_os = new Swift_Cache_JointOutputStream(); 486 $this->cache->clear("headers"); 487 $this->cache->write("headers", $this->headers->build()); 488 $joint_os->addStream($this->cache->getOutputStream("headers")); 489 $this->cache->clear("dbl_le"); 490 $this->cache->write("dbl_le", str_repeat($this->LE, 2)); 491 $joint_os->addStream($this->cache->getOutputStream("dbl_le")); 492 $joint_os->addStream($data); 493 return $joint_os; 494 //return $this->headers->build() . str_repeat($this->LE, 2) . $data; 495 } 496 /** 497 * Execute any logic needed prior to building 498 */ 499 abstract public function preBuild(); 500} 501