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