From f3a02e71b11583cd893169ed6edb3a1f6abd6212 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 15 Jun 2015 17:06:42 +0200 Subject: [PATCH] Add support for email body --- .../files/lib/system/email/Email.class.php | 181 +++++++++++++++++- .../email/mime/AbstractMimePart.class.php | 49 +++++ .../email/mime/AttachmentMimePart.class.php | 83 ++++++++ .../system/email/mime/TextMimePart.class.php | 59 ++++++ 4 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 wcfsetup/install/files/lib/system/email/mime/AbstractMimePart.class.php create mode 100644 wcfsetup/install/files/lib/system/email/mime/AttachmentMimePart.class.php create mode 100644 wcfsetup/install/files/lib/system/email/mime/TextMimePart.class.php diff --git a/wcfsetup/install/files/lib/system/email/Email.class.php b/wcfsetup/install/files/lib/system/email/Email.class.php index fe2e74d7a6..b79e440816 100644 --- a/wcfsetup/install/files/lib/system/email/Email.class.php +++ b/wcfsetup/install/files/lib/system/email/Email.class.php @@ -1,5 +1,7 @@ textBoundary = "WoltLab_Community_Framework=_".StringUtil::getRandomID(); + $this->mimeBoundary = "WoltLab_Community_Framework=_".StringUtil::getRandomID(); + } + /** * Returns the mail host for use in the Message-Id. * @@ -330,6 +364,43 @@ class Email { $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 @@ -361,7 +432,7 @@ class Email { $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."); @@ -377,6 +448,23 @@ class Email { } $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); } @@ -391,4 +479,95 @@ class Email { 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(); + } } diff --git a/wcfsetup/install/files/lib/system/email/mime/AbstractMimePart.class.php b/wcfsetup/install/files/lib/system/email/mime/AbstractMimePart.class.php new file mode 100644 index 0000000000..18851a7773 --- /dev/null +++ b/wcfsetup/install/files/lib/system/email/mime/AbstractMimePart.class.php @@ -0,0 +1,49 @@ + + * @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(); +} diff --git a/wcfsetup/install/files/lib/system/email/mime/AttachmentMimePart.class.php b/wcfsetup/install/files/lib/system/email/mime/AttachmentMimePart.class.php new file mode 100644 index 0000000000..ee1a16903d --- /dev/null +++ b/wcfsetup/install/files/lib/system/email/mime/AttachmentMimePart.class.php @@ -0,0 +1,83 @@ + + * @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); + } +} diff --git a/wcfsetup/install/files/lib/system/email/mime/TextMimePart.class.php b/wcfsetup/install/files/lib/system/email/mime/TextMimePart.class.php new file mode 100644 index 0000000000..30965accaf --- /dev/null +++ b/wcfsetup/install/files/lib/system/email/mime/TextMimePart.class.php @@ -0,0 +1,59 @@ + + * @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; + } +} -- 2.20.1