1<?php
2
3/**
4 * Swift Mailer MIME Library central component
5 * Please read the LICENSE file
6 * @copyright Chris Corbyn <chris@w3style.co.uk>
7 * @author Chris Corbyn <chris@w3style.co.uk>
8 * @package Swift_Message
9 * @license GNU Lesser General Public License
10 */
11
12require_once dirname(__FILE__) . "/../ClassLoader.php";
13Swift_ClassLoader::load("Swift_File");
14Swift_ClassLoader::load("Swift_Message_MimeException");
15
16/**
17 * Mime is the underbelly for Messages, Attachments, Parts, Embedded Images, Forwarded Mail, etc
18 * In fact, every single component of the composed email is simply a new Mime document nested inside another
19 * When you piece an email together in this way you see just how straight-forward it really is
20 * @package Swift_Message
21 * @author Chris Corbyn <chris@w3style.co.uk>
22 */
23abstract class Swift_Message_Mime
24{
25  /**
26   * Constant for plain-text emails
27   */
28  const PLAIN = "text/plain";
29  /**
30   * Constant for HTML emails
31   */
32  const HTML = "text/html";
33  /**
34   * Constant for miscellaneous mime type
35   */
36  const MISC = "application/octet-stream";
37  /**
38   * Constant for MIME sections which must appear in the multipart/alternative section.
39   */
40  const LEVEL_ALTERNATIVE = "alternative";
41  /**
42   * Constant for MIME sections which must appear in the multipart/related section.
43   */
44  const LEVEL_RELATED = "related";
45  /**
46   * Constant for MIME sections which must appear in the multipart/mixed section.
47   */
48  const LEVEL_MIXED = "mixed";
49  /**
50   * Constant for MIME sections which must appear in the multipart/mixed section.
51   */
52  const LEVEL_TOP = "top";
53  /**
54   * Constant for safe line length in almost all places
55   */
56  const SAFE_LENGTH = 1000; //RFC 2822
57  /**
58   * Constant for really safe line length
59   */
60  const VERY_SAFE_LENGTH = 76; //For command line mail clients such as pine
61  /**
62   * The header part of this MIME document
63   * @var Swift_Message_Headers
64   */
65  public $headers = null;
66  /**
67   * The body of the documented (unencoded)
68   * @var string data
69   */
70  protected $data = "";
71  /**
72   * Maximum line length
73   * @var int
74   */
75  protected $wrap = 1000; //RFC 2822
76  /**
77   * Nested mime parts
78   * @var array
79   */
80  protected $children = array();
81  /**
82   * The boundary used to separate mime parts
83   * @var string
84   */
85  protected $boundary = null;
86  /**
87   * The line ending characters needed
88   * @var string
89   */
90  protected $LE = "\r\n";
91  /**
92   * An instance of Swift_Cache
93   * @var Swift_Cache
94   */
95  protected $cache;
96  /**
97   * A list of used MIME boundaries after they're generated.
98   * @var array
99   */
100  protected static $usedBoundaries = array();
101
102  /**
103   * Constructor
104   */
105  public function __construct()
106  {
107    Swift_ClassLoader::load("Swift_Message_Headers");
108    $this->setHeaders(new Swift_Message_Headers());
109    Swift_ClassLoader::load("Swift_CacheFactory");
110    $this->cache = Swift_CacheFactory::getCache();
111  }
112  /**
113   * Compute a unique boundary
114   * @return string
115   */
116  public static function generateBoundary()
117  {
118    do
119    {
120      $boundary = uniqid(rand(), true);
121    } while (in_array($boundary, self::$usedBoundaries));
122    self::$usedBoundaries[] = $boundary;
123    return "_=_swift-" . $boundary . "_=_";
124  }
125  /**
126   * Replace the current headers with new ones
127   * DO NOT DO THIS UNLESS YOU KNOW WHAT YOU'RE DOING!
128   * @param Swift_Message_Headers The headers to use
129   */
130  public function setHeaders($headers)
131  {
132    $this->headers = $headers;
133  }
134  /**
135   * Set the line ending character to use
136   * @param string The line ending sequence
137   * @return boolean
138   */
139  public function setLE($le)
140  {
141    if (in_array($le, array("\r", "\n", "\r\n")))
142    {
143      $this->cache->clear("body");
144      $this->LE = $le;
145      //This change should be recursive
146      $this->headers->setLE($le);
147      foreach ($this->children as $id => $child)
148      {
149        $this->children[$id]->setLE($le);
150      }
151
152      return true;
153    }
154    else return false;
155  }
156  /**
157   * Get the line ending sequence
158   * @return string
159   */
160  public function getLE()
161  {
162    return $this->LE;
163  }
164  /**
165   * Reset the entire cache state from this branch of the tree and traversing down through the children
166   */
167  public function uncacheAll()
168  {
169    $this->cache->clear("body");
170    $this->cache->clear("append");
171    $this->cache->clear("headers");
172    $this->cache->clear("dbl_le");
173    $this->headers->uncacheAll();
174    foreach ($this->children as $id => $child)
175    {
176      $this->children[$id]->uncacheAll();
177    }
178  }
179  /**
180   * Set the content type of this MIME document
181   * @param string The content type to use in the same format as MIME 1.0 expects
182   */
183  public function setContentType($type)
184  {
185    $this->headers->set("Content-Type", $type);
186  }
187  /**
188   * Get the content type which has been set
189   * The MIME 1.0 Content-Type is provided as a string
190   * @return string
191   */
192  public function getContentType()
193  {
194    try {
195      return $this->headers->get("Content-Type");
196    } catch (Swift_Message_MimeException $e) {
197      return false;
198    }
199  }
200  /**
201   * Set the encoding format to be used on the body of the document
202   * @param string The encoding type used
203   * @param boolean If this encoding format should be used recursively. Note, this only takes effect if no encoding is set in the children.
204   * @param boolean If the encoding should only be applied when the string is not ascii.
205   */
206  public function setEncoding($encoding, $recursive=false, $non_ascii=false)
207  {
208    $this->cache->clear("body");
209    switch (strtolower($encoding))
210    {
211      case "q": case "qp": case "quoted-printable":
212        $encoding = "quoted-printable";
213        break;
214      case "b": case "base64":
215        $encoding = "base64";
216        break;
217      case "7bit": case "8bit": case "binary":
218        $encoding = strtolower($encoding);
219        break;
220    }
221
222    $data = $this->getData();
223    Swift_ClassLoader::load("Swift_Message_Encoder");
224    if ($non_ascii && is_string($data) && strlen($data) > 0 && !Swift_Message_Encoder::instance()->is7BitAscii($data))
225    {
226      $this->headers->set("Content-Transfer-Encoding", $encoding);
227    }
228    elseif (!$non_ascii || !is_string($data))
229    {
230      $this->headers->set("Content-Transfer-Encoding", $encoding);
231    }
232
233    if ($recursive)
234    {
235      foreach ($this->children as $id => $child)
236      {
237        if (!$child->getEncoding()) $this->children[$id]->setEncoding($encoding, $recursive, $non_ascii);
238      }
239    }
240  }
241  /**
242   * Get the encoding format used in this document
243   * @return string
244   */
245  public function getEncoding()
246  {
247    try {
248      return $this->headers->get("Content-Transfer-Encoding");
249    } catch (Swift_Message_MimeException $e) {
250      return false;
251    }
252  }
253  /**
254   * Specify the string which makes up the body of this message
255   * HINT: You can always nest another MIME document here if you call it's build() method.
256   * $data can be an object of Swift_File or a string
257   * @param mixed The body of the document
258   */
259  public function setData($data)
260  {
261    $this->cache->clear("body");
262    if ($data instanceof Swift_File) $this->data = $data;
263    else $this->data = (string) $data;
264  }
265  /**
266   * Return the string which makes up the body of this MIME document
267   * @return string,Swift_File
268   */
269  public function getData()
270  {
271    return $this->data;
272  }
273  /**
274   * Get the data in the format suitable for sending
275   * @return Swift_Cache_OutputStream
276   * @throws Swift_FileException If the file stream given cannot be read
277   * @throws Swift_Message_MimeException If some required headers have been forcefully removed
278   */
279  public function buildData()
280  {
281    Swift_ClassLoader::load("Swift_Message_Encoder");
282    Swift_ClassLoader::load("Swift_Cache_JointOutputStream");
283    if (!empty($this->children)) //If we've got some mime parts we need to stick them onto the end of the message
284    {
285      if ($this->boundary === null) $this->boundary = self::generateBoundary();
286      $this->headers->setAttribute("Content-Type", "boundary", $this->boundary);
287
288      $this->cache->clear("append");
289      foreach ($this->children as $part)
290      {
291        $this->cache->write("append", $this->LE . "--" . $this->boundary . $this->LE);
292        $part_stream = $part->build();
293        while (false !== $bytes = $part_stream->read()) $this->cache->write("append", $bytes);
294      }
295      $this->cache->write("append", $this->LE . "--" . $this->boundary . "--" . $this->LE);
296    }
297
298    $joint_os = new Swift_Cache_JointOutputStream();
299
300    //Try using a cached version to save some cycles (at the expense of memory)
301    //if ($this->cache !== null) return $this->cache . $append;
302    if ($this->cache->has("body"))
303    {
304      $joint_os->addStream($this->cache->getOutputStream("body"));
305      $joint_os->addStream($this->cache->getOutputStream("append"));
306      return $joint_os;
307    }
308
309    $is_file = ($this->getData() instanceof Swift_File);
310    switch ($this->getEncoding())
311    {
312      case "quoted-printable":
313        if ($is_file)
314        {
315          $qp_os = Swift_Message_Encoder::instance()->QPEncodeFile($this->getData(), 76, $this->LE);
316          while (false !== $bytes = $qp_os->read())
317            $this->cache->write("body", $bytes);
318        }
319        else
320        {
321          $this->cache->write("body", Swift_Message_Encoder::instance()->QPEncode($this->getData(), 76, 0, false, $this->LE));
322        }
323        break;
324      case "base64":
325        if ($is_file)
326        {
327          $b64_os = Swift_Message_Encoder::instance()->base64EncodeFile($this->getData(), 76, $this->LE);
328          while (false !== $bytes = $b64_os->read())
329            $this->cache->write("body", $bytes);
330        }
331        else
332        {
333          $this->cache->write("body", Swift_Message_Encoder::instance()->base64Encode($this->getData(), 76, 0, false, $this->LE));
334        }
335        break;
336      case "binary":
337        if ($is_file)
338        {
339          $data = $this->getData();
340          while (false !== $bytes = $data->read(8192))
341            $this->cache->write("body", $bytes);
342        }
343        else
344        {
345          $this->cache->write("body", $this->getData());
346        }
347        break;
348      case "7bit":
349        if ($is_file)
350        {
351          $os = Swift_Message_Encoder::instance()->encode7BitFile($this->getData(), $this->wrap, $this->LE);
352          while (false !== $bytes = $os->read())
353            $this->cache->write("body", $bytes);
354        }
355        else
356        {
357          $this->cache->write("body", Swift_Message_Encoder::instance()->encode7Bit($this->getData(), $this->wrap, $this->LE));
358        }
359        break;
360      case "8bit": default:
361        if ($is_file)
362        {
363          $os = Swift_Message_Encoder::instance()->encode8BitFile($this->getData(), $this->wrap, $this->LE);
364          while (false !== $bytes = $os->read())
365            $this->cache->write("body", $bytes);
366        }
367        else
368        {
369          $this->cache->write("body", Swift_Message_Encoder::instance()->encode8Bit($this->getData(), $this->wrap, $this->LE));
370        }
371        break;
372    }
373    $joint_os->addStream($this->cache->getOutputStream("body"));
374    $joint_os->addStream($this->cache->getOutputStream("append"));
375    return $joint_os;
376  }
377  /**
378   * Set the size at which lines wrap around (includes the CRLF)
379   * @param int The length of a line
380   */
381  public function setLineWrap($len)
382  {
383    $this->cache->clear("body");
384    $this->wrap = (int) $len;
385  }
386  /**
387   * Nest a child mime part in this document
388   * @param Swift_Message_Mime
389   * @param string The identifier to use, optional
390   * @param int Add the part before (-1) or after (+1) the other parts
391   * @return string The identifier for this part
392   */
393  public function addChild(Swift_Message_Mime $mime, $id=null, $after=1)
394  {
395    if (empty($id))
396    {
397      do
398      {
399        $id = uniqid();
400      } while (array_key_exists($id, $this->children));
401    }
402    $id = (string) $id;
403    if ($after == -1) $this->children = array_merge(array($id => $mime), $this->children);
404    else $this->children[$id] = $mime;
405
406    return $id;
407  }
408  /**
409   * Check if a child exists identified by $id
410   * @param string Identifier to look for
411   * @return boolean
412   */
413  public function hasChild($id)
414  {
415    return array_key_exists($id, $this->children);
416  }
417  /**
418   * Get a child document, identified by $id
419   * @param string The identifier for this child
420   * @return Swift_Message_Mime The child document
421   * @throws Swift_Message_MimeException If no such child exists
422   */
423  public function getChild($id)
424  {
425    if ($this->hasChild($id))
426    {
427      return $this->children[$id];
428    }
429    else
430    {
431      throw new Swift_Message_MimeException(
432      "Cannot retrieve child part identified by '" . $id . "' as it does not exist.  Consider using hasChild() to check.");
433    }
434  }
435  /**
436   * Remove a part from the document
437   * @param string The identifier of the child
438   * @throws Swift_Message_MimeException If no such part exists
439   */
440  public function removeChild($id)
441  {
442    $id = (string) $id;
443    if (!$this->hasChild($id))
444    {
445      throw new Swift_Message_MimeException(
446      "Cannot remove child part identified by '" . $id . "' as it does not exist. Consider using hasChild() to check.");
447    }
448    else
449    {
450      $this->children[$id] = null;
451      unset($this->children[$id]);
452    }
453  }
454  /**
455   * List the IDs of all children in this document
456   * @return array
457   */
458  public function listChildren()
459  {
460    return array_keys($this->children);
461  }
462  /**
463   * Get the total number of children present in this document
464   * @return int
465   */
466  public function numChildren()
467  {
468    return count($this->children);
469  }
470  /**
471   * Get the level at which this mime part would appear in a document
472   * One of "mixed", "alternative" or "related"
473   * @return string
474   */
475  abstract public function getLevel();
476  /**
477   * Compile the entire MIME document into a string
478   * The returned string may be used in other documents if needed.
479   * @return Swift_Cache_OutputStream
480   */
481  public function build()
482  {
483    $this->preBuild();
484    $data = $this->buildData();
485    $joint_os = new Swift_Cache_JointOutputStream();
486    $this->cache->clear("headers");
487    $this->cache->write("headers", $this->headers->build());
488    $joint_os->addStream($this->cache->getOutputStream("headers"));
489    $this->cache->clear("dbl_le");
490    $this->cache->write("dbl_le", str_repeat($this->LE, 2));
491    $joint_os->addStream($this->cache->getOutputStream("dbl_le"));
492    $joint_os->addStream($data);
493    return $joint_os;
494    //return $this->headers->build() . str_repeat($this->LE, 2) . $data;
495  }
496  /**
497   * Execute any logic needed prior to building
498   */
499  abstract public function preBuild();
500}
501