<?php
namespace wcf\system\email;
+use wcf\system\email\mime\AbstractMimePart;
+use wcf\system\email\mime\TextMimePart;
use wcf\system\exception\SystemException;
use wcf\util\DateUtil;
use wcf\util\StringUtil;
*/
protected $extraHeaders = [ ];
+ /**
+ * Text parts of this email
+ * @var array
+ */
+ protected $text = [ ];
+
+ /**
+ * Attachments of this email
+ * @var array
+ */
+ protected $attachments = [ ];
+
+ /**
+ * Boundary between the 'Text' parts of this email
+ * @var string
+ */
+ private $textBoundary;
+
+ /**
+ * Boundary between the mime parts of this email
+ * @var string
+ */
+ private $mimeBoundary;
+
/**
* Mail host for use in the Message-Id
* @var string
*/
private static $host = null;
+ /**
+ * Generates boundaries for the mime parts.
+ */
+ public function __construct() {
+ $this->textBoundary = "WoltLab_Community_Framework=_".StringUtil::getRandomID();
+ $this->mimeBoundary = "WoltLab_Community_Framework=_".StringUtil::getRandomID();
+ }
+
/**
* Returns the mail host for use in the Message-Id.
*
$this->extraHeaders[] = [ $header, EmailGrammar::encodeMimeHeader($value) ];
}
+ /**
+ * Adds a mime part to this email. Should be either \wcf\system\email\mime\TextMimePart
+ * or \wcf\system\email\mime\AttachmentMimePart.
+ * The given priority determines the ordering within the Email. A higher priority
+ * mime part will be further down the email (see RFC 2046, 5.1.4).
+ *
+ * @param \wcf\system\email\mime\AbstractMimePart $part
+ * @param integer $priority
+ */
+ public function addMimePart(AbstractMimePart $part, $priority = 1000) {
+ foreach ($part->getAdditionalHeaders() as $header) {
+ $header[0] = mb_strtolower($header[0]);
+ if ($header[0] == 'content-type' || $header[0] == 'content-transfer-encoding') {
+ throw new SystemException("The header '".$header."' may not be set. Use the proper methods.");
+ }
+
+ if (!StringUtil::startsWith($header[0], 'x-') && !StringUtil::startsWith($header[0], 'content-')) {
+ throw new SystemException("The header '".$header."' may not be set. You may only set headers starting with 'X-' or 'Content-'.");
+ }
+ }
+
+ switch ($part->getContentTransferEncoding()) {
+ case 'base64':
+ case 'quoted-printable':
+ break;
+ default:
+ throw new SystemException("The Content-Transfer-Encoding '".$header."' may not be set. You may only use 'quoted-printable' or 'base64'.");
+ }
+
+ if ($part instanceof TextMimePart) {
+ $this->text[] = [ $priority, $part ];
+ }
+ else {
+ $this->attachments[] = [ $priority, $part ];
+ }
+ }
+
/**
* Returns an array of [ name, value ] tuples representing the email's headers.
* Note: You must have set a Subject and at least one recipient, otherwise fetching the
$headers[] = [ 'cc', implode(",\r\n ", $cc) ];
}
if ($this->getSubject()) {
- $headers[] = [ 'subject', EmailGrammar::encodeMimeHeader($this->getSubject()) ];
+ $headers[] = [ 'subject', EmailGrammar::encodeQuotedPrintableHeader($this->getSubject()) ];
}
else {
throw new SystemException("Cannot generate message headers, you must specify a subject.");
}
$headers[] = [ 'mime-version', '1.0' ];
+ if (!$this->text) {
+ throw new SystemException("Cannot generate message headers, you must specify at least one 'Text' part.");
+ }
+ if ($this->attachments) {
+ $headers[] = [ 'content-type', "multipart/mixed;\r\n boundary=\"".$this->mimeBoundary."\"" ];
+ }
+ else {
+ if (count($this->text) > 1) {
+ $headers[] = [ 'content-type', "multipart/alternative;\r\n boundary=\"".$this->textBoundary."\"" ];
+ }
+ else {
+ $headers[] = [ 'content-type', $this->text[0][1]->getContentType() ];
+ $headers[] = [ 'content-transfer-encoding', $this->text[0][1]->getContentTransferEncoding() ];
+ $headers = array_merge($headers, $this->text[0][1]->getAdditionalHeaders());
+ }
+ }
+
return array_merge($headers, $this->extraHeaders);
}
return implode(': ', $item);
}, $this->getHeaders()));
}
+
+ /**
+ * Returns the email's body as a string.
+ *
+ * @return string
+ */
+ public function getBodyString() {
+ $text = "";
+ $body = "";
+
+ if (count($this->text) > 1 || $this->attachments) {
+ $body .= StringUtil::wordWrap("This is a MIME encoded email. As you are seeing this your user agent does not support these.");
+ $body .= "\r\n\r\n";
+ }
+
+ usort($this->text, function ($a, $b) {
+ return $a[0] - $b[0];
+ });
+ foreach ($this->text as $part) {
+ if (count($this->text) > 1) {
+ $text .= "--".$this->textBoundary."\r\n";
+ }
+ if (count($this->text) > 1 || $this->attachments) {
+ $text .= "content-type: ".$part[1]->getContentType()."\r\n";
+ $text .= "content-transfer-encoding: ".$part[1]->getContentTransferEncoding()."\r\n";
+ if ($part[1]->getAdditionalHeaders()) {
+ $text .= implode("\r\n", array_map(function ($item) {
+ return implode(': ', $item);
+ }, $part[1]->getAdditionalHeaders()))."\r\n";
+ }
+ $text .= "\r\n";
+ }
+ switch ($part[1]->getContentTransferEncoding()) {
+ case 'quoted-printable':
+ $text .= quoted_printable_encode($part[1]->getContent());
+ break;
+ case 'base64':
+ $text .= chunk_split(base64_encode($part[1]->getContent()));
+ break;
+ }
+ $text .= "\r\n";
+ }
+ if (count($this->text) > 1) {
+ $text .= "--".$this->textBoundary."--\r\n";
+ }
+
+ if ($this->attachments) {
+ $body .= "--".$this->mimeBoundary."\r\n";
+ if (count($this->text) > 1) {
+ $body .= "Content-Type: multipart/alternative;\r\n boundary=\"".$this->textBoundary."\"\r\n";
+ $body .= "\r\n";
+ }
+ $body .= $text;
+
+ foreach ($this->attachments as $part) {
+ $body .= "\r\n--".$this->mimeBoundary."\r\n";
+ $body .= "content-type: ".$part[1]->getContentType()."\r\n";
+ $body .= "content-transfer-encoding: ".$part[1]->getContentTransferEncoding()."\r\n";
+ if ($part[1]->getAdditionalHeaders()) {
+ $body .= implode("\r\n", array_map(function ($item) {
+ return implode(': ', $item);
+ }, $part[1]->getAdditionalHeaders()))."\r\n";
+ }
+ $body .= "\r\n";
+ switch ($part[1]->getContentTransferEncoding()) {
+ case 'quoted-printable':
+ $body .= quoted_printable_encode($part[1]->getContent());
+ break;
+ case 'base64':
+ $body .= chunk_split(base64_encode($part[1]->getContent()));
+ break;
+ }
+ $body .= "\r\n";
+ }
+ $body .= "--".$this->mimeBoundary."--\r\n";
+ }
+ else {
+ $body .= $text;
+ }
+
+ return $body;
+ }
+
+ /**
+ * Returns the email RFC 2822 representation of this email.
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->getHeaderString()."\r\n\r\n".$this->getBodyString();
+ }
}
--- /dev/null
+<?php
+namespace wcf\system\email\mime;
+
+/**
+ * Represents a RFC 2045 / 2046 mime part of an email.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2015 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.email.mime
+ * @category Community Framework
+ */
+abstract class AbstractMimePart {
+ /**
+ * Returns the Content-Type header value.
+ *
+ * @return string
+ */
+ abstract public function getContentType();
+
+ /**
+ * Returns the transfer encoding to use. Must either be
+ * 'quoted-printable' or 'base64'.
+ *
+ * @return Either 'quoted-printable' or 'base64'
+ */
+ abstract public function getContentTransferEncoding();
+
+ /**
+ * Extra headers as an array of [ name, value] tuple for this mime part.
+ * As per RFC 2046 they may only start with X-* or Content-*. Content-Type
+ * and Content-Transfer-Encoding are blacklisted.
+ *
+ * Returns an empty array by default.
+ *
+ * @return array
+ */
+ public function getAdditionalHeaders() {
+ return [ ];
+ }
+
+ /**
+ * The body of this mime part.
+ *
+ * @return string
+ */
+ abstract public function getContent();
+}
--- /dev/null
+<?php
+namespace wcf\system\email\mime;
+use wcf\system\email\EmailGrammar;
+use wcf\util\FileUtil;
+
+/**
+ * Represents an email attachment.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2015 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.email.mime
+ * @category Community Framework
+ */
+class AttachmentMimePart extends AbstractMimePart {
+ /**
+ * the path the attachment is read from
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * the filename to provide in the email
+ * @var string
+ */
+ protected $filename;
+
+ /**
+ * the mime type to provide in the email
+ * @var string
+ */
+ protected $mimeType;
+
+ /**
+ * Creates a new Attachment.
+ *
+ * @param string $path Path to read the file from.
+ * @param string $filename Filename to provide in the email or null to use the $path's basename.
+ * @param string $mimeType Mime type to provide in the email or null to guess the mime type.
+ */
+ public function __construct($path, $filename = null, $mimeType = null) {
+ if (!is_file($path) || !is_readable($path)) {
+ throw new SystemException("Cannot attach file '".$path."'. It either does not exist or is not readable.");
+ }
+
+ $this->mimeType = $mimeType ?: (FileUtil::getMimeType($path) ?: 'application/octet-stream');
+ $this->path = $path;
+ $this->filename = $filename ?: basename($path);
+ }
+
+ /**
+ * @see \wcf\system\email\mime\AbstractMimePart::getContentType()
+ */
+ public function getContentType() {
+ return $this->mimeType;
+ }
+
+ /**
+ * @see \wcf\system\email\mime\AbstractMimePart::getContentTransferEncoding()
+ */
+ public function getContentTransferEncoding() {
+ return 'base64';
+ }
+
+ /**
+ * Adds the Content-Disposition header.
+ *
+ * @see \wcf\system\email\mime\AbstractMimePart::getAdditionalHeaders()
+ */
+ public function getAdditionalHeaders() {
+ return [
+ [ 'Content-Disposition', 'attachment; filename='.EmailGrammar::encodeHeader($this->filename) ]
+ ];
+ }
+
+ /**
+ * @see \wcf\system\email\mime\AbstractMimePart::getContent()
+ */
+ public function getContent() {
+ return file_get_contents($this->path);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\email\mime;
+
+/**
+ * Represents the visible text of an email.
+ * The content type usually is either text/plain or text/html.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2015 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package com.woltlab.wcf
+ * @subpackage system.email.mime
+ * @category Community Framework
+ */
+class TextMimePart extends AbstractMimePart {
+ /**
+ * the content of this text part
+ * @var string
+ */
+ protected $content;
+
+ /**
+ * the mime type to provide in the email
+ * @var string
+ */
+ protected $mimeType;
+
+ /**
+ * Creates a new Text.
+ *
+ * @param string $content Content of this text part.
+ * @param string $mimeType Mime type to provide in the email. You *must* not provide a charset. UTF-8 will be used automatically.
+ */
+ public function __construct($content, $mimeType) {
+ $this->mimeType = $mimeType;
+ $this->content = $content;
+ }
+
+ /**
+ * @see \wcf\system\email\mime\AbstractMimePart::getContentType()
+ */
+ public function getContentType() {
+ return $this->mimeType."; charset=UTF-8";
+ }
+
+ /**
+ * @see \wcf\system\email\mime\AbstractMimePart::getContentTransferEncoding()
+ */
+ public function getContentTransferEncoding() {
+ return 'quoted-printable';
+ }
+
+ /**
+ * @see \wcf\system\email\mime\AbstractMimePart::getContent()
+ */
+ public function getContent() {
+ return $this->content;
+ }
+}