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
This commit is contained in:
Mark Story 2011-12-26 22:07:52 -05:00
parent 53598c722c
commit 0e4af546d6
2 changed files with 145 additions and 131 deletions

View file

@ -963,37 +963,8 @@ class CakeEmail {
} }
$this->_textMessage = $this->_htmlMessage = ''; $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->_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); $contents = $this->transportClass()->send($this);
if (!empty($this->_config['log'])) { if (!empty($this->_config['log'])) {
@ -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() { protected function _attachFiles() {
$msg = array();
foreach ($this->_attachments as $filename => $fileInfo) { foreach ($this->_attachments as $filename => $fileInfo) {
$handle = fopen($fileInfo['file'], 'rb'); if (!empty($fileInfo['contentId'])) {
$data = fread($handle, filesize($fileInfo['file'])); continue;
$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 . '"';
} }
$this->_message[] = ''; $data = $this->_readFile($fileInfo['file']);
$this->_message[] = $data;
$this->_message[] = ''; $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 * @param string $file The file to read.
* @return array * @return string File contents in base64 encoding
*/ */
protected function _formatMessage($message) { protected function _readFile($file) {
if (!empty($this->_attachments)) { $handle = fopen($file, 'rb');
$prefix = array('--' . $this->_boundary); $data = fread($handle, filesize($file));
if ($this->_emailFormat === 'text') { $data = chunk_split(base64_encode($data)) ;
$prefix[] = 'Content-Type: text/plain; charset=' . $this->charset; fclose($handle);
} elseif ($this->_emailFormat === 'html') { return $data;
$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;
} }
/** /**
* 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 * @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) { protected function _render($content) {
$content = implode("\n", $content); $content = implode("\n", $content);
$rendered = $this->_renderTemplates($content); $rendered = $this->_renderTemplates($content);
$msg = array(); $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']; $contentIds = array_filter((array)Set::classicExtract($this->_attachments, '{s}.contentId'));
$content = explode("\n", $this->_textMessage); $hasInlineAttachments = count($contentIds) > 0;
$msg = array_merge($msg, $content); $hasAttachments = !empty($this->_attachments);
$hasMultipleTypes = count($rendered) > 1;
$msg[] = ''; $boundary = $relBoundary = $textBoundary = $this->_boundary;
$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);
if ($hasInlineAttachments) {
$msg[] = '--' . $boundary;
$msg[] = 'Content-Type: multipart/related; boundary="rel-' . $boundary . '"';
$msg[] = ''; $msg[] = '';
$msg[] = '--alt-' . $this->_boundary . '--'; $relBoundary = 'rel-' . $boundary;
$msg[] = '';
return $msg;
} }
if (!empty($this->_attachments)) { if ($hasMultipleTypes) {
if ($this->_emailFormat === 'html') { $msg[] = '--' . $relBoundary;
$msg[] = ''; $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"';
$msg[] = '--' . $this->_boundary; $msg[] = '';
$msg[] = 'Content-Type: text/html; charset=' . $this->charset; $textBoundary = 'alt-' . $boundary;
$msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); }
$msg[] = '';
} else { if (isset($rendered['text'])) {
$msg[] = '--' . $this->_boundary; if ($textBoundary !== $boundary || $hasAttachments) {
$msg[] = '--' . $textBoundary;
$msg[] = 'Content-Type: text/plain; charset=' . $this->charset; $msg[] = 'Content-Type: text/plain; charset=' . $this->charset;
$msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding(); $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding();
$msg[] = ''; $msg[] = '';
} }
$this->_textMessage = $rendered['text'];
$content = explode("\n", $this->_textMessage);
$msg = array_merge($msg, $content);
$msg[] = '';
} }
$rendered = $this->_encodeString($rendered[$this->_emailFormat], $this->charset); if (isset($rendered['html'])) {
$content = explode("\n", $rendered); if ($textBoundary !== $boundary || $hasAttachments) {
$msg[] = '--' . $textBoundary;
if ($this->_emailFormat === 'html') { $msg[] = 'Content-Type: text/html; charset=' . $this->charset;
$this->_htmlMessage = $rendered; $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding();
} else { $msg[] = '';
$this->_textMessage = $rendered; }
$this->_htmlMessage = $rendered['html'];
$content = explode("\n", $this->_htmlMessage);
$msg = array_merge($msg, $content);
$msg[] = '';
} }
return array_merge($msg, $content); if ($hasMultipleTypes) {
$msg[] = '--' . $textBoundary . '--';
$msg[] = '';
}
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. * 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. * @param string $content The content passed in from send() in most cases.
* @return array The rendered content with html and text keys. * @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)) { if (empty($this->_template)) {
return false; foreach ($types as $type) {
$rendered[$type] = $this->_encodeString($content, $this->charset);
}
return $rendered;
} }
$viewClass = $this->_viewRender; $viewClass = $this->_viewRender;
if ($viewClass !== 'View') { if ($viewClass !== 'View') {
@ -1425,12 +1433,6 @@ class CakeEmail {
$View->plugin = $layoutPlugin; $View->plugin = $layoutPlugin;
} }
$types = array($this->_emailFormat);
if ($this->_emailFormat == 'both') {
$types = array('html', 'text');
}
$rendered = array();
foreach ($types as $type) { foreach ($types as $type) {
$View->set('content', $content); $View->set('content', $content);
$View->hasRendered = false; $View->hasRendered = false;
@ -1438,7 +1440,7 @@ class CakeEmail {
$render = $View->render($template, $layout); $render = $View->render($template, $layout);
$render = str_replace(array("\r\n", "\r"), "\n", $render); $render = str_replace(array("\r\n", "\r"), "\n", $render);
$rendered[$type] = $render; $rendered[$type] = $this->_encodeString($render, $this->charset);
} }
return $rendered; return $rendered;
} }

View file

@ -794,12 +794,25 @@ class CakeEmailTest extends CakeTestCase {
$this->assertContains('Content-Type: multipart/mixed; boundary="' . $boundary . '"', $result['headers']); $this->assertContains('Content-Type: multipart/mixed; boundary="' . $boundary . '"', $result['headers']);
$expected = "--$boundary\r\n" . $expected = "--$boundary\r\n" .
"Content-Type: multipart/alternative; boundary=\"alt-$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" . "Content-Transfer-Encoding: 8bit\r\n" .
"\r\n" . "\r\n" .
"Hello" . "Hello" .
"\r\n" . "\r\n" .
"\r\n" . "\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" . "--$boundary\r\n" .
"Content-Type: application/octet-stream\r\n" . "Content-Type: application/octet-stream\r\n" .
"Content-Transfer-Encoding: base64\r\n" . "Content-Transfer-Encoding: base64\r\n" .
@ -875,7 +888,7 @@ class CakeEmailTest extends CakeTestCase {
$this->CakeEmail->charset = 'ISO-2022-JP'; $this->CakeEmail->charset = 'ISO-2022-JP';
$result = $this->CakeEmail->send(); $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($expected, $result['message']);
$this->assertContains('Message-ID: ', $result['headers']); $this->assertContains('Message-ID: ', $result['headers']);
$this->assertContains('To: ', $result['headers']); $this->assertContains('To: ', $result['headers']);
@ -1011,7 +1024,7 @@ class CakeEmailTest extends CakeTestCase {
$message = $this->CakeEmail->message(); $message = $this->CakeEmail->message();
$boundary = $this->CakeEmail->getBoundary(); $boundary = $this->CakeEmail->getBoundary();
$this->assertFalse(empty($boundary)); $this->assertFalse(empty($boundary));
$this->assertNotContains('--' . $boundary, $message); $this->assertContains('--' . $boundary, $message);
$this->assertNotContains('--' . $boundary . '--', $message); $this->assertNotContains('--' . $boundary . '--', $message);
$this->assertContains('--alt-' . $boundary, $message); $this->assertContains('--alt-' . $boundary, $message);
$this->assertContains('--alt-' . $boundary . '--', $message); $this->assertContains('--alt-' . $boundary . '--', $message);
@ -1121,7 +1134,6 @@ class CakeEmailTest extends CakeTestCase {
// UTF-8 is 8bit // UTF-8 is 8bit
$this->assertTrue($this->checkContentTransferEncoding($message, '8bit')); $this->assertTrue($this->checkContentTransferEncoding($message, '8bit'));
$this->CakeEmail->charset = 'ISO-2022-JP'; $this->CakeEmail->charset = 'ISO-2022-JP';
$this->CakeEmail->send(); $this->CakeEmail->send();
$message = $this->CakeEmail->message(); $message = $this->CakeEmail->message();