From 0e4af546d66b4ccd90632be117aee74ef9986e02 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 26 Dec 2011 22:07:52 -0500 Subject: [PATCH] Update sending attachments. Both inline and external attachments, as well as mixed sets of inline and external attachments should work now. Re-built the internals of message rendering to remove duplication and redundant code paths. Fixes #2413 Fixes #2320 --- lib/Cake/Network/Email/CakeEmail.php | 258 +++++++++--------- .../Test/Case/Network/Email/CakeEmailTest.php | 18 +- 2 files changed, 145 insertions(+), 131 deletions(-) diff --git a/lib/Cake/Network/Email/CakeEmail.php b/lib/Cake/Network/Email/CakeEmail.php index f4934ee00..c078edd2d 100644 --- a/lib/Cake/Network/Email/CakeEmail.php +++ b/lib/Cake/Network/Email/CakeEmail.php @@ -963,38 +963,9 @@ class CakeEmail { } $this->_textMessage = $this->_htmlMessage = ''; - if ($content !== null) { - if ($this->_emailFormat === 'text') { - $this->_textMessage = $content; - } elseif ($this->_emailFormat === 'html') { - $this->_htmlMessage = $content; - } elseif ($this->_emailFormat === 'both') { - $this->_textMessage = $this->_htmlMessage = $content; - } - } - $this->_createBoundary(); + $this->_message = $this->_render($this->_wrap($content)); - $message = $this->_wrap($content); - // two methods doing similar things seems silly. - // both handle attachments. - if (empty($this->_template)) { - $message = $this->_formatMessage($message); - } else { - $message = $this->_render($message); - } - $message[] = ''; - $this->_message = $message; - - // should be part of a compose method. - if (!empty($this->_attachments)) { - $this->_attachFiles(); - - $this->_message[] = ''; - $this->_message[] = '--' . $this->_boundary . '--'; - $this->_message[] = ''; - } - $contents = $this->transportClass()->send($this); if (!empty($this->_config['log'])) { $level = LOG_DEBUG; @@ -1269,142 +1240,179 @@ class CakeEmail { } /** - * Attach files by adding file contents inside boundaries. + * Attach non-embedded files by adding file contents inside boundaries. * - * @return void + * @return array An array of lines to add to the message */ protected function _attachFiles() { + $msg = array(); foreach ($this->_attachments as $filename => $fileInfo) { - $handle = fopen($fileInfo['file'], 'rb'); - $data = fread($handle, filesize($fileInfo['file'])); - $data = chunk_split(base64_encode($data)) ; - fclose($handle); - - $this->_message[] = '--' . $this->_boundary; - $this->_message[] = 'Content-Type: ' . $fileInfo['mimetype']; - $this->_message[] = 'Content-Transfer-Encoding: base64'; - if (empty($fileInfo['contentId'])) { - $this->_message[] = 'Content-Disposition: attachment; filename="' . $filename . '"'; - } else { - $this->_message[] = 'Content-ID: <' . $fileInfo['contentId'] . '>'; - $this->_message[] = 'Content-Disposition: inline; filename="' . $filename . '"'; + if (!empty($fileInfo['contentId'])) { + continue; } - $this->_message[] = ''; - $this->_message[] = $data; - $this->_message[] = ''; + $data = $this->_readFile($fileInfo['file']); + + $msg[] = '--' . $this->_boundary; + $msg[] = 'Content-Type: ' . $fileInfo['mimetype']; + $msg[] = 'Content-Transfer-Encoding: base64'; + $msg[] = 'Content-Disposition: attachment; filename="' . $filename . '"'; + $msg[] = ''; + $msg[] = $data; + $msg[] = ''; } + return $msg; } /** - * Format the message by seeing if it has attachments. + * Read the file contents and return a base64 version of the file contents. * - * @param array $message Message to format - * @return array + * @param string $file The file to read. + * @return string File contents in base64 encoding */ - protected function _formatMessage($message) { - if (!empty($this->_attachments)) { - $prefix = array('--' . $this->_boundary); - if ($this->_emailFormat === 'text') { - $prefix[] = 'Content-Type: text/plain; charset=' . $this->charset; - } elseif ($this->_emailFormat === 'html') { - $prefix[] = 'Content-Type: text/html; charset=' . $this->charset; - } elseif ($this->_emailFormat === 'both') { - $prefix[] = 'Content-Type: multipart/alternative; boundary="alt-' . $this->_boundary . '"'; - } - $prefix[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); - $prefix[] = ''; - $message = array_merge($prefix, $message); - } - - $tmp = array(); - foreach ($message as $msg) { - $tmp[] = $this->_encodeString($msg, $this->charset); - } - $message = $tmp; - - return $message; + protected function _readFile($file) { + $handle = fopen($file, 'rb'); + $data = fread($handle, filesize($file)); + $data = chunk_split(base64_encode($data)) ; + fclose($handle); + return $data; } /** - * Render the contents using the current layout and template. + * Attach inline/embedded files to the message. + * + * @return array An array of lines to add to the message + */ + protected function _attachInlineFiles() { + $msg = array(); + foreach ($this->_attachments as $filename => $fileInfo) { + if (empty($fileInfo['contentId'])) { + continue; + } + $data = $this->_readFile($fileInfo['file']); + + $msg[] = '--' . $this->_boundary; + $msg[] = 'Content-Type: ' . $fileInfo['mimetype']; + $msg[] = 'Content-Transfer-Encoding: base64'; + $msg[] = 'Content-ID: <' . $fileInfo['contentId'] . '>'; + $msg[] = 'Content-Disposition: inline; filename="' . $filename . '"'; + $msg[] = ''; + $msg[] = $data; + $msg[] = ''; + } + return $msg; + } + +/** + * Render the body of the email. * * @param string $content Content to render - * @return array Email ready to be sent + * @return array Email body ready to be sent */ protected function _render($content) { $content = implode("\n", $content); $rendered = $this->_renderTemplates($content); $msg = array(); - if ($this->_emailFormat === 'both') { - if (!empty($this->_attachments)) { - $msg[] = '--' . $this->_boundary; - $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $this->_boundary . '"'; - $msg[] = ''; - } - $msg[] = '--alt-' . $this->_boundary; - $msg[] = 'Content-Type: text/plain; charset=' . $this->charset; - $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); - $msg[] = ''; - $this->_textMessage = $rendered['text']; - $content = explode("\n", $this->_textMessage); - $msg = array_merge($msg, $content); + $contentIds = array_filter((array)Set::classicExtract($this->_attachments, '{s}.contentId')); + $hasInlineAttachments = count($contentIds) > 0; + $hasAttachments = !empty($this->_attachments); + $hasMultipleTypes = count($rendered) > 1; - $msg[] = ''; - $msg[] = '--alt-' . $this->_boundary; - $msg[] = 'Content-Type: text/html; charset=' . $this->charset; - $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); - $msg[] = ''; - - $this->_htmlMessage = $rendered['html']; - $content = explode("\n", $this->_htmlMessage); - $msg = array_merge($msg, $content); + $boundary = $relBoundary = $textBoundary = $this->_boundary; + if ($hasInlineAttachments) { + $msg[] = '--' . $boundary; + $msg[] = 'Content-Type: multipart/related; boundary="rel-' . $boundary . '"'; $msg[] = ''; - $msg[] = '--alt-' . $this->_boundary . '--'; - $msg[] = ''; - - return $msg; + $relBoundary = 'rel-' . $boundary; } - if (!empty($this->_attachments)) { - if ($this->_emailFormat === 'html') { - $msg[] = ''; - $msg[] = '--' . $this->_boundary; - $msg[] = 'Content-Type: text/html; charset=' . $this->charset; - $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); - $msg[] = ''; - } else { - $msg[] = '--' . $this->_boundary; + if ($hasMultipleTypes) { + $msg[] = '--' . $relBoundary; + $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"'; + $msg[] = ''; + $textBoundary = 'alt-' . $boundary; + } + + if (isset($rendered['text'])) { + if ($textBoundary !== $boundary || $hasAttachments) { + $msg[] = '--' . $textBoundary; $msg[] = 'Content-Type: text/plain; charset=' . $this->charset; $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); $msg[] = ''; } + $this->_textMessage = $rendered['text']; + $content = explode("\n", $this->_textMessage); + $msg = array_merge($msg, $content); + $msg[] = ''; + } + + if (isset($rendered['html'])) { + if ($textBoundary !== $boundary || $hasAttachments) { + $msg[] = '--' . $textBoundary; + $msg[] = 'Content-Type: text/html; charset=' . $this->charset; + $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); + $msg[] = ''; + } + $this->_htmlMessage = $rendered['html']; + $content = explode("\n", $this->_htmlMessage); + $msg = array_merge($msg, $content); + $msg[] = ''; } - $rendered = $this->_encodeString($rendered[$this->_emailFormat], $this->charset); - $content = explode("\n", $rendered); - - if ($this->_emailFormat === 'html') { - $this->_htmlMessage = $rendered; - } else { - $this->_textMessage = $rendered; + if ($hasMultipleTypes) { + $msg[] = '--' . $textBoundary . '--'; + $msg[] = ''; } - return array_merge($msg, $content); + if ($hasInlineAttachments) { + $attachments = $this->_attachInlineFiles(); + $msg = array_merge($msg, $attachments); + $msg[] = ''; + $msg[] = '--' . $relBoundary . '--'; + $msg[] = ''; + } + + if ($hasAttachments) { + $attachments = $this->_attachFiles(); + $msg = array_merge($msg, $attachments); + $msg[] = ''; + $msg[] = '--' . $boundary . '--'; + $msg[] = ''; + } + return $msg; + } + +/** + * Gets the text body types that are in this email message + * + * @return array Array of types. Valid types are 'text' and 'html' + */ + protected function _getTypes() { + $types = array($this->_emailFormat); + if ($this->_emailFormat == 'both') { + $types = array('html', 'text'); + } + return $types; } /** * Build and set all the view properties needed to render the templated emails. - * Returns false if the email is not templated. + * If there is no template set, the $content will be returned in a hash + * of the text content types for the email. * * @param string $content The content passed in from send() in most cases. * @return array The rendered content with html and text keys. */ - public function _renderTemplates($content) { + protected function _renderTemplates($content) { + $types = $this->_getTypes(); + $rendered = array(); if (empty($this->_template)) { - return false; + foreach ($types as $type) { + $rendered[$type] = $this->_encodeString($content, $this->charset); + } + return $rendered; } $viewClass = $this->_viewRender; if ($viewClass !== 'View') { @@ -1425,12 +1433,6 @@ class CakeEmail { $View->plugin = $layoutPlugin; } - $types = array($this->_emailFormat); - if ($this->_emailFormat == 'both') { - $types = array('html', 'text'); - } - - $rendered = array(); foreach ($types as $type) { $View->set('content', $content); $View->hasRendered = false; @@ -1438,7 +1440,7 @@ class CakeEmail { $render = $View->render($template, $layout); $render = str_replace(array("\r\n", "\r"), "\n", $render); - $rendered[$type] = $render; + $rendered[$type] = $this->_encodeString($render, $this->charset); } return $rendered; } diff --git a/lib/Cake/Test/Case/Network/Email/CakeEmailTest.php b/lib/Cake/Test/Case/Network/Email/CakeEmailTest.php index 4ee443c4e..47a47fd33 100644 --- a/lib/Cake/Test/Case/Network/Email/CakeEmailTest.php +++ b/lib/Cake/Test/Case/Network/Email/CakeEmailTest.php @@ -794,12 +794,25 @@ class CakeEmailTest extends CakeTestCase { $this->assertContains('Content-Type: multipart/mixed; boundary="' . $boundary . '"', $result['headers']); $expected = "--$boundary\r\n" . "Content-Type: multipart/alternative; boundary=\"alt-$boundary\"\r\n" . + "\r\n" . + "--alt-$boundary\r\n" . + "Content-Type: text/plain; charset=UTF-8\r\n" . "Content-Transfer-Encoding: 8bit\r\n" . "\r\n" . "Hello" . "\r\n" . "\r\n" . "\r\n" . + "--alt-$boundary\r\n" . + "Content-Type: text/html; charset=UTF-8\r\n" . + "Content-Transfer-Encoding: 8bit\r\n" . + "\r\n" . + "Hello" . + "\r\n" . + "\r\n" . + "\r\n" . + "--alt-{$boundary}--\r\n" . + "\r\n" . "--$boundary\r\n" . "Content-Type: application/octet-stream\r\n" . "Content-Transfer-Encoding: base64\r\n" . @@ -875,7 +888,7 @@ class CakeEmailTest extends CakeTestCase { $this->CakeEmail->charset = 'ISO-2022-JP'; $result = $this->CakeEmail->send(); - $expected = mb_convert_encoding('CakePHP Framework を使って送信したメールです。 http://cakephp.org.','ISO-2022-JP'); + $expected = mb_convert_encoding('CakePHP Framework を使って送信したメールです。 http://cakephp.org.', 'ISO-2022-JP'); $this->assertContains($expected, $result['message']); $this->assertContains('Message-ID: ', $result['headers']); $this->assertContains('To: ', $result['headers']); @@ -1011,7 +1024,7 @@ class CakeEmailTest extends CakeTestCase { $message = $this->CakeEmail->message(); $boundary = $this->CakeEmail->getBoundary(); $this->assertFalse(empty($boundary)); - $this->assertNotContains('--' . $boundary, $message); + $this->assertContains('--' . $boundary, $message); $this->assertNotContains('--' . $boundary . '--', $message); $this->assertContains('--alt-' . $boundary, $message); $this->assertContains('--alt-' . $boundary . '--', $message); @@ -1121,7 +1134,6 @@ class CakeEmailTest extends CakeTestCase { // UTF-8 is 8bit $this->assertTrue($this->checkContentTransferEncoding($message, '8bit')); - $this->CakeEmail->charset = 'ISO-2022-JP'; $this->CakeEmail->send(); $message = $this->CakeEmail->message();