* @author Chris Corbyn * @package Swift_Message * @license GNU Lesser General Public License */ require_once dirname(__FILE__) . "/../ClassLoader.php"; Swift_ClassLoader::load("Swift_File"); Swift_ClassLoader::load("Swift_Message_MimeException"); /** * Mime is the underbelly for Messages, Attachments, Parts, Embedded Images, Forwarded Mail, etc * In fact, every single component of the composed email is simply a new Mime document nested inside another * When you piece an email together in this way you see just how straight-forward it really is * @package Swift_Message * @author Chris Corbyn */ abstract class Swift_Message_Mime { /** * Constant for plain-text emails */ const PLAIN = "text/plain"; /** * Constant for HTML emails */ const HTML = "text/html"; /** * Constant for miscellaneous mime type */ const MISC = "application/octet-stream"; /** * Constant for MIME sections which must appear in the multipart/alternative section. */ const LEVEL_ALTERNATIVE = "alternative"; /** * Constant for MIME sections which must appear in the multipart/related section. */ const LEVEL_RELATED = "related"; /** * Constant for MIME sections which must appear in the multipart/mixed section. */ const LEVEL_MIXED = "mixed"; /** * Constant for MIME sections which must appear in the multipart/mixed section. */ const LEVEL_TOP = "top"; /** * Constant for safe line length in almost all places */ const SAFE_LENGTH = 1000; //RFC 2822 /** * Constant for really safe line length */ const VERY_SAFE_LENGTH = 76; //For command line mail clients such as pine /** * The header part of this MIME document * @var Swift_Message_Headers */ public $headers = null; /** * The body of the documented (unencoded) * @var string data */ protected $data = ""; /** * Maximum line length * @var int */ protected $wrap = 1000; //RFC 2822 /** * Nested mime parts * @var array */ protected $children = array(); /** * The boundary used to separate mime parts * @var string */ protected $boundary = null; /** * The line ending characters needed * @var string */ protected $LE = "\r\n"; /** * An instance of Swift_Cache * @var Swift_Cache */ protected $cache; /** * A list of used MIME boundaries after they're generated. * @var array */ protected static $usedBoundaries = array(); /** * Constructor */ public function __construct() { Swift_ClassLoader::load("Swift_Message_Headers"); $this->setHeaders(new Swift_Message_Headers()); Swift_ClassLoader::load("Swift_CacheFactory"); $this->cache = Swift_CacheFactory::getCache(); } /** * Compute a unique boundary * @return string */ public static function generateBoundary() { do { $boundary = uniqid(rand(), true); } while (in_array($boundary, self::$usedBoundaries)); self::$usedBoundaries[] = $boundary; return "_=_swift-" . $boundary . "_=_"; } /** * Replace the current headers with new ones * DO NOT DO THIS UNLESS YOU KNOW WHAT YOU'RE DOING! * @param Swift_Message_Headers The headers to use */ public function setHeaders($headers) { $this->headers = $headers; } /** * Set the line ending character to use * @param string The line ending sequence * @return boolean */ public function setLE($le) { if (in_array($le, array("\r", "\n", "\r\n"))) { $this->cache->clear("body"); $this->LE = $le; //This change should be recursive $this->headers->setLE($le); foreach ($this->children as $id => $child) { $this->children[$id]->setLE($le); } return true; } else return false; } /** * Get the line ending sequence * @return string */ public function getLE() { return $this->LE; } /** * Reset the entire cache state from this branch of the tree and traversing down through the children */ public function uncacheAll() { $this->cache->clear("body"); $this->cache->clear("append"); $this->cache->clear("headers"); $this->cache->clear("dbl_le"); $this->headers->uncacheAll(); foreach ($this->children as $id => $child) { $this->children[$id]->uncacheAll(); } } /** * Set the content type of this MIME document * @param string The content type to use in the same format as MIME 1.0 expects */ public function setContentType($type) { $this->headers->set("Content-Type", $type); } /** * Get the content type which has been set * The MIME 1.0 Content-Type is provided as a string * @return string */ public function getContentType() { try { return $this->headers->get("Content-Type"); } catch (Swift_Message_MimeException $e) { return false; } } /** * Set the encoding format to be used on the body of the document * @param string The encoding type used * @param boolean If this encoding format should be used recursively. Note, this only takes effect if no encoding is set in the children. * @param boolean If the encoding should only be applied when the string is not ascii. */ public function setEncoding($encoding, $recursive=false, $non_ascii=false) { $this->cache->clear("body"); switch (strtolower($encoding)) { case "q": case "qp": case "quoted-printable": $encoding = "quoted-printable"; break; case "b": case "base64": $encoding = "base64"; break; case "7bit": case "8bit": case "binary": $encoding = strtolower($encoding); break; } $data = $this->getData(); Swift_ClassLoader::load("Swift_Message_Encoder"); if ($non_ascii && is_string($data) && strlen($data) > 0 && !Swift_Message_Encoder::instance()->is7BitAscii($data)) { $this->headers->set("Content-Transfer-Encoding", $encoding); } elseif (!$non_ascii || !is_string($data)) { $this->headers->set("Content-Transfer-Encoding", $encoding); } if ($recursive) { foreach ($this->children as $id => $child) { if (!$child->getEncoding()) $this->children[$id]->setEncoding($encoding, $recursive, $non_ascii); } } } /** * Get the encoding format used in this document * @return string */ public function getEncoding() { try { return $this->headers->get("Content-Transfer-Encoding"); } catch (Swift_Message_MimeException $e) { return false; } } /** * Specify the string which makes up the body of this message * HINT: You can always nest another MIME document here if you call it's build() method. * $data can be an object of Swift_File or a string * @param mixed The body of the document */ public function setData($data) { $this->cache->clear("body"); if ($data instanceof Swift_File) $this->data = $data; else $this->data = (string) $data; } /** * Return the string which makes up the body of this MIME document * @return string,Swift_File */ public function getData() { return $this->data; } /** * Get the data in the format suitable for sending * @return Swift_Cache_OutputStream * @throws Swift_FileException If the file stream given cannot be read * @throws Swift_Message_MimeException If some required headers have been forcefully removed */ public function buildData() { Swift_ClassLoader::load("Swift_Message_Encoder"); Swift_ClassLoader::load("Swift_Cache_JointOutputStream"); if (!empty($this->children)) //If we've got some mime parts we need to stick them onto the end of the message { if ($this->boundary === null) $this->boundary = self::generateBoundary(); $this->headers->setAttribute("Content-Type", "boundary", $this->boundary); $this->cache->clear("append"); foreach ($this->children as $part) { $this->cache->write("append", $this->LE . "--" . $this->boundary . $this->LE); $part_stream = $part->build(); while (false !== $bytes = $part_stream->read()) $this->cache->write("append", $bytes); } $this->cache->write("append", $this->LE . "--" . $this->boundary . "--" . $this->LE); } $joint_os = new Swift_Cache_JointOutputStream(); //Try using a cached version to save some cycles (at the expense of memory) //if ($this->cache !== null) return $this->cache . $append; if ($this->cache->has("body")) { $joint_os->addStream($this->cache->getOutputStream("body")); $joint_os->addStream($this->cache->getOutputStream("append")); return $joint_os; } $is_file = ($this->getData() instanceof Swift_File); switch ($this->getEncoding()) { case "quoted-printable": if ($is_file) { $qp_os = Swift_Message_Encoder::instance()->QPEncodeFile($this->getData(), 76, $this->LE); while (false !== $bytes = $qp_os->read()) $this->cache->write("body", $bytes); } else { $this->cache->write("body", Swift_Message_Encoder::instance()->QPEncode($this->getData(), 76, 0, false, $this->LE)); } break; case "base64": if ($is_file) { $b64_os = Swift_Message_Encoder::instance()->base64EncodeFile($this->getData(), 76, $this->LE); while (false !== $bytes = $b64_os->read()) $this->cache->write("body", $bytes); } else { $this->cache->write("body", Swift_Message_Encoder::instance()->base64Encode($this->getData(), 76, 0, false, $this->LE)); } break; case "binary": if ($is_file) { $data = $this->getData(); while (false !== $bytes = $data->read(8192)) $this->cache->write("body", $bytes); } else { $this->cache->write("body", $this->getData()); } break; case "7bit": if ($is_file) { $os = Swift_Message_Encoder::instance()->encode7BitFile($this->getData(), $this->wrap, $this->LE); while (false !== $bytes = $os->read()) $this->cache->write("body", $bytes); } else { $this->cache->write("body", Swift_Message_Encoder::instance()->encode7Bit($this->getData(), $this->wrap, $this->LE)); } break; case "8bit": default: if ($is_file) { $os = Swift_Message_Encoder::instance()->encode8BitFile($this->getData(), $this->wrap, $this->LE); while (false !== $bytes = $os->read()) $this->cache->write("body", $bytes); } else { $this->cache->write("body", Swift_Message_Encoder::instance()->encode8Bit($this->getData(), $this->wrap, $this->LE)); } break; } $joint_os->addStream($this->cache->getOutputStream("body")); $joint_os->addStream($this->cache->getOutputStream("append")); return $joint_os; } /** * Set the size at which lines wrap around (includes the CRLF) * @param int The length of a line */ public function setLineWrap($len) { $this->cache->clear("body"); $this->wrap = (int) $len; } /** * Nest a child mime part in this document * @param Swift_Message_Mime * @param string The identifier to use, optional * @param int Add the part before (-1) or after (+1) the other parts * @return string The identifier for this part */ public function addChild(Swift_Message_Mime $mime, $id=null, $after=1) { if (empty($id)) { do { $id = uniqid(); } while (array_key_exists($id, $this->children)); } $id = (string) $id; if ($after == -1) $this->children = array_merge(array($id => $mime), $this->children); else $this->children[$id] = $mime; return $id; } /** * Check if a child exists identified by $id * @param string Identifier to look for * @return boolean */ public function hasChild($id) { return array_key_exists($id, $this->children); } /** * Get a child document, identified by $id * @param string The identifier for this child * @return Swift_Message_Mime The child document * @throws Swift_Message_MimeException If no such child exists */ public function getChild($id) { if ($this->hasChild($id)) { return $this->children[$id]; } else { throw new Swift_Message_MimeException( "Cannot retrieve child part identified by '" . $id . "' as it does not exist. Consider using hasChild() to check."); } } /** * Remove a part from the document * @param string The identifier of the child * @throws Swift_Message_MimeException If no such part exists */ public function removeChild($id) { $id = (string) $id; if (!$this->hasChild($id)) { throw new Swift_Message_MimeException( "Cannot remove child part identified by '" . $id . "' as it does not exist. Consider using hasChild() to check."); } else { $this->children[$id] = null; unset($this->children[$id]); } } /** * List the IDs of all children in this document * @return array */ public function listChildren() { return array_keys($this->children); } /** * Get the total number of children present in this document * @return int */ public function numChildren() { return count($this->children); } /** * Get the level at which this mime part would appear in a document * One of "mixed", "alternative" or "related" * @return string */ abstract public function getLevel(); /** * Compile the entire MIME document into a string * The returned string may be used in other documents if needed. * @return Swift_Cache_OutputStream */ public function build() { $this->preBuild(); $data = $this->buildData(); $joint_os = new Swift_Cache_JointOutputStream(); $this->cache->clear("headers"); $this->cache->write("headers", $this->headers->build()); $joint_os->addStream($this->cache->getOutputStream("headers")); $this->cache->clear("dbl_le"); $this->cache->write("dbl_le", str_repeat($this->LE, 2)); $joint_os->addStream($this->cache->getOutputStream("dbl_le")); $joint_os->addStream($data); return $joint_os; //return $this->headers->build() . str_repeat($this->LE, 2) . $data; } /** * Execute any logic needed prior to building */ abstract public function preBuild(); }