TYPO3  7.6
SimpleMimeEntity.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of SwiftMailer.
5  * (c) 2004-2009 Chris Corbyn
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10 
17 {
19  private $_headers;
20 
22  private $_body;
23 
25  private $_encoder;
26 
28  private $_grammar;
29 
31  private $_boundary;
32 
34  private $_compositeRanges = array(
35  'multipart/mixed' => array(self::LEVEL_TOP, self::LEVEL_MIXED),
36  'multipart/alternative' => array(self::LEVEL_MIXED, self::LEVEL_ALTERNATIVE),
37  'multipart/related' => array(self::LEVEL_ALTERNATIVE, self::LEVEL_RELATED),
38  );
39 
41  private $_compoundLevelFilters = array();
42 
44  private $_nestingLevel = self::LEVEL_ALTERNATIVE;
45 
47  private $_cache;
48 
50  private $_immediateChildren = array();
51 
53  private $_children = array();
54 
56  private $_maxLineLength = 78;
57 
59  private $_alternativePartOrder = array(
60  'text/plain' => 1,
61  'text/html' => 2,
62  'multipart/related' => 3,
63  );
64 
66  private $_id;
67 
69  private $_cacheKey;
70 
71  protected $_userContentType;
72 
81  public function __construct(Swift_Mime_HeaderSet $headers, Swift_Mime_ContentEncoder $encoder, Swift_KeyCache $cache, Swift_Mime_Grammar $grammar)
82  {
83  $this->_cacheKey = md5(uniqid(getmypid().mt_rand(), true));
84  $this->_cache = $cache;
85  $this->_headers = $headers;
86  $this->_grammar = $grammar;
87  $this->setEncoder($encoder);
88  $this->_headers->defineOrdering(array('Content-Type', 'Content-Transfer-Encoding'));
89 
90  // This array specifies that, when the entire MIME document contains
91  // $compoundLevel, then for each child within $level, if its Content-Type
92  // is $contentType then it should be treated as if it's level is
93  // $neededLevel instead. I tried to write that unambiguously! :-\
94  // Data Structure:
95  // array (
96  // $compoundLevel => array(
97  // $level => array(
98  // $contentType => $neededLevel
99  // )
100  // )
101  // )
102 
103  $this->_compoundLevelFilters = array(
104  (self::LEVEL_ALTERNATIVE + self::LEVEL_RELATED) => array(
105  self::LEVEL_ALTERNATIVE => array(
106  'text/plain' => self::LEVEL_ALTERNATIVE,
107  'text/html' => self::LEVEL_RELATED,
108  ),
109  ),
110  );
111 
112  $this->_id = $this->getRandomId();
113  }
114 
120  public function generateId()
121  {
122  $this->setId($this->getRandomId());
123 
124  return $this->_id;
125  }
126 
132  public function getHeaders()
133  {
134  return $this->_headers;
135  }
136 
144  public function getNestingLevel()
145  {
146  return $this->_nestingLevel;
147  }
148 
154  public function getContentType()
155  {
156  return $this->_getHeaderFieldModel('Content-Type');
157  }
158 
166  public function setContentType($type)
167  {
168  $this->_setContentTypeInHeaders($type);
169  // Keep track of the value so that if the content-type changes automatically
170  // due to added child entities, it can be restored if they are later removed
171  $this->_userContentType = $type;
172 
173  return $this;
174  }
175 
183  public function getId()
184  {
185  $tmp = (array) $this->_getHeaderFieldModel($this->_getIdField());
186 
187  return $this->_headers->has($this->_getIdField()) ? current($tmp) : $this->_id;
188  }
189 
197  public function setId($id)
198  {
199  if (!$this->_setHeaderFieldModel($this->_getIdField(), $id)) {
200  $this->_headers->addIdHeader($this->_getIdField(), $id);
201  }
202  $this->_id = $id;
203 
204  return $this;
205  }
206 
214  public function getDescription()
215  {
216  return $this->_getHeaderFieldModel('Content-Description');
217  }
218 
228  public function setDescription($description)
229  {
230  if (!$this->_setHeaderFieldModel('Content-Description', $description)) {
231  $this->_headers->addTextHeader('Content-Description', $description);
232  }
233 
234  return $this;
235  }
236 
242  public function getMaxLineLength()
243  {
244  return $this->_maxLineLength;
245  }
246 
256  public function setMaxLineLength($length)
257  {
258  $this->_maxLineLength = $length;
259 
260  return $this;
261  }
262 
268  public function getChildren()
269  {
270  return $this->_children;
271  }
272 
281  public function setChildren(array $children, $compoundLevel = null)
282  {
283  // TODO: Try to refactor this logic
284 
285  $compoundLevel = isset($compoundLevel)
286  ? $compoundLevel
287  : $this->_getCompoundLevel($children)
288  ;
289 
290  $immediateChildren = array();
291  $grandchildren = array();
292  $newContentType = $this->_userContentType;
293 
294  foreach ($children as $child) {
295  $level = $this->_getNeededChildLevel($child, $compoundLevel);
296  if (empty($immediateChildren)) {
297  //first iteration
298  $immediateChildren = array($child);
299  } else {
300  $nextLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel);
301  if ($nextLevel == $level) {
302  $immediateChildren[] = $child;
303  } elseif ($level < $nextLevel) {
304  // Re-assign immediateChildren to grandchildren
305  $grandchildren = array_merge($grandchildren, $immediateChildren);
306  // Set new children
307  $immediateChildren = array($child);
308  } else {
309  $grandchildren[] = $child;
310  }
311  }
312  }
313 
314  if (!empty($immediateChildren)) {
315  $lowestLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel);
316 
317  // Determine which composite media type is needed to accommodate the
318  // immediate children
319  foreach ($this->_compositeRanges as $mediaType => $range) {
320  if ($lowestLevel > $range[0]
321  && $lowestLevel <= $range[1]) {
322  $newContentType = $mediaType;
323  break;
324  }
325  }
326 
327  // Put any grandchildren in a subpart
328  if (!empty($grandchildren)) {
329  $subentity = $this->_createChild();
330  $subentity->_setNestingLevel($lowestLevel);
331  $subentity->setChildren($grandchildren, $compoundLevel);
332  array_unshift($immediateChildren, $subentity);
333  }
334  }
335 
336  $this->_immediateChildren = $immediateChildren;
337  $this->_children = $children;
338  $this->_setContentTypeInHeaders($newContentType);
339  $this->_fixHeaders();
340  $this->_sortChildren();
341 
342  return $this;
343  }
344 
350  public function getBody()
351  {
352  return ($this->_body instanceof Swift_OutputByteStream)
353  ? $this->_readStream($this->_body)
354  : $this->_body;
355  }
356 
366  public function setBody($body, $contentType = null)
367  {
368  if ($body !== $this->_body) {
369  $this->_clearCache();
370  }
371 
372  $this->_body = $body;
373  if (isset($contentType)) {
374  $this->setContentType($contentType);
375  }
376 
377  return $this;
378  }
379 
385  public function getEncoder()
386  {
387  return $this->_encoder;
388  }
389 
397  public function setEncoder(Swift_Mime_ContentEncoder $encoder)
398  {
399  if ($encoder !== $this->_encoder) {
400  $this->_clearCache();
401  }
402 
403  $this->_encoder = $encoder;
404  $this->_setEncoding($encoder->getName());
405  $this->_notifyEncoderChanged($encoder);
406 
407  return $this;
408  }
409 
415  public function getBoundary()
416  {
417  if (!isset($this->_boundary)) {
418  $this->_boundary = '_=_swift_v4_'.time().'_'.md5(getmypid().mt_rand().uniqid('', true)).'_=_';
419  }
420 
421  return $this->_boundary;
422  }
423 
433  public function setBoundary($boundary)
434  {
435  $this->_assertValidBoundary($boundary);
436  $this->_boundary = $boundary;
437 
438  return $this;
439  }
440 
447  public function charsetChanged($charset)
448  {
449  $this->_notifyCharsetChanged($charset);
450  }
451 
458  public function encoderChanged(Swift_Mime_ContentEncoder $encoder)
459  {
460  $this->_notifyEncoderChanged($encoder);
461  }
462 
468  public function toString()
469  {
470  $string = $this->_headers->toString();
471  $string .= $this->_bodyToString();
472 
473  return $string;
474  }
475 
481  protected function _bodyToString()
482  {
483  $string = '';
484 
485  if (isset($this->_body) && empty($this->_immediateChildren)) {
486  if ($this->_cache->hasKey($this->_cacheKey, 'body')) {
487  $body = $this->_cache->getString($this->_cacheKey, 'body');
488  } else {
489  $body = "\r\n".$this->_encoder->encodeString($this->getBody(), 0,
490  $this->getMaxLineLength()
491  );
492  $this->_cache->setString($this->_cacheKey, 'body', $body,
494  );
495  }
496  $string .= $body;
497  }
498 
499  if (!empty($this->_immediateChildren)) {
500  foreach ($this->_immediateChildren as $child) {
501  $string .= "\r\n\r\n--".$this->getBoundary()."\r\n";
502  $string .= $child->toString();
503  }
504  $string .= "\r\n\r\n--".$this->getBoundary()."--\r\n";
505  }
506 
507  return $string;
508  }
509 
517  public function __toString()
518  {
519  return $this->toString();
520  }
521 
527  public function toByteStream(Swift_InputByteStream $is)
528  {
529  $is->write($this->_headers->toString());
530  $is->commit();
531 
532  $this->_bodyToByteStream($is);
533  }
534 
540  protected function _bodyToByteStream(Swift_InputByteStream $is)
541  {
542  if (empty($this->_immediateChildren)) {
543  if (isset($this->_body)) {
544  if ($this->_cache->hasKey($this->_cacheKey, 'body')) {
545  $this->_cache->exportToByteStream($this->_cacheKey, 'body', $is);
546  } else {
547  $cacheIs = $this->_cache->getInputByteStream($this->_cacheKey, 'body');
548  if ($cacheIs) {
549  $is->bind($cacheIs);
550  }
551 
552  $is->write("\r\n");
553 
554  if ($this->_body instanceof Swift_OutputByteStream) {
555  $this->_body->setReadPointer(0);
556 
557  $this->_encoder->encodeByteStream($this->_body, $is, 0, $this->getMaxLineLength());
558  } else {
559  $is->write($this->_encoder->encodeString($this->getBody(), 0, $this->getMaxLineLength()));
560  }
561 
562  if ($cacheIs) {
563  $is->unbind($cacheIs);
564  }
565  }
566  }
567  }
568 
569  if (!empty($this->_immediateChildren)) {
570  foreach ($this->_immediateChildren as $child) {
571  $is->write("\r\n\r\n--".$this->getBoundary()."\r\n");
572  $child->toByteStream($is);
573  }
574  $is->write("\r\n\r\n--".$this->getBoundary()."--\r\n");
575  }
576  }
577 
581  protected function _getIdField()
582  {
583  return 'Content-ID';
584  }
585 
589  protected function _getHeaderFieldModel($field)
590  {
591  if ($this->_headers->has($field)) {
592  return $this->_headers->get($field)->getFieldBodyModel();
593  }
594  }
595 
599  protected function _setHeaderFieldModel($field, $model)
600  {
601  if ($this->_headers->has($field)) {
602  $this->_headers->get($field)->setFieldBodyModel($model);
603 
604  return true;
605  } else {
606  return false;
607  }
608  }
609 
613  protected function _getHeaderParameter($field, $parameter)
614  {
615  if ($this->_headers->has($field)) {
616  return $this->_headers->get($field)->getParameter($parameter);
617  }
618  }
619 
623  protected function _setHeaderParameter($field, $parameter, $value)
624  {
625  if ($this->_headers->has($field)) {
626  $this->_headers->get($field)->setParameter($parameter, $value);
627 
628  return true;
629  } else {
630  return false;
631  }
632  }
633 
637  protected function _fixHeaders()
638  {
639  if (count($this->_immediateChildren)) {
640  $this->_setHeaderParameter('Content-Type', 'boundary',
641  $this->getBoundary()
642  );
643  $this->_headers->remove('Content-Transfer-Encoding');
644  } else {
645  $this->_setHeaderParameter('Content-Type', 'boundary', null);
646  $this->_setEncoding($this->_encoder->getName());
647  }
648  }
649 
655  protected function _getCache()
656  {
657  return $this->_cache;
658  }
659 
665  protected function _getGrammar()
666  {
667  return $this->_grammar;
668  }
669 
673  protected function _clearCache()
674  {
675  $this->_cache->clearKey($this->_cacheKey, 'body');
676  }
677 
683  protected function getRandomId()
684  {
685  $idLeft = md5(getmypid().'.'.time().'.'.uniqid(mt_rand(), true));
686  $idRight = !empty($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'swift.generated';
687  $id = $idLeft.'@'.$idRight;
688 
689  try {
690  $this->_assertValidId($id);
691  } catch (Swift_RfcComplianceException $e) {
692  $id = $idLeft.'@swift.generated';
693  }
694 
695  return $id;
696  }
697 
698  private function _readStream(Swift_OutputByteStream $os)
699  {
700  $string = '';
701  while (false !== $bytes = $os->read(8192)) {
702  $string .= $bytes;
703  }
704 
705  $os->setReadPointer(0);
706 
707  return $string;
708  }
709 
710  private function _setEncoding($encoding)
711  {
712  if (!$this->_setHeaderFieldModel('Content-Transfer-Encoding', $encoding)) {
713  $this->_headers->addTextHeader('Content-Transfer-Encoding', $encoding);
714  }
715  }
716 
717  private function _assertValidBoundary($boundary)
718  {
719  if (!preg_match(
720  '/^[a-z0-9\'\(\)\+_\-,\.\/:=\?\ ]{0,69}[a-z0-9\'\(\)\+_\-,\.\/:=\?]$/Di',
721  $boundary)) {
722  throw new Swift_RfcComplianceException('Mime boundary set is not RFC 2046 compliant.');
723  }
724  }
725 
726  private function _setContentTypeInHeaders($type)
727  {
728  if (!$this->_setHeaderFieldModel('Content-Type', $type)) {
729  $this->_headers->addParameterizedHeader('Content-Type', $type);
730  }
731  }
732 
733  private function _setNestingLevel($level)
734  {
735  $this->_nestingLevel = $level;
736  }
737 
738  private function _getCompoundLevel($children)
739  {
740  $level = 0;
741  foreach ($children as $child) {
742  $level |= $child->getNestingLevel();
743  }
744 
745  return $level;
746  }
747 
748  private function _getNeededChildLevel($child, $compoundLevel)
749  {
750  $filter = array();
751  foreach ($this->_compoundLevelFilters as $bitmask => $rules) {
752  if (($compoundLevel & $bitmask) === $bitmask) {
753  $filter = $rules + $filter;
754  }
755  }
756 
757  $realLevel = $child->getNestingLevel();
758  $lowercaseType = strtolower($child->getContentType());
759 
760  if (isset($filter[$realLevel])
761  && isset($filter[$realLevel][$lowercaseType])) {
762  return $filter[$realLevel][$lowercaseType];
763  } else {
764  return $realLevel;
765  }
766  }
767 
768  private function _createChild()
769  {
770  return new self($this->_headers->newInstance(),
772  }
773 
775  {
776  foreach ($this->_immediateChildren as $child) {
777  $child->encoderChanged($encoder);
778  }
779  }
780 
781  private function _notifyCharsetChanged($charset)
782  {
783  $this->_encoder->charsetChanged($charset);
784  $this->_headers->charsetChanged($charset);
785  foreach ($this->_immediateChildren as $child) {
786  $child->charsetChanged($charset);
787  }
788  }
789 
790  private function _sortChildren()
791  {
792  $shouldSort = false;
793  foreach ($this->_immediateChildren as $child) {
794  // NOTE: This include alternative parts moved into a related part
795  if ($child->getNestingLevel() == self::LEVEL_ALTERNATIVE) {
796  $shouldSort = true;
797  break;
798  }
799  }
800 
801  // Sort in order of preference, if there is one
802  if ($shouldSort) {
803  usort($this->_immediateChildren, array($this, '_childSortAlgorithm'));
804  }
805  }
806 
807  private function _childSortAlgorithm($a, $b)
808  {
809  $typePrefs = array();
810  $types = array(
811  strtolower($a->getContentType()),
812  strtolower($b->getContentType()),
813  );
814  foreach ($types as $type) {
815  $typePrefs[] = (array_key_exists($type, $this->_alternativePartOrder))
816  ? $this->_alternativePartOrder[$type]
817  : (max($this->_alternativePartOrder) + 1);
818  }
819 
820  return ($typePrefs[0] >= $typePrefs[1]) ? 1 : -1;
821  }
822 
823  // -- Destructor
824 
828  public function __destruct()
829  {
830  $this->_cache->clearAll($this->_cacheKey);
831  }
832 
840  private function _assertValidId($id)
841  {
842  if (!preg_match(
843  '/^'.$this->_grammar->getDefinition('id-left').'@'.
844  $this->_grammar->getDefinition('id-right').'$/D',
845  $id
846  )) {
848  'Invalid ID given <'.$id.'>'
849  );
850  }
851  }
852 
856  public function __clone()
857  {
858  $this->_headers = clone $this->_headers;
859  $this->_encoder = clone $this->_encoder;
860  $this->_cacheKey = uniqid();
861  $children = array();
862  foreach ($this->_children as $pos => $child) {
863  $children[$pos] = clone $child;
864  }
865  $this->setChildren($children);
866  }
867 }