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
11//@require 'Swift/Mime/HeaderSet.php';
12//@require 'Swift/OutputByteStream.php';
13//@require 'Swift/Mime/ContentEncoder.php';
14//@require 'Swift/KeyCache.php';
15
16/**
17 * A MIME entity, in a multipart message.
18 * @package Swift
19 * @subpackage Mime
20 * @author Chris Corbyn
21 */
22class Swift_Mime_SimpleMimeEntity implements Swift_Mime_MimeEntity
23{
24
25  /** A collection of Headers for this mime entity */
26  private $_headers;
27
28  /** The body as a string, or a stream */
29  private $_body;
30
31  /** The encoder that encodes the body into a streamable format */
32  private $_encoder;
33
34  /** A mime bounary, if any is used */
35  private $_boundary;
36
37  /** Mime types to be used based on the nesting level */
38  private $_compositeRanges = array(
39    'multipart/mixed' => array(self::LEVEL_TOP, self::LEVEL_MIXED),
40    'multipart/alternative' => array(self::LEVEL_MIXED, self::LEVEL_ALTERNATIVE),
41    'multipart/related' => array(self::LEVEL_ALTERNATIVE, self::LEVEL_RELATED)
42    );
43
44  /** A set of filter rules to define what level an entity should be nested at */
45  private $_compoundLevelFilters = array();
46
47  /** The nesting level of this entity */
48  private $_nestingLevel = self::LEVEL_ALTERNATIVE;
49
50  /** A KeyCache instance used during encoding and streaming */
51  private $_cache;
52
53  /** Direct descendants of this entity */
54  private $_immediateChildren = array();
55
56  /** All descendants of this entity */
57  private $_children = array();
58
59  /** The maximum line length of the body of this entity */
60  private $_maxLineLength = 78;
61
62  /** The order in which alternative mime types should appear */
63  private $_alternativePartOrder = array(
64    'text/plain' => 1,
65    'text/html' => 2,
66    'multipart/related' => 3
67    );
68
69  /** The CID of this entity */
70  private $_id;
71
72  /** The key used for accessing the cache */
73  private $_cacheKey;
74
75  protected $_userContentType;
76
77  /**
78   * Create a new SimpleMimeEntity with $headers, $encoder and $cache.
79   * @param Swift_Mime_HeaderSet $headers
80   * @param Swift_Mime_ContentEncoder $encoder
81   * @param Swift_KeyCache $cache
82   */
83  public function __construct(Swift_Mime_HeaderSet $headers,
84    Swift_Mime_ContentEncoder $encoder, Swift_KeyCache $cache)
85  {
86    $this->_cacheKey = uniqid();
87    $this->_cache = $cache;
88    $this->_headers = $headers;
89    $this->setEncoder($encoder);
90    $this->_headers->defineOrdering(
91      array('Content-Type', 'Content-Transfer-Encoding')
92      );
93
94    // This array specifies that, when the entire MIME document contains
95    // $compoundLevel, then for each child within $level, if its Content-Type
96    // is $contentType then it should be treated as if it's level is
97    // $neededLevel instead.  I tried to write that unambiguously! :-\
98    // Data Structure:
99    // array (
100    //   $compoundLevel => array(
101    //     $level => array(
102    //       $contentType => $neededLevel
103    //     )
104    //   )
105    // )
106
107    $this->_compoundLevelFilters = array(
108      (self::LEVEL_ALTERNATIVE + self::LEVEL_RELATED) => array(
109        self::LEVEL_ALTERNATIVE => array(
110          'text/plain' => self::LEVEL_ALTERNATIVE,
111          'text/html' => self::LEVEL_RELATED
112          )
113        )
114      );
115    $this->generateId();
116  }
117
118  /**
119   * Generate a new Content-ID or Message-ID for this MIME entity.
120   * @return string
121   */
122  public function generateId()
123  {
124    $idLeft = time() . '.' . uniqid();
125    $idRight = !empty($_SERVER['SERVER_NAME'])
126      ? $_SERVER['SERVER_NAME']
127      : 'swift.generated';
128    $this->_id = $idLeft . '@' . $idRight;
129    return $this->getId();
130  }
131
132  /**
133   * Get the {@link Swift_Mime_HeaderSet} for this entity.
134   * @return Swift_Mime_HeaderSet
135   */
136  public function getHeaders()
137  {
138    return $this->_headers;
139  }
140
141  /**
142   * Get the nesting level of this entity.
143   * @return int
144   * @see LEVEL_TOP, LEVEL_MIXED, LEVEL_RELATED, LEVEL_ALTERNATIVE
145   */
146  public function getNestingLevel()
147  {
148    return $this->_nestingLevel;
149  }
150
151  /**
152   * Get the Content-type of this entity.
153   * @return string
154   */
155  public function getContentType()
156  {
157    return $this->_getHeaderFieldModel('Content-Type');
158  }
159
160  /**
161   * Set the Content-type of this entity.
162   * @param string $type
163   */
164  public function setContentType($type)
165  {
166    $this->_setContentTypeInHeaders($type);
167    // Keep track of the value so that if the content-type changes automatically
168    // due to added child entities, it can be restored if they are later removed
169    $this->_userContentType = $type;
170    return $this;
171  }
172
173  /**
174   * Get the CID of this entity.
175   * The CID will only be present in headers if a Content-ID header is present.
176   * @return string
177   */
178  public function getId()
179  {
180    return $this->_headers->has($this->_getIdField())
181      ? current((array) $this->_getHeaderFieldModel($this->_getIdField()))
182      : $this->_id;
183  }
184
185  /**
186   * Set the CID of this entity.
187   * @param string $id
188   */
189  public function setId($id)
190  {
191    if (!$this->_setHeaderFieldModel($this->_getIdField(), $id))
192    {
193      $this->_headers->addIdHeader($this->_getIdField(), $id);
194    }
195    $this->_id = $id;
196    return $this;
197  }
198
199  /**
200   * Get the description of this entity.
201   * This value comes from the Content-Description header if set.
202   * @return string
203   */
204  public function getDescription()
205  {
206    return $this->_getHeaderFieldModel('Content-Description');
207  }
208
209  /**
210   * Set the description of this entity.
211   * This method sets a value in the Content-ID header.
212   * @param string $description
213   */
214  public function setDescription($description)
215  {
216    if (!$this->_setHeaderFieldModel('Content-Description', $description))
217    {
218      $this->_headers->addTextHeader('Content-Description', $description);
219    }
220    return $this;
221  }
222
223  /**
224   * Get the maximum line length of the body of this entity.
225   * @return int
226   */
227  public function getMaxLineLength()
228  {
229    return $this->_maxLineLength;
230  }
231
232  /**
233   * Set the maximum line length of lines in this body.
234   * Though not enforced by the library, lines should not exceed 1000 chars.
235   * @param int $length
236   */
237  public function setMaxLineLength($length)
238  {
239    $this->_maxLineLength = $length;
240    return $this;
241  }
242
243  /**
244   * Get all children added to this entity.
245   * @return array of Swift_Mime_Entity
246   */
247  public function getChildren()
248  {
249    return $this->_children;
250  }
251
252  /**
253   * Set all children of this entity.
254   * @param array $children Swiift_Mime_Entity instances
255   * @param int $compoundLevel For internal use only
256   */
257  public function setChildren(array $children, $compoundLevel = null)
258  {
259    //TODO: Try to refactor this logic
260
261    $compoundLevel = isset($compoundLevel)
262      ? $compoundLevel
263      : $this->_getCompoundLevel($children)
264      ;
265
266    $immediateChildren = array();
267    $grandchildren = array();
268    $newContentType = $this->_userContentType;
269
270    foreach ($children as $child)
271    {
272      $level = $this->_getNeededChildLevel($child, $compoundLevel);
273      if (empty($immediateChildren)) //first iteration
274      {
275        $immediateChildren = array($child);
276      }
277      else
278      {
279        $nextLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel);
280        if ($nextLevel == $level)
281        {
282          $immediateChildren[] = $child;
283        }
284        elseif ($level < $nextLevel)
285        {
286          //Re-assign immediateChildren to grandchilden
287          $grandchildren = array_merge($grandchildren, $immediateChildren);
288          //Set new children
289          $immediateChildren = array($child);
290        }
291        else
292        {
293          $grandchildren[] = $child;
294        }
295      }
296    }
297
298    if (!empty($immediateChildren))
299    {
300      $lowestLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel);
301
302      //Determine which composite media type is needed to accomodate the
303      // immediate children
304      foreach ($this->_compositeRanges as $mediaType => $range)
305      {
306        if ($lowestLevel > $range[0]
307          && $lowestLevel <= $range[1])
308        {
309          $newContentType = $mediaType;
310          break;
311        }
312      }
313
314      //Put any grandchildren in a subpart
315      if (!empty($grandchildren))
316      {
317        $subentity = $this->_createChild();
318        $subentity->_setNestingLevel($lowestLevel);
319        $subentity->setChildren($grandchildren, $compoundLevel);
320        array_unshift($immediateChildren, $subentity);
321      }
322    }
323
324    $this->_immediateChildren = $immediateChildren;
325    $this->_children = $children;
326    $this->_setContentTypeInHeaders($newContentType);
327    $this->_fixHeaders();
328    $this->_sortChildren();
329
330    return $this;
331  }
332
333  /**
334   * Get the body of this entity as a string.
335   * @return string
336   */
337  public function getBody()
338  {
339    return ($this->_body instanceof Swift_OutputByteStream)
340      ? $this->_readStream($this->_body)
341      : $this->_body;
342  }
343
344  /**
345   * Set the body of this entity, either as a string, or as an instance of
346   * {@link Swift_OutputByteStream}.
347   * @param mixed $body
348   * @param string $contentType optional
349   */
350  public function setBody($body, $contentType = null)
351  {
352    if ($body !== $this->_body)
353    {
354      $this->_clearCache();
355    }
356
357    $this->_body = $body;
358    if (isset($contentType))
359    {
360      $this->setContentType($contentType);
361    }
362    return $this;
363  }
364
365  /**
366   * Get the encoder used for the body of this entity.
367   * @return Swift_Mime_ContentEncoder
368   */
369  public function getEncoder()
370  {
371    return $this->_encoder;
372  }
373
374  /**
375   * Set the encoder used for the body of this entity.
376   * @param Swift_Mime_ContentEncoder $encoder
377   */
378  public function setEncoder(Swift_Mime_ContentEncoder $encoder)
379  {
380    if ($encoder !== $this->_encoder)
381    {
382      $this->_clearCache();
383    }
384
385    $this->_encoder = $encoder;
386    $this->_setEncoding($encoder->getName());
387    $this->_notifyEncoderChanged($encoder);
388    return $this;
389  }
390
391  /**
392   * Get the boundary used to separate children in this entity.
393   * @return string
394   */
395  public function getBoundary()
396  {
397    if (!isset($this->_boundary))
398    {
399      $this->_boundary = '_=_swift_v4_' . time() . uniqid() . '_=_';
400    }
401    return $this->_boundary;
402  }
403
404  /**
405   * Set the boundary used to separate children in this entity.
406   * @param string $boundary
407   * @throws Swift_RfcComplianceException
408   */
409  public function setBoundary($boundary)
410  {
411    $this->_assertValidBoundary($boundary);
412    $this->_boundary = $boundary;
413    return $this;
414  }
415
416  /**
417   * Receive notification that the charset of this entity, or a parent entity
418   * has changed.
419   * @param string $charset
420   */
421  public function charsetChanged($charset)
422  {
423    $this->_notifyCharsetChanged($charset);
424  }
425
426  /**
427   * Receive notification that the encoder of this entity or a parent entity
428   * has changed.
429   * @param Swift_Mime_ContentEncoder $encoder
430   */
431  public function encoderChanged(Swift_Mime_ContentEncoder $encoder)
432  {
433    $this->_notifyEncoderChanged($encoder);
434  }
435
436  /**
437   * Get this entire entity as a string.
438   * @return string
439   */
440  public function toString()
441  {
442    $string = $this->_headers->toString();
443    if (isset($this->_body) && empty($this->_immediateChildren))
444    {
445      if ($this->_cache->hasKey($this->_cacheKey, 'body'))
446      {
447        $body = $this->_cache->getString($this->_cacheKey, 'body');
448      }
449      else
450      {
451        $body = "\r\n" . $this->_encoder->encodeString($this->getBody(), 0,
452          $this->getMaxLineLength()
453          );
454        $this->_cache->setString($this->_cacheKey, 'body', $body,
455          Swift_KeyCache::MODE_WRITE
456          );
457      }
458      $string .= $body;
459    }
460
461    if (!empty($this->_immediateChildren))
462    {
463      foreach ($this->_immediateChildren as $child)
464      {
465        $string .= "\r\n\r\n--" . $this->getBoundary() . "\r\n";
466        $string .= $child->toString();
467      }
468      $string .= "\r\n\r\n--" . $this->getBoundary() . "--\r\n";
469    }
470
471    return $string;
472  }
473
474  /**
475   * Returns a string representation of this object.
476   *
477   * @return string
478   *
479   * @see toString()
480   */
481  public function __toString()
482  {
483    return $this->toString();
484  }
485
486  /**
487   * Write this entire entity to a {@link Swift_InputByteStream}.
488   * @param Swift_InputByteStream
489   */
490  public function toByteStream(Swift_InputByteStream $is)
491  {
492    $is->write($this->_headers->toString());
493    $is->commit();
494
495    if (empty($this->_immediateChildren))
496    {
497      if (isset($this->_body))
498      {
499        if ($this->_cache->hasKey($this->_cacheKey, 'body'))
500        {
501          $this->_cache->exportToByteStream($this->_cacheKey, 'body', $is);
502        }
503        else
504        {
505          $cacheIs = $this->_cache->getInputByteStream($this->_cacheKey, 'body');
506          if ($cacheIs)
507          {
508            $is->bind($cacheIs);
509          }
510
511          $is->write("\r\n");
512
513          if ($this->_body instanceof Swift_OutputByteStream)
514          {
515            $this->_body->setReadPointer(0);
516
517            $this->_encoder->encodeByteStream($this->_body, $is, 0,
518              $this->getMaxLineLength()
519              );
520          }
521          else
522          {
523            $is->write($this->_encoder->encodeString(
524              $this->getBody(), 0, $this->getMaxLineLength()
525              ));
526          }
527
528          if ($cacheIs)
529          {
530            $is->unbind($cacheIs);
531          }
532        }
533      }
534    }
535
536    if (!empty($this->_immediateChildren))
537    {
538      foreach ($this->_immediateChildren as $child)
539      {
540        $is->write("\r\n\r\n--" . $this->getBoundary() . "\r\n");
541        $child->toByteStream($is);
542      }
543      $is->write("\r\n\r\n--" . $this->getBoundary() . "--\r\n");
544    }
545  }
546
547  // -- Protected methods
548
549  /**
550   * Get the name of the header that provides the ID of this entity */
551  protected function _getIdField()
552  {
553    return 'Content-ID';
554  }
555
556  /**
557   * Get the model data (usually an array or a string) for $field.
558   */
559  protected function _getHeaderFieldModel($field)
560  {
561    if ($this->_headers->has($field))
562    {
563      return $this->_headers->get($field)->getFieldBodyModel();
564    }
565  }
566
567  /**
568   * Set the model data for $field.
569   */
570  protected function _setHeaderFieldModel($field, $model)
571  {
572    if ($this->_headers->has($field))
573    {
574      $this->_headers->get($field)->setFieldBodyModel($model);
575      return true;
576    }
577    else
578    {
579      return false;
580    }
581  }
582
583  /**
584   * Get the parameter value of $parameter on $field header.
585   */
586  protected function _getHeaderParameter($field, $parameter)
587  {
588    if ($this->_headers->has($field))
589    {
590      return $this->_headers->get($field)->getParameter($parameter);
591    }
592  }
593
594  /**
595   * Set the parameter value of $parameter on $field header.
596   */
597  protected function _setHeaderParameter($field, $parameter, $value)
598  {
599    if ($this->_headers->has($field))
600    {
601      $this->_headers->get($field)->setParameter($parameter, $value);
602      return true;
603    }
604    else
605    {
606      return false;
607    }
608  }
609
610  /**
611   * Re-evaluate what content type and encoding should be used on this entity.
612   */
613  protected function _fixHeaders()
614  {
615    if (count($this->_immediateChildren))
616    {
617      $this->_setHeaderParameter('Content-Type', 'boundary',
618        $this->getBoundary()
619        );
620      $this->_headers->remove('Content-Transfer-Encoding');
621    }
622    else
623    {
624      $this->_setHeaderParameter('Content-Type', 'boundary', null);
625      $this->_setEncoding($this->_encoder->getName());
626    }
627  }
628
629  /**
630   * Get the KeyCache used in this entity.
631   */
632  protected function _getCache()
633  {
634    return $this->_cache;
635  }
636
637  /**
638   * Empty the KeyCache for this entity.
639   */
640  protected function _clearCache()
641  {
642    $this->_cache->clearKey($this->_cacheKey, 'body');
643  }
644
645  // -- Private methods
646
647  private function _readStream(Swift_OutputByteStream $os)
648  {
649    $string = '';
650    while (false !== $bytes = $os->read(8192))
651    {
652      $string .= $bytes;
653    }
654    return $string;
655  }
656
657  private function _setEncoding($encoding)
658  {
659    if (!$this->_setHeaderFieldModel('Content-Transfer-Encoding', $encoding))
660    {
661      $this->_headers->addTextHeader('Content-Transfer-Encoding', $encoding);
662    }
663  }
664
665  private function _assertValidBoundary($boundary)
666  {
667    if (!preg_match(
668      '/^[a-z0-9\'\(\)\+_\-,\.\/:=\?\ ]{0,69}[a-z0-9\'\(\)\+_\-,\.\/:=\?]$/Di',
669      $boundary))
670    {
671      throw new Exception('Mime boundary set is not RFC 2046 compliant.');
672    }
673  }
674
675  private function _setContentTypeInHeaders($type)
676  {
677    if (!$this->_setHeaderFieldModel('Content-Type', $type))
678    {
679      $this->_headers->addParameterizedHeader('Content-Type', $type);
680    }
681  }
682
683  private function _setNestingLevel($level)
684  {
685    $this->_nestingLevel = $level;
686  }
687
688  private function _getCompoundLevel($children)
689  {
690    $level = 0;
691    foreach ($children as $child)
692    {
693      $level |= $child->getNestingLevel();
694    }
695    return $level;
696  }
697
698  private function _getNeededChildLevel($child, $compoundLevel)
699  {
700    $filter = array();
701    foreach ($this->_compoundLevelFilters as $bitmask => $rules)
702    {
703      if (($compoundLevel & $bitmask) === $bitmask)
704      {
705        $filter = $rules + $filter;
706      }
707    }
708
709    $realLevel = $child->getNestingLevel();
710    $lowercaseType = strtolower($child->getContentType());
711
712    if (isset($filter[$realLevel])
713      && isset($filter[$realLevel][$lowercaseType]))
714    {
715      return $filter[$realLevel][$lowercaseType];
716    }
717    else
718    {
719      return $realLevel;
720    }
721  }
722
723  private function _createChild()
724  {
725    return new self($this->_headers->newInstance(),
726      $this->_encoder, $this->_cache);
727  }
728
729  private function _notifyEncoderChanged(Swift_Mime_ContentEncoder $encoder)
730  {
731    foreach ($this->_immediateChildren as $child)
732    {
733      $child->encoderChanged($encoder);
734    }
735  }
736
737  private function _notifyCharsetChanged($charset)
738  {
739    $this->_encoder->charsetChanged($charset);
740    $this->_headers->charsetChanged($charset);
741    foreach ($this->_immediateChildren as $child)
742    {
743      $child->charsetChanged($charset);
744    }
745  }
746
747  private function _sortChildren()
748  {
749    $shouldSort = false;
750    foreach ($this->_immediateChildren as $child)
751    {
752      //NOTE: This include alternative parts moved into a related part
753      if ($child->getNestingLevel() == self::LEVEL_ALTERNATIVE)
754      {
755        $shouldSort = true;
756        break;
757      }
758    }
759
760    //Sort in order of preference, if there is one
761    if ($shouldSort)
762    {
763      usort($this->_immediateChildren, array($this, '_childSortAlgorithm'));
764    }
765  }
766
767  private function _childSortAlgorithm($a, $b)
768  {
769    $typePrefs = array();
770    $types = array(
771      strtolower($a->getContentType()),
772      strtolower($b->getContentType())
773      );
774    foreach ($types as $type)
775    {
776      $typePrefs[] = (array_key_exists($type, $this->_alternativePartOrder))
777        ? $this->_alternativePartOrder[$type]
778        : (max($this->_alternativePartOrder) + 1);
779    }
780    return ($typePrefs[0] >= $typePrefs[1]) ? 1 : -1;
781  }
782
783  // -- Destructor
784
785  /**
786   * Empties it's own contents from the cache.
787   */
788  public function __destruct()
789  {
790    $this->_cache->clearAll($this->_cacheKey);
791  }
792
793}
794