Fixed inline attachment boundary, added transfer encoding of body parts, updated tests #432

Closed
wants to merge 5 commits into
from
View
96 lib/Cake/Network/Email/CakeEmail.php
@@ -239,6 +239,24 @@ class CakeEmail {
*/
protected $_transportClass = null;
+
+/**
+ * Available transfer encodings.
+ *
+ * @var array
+ */
+ protected $_transferEncodingsAvailable = array('quoted-printable', 'base64');
+
+/**
+ * Encoding used for the text parts (text and html respectively)
+ * If null, text is sent plain (7bit or 8bit encoded)
+ * depending on the $charset property, see _getContentTransferEncoding()
+ *
+ * Options: null (default), 'quoted-printable', 'base64'
+ * @var string
+ */
+ public $_transferEncoding = null;
+
/**
* Charset the email body is sent in
*
@@ -669,7 +687,7 @@ public function getHeaders($include = array()) {
} elseif ($this->_emailFormat === 'html') {
$headers['Content-Type'] = 'text/html; charset=' . $this->charset;
}
- $headers['Content-Transfer-Encoding'] = $this->_getContentTransferEncoding();
+ $headers['Content-Transfer-Encoding'] = $this->_getContentTransferEncoding(false);
return $headers;
}
@@ -811,6 +829,28 @@ public function transportClass() {
}
/**
+ * Email content transfer encoding schema
+ *
+ * @param string $encoding
+ * @return mixed
+ * @throws SocketException
+ */
+ public function transferEncoding($encoding = null) {
+ if ($encoding === null) {
+ return $this->_transferEncoding;
+ }
+ if (!in_array($encoding, $this->_transferEncodingsAvailable) ||
+ $encoding == 'quoted-printable' &&
+ !function_exists('quoted_printable_encode') &&
+ !function_exists('imap_8bit')
+ ) {
+ throw new SocketException(__d('cake_dev', 'Encoding not available.'));
+ }
+ $this->_transferEncoding = $encoding;
+ return $this;
+ }
+
+/**
* Message-ID
*
* @param mixed $message True to generate a new Message-ID, False to ignore (not send in email), String to set as Message-ID
@@ -1040,7 +1080,7 @@ protected function _applyConfig($config) {
$simpleMethods = array(
'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath', 'cc', 'bcc',
'messageId', 'subject', 'viewRender', 'viewVars', 'attachments',
- 'transport', 'emailFormat'
+ 'transport', 'emailFormat', 'transferEncoding'
);
foreach ($simpleMethods as $method) {
if (isset($config[$method])) {
@@ -1092,6 +1132,7 @@ public function reset() {
$this->_emailFormat = 'text';
$this->_transportName = 'Mail';
$this->_transportClass = null;
+ $this->_transferEncoding = null;
$this->_attachments = array();
$this->_config = array();
return $this;
@@ -1240,17 +1281,22 @@ protected function _createBoundary() {
/**
* Attach non-embedded files by adding file contents inside boundaries.
*
+ * @param string $boundary Boundary to use. If null, will default to $this->_boundary
* @return array An array of lines to add to the message
*/
- protected function _attachFiles() {
+ protected function _attachFiles($boundary = null) {
+ if($boundary === null) {
+ $boundary = $this->_boundary;
+ }
+
$msg = array();
foreach ($this->_attachments as $filename => $fileInfo) {
if (!empty($fileInfo['contentId'])) {
continue;
}
$data = $this->_readFile($fileInfo['file']);
- $msg[] = '--' . $this->_boundary;
+ $msg[] = '--' . $boundary;
$msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
$msg[] = 'Content-Transfer-Encoding: base64';
$msg[] = 'Content-Disposition: attachment; filename="' . $filename . '"';
@@ -1278,17 +1324,22 @@ protected function _readFile($file) {
/**
* Attach inline/embedded files to the message.
*
+ * @param string $boundary Boundary to use. If null, will default to $this->_boundary
* @return array An array of lines to add to the message
*/
- protected function _attachInlineFiles() {
+ protected function _attachInlineFiles($boundary = null) {
+ if($boundary === null) {
+ $boundary = $this->_boundary;
+ }
+
$msg = array();
foreach ($this->_attachments as $filename => $fileInfo) {
if (empty($fileInfo['contentId'])) {
continue;
}
$data = $this->_readFile($fileInfo['file']);
- $msg[] = '--' . $this->_boundary;
+ $msg[] = '--' . $boundary;
$msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
$msg[] = 'Content-Transfer-Encoding: base64';
$msg[] = 'Content-ID: <' . $fileInfo['contentId'] . '>';
@@ -1310,6 +1361,27 @@ protected function _render($content) {
$content = implode("\n", $content);
$rendered = $this->_renderTemplates($content);
+ switch($this->_transferEncoding) {
+ case 'quoted-printable':
+ foreach($rendered as &$part) {
+ if(function_exists('quoted_printable_encode')) {
+ $part = quoted_printable_encode($part);
+ } elseif(function_exists('imap_8bit')) {
+ $part = imap_8bit($part);
+ } else {
+ throw new SocketException(__d('cake_dev', 'Encoding not available. Need php-5.3 (quoted_printable_encode) or imap extension (imap_8bit).'));
+ }
+ }
+ break;
+ case 'base64':
+ foreach($rendered as &$part) {
+ $part = chunk_split(base64_encode($part));
+ }
+ break;
+ default:
+ break;
+ }
+
$msg = array();
$contentIds = array_filter((array)Set::classicExtract($this->_attachments, '{s}.contentId'));
@@ -1365,15 +1437,15 @@ protected function _render($content) {
}
if ($hasInlineAttachments) {
- $attachments = $this->_attachInlineFiles();
+ $attachments = $this->_attachInlineFiles($relBoundary);
$msg = array_merge($msg, $attachments);
$msg[] = '';
$msg[] = '--' . $relBoundary . '--';
$msg[] = '';
}
if ($hasAttachments) {
- $attachments = $this->_attachFiles();
+ $attachments = $this->_attachFiles($boundary);
$msg = array_merge($msg, $attachments);
}
if ($hasAttachments || $hasMultipleTypes) {
@@ -1446,11 +1518,15 @@ protected function _renderTemplates($content) {
}
/**
- * Return the Content-Transfer Encoding value based on the set charset
+ * Return the Content-Transfer Encoding value based on the set charset and transfer encoding
*
+ * @param boolean $content If false, return the header encoding, else text content encoding.
* @return void
*/
- protected function _getContentTransferEncoding() {
+ protected function _getContentTransferEncoding($content = true) {
+ if($content && $this->_transferEncoding) {
+ return $this->_transferEncoding;
+ }
$charset = strtoupper($this->charset);
if (in_array($charset, $this->_charset8bit)) {
return '8bit';
View
162 lib/Cake/Test/Case/Network/Email/CakeEmailTest.php
@@ -37,15 +37,23 @@ class TestCakeEmail extends CakeEmail {
public function formatAddress($address) {
return parent::_formatAddress($address);
}
-
+
/**
* Wrap to protected method
*
*/
public function wrap($text) {
return parent::_wrap($text);
}
-
+
+/**
+ * Wrap to protected method
+ *
+ */
+ public function getContentTransferEncoding($content = true) {
+ return parent::_getContentTransferEncoding($content);
+ }
+
/**
* Get the boundary attribute
*
@@ -865,7 +873,7 @@ public function testSendWithInlineAttachments() {
"\r\n" .
"--alt-{$boundary}--\r\n" .
"\r\n" .
- "--$boundary\r\n" .
+ "--rel-$boundary\r\n" .
"Content-Type: application/octet-stream\r\n" .
"Content-Transfer-Encoding: base64\r\n" .
"Content-ID: <abc123>\r\n" .
@@ -1188,6 +1196,7 @@ public function testMessage() {
// UTF-8 is 8bit
$this->assertTrue($this->checkContentTransferEncoding($message, '8bit'));
+ $this->assertTrue($this->checkAlternativesCharset($message, 'UTF-8'));
$this->CakeEmail->charset = 'ISO-2022-JP';
$this->CakeEmail->send();
@@ -1197,9 +1206,9 @@ public function testMessage() {
// ISO-2022-JP is 7bit
$this->assertTrue($this->checkContentTransferEncoding($message, '7bit'));
+ $this->assertTrue($this->checkAlternativesCharset($message, 'ISO-2022-JP'));
}
-
/**
* testReset method
*
@@ -1443,7 +1452,7 @@ public function testBodyEncoding() {
$this->assertContains(mb_convert_encoding('ってテーブルを作ってやってたらう','ISO-2022-JP'), $result['message']);
}
- private function checkContentTransferEncoding($message, $charset) {
+ private function checkContentTransferEncoding($message, $encoding) {
$boundary = '--alt-' . $this->CakeEmail->getBoundary();
$result['text'] = false;
$result['html'] = false;
@@ -1458,7 +1467,7 @@ private function checkContentTransferEncoding($message, $charset) {
if (preg_match('/^Content-Type: text\/html/', $message[$i])) {
$type = 'html';
}
- if ($message[$i] === 'Content-Transfer-Encoding: ' . $charset) {
+ if ($message[$i] === 'Content-Transfer-Encoding: ' . $encoding) {
$flag = true;
}
++$i;
@@ -1469,6 +1478,29 @@ private function checkContentTransferEncoding($message, $charset) {
return $result['text'] && $result['html'];
}
+ private function checkAlternativesCharset($message, $charset) {
+ $boundary = '--alt-' . $this->CakeEmail->getBoundary();
+ $result['text'] = false;
+ $result['html'] = false;
+ for ($i = 0; $i < count($message); ++$i) {
+ if ($message[$i] == $boundary) {
+ $flag = false;
+ $type = '';
+ while (!preg_match('/^$/', $message[$i])) {
+ if (preg_match('/^Content-Type: text\/plain; charset=' . $charset . '/', $message[$i])) {
+ $result['text'] = true;;
+ }
+ if (preg_match('/^Content-Type: text\/html; charset=' . $charset . '/', $message[$i])) {
+ $result['html'] = true;;
+ }
+ ++$i;
+ }
+ $result[$type] = $flag;
+ }
+ }
+ return $result['text'] && $result['html'];
+ }
+
/**
* Test CakeEmail::_encode function
*
@@ -1488,4 +1520,122 @@ public function testEncode() {
. " =?ISO-2022-JP?B?GyRCJCYkSiRrJHMkQCRtJCYhKRsoQg==?=";
$this->assertSame($expected, $result);
}
+
+/**
+ * Test CakeEmail::_getContentTransferEncoding
+ *
+ */
+ public function testGetContentTransferEncoding() {
+ $this->CakeEmail->charset = 'utf-8';
+ $result = $this->CakeEmail->getContentTransferEncoding();
+ $this->assertSame('8bit', $result);
+
+ $result = $this->CakeEmail->getContentTransferEncoding(false);
+ $this->assertSame('8bit', $result);
+
+ $this->CakeEmail->transferEncoding('base64');
+ $result = $this->CakeEmail->getContentTransferEncoding();
+ $this->assertSame('base64', $result);
+
+ $result = $this->CakeEmail->getContentTransferEncoding(false);
+ $this->assertSame('8bit', $result);
+ }
+
+/**
+ * Test CakeEmail::transferEncoding
+ *
+ */
+ public function testTransferEncoding() {
+ $this->CakeEmail->transferEncoding('quoted-printable');
+ $result = $this->CakeEmail->_transferEncoding;
+ $this->assertSame('quoted-printable', $result);
+
+ $result = $this->CakeEmail->transferEncoding('base64');
+ $this->assertInstanceOf('CakeEmail', $result);
+ $result = $this->CakeEmail->_transferEncoding;
+ $this->assertSame('base64', $result);
+
+ $this->setExpectedException('SocketException');
+ $this->CakeEmail->transferEncoding('xcode');
+ }
+
+/**
+ * testMessage method
+ *
+ * @return void
+ */
+ public function testTransferEncodedMessageBase64() {
+ $this->CakeEmail->reset();
+ $this->CakeEmail->transport('debug');
+ $this->CakeEmail->from('cake@cakephp.org');
+ $this->CakeEmail->to(array('you@cakephp.org' => 'You'));
+ $this->CakeEmail->subject('My title');
+ $this->CakeEmail->config(array('empty'));
+ $this->CakeEmail->template('default', 'default');
+ $this->CakeEmail->emailFormat('both');
+
+ $this->CakeEmail->transferEncoding('base64');
+ $result = $this->CakeEmail->send();
+
+ $expected = "PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMDEvL0VOIj4KCjxodG1s\r\n";
+ $expected .= "Pgo8aGVhZD4KCTx0aXRsZT5FbWFpbHMvaHRtbDwvdGl0bGU+CjwvaGVhZD4KCjxib2R5PgoJPHA+\r\n";
+ $expected .= "IDwvcD48cD4gPC9wPgoJPHA+VGhpcyBlbWFpbCB3YXMgc2VudCB1c2luZyB0aGUgPGEgaHJlZj0i\r\n";
+ $expected .= "aHR0cDovL2Nha2VwaHAub3JnIj5DYWtlUEhQIEZyYW1ld29yazwvYT48L3A+CjwvYm9keT4KPC9o\r\n";
+ $expected .= "dG1sPg==\r\n";
+ $this->assertSame($expected, $this->CakeEmail->message(CakeEmail::MESSAGE_HTML));
+
+ $expected = "CgoKVGhpcyBlbWFpbCB3YXMgc2VudCB1c2luZyB0aGUgQ2FrZVBIUCBGcmFtZXdvcmssIGh0dHA6\r\n";
+ $expected .= "Ly9jYWtlcGhwLm9yZy4=\r\n";
+ $this->assertSame($expected, $this->CakeEmail->message(CakeEmail::MESSAGE_TEXT));
+
+ $message = $this->CakeEmail->message();
+
+ // UTF-8 is 8bit
+ $this->assertTrue($this->checkContentTransferEncoding($message, 'base64'));
+ $this->assertContains('Content-Type: text/plain; charset=UTF-8', $message);
+ $this->assertContains('Content-Type: text/html; charset=UTF-8', $message);
+ }
+
+/**
+ * testMessage method
+ *
+ * @return void
+ */
+ public function testTransferEncodedMessageQuotedPrintable() {
+ if(!function_exists('quoted_printable') && !function_exists('imap_8bit')) {
+ $this->markTestSkipped('quoted_printable encoding not available');
+ return;
+ }
+
+ $this->CakeEmail->reset();
+ $this->CakeEmail->transport('debug');
+ $this->CakeEmail->from('cake@cakephp.org');
+ $this->CakeEmail->to(array('you@cakephp.org' => 'You'));
+ $this->CakeEmail->subject('My title');
+ $this->CakeEmail->config(array('empty'));
+ $this->CakeEmail->template('default', 'default');
+ $this->CakeEmail->emailFormat('both');
+
+ // ISO-8859-1 is 7bit
+ $this->CakeEmail->charset = 'ISO-8859-1';
+ $this->CakeEmail->transferEncoding('quoted-printable');
+ $result = $this->CakeEmail->send();
+
+ $expected = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\">=0A=0A<html>=0A<head>=0A=\r\n";
+ $expected .= "=09<title>Emails/html</title>=0A</head>=0A=0A<body>=0A=09<p> </p><p> </p>=\r\n";
+ $expected .= "=0A=09<p>This email was sent using the <a href=3D\"http://cakephp.org\">CakeP=\r\n";
+ $expected .= "HP Framework</a></p>=0A</body>=0A</html>";
+ $this->assertSame($expected, $this->CakeEmail->message(CakeEmail::MESSAGE_HTML));
+
+ $expected = "=0A=0A=0AThis email was sent using the CakePHP Framework, http://cakephp.or=\r\n";
+ $expected .= "g.";
+ $this->assertSame($expected, $this->CakeEmail->message(CakeEmail::MESSAGE_TEXT));
+
+ $message = $this->CakeEmail->message();
+
+ $this->assertTrue($this->checkContentTransferEncoding($message, 'quoted-printable'));
+ $this->assertContains('Content-Type: text/plain; charset=ISO-8859-1', $message);
+ $this->assertContains('Content-Type: text/html; charset=ISO-8859-1', $message);
+ }
+
}