TYPO3 API  SVNRelease
SimpleMimeEntity.php
Go to the documentation of this file.
00001 <?php
00002 
00003 /*
00004  * This file is part of SwiftMailer.
00005  * (c) 2004-2009 Chris Corbyn
00006  *
00007  * For the full copyright and license information, please view the LICENSE
00008  * file that was distributed with this source code.
00009  */
00010 
00011 //@require 'Swift/Mime/HeaderSet.php';
00012 //@require 'Swift/OutputByteStream.php';
00013 //@require 'Swift/Mime/ContentEncoder.php';
00014 //@require 'Swift/KeyCache.php';
00015 
00016 /**
00017  * A MIME entity, in a multipart message.
00018  * @package Swift
00019  * @subpackage Mime
00020  * @author Chris Corbyn
00021  */
00022 class Swift_Mime_SimpleMimeEntity implements Swift_Mime_MimeEntity
00023 {
00024 
00025   /** A collection of Headers for this mime entity */
00026   private $_headers;
00027 
00028   /** The body as a string, or a stream */
00029   private $_body;
00030 
00031   /** The encoder that encodes the body into a streamable format */
00032   private $_encoder;
00033 
00034   /** A mime bounary, if any is used */
00035   private $_boundary;
00036 
00037   /** Mime types to be used based on the nesting level */
00038   private $_compositeRanges = array(
00039     'multipart/mixed' => array(self::LEVEL_TOP, self::LEVEL_MIXED),
00040     'multipart/alternative' => array(self::LEVEL_MIXED, self::LEVEL_ALTERNATIVE),
00041     'multipart/related' => array(self::LEVEL_ALTERNATIVE, self::LEVEL_RELATED)
00042     );
00043 
00044   /** A set of filter rules to define what level an entity should be nested at */
00045   private $_compoundLevelFilters = array();
00046 
00047   /** The nesting level of this entity */
00048   private $_nestingLevel = self::LEVEL_ALTERNATIVE;
00049 
00050   /** A KeyCache instance used during encoding and streaming */
00051   private $_cache;
00052 
00053   /** Direct descendants of this entity */
00054   private $_immediateChildren = array();
00055 
00056   /** All descendants of this entity */
00057   private $_children = array();
00058 
00059   /** The maximum line length of the body of this entity */
00060   private $_maxLineLength = 78;
00061 
00062   /** The order in which alternative mime types should appear */
00063   private $_alternativePartOrder = array(
00064     'text/plain' => 1,
00065     'text/html' => 2,
00066     'multipart/related' => 3
00067     );
00068 
00069   /** The CID of this entity */
00070   private $_id;
00071 
00072   /** The key used for accessing the cache */
00073   private $_cacheKey;
00074 
00075   protected $_userContentType;
00076 
00077   /**
00078    * Create a new SimpleMimeEntity with $headers, $encoder and $cache.
00079    * @param Swift_Mime_HeaderSet $headers
00080    * @param Swift_Mime_ContentEncoder $encoder
00081    * @param Swift_KeyCache $cache
00082    */
00083   public function __construct(Swift_Mime_HeaderSet $headers,
00084     Swift_Mime_ContentEncoder $encoder, Swift_KeyCache $cache)
00085   {
00086     $this->_cacheKey = uniqid();
00087     $this->_cache = $cache;
00088     $this->_headers = $headers;
00089     $this->setEncoder($encoder);
00090     $this->_headers->defineOrdering(
00091       array('Content-Type', 'Content-Transfer-Encoding')
00092       );
00093 
00094     // This array specifies that, when the entire MIME document contains
00095     // $compoundLevel, then for each child within $level, if its Content-Type
00096     // is $contentType then it should be treated as if it's level is
00097     // $neededLevel instead.  I tried to write that unambiguously! :-\
00098     // Data Structure:
00099     // array (
00100     //   $compoundLevel => array(
00101     //     $level => array(
00102     //       $contentType => $neededLevel
00103     //     )
00104     //   )
00105     // )
00106 
00107     $this->_compoundLevelFilters = array(
00108       (self::LEVEL_ALTERNATIVE + self::LEVEL_RELATED) => array(
00109         self::LEVEL_ALTERNATIVE => array(
00110           'text/plain' => self::LEVEL_ALTERNATIVE,
00111           'text/html' => self::LEVEL_RELATED
00112           )
00113         )
00114       );
00115 
00116     $this->_id = $this->getRandomId();
00117   }
00118 
00119   /**
00120    * Generate a new Content-ID or Message-ID for this MIME entity.
00121    * @return string
00122    */
00123   public function generateId()
00124   {
00125     $this->setId($this->getRandomId());
00126     return $this->_id;
00127   }
00128 
00129   /**
00130    * Get the {@link Swift_Mime_HeaderSet} for this entity.
00131    * @return Swift_Mime_HeaderSet
00132    */
00133   public function getHeaders()
00134   {
00135     return $this->_headers;
00136   }
00137 
00138   /**
00139    * Get the nesting level of this entity.
00140    * @return int
00141    * @see LEVEL_TOP, LEVEL_MIXED, LEVEL_RELATED, LEVEL_ALTERNATIVE
00142    */
00143   public function getNestingLevel()
00144   {
00145     return $this->_nestingLevel;
00146   }
00147 
00148   /**
00149    * Get the Content-type of this entity.
00150    * @return string
00151    */
00152   public function getContentType()
00153   {
00154     return $this->_getHeaderFieldModel('Content-Type');
00155   }
00156 
00157   /**
00158    * Set the Content-type of this entity.
00159    * @param string $type
00160    */
00161   public function setContentType($type)
00162   {
00163     $this->_setContentTypeInHeaders($type);
00164     // Keep track of the value so that if the content-type changes automatically
00165     // due to added child entities, it can be restored if they are later removed
00166     $this->_userContentType = $type;
00167     return $this;
00168   }
00169 
00170   /**
00171    * Get the CID of this entity.
00172    * The CID will only be present in headers if a Content-ID header is present.
00173    * @return string
00174    */
00175   public function getId()
00176   {
00177     return $this->_headers->has($this->_getIdField())
00178       ? current((array) $this->_getHeaderFieldModel($this->_getIdField()))
00179       : $this->_id;
00180   }
00181 
00182   /**
00183    * Set the CID of this entity.
00184    * @param string $id
00185    */
00186   public function setId($id)
00187   {
00188     if (!$this->_setHeaderFieldModel($this->_getIdField(), $id))
00189     {
00190       $this->_headers->addIdHeader($this->_getIdField(), $id);
00191     }
00192     $this->_id = $id;
00193     return $this;
00194   }
00195 
00196   /**
00197    * Get the description of this entity.
00198    * This value comes from the Content-Description header if set.
00199    * @return string
00200    */
00201   public function getDescription()
00202   {
00203     return $this->_getHeaderFieldModel('Content-Description');
00204   }
00205 
00206   /**
00207    * Set the description of this entity.
00208    * This method sets a value in the Content-ID header.
00209    * @param string $description
00210    */
00211   public function setDescription($description)
00212   {
00213     if (!$this->_setHeaderFieldModel('Content-Description', $description))
00214     {
00215       $this->_headers->addTextHeader('Content-Description', $description);
00216     }
00217     return $this;
00218   }
00219 
00220   /**
00221    * Get the maximum line length of the body of this entity.
00222    * @return int
00223    */
00224   public function getMaxLineLength()
00225   {
00226     return $this->_maxLineLength;
00227   }
00228 
00229   /**
00230    * Set the maximum line length of lines in this body.
00231    * Though not enforced by the library, lines should not exceed 1000 chars.
00232    * @param int $length
00233    */
00234   public function setMaxLineLength($length)
00235   {
00236     $this->_maxLineLength = $length;
00237     return $this;
00238   }
00239 
00240   /**
00241    * Get all children added to this entity.
00242    * @return array of Swift_Mime_Entity
00243    */
00244   public function getChildren()
00245   {
00246     return $this->_children;
00247   }
00248 
00249   /**
00250    * Set all children of this entity.
00251    * @param array $children Swiift_Mime_Entity instances
00252    * @param int $compoundLevel For internal use only
00253    */
00254   public function setChildren(array $children, $compoundLevel = null)
00255   {
00256     //TODO: Try to refactor this logic
00257 
00258     $compoundLevel = isset($compoundLevel)
00259       ? $compoundLevel
00260       : $this->_getCompoundLevel($children)
00261       ;
00262 
00263     $immediateChildren = array();
00264     $grandchildren = array();
00265     $newContentType = $this->_userContentType;
00266 
00267     foreach ($children as $child)
00268     {
00269       $level = $this->_getNeededChildLevel($child, $compoundLevel);
00270       if (empty($immediateChildren)) //first iteration
00271       {
00272         $immediateChildren = array($child);
00273       }
00274       else
00275       {
00276         $nextLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel);
00277         if ($nextLevel == $level)
00278         {
00279           $immediateChildren[] = $child;
00280         }
00281         elseif ($level < $nextLevel)
00282         {
00283           //Re-assign immediateChildren to grandchilden
00284           $grandchildren = array_merge($grandchildren, $immediateChildren);
00285           //Set new children
00286           $immediateChildren = array($child);
00287         }
00288         else
00289         {
00290           $grandchildren[] = $child;
00291         }
00292       }
00293     }
00294 
00295     if (!empty($immediateChildren))
00296     {
00297       $lowestLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel);
00298 
00299       //Determine which composite media type is needed to accomodate the
00300       // immediate children
00301       foreach ($this->_compositeRanges as $mediaType => $range)
00302       {
00303         if ($lowestLevel > $range[0]
00304           && $lowestLevel <= $range[1])
00305         {
00306           $newContentType = $mediaType;
00307           break;
00308         }
00309       }
00310 
00311       //Put any grandchildren in a subpart
00312       if (!empty($grandchildren))
00313       {
00314         $subentity = $this->_createChild();
00315         $subentity->_setNestingLevel($lowestLevel);
00316         $subentity->setChildren($grandchildren, $compoundLevel);
00317         array_unshift($immediateChildren, $subentity);
00318       }
00319     }
00320 
00321     $this->_immediateChildren = $immediateChildren;
00322     $this->_children = $children;
00323     $this->_setContentTypeInHeaders($newContentType);
00324     $this->_fixHeaders();
00325     $this->_sortChildren();
00326 
00327     return $this;
00328   }
00329 
00330   /**
00331    * Get the body of this entity as a string.
00332    * @return string
00333    */
00334   public function getBody()
00335   {
00336     return ($this->_body instanceof Swift_OutputByteStream)
00337       ? $this->_readStream($this->_body)
00338       : $this->_body;
00339   }
00340 
00341   /**
00342    * Set the body of this entity, either as a string, or as an instance of
00343    * {@link Swift_OutputByteStream}.
00344    * @param mixed $body
00345    * @param string $contentType optional
00346    */
00347   public function setBody($body, $contentType = null)
00348   {
00349     if ($body !== $this->_body)
00350     {
00351       $this->_clearCache();
00352     }
00353 
00354     $this->_body = $body;
00355     if (isset($contentType))
00356     {
00357       $this->setContentType($contentType);
00358     }
00359     return $this;
00360   }
00361 
00362   /**
00363    * Get the encoder used for the body of this entity.
00364    * @return Swift_Mime_ContentEncoder
00365    */
00366   public function getEncoder()
00367   {
00368     return $this->_encoder;
00369   }
00370 
00371   /**
00372    * Set the encoder used for the body of this entity.
00373    * @param Swift_Mime_ContentEncoder $encoder
00374    */
00375   public function setEncoder(Swift_Mime_ContentEncoder $encoder)
00376   {
00377     if ($encoder !== $this->_encoder)
00378     {
00379       $this->_clearCache();
00380     }
00381 
00382     $this->_encoder = $encoder;
00383     $this->_setEncoding($encoder->getName());
00384     $this->_notifyEncoderChanged($encoder);
00385     return $this;
00386   }
00387 
00388   /**
00389    * Get the boundary used to separate children in this entity.
00390    * @return string
00391    */
00392   public function getBoundary()
00393   {
00394     if (!isset($this->_boundary))
00395     {
00396       $this->_boundary = '_=_swift_v4_' . time() . uniqid() . '_=_';
00397     }
00398     return $this->_boundary;
00399   }
00400 
00401   /**
00402    * Set the boundary used to separate children in this entity.
00403    * @param string $boundary
00404    * @throws Swift_RfcComplianceException
00405    */
00406   public function setBoundary($boundary)
00407   {
00408     $this->_assertValidBoundary($boundary);
00409     $this->_boundary = $boundary;
00410     return $this;
00411   }
00412 
00413   /**
00414    * Receive notification that the charset of this entity, or a parent entity
00415    * has changed.
00416    * @param string $charset
00417    */
00418   public function charsetChanged($charset)
00419   {
00420     $this->_notifyCharsetChanged($charset);
00421   }
00422 
00423   /**
00424    * Receive notification that the encoder of this entity or a parent entity
00425    * has changed.
00426    * @param Swift_Mime_ContentEncoder $encoder
00427    */
00428   public function encoderChanged(Swift_Mime_ContentEncoder $encoder)
00429   {
00430     $this->_notifyEncoderChanged($encoder);
00431   }
00432 
00433   /**
00434    * Get this entire entity as a string.
00435    * @return string
00436    */
00437   public function toString()
00438   {
00439     $string = $this->_headers->toString();
00440     if (isset($this->_body) && empty($this->_immediateChildren))
00441     {
00442       if ($this->_cache->hasKey($this->_cacheKey, 'body'))
00443       {
00444         $body = $this->_cache->getString($this->_cacheKey, 'body');
00445       }
00446       else
00447       {
00448         $body = "\r\n" . $this->_encoder->encodeString($this->getBody(), 0,
00449           $this->getMaxLineLength()
00450           );
00451         $this->_cache->setString($this->_cacheKey, 'body', $body,
00452           Swift_KeyCache::MODE_WRITE
00453           );
00454       }
00455       $string .= $body;
00456     }
00457 
00458     if (!empty($this->_immediateChildren))
00459     {
00460       foreach ($this->_immediateChildren as $child)
00461       {
00462         $string .= "\r\n\r\n--" . $this->getBoundary() . "\r\n";
00463         $string .= $child->toString();
00464       }
00465       $string .= "\r\n\r\n--" . $this->getBoundary() . "--\r\n";
00466     }
00467 
00468     return $string;
00469   }
00470 
00471   /**
00472    * Returns a string representation of this object.
00473    *
00474    * @return string
00475    *
00476    * @see toString()
00477    */
00478   public function __toString()
00479   {
00480     return $this->toString();
00481   }
00482 
00483   /**
00484    * Write this entire entity to a {@link Swift_InputByteStream}.
00485    * @param Swift_InputByteStream
00486    */
00487   public function toByteStream(Swift_InputByteStream $is)
00488   {
00489     $is->write($this->_headers->toString());
00490     $is->commit();
00491 
00492     if (empty($this->_immediateChildren))
00493     {
00494       if (isset($this->_body))
00495       {
00496         if ($this->_cache->hasKey($this->_cacheKey, 'body'))
00497         {
00498           $this->_cache->exportToByteStream($this->_cacheKey, 'body', $is);
00499         }
00500         else
00501         {
00502           $cacheIs = $this->_cache->getInputByteStream($this->_cacheKey, 'body');
00503           if ($cacheIs)
00504           {
00505             $is->bind($cacheIs);
00506           }
00507 
00508           $is->write("\r\n");
00509 
00510           if ($this->_body instanceof Swift_OutputByteStream)
00511           {
00512             $this->_body->setReadPointer(0);
00513 
00514             $this->_encoder->encodeByteStream($this->_body, $is, 0,
00515               $this->getMaxLineLength()
00516               );
00517           }
00518           else
00519           {
00520             $is->write($this->_encoder->encodeString(
00521               $this->getBody(), 0, $this->getMaxLineLength()
00522               ));
00523           }
00524 
00525           if ($cacheIs)
00526           {
00527             $is->unbind($cacheIs);
00528           }
00529         }
00530       }
00531     }
00532 
00533     if (!empty($this->_immediateChildren))
00534     {
00535       foreach ($this->_immediateChildren as $child)
00536       {
00537         $is->write("\r\n\r\n--" . $this->getBoundary() . "\r\n");
00538         $child->toByteStream($is);
00539       }
00540       $is->write("\r\n\r\n--" . $this->getBoundary() . "--\r\n");
00541     }
00542   }
00543 
00544   // -- Protected methods
00545 
00546   /**
00547    * Get the name of the header that provides the ID of this entity */
00548   protected function _getIdField()
00549   {
00550     return 'Content-ID';
00551   }
00552 
00553   /**
00554    * Get the model data (usually an array or a string) for $field.
00555    */
00556   protected function _getHeaderFieldModel($field)
00557   {
00558     if ($this->_headers->has($field))
00559     {
00560       return $this->_headers->get($field)->getFieldBodyModel();
00561     }
00562   }
00563 
00564   /**
00565    * Set the model data for $field.
00566    */
00567   protected function _setHeaderFieldModel($field, $model)
00568   {
00569     if ($this->_headers->has($field))
00570     {
00571       $this->_headers->get($field)->setFieldBodyModel($model);
00572       return true;
00573     }
00574     else
00575     {
00576       return false;
00577     }
00578   }
00579 
00580   /**
00581    * Get the parameter value of $parameter on $field header.
00582    */
00583   protected function _getHeaderParameter($field, $parameter)
00584   {
00585     if ($this->_headers->has($field))
00586     {
00587       return $this->_headers->get($field)->getParameter($parameter);
00588     }
00589   }
00590 
00591   /**
00592    * Set the parameter value of $parameter on $field header.
00593    */
00594   protected function _setHeaderParameter($field, $parameter, $value)
00595   {
00596     if ($this->_headers->has($field))
00597     {
00598       $this->_headers->get($field)->setParameter($parameter, $value);
00599       return true;
00600     }
00601     else
00602     {
00603       return false;
00604     }
00605   }
00606 
00607   /**
00608    * Re-evaluate what content type and encoding should be used on this entity.
00609    */
00610   protected function _fixHeaders()
00611   {
00612     if (count($this->_immediateChildren))
00613     {
00614       $this->_setHeaderParameter('Content-Type', 'boundary',
00615         $this->getBoundary()
00616         );
00617       $this->_headers->remove('Content-Transfer-Encoding');
00618     }
00619     else
00620     {
00621       $this->_setHeaderParameter('Content-Type', 'boundary', null);
00622       $this->_setEncoding($this->_encoder->getName());
00623     }
00624   }
00625 
00626   /**
00627    * Get the KeyCache used in this entity.
00628    */
00629   protected function _getCache()
00630   {
00631     return $this->_cache;
00632   }
00633 
00634   /**
00635    * Empty the KeyCache for this entity.
00636    */
00637   protected function _clearCache()
00638   {
00639     $this->_cache->clearKey($this->_cacheKey, 'body');
00640   }
00641 
00642   /**
00643    * Returns a random Content-ID or Message-ID.
00644    * @return string
00645    */
00646   protected function getRandomId()
00647   {
00648     $idLeft = time() . '.' . uniqid();
00649     $idRight = !empty($_SERVER['SERVER_NAME'])
00650       ? $_SERVER['SERVER_NAME']
00651       : 'swift.generated';
00652     return $idLeft . '@' . $idRight;
00653   }
00654 
00655   // -- Private methods
00656 
00657   private function _readStream(Swift_OutputByteStream $os)
00658   {
00659     $string = '';
00660     while (false !== $bytes = $os->read(8192))
00661     {
00662       $string .= $bytes;
00663     }
00664     return $string;
00665   }
00666 
00667   private function _setEncoding($encoding)
00668   {
00669     if (!$this->_setHeaderFieldModel('Content-Transfer-Encoding', $encoding))
00670     {
00671       $this->_headers->addTextHeader('Content-Transfer-Encoding', $encoding);
00672     }
00673   }
00674 
00675   private function _assertValidBoundary($boundary)
00676   {
00677     if (!preg_match(
00678       '/^[a-z0-9\'\(\)\+_\-,\.\/:=\?\ ]{0,69}[a-z0-9\'\(\)\+_\-,\.\/:=\?]$/Di',
00679       $boundary))
00680     {
00681       throw new Swift_RfcComplianceException('Mime boundary set is not RFC 2046 compliant.');
00682     }
00683   }
00684 
00685   private function _setContentTypeInHeaders($type)
00686   {
00687     if (!$this->_setHeaderFieldModel('Content-Type', $type))
00688     {
00689       $this->_headers->addParameterizedHeader('Content-Type', $type);
00690     }
00691   }
00692 
00693   private function _setNestingLevel($level)
00694   {
00695     $this->_nestingLevel = $level;
00696   }
00697 
00698   private function _getCompoundLevel($children)
00699   {
00700     $level = 0;
00701     foreach ($children as $child)
00702     {
00703       $level |= $child->getNestingLevel();
00704     }
00705     return $level;
00706   }
00707 
00708   private function _getNeededChildLevel($child, $compoundLevel)
00709   {
00710     $filter = array();
00711     foreach ($this->_compoundLevelFilters as $bitmask => $rules)
00712     {
00713       if (($compoundLevel & $bitmask) === $bitmask)
00714       {
00715         $filter = $rules + $filter;
00716       }
00717     }
00718 
00719     $realLevel = $child->getNestingLevel();
00720     $lowercaseType = strtolower($child->getContentType());
00721 
00722     if (isset($filter[$realLevel])
00723       && isset($filter[$realLevel][$lowercaseType]))
00724     {
00725       return $filter[$realLevel][$lowercaseType];
00726     }
00727     else
00728     {
00729       return $realLevel;
00730     }
00731   }
00732 
00733   private function _createChild()
00734   {
00735     return new self($this->_headers->newInstance(),
00736       $this->_encoder, $this->_cache);
00737   }
00738 
00739   private function _notifyEncoderChanged(Swift_Mime_ContentEncoder $encoder)
00740   {
00741     foreach ($this->_immediateChildren as $child)
00742     {
00743       $child->encoderChanged($encoder);
00744     }
00745   }
00746 
00747   private function _notifyCharsetChanged($charset)
00748   {
00749     $this->_encoder->charsetChanged($charset);
00750     $this->_headers->charsetChanged($charset);
00751     foreach ($this->_immediateChildren as $child)
00752     {
00753       $child->charsetChanged($charset);
00754     }
00755   }
00756 
00757   private function _sortChildren()
00758   {
00759     $shouldSort = false;
00760     foreach ($this->_immediateChildren as $child)
00761     {
00762       //NOTE: This include alternative parts moved into a related part
00763       if ($child->getNestingLevel() == self::LEVEL_ALTERNATIVE)
00764       {
00765         $shouldSort = true;
00766         break;
00767       }
00768     }
00769 
00770     //Sort in order of preference, if there is one
00771     if ($shouldSort)
00772     {
00773       usort($this->_immediateChildren, array($this, '_childSortAlgorithm'));
00774     }
00775   }
00776 
00777   private function _childSortAlgorithm($a, $b)
00778   {
00779     $typePrefs = array();
00780     $types = array(
00781       strtolower($a->getContentType()),
00782       strtolower($b->getContentType())
00783       );
00784     foreach ($types as $type)
00785     {
00786       $typePrefs[] = (array_key_exists($type, $this->_alternativePartOrder))
00787         ? $this->_alternativePartOrder[$type]
00788         : (max($this->_alternativePartOrder) + 1);
00789     }
00790     return ($typePrefs[0] >= $typePrefs[1]) ? 1 : -1;
00791   }
00792 
00793   // -- Destructor
00794 
00795   /**
00796    * Empties it's own contents from the cache.
00797    */
00798   public function __destruct()
00799   {
00800     $this->_cache->clearAll($this->_cacheKey);
00801   }
00802 
00803 }