Skip to content
This repository

CakeEmail #428

Closed
wants to merge 5 commits into from

1 participant

Hans-Joachim Michl
Hans-Joachim Michl

Added content-transfer-encodings (base64 and quoted-printable) to the text-parts (html and text) to support old/text mailclients and buggy MTAs.
Added/updated testcases accordingly.

added some commits January 11, 2012
Hans-Joachim Michl Fixed wrong boundary marker for inline-files, inline-files should sta…
…rt new rel-boundaries, not (outer-)mixed-boundaries.


Amavis spits out this error:
X-Amavis-Alert: BAD HEADER MIME error: error: unexpected end of parts before epilogue
0208933
Hans-Joachim Michl - Changed handling of attaching files, attachFiles() and attachInline…
…Files()

  now take the boundary to be used as (optional) parameter, followup to #02089335e0
- Added transfer encoding for text parts (text and html): quoted-printable, base64 and plain (no encoding)
  can be set in the $config array or via CakeEmail->transferEncoding()
c508658
Hans-Joachim Michl Added/updated testcases for transfer-encoding changes 51704db
Hans-Joachim Michl Added missing line d1e117f
Hans-Joachim Michl Changed variables to camelCase, added checks for available functional…
…ity, updated tests
7a8a40d
Hans-Joachim Michl hmic closed this January 20, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 5 unique commits by 1 author.

Jan 11, 2012
Hans-Joachim Michl Fixed wrong boundary marker for inline-files, inline-files should sta…
…rt new rel-boundaries, not (outer-)mixed-boundaries.


Amavis spits out this error:
X-Amavis-Alert: BAD HEADER MIME error: error: unexpected end of parts before epilogue
0208933
Jan 19, 2012
Hans-Joachim Michl - Changed handling of attaching files, attachFiles() and attachInline…
…Files()

  now take the boundary to be used as (optional) parameter, followup to #02089335e0
- Added transfer encoding for text parts (text and html): quoted-printable, base64 and plain (no encoding)
  can be set in the $config array or via CakeEmail->transferEncoding()
c508658
Hans-Joachim Michl Added/updated testcases for transfer-encoding changes 51704db
Hans-Joachim Michl Added missing line d1e117f
Jan 20, 2012
Hans-Joachim Michl Changed variables to camelCase, added checks for available functional…
…ity, updated tests
7a8a40d
This page is out of date. Refresh to see the latest.
96  lib/Cake/Network/Email/CakeEmail.php
@@ -239,6 +239,24 @@ class CakeEmail {
239 239
  */
240 240
 	protected $_transportClass = null;
241 241
 
  242
+
  243
+/**
  244
+ * Available transfer encodings.
  245
+ *
  246
+ * @var array
  247
+ */
  248
+	protected $_transferEncodingsAvailable = array('quoted-printable', 'base64');
  249
+
  250
+/**
  251
+ * Encoding used for the text parts (text and html respectively)
  252
+ * If null, text is sent plain (7bit or 8bit encoded)
  253
+ * depending on the $charset property, see _getContentTransferEncoding()
  254
+ * 
  255
+ * Options: null (default), 'quoted-printable', 'base64'
  256
+ * @var string
  257
+ */
  258
+	public $_transferEncoding = null;
  259
+
242 260
 /**
243 261
  * Charset the email body is sent in
244 262
  *
@@ -669,7 +687,7 @@ public function getHeaders($include = array()) {
669 687
 		} elseif ($this->_emailFormat === 'html') {
670 688
 			$headers['Content-Type'] = 'text/html; charset=' . $this->charset;
671 689
 		}
672  
-		$headers['Content-Transfer-Encoding'] = $this->_getContentTransferEncoding();
  690
+		$headers['Content-Transfer-Encoding'] = $this->_getContentTransferEncoding(false);
673 691
 
674 692
 		return $headers;
675 693
 	}
@@ -811,6 +829,28 @@ public function transportClass() {
811 829
 	}
812 830
 
813 831
 /**
  832
+ * Email content transfer encoding schema
  833
+ *
  834
+ * @param string $encoding
  835
+ * @return mixed
  836
+ * @throws SocketException
  837
+ */
  838
+	public function transferEncoding($encoding = null) {
  839
+		if ($encoding === null) {
  840
+			return $this->_transferEncoding;
  841
+		}
  842
+		if (!in_array($encoding, $this->_transferEncodingsAvailable) ||
  843
+			$encoding == 'quoted-printable' &&
  844
+			!function_exists('quoted_printable_encode') &&
  845
+			!function_exists('imap_8bit')
  846
+		) {
  847
+			throw new SocketException(__d('cake_dev', 'Encoding not available.'));
  848
+		}
  849
+		$this->_transferEncoding = $encoding;
  850
+		return $this;
  851
+	}
  852
+
  853
+/**
814 854
  * Message-ID
815 855
  *
816 856
  * @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) {
1040 1080
 		$simpleMethods = array(
1041 1081
 			'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath', 'cc', 'bcc',
1042 1082
 			'messageId', 'subject', 'viewRender', 'viewVars', 'attachments',
1043  
-			'transport', 'emailFormat'
  1083
+			'transport', 'emailFormat', 'transferEncoding'
1044 1084
 		);
1045 1085
 		foreach ($simpleMethods as $method) {
1046 1086
 			if (isset($config[$method])) {
@@ -1092,6 +1132,7 @@ public function reset() {
1092 1132
 		$this->_emailFormat = 'text';
1093 1133
 		$this->_transportName = 'Mail';
1094 1134
 		$this->_transportClass = null;
  1135
+		$this->_transferEncoding = null;
1095 1136
 		$this->_attachments = array();
1096 1137
 		$this->_config = array();
1097 1138
 		return $this;
@@ -1240,9 +1281,14 @@ protected function _createBoundary() {
1240 1281
 /**
1241 1282
  * Attach non-embedded files by adding file contents inside boundaries.
1242 1283
  *
  1284
+ * @param string $boundary Boundary to use. If null, will default to $this->_boundary 
1243 1285
  * @return array An array of lines to add to the message
1244 1286
  */
1245  
-	protected function _attachFiles() {
  1287
+	protected function _attachFiles($boundary = null) {
  1288
+		if($boundary === null) {
  1289
+			$boundary = $this->_boundary;
  1290
+		}
  1291
+
1246 1292
 		$msg = array();
1247 1293
 		foreach ($this->_attachments as $filename => $fileInfo) {
1248 1294
 			if (!empty($fileInfo['contentId'])) {
@@ -1250,7 +1296,7 @@ protected function _attachFiles() {
1250 1296
 			}
1251 1297
 			$data = $this->_readFile($fileInfo['file']);
1252 1298
 
1253  
-			$msg[] = '--' . $this->_boundary;
  1299
+			$msg[] = '--' . $boundary;
1254 1300
 			$msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
1255 1301
 			$msg[] = 'Content-Transfer-Encoding: base64';
1256 1302
 			$msg[] = 'Content-Disposition: attachment; filename="' . $filename . '"';
@@ -1278,9 +1324,14 @@ protected function _readFile($file) {
1278 1324
 /**
1279 1325
  * Attach inline/embedded files to the message.
1280 1326
  *
  1327
+ * @param string $boundary Boundary to use. If null, will default to $this->_boundary 
1281 1328
  * @return array An array of lines to add to the message
1282 1329
  */
1283  
-	protected function _attachInlineFiles() {
  1330
+	protected function _attachInlineFiles($boundary = null) {
  1331
+		if($boundary === null) {
  1332
+			$boundary = $this->_boundary;
  1333
+		}
  1334
+
1284 1335
 		$msg = array();
1285 1336
 		foreach ($this->_attachments as $filename => $fileInfo) {
1286 1337
 			if (empty($fileInfo['contentId'])) {
@@ -1288,7 +1339,7 @@ protected function _attachInlineFiles() {
1288 1339
 			}
1289 1340
 			$data = $this->_readFile($fileInfo['file']);
1290 1341
 
1291  
-			$msg[] = '--' . $this->_boundary;
  1342
+			$msg[] = '--' . $boundary;
1292 1343
 			$msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
1293 1344
 			$msg[] = 'Content-Transfer-Encoding: base64';
1294 1345
 			$msg[] = 'Content-ID: <' . $fileInfo['contentId'] . '>';
@@ -1310,6 +1361,27 @@ protected function _render($content) {
1310 1361
 		$content = implode("\n", $content);
1311 1362
 		$rendered = $this->_renderTemplates($content);
1312 1363
 
  1364
+		switch($this->_transferEncoding) {
  1365
+			case 'quoted-printable':
  1366
+				foreach($rendered as &$part) {
  1367
+					if(function_exists('quoted_printable_encode')) {
  1368
+						$part = quoted_printable_encode($part);
  1369
+					} elseif(function_exists('imap_8bit')) {
  1370
+						$part = imap_8bit($part);
  1371
+					} else {
  1372
+						throw new SocketException(__d('cake_dev', 'Encoding not available. Need php-5.3 (quoted_printable_encode) or imap extension (imap_8bit).'));
  1373
+					}
  1374
+				}
  1375
+				break;
  1376
+			case 'base64':
  1377
+				foreach($rendered as &$part) {
  1378
+					$part = chunk_split(base64_encode($part));
  1379
+				}
  1380
+				break;
  1381
+			default:
  1382
+				break;
  1383
+		}
  1384
+
1313 1385
 		$msg = array();
1314 1386
 
1315 1387
 		$contentIds = array_filter((array)Set::classicExtract($this->_attachments, '{s}.contentId'));
@@ -1365,7 +1437,7 @@ protected function _render($content) {
1365 1437
 		}
1366 1438
 
1367 1439
 		if ($hasInlineAttachments) {
1368  
-			$attachments = $this->_attachInlineFiles();
  1440
+			$attachments = $this->_attachInlineFiles($relBoundary);
1369 1441
 			$msg = array_merge($msg, $attachments);
1370 1442
 			$msg[] = '';
1371 1443
 			$msg[] = '--' . $relBoundary . '--';
@@ -1373,7 +1445,7 @@ protected function _render($content) {
1373 1445
 		}
1374 1446
 
1375 1447
 		if ($hasAttachments) {
1376  
-			$attachments = $this->_attachFiles();
  1448
+			$attachments = $this->_attachFiles($boundary);
1377 1449
 			$msg = array_merge($msg, $attachments);
1378 1450
 		}
1379 1451
 		if ($hasAttachments || $hasMultipleTypes) {
@@ -1446,11 +1518,15 @@ protected function _renderTemplates($content) {
1446 1518
 	}
1447 1519
 
1448 1520
 /**
1449  
- * Return the Content-Transfer Encoding value based on the set charset
  1521
+ * Return the Content-Transfer Encoding value based on the set charset and transfer encoding
1450 1522
  *
  1523
+ * @param boolean $content If false, return the header encoding, else text content encoding.
1451 1524
  * @return void
1452 1525
  */
1453  
-	protected function _getContentTransferEncoding() {
  1526
+	protected function _getContentTransferEncoding($content = true) {
  1527
+		if($content && $this->_transferEncoding) {
  1528
+			return $this->_transferEncoding;
  1529
+		}
1454 1530
 		$charset = strtoupper($this->charset);
1455 1531
 		if (in_array($charset, $this->_charset8bit)) {
1456 1532
 			return '8bit';
162  lib/Cake/Test/Case/Network/Email/CakeEmailTest.php
@@ -37,7 +37,7 @@ class TestCakeEmail extends CakeEmail {
37 37
 	public function formatAddress($address) {
38 38
 		return parent::_formatAddress($address);
39 39
 	}
40  
-
  40
+	
41 41
 /**
42 42
  * Wrap to protected method
43 43
  *
@@ -45,7 +45,15 @@ public function formatAddress($address) {
45 45
 	public function wrap($text) {
46 46
 		return parent::_wrap($text);
47 47
 	}
48  
-
  48
+	
  49
+/**
  50
+ * Wrap to protected method
  51
+ *
  52
+ */
  53
+	public function getContentTransferEncoding($content = true) {
  54
+		return parent::_getContentTransferEncoding($content);
  55
+	}
  56
+	
49 57
 /**
50 58
  * Get the boundary attribute
51 59
  *
@@ -865,7 +873,7 @@ public function testSendWithInlineAttachments() {
865 873
 			"\r\n" .
866 874
 			"--alt-{$boundary}--\r\n" .
867 875
 			"\r\n" .
868  
-			"--$boundary\r\n" .
  876
+			"--rel-$boundary\r\n" .
869 877
 			"Content-Type: application/octet-stream\r\n" .
870 878
 			"Content-Transfer-Encoding: base64\r\n" .
871 879
 			"Content-ID: <abc123>\r\n" .
@@ -1188,6 +1196,7 @@ public function testMessage() {
1188 1196
 
1189 1197
 		// UTF-8 is 8bit
1190 1198
 		$this->assertTrue($this->checkContentTransferEncoding($message, '8bit'));
  1199
+		$this->assertTrue($this->checkAlternativesCharset($message, 'UTF-8'));
1191 1200
 
1192 1201
 		$this->CakeEmail->charset = 'ISO-2022-JP';
1193 1202
 		$this->CakeEmail->send();
@@ -1197,9 +1206,9 @@ public function testMessage() {
1197 1206
 
1198 1207
 		// ISO-2022-JP is 7bit
1199 1208
 		$this->assertTrue($this->checkContentTransferEncoding($message, '7bit'));
  1209
+		$this->assertTrue($this->checkAlternativesCharset($message, 'ISO-2022-JP'));
1200 1210
 	}
1201 1211
 
1202  
-
1203 1212
 /**
1204 1213
  * testReset method
1205 1214
  *
@@ -1443,7 +1452,7 @@ public function testBodyEncoding() {
1443 1452
 		$this->assertContains(mb_convert_encoding('ってテーブルを作ってやってたらう','ISO-2022-JP'), $result['message']);
1444 1453
 	}
1445 1454
 
1446  
-	private function checkContentTransferEncoding($message, $charset) {
  1455
+	private function checkContentTransferEncoding($message, $encoding) {
1447 1456
 		$boundary = '--alt-' . $this->CakeEmail->getBoundary();
1448 1457
 		$result['text'] = false;
1449 1458
 		$result['html'] = false;
@@ -1458,7 +1467,7 @@ private function checkContentTransferEncoding($message, $charset) {
1458 1467
 					if (preg_match('/^Content-Type: text\/html/', $message[$i])) {
1459 1468
 						$type = 'html';
1460 1469
 					}
1461  
-					if ($message[$i] === 'Content-Transfer-Encoding: ' . $charset) {
  1470
+					if ($message[$i] === 'Content-Transfer-Encoding: ' . $encoding) {
1462 1471
 						$flag = true;
1463 1472
 					}
1464 1473
 					++$i;
@@ -1469,6 +1478,29 @@ private function checkContentTransferEncoding($message, $charset) {
1469 1478
 		return $result['text'] && $result['html'];
1470 1479
 	}
1471 1480
 
  1481
+	private function checkAlternativesCharset($message, $charset) {
  1482
+		$boundary = '--alt-' . $this->CakeEmail->getBoundary();
  1483
+		$result['text'] = false;
  1484
+		$result['html'] = false;
  1485
+		for ($i = 0; $i < count($message); ++$i) {
  1486
+			if ($message[$i] == $boundary) {
  1487
+				$flag = false;
  1488
+				$type = '';
  1489
+				while (!preg_match('/^$/', $message[$i])) {
  1490
+					if (preg_match('/^Content-Type: text\/plain; charset=' . $charset . '/', $message[$i])) {
  1491
+						$result['text'] = true;;
  1492
+					}
  1493
+					if (preg_match('/^Content-Type: text\/html; charset=' . $charset . '/', $message[$i])) {
  1494
+						$result['html'] = true;;
  1495
+					}
  1496
+					++$i;
  1497
+				}
  1498
+				$result[$type] = $flag;
  1499
+			}
  1500
+		}
  1501
+		return $result['text'] && $result['html'];
  1502
+	}
  1503
+
1472 1504
 /**
1473 1505
  * Test CakeEmail::_encode function
1474 1506
  *
@@ -1488,4 +1520,122 @@ public function testEncode() {
1488 1520
 					. " =?ISO-2022-JP?B?GyRCJCYkSiRrJHMkQCRtJCYhKRsoQg==?=";
1489 1521
 		$this->assertSame($expected, $result);
1490 1522
 	}
  1523
+
  1524
+/**
  1525
+ * Test CakeEmail::_getContentTransferEncoding
  1526
+ * 
  1527
+ */
  1528
+	public function testGetContentTransferEncoding() {
  1529
+		$this->CakeEmail->charset = 'utf-8';
  1530
+		$result = $this->CakeEmail->getContentTransferEncoding();
  1531
+		$this->assertSame('8bit', $result);
  1532
+
  1533
+		$result = $this->CakeEmail->getContentTransferEncoding(false);
  1534
+		$this->assertSame('8bit', $result);
  1535
+
  1536
+		$this->CakeEmail->transferEncoding('base64');
  1537
+		$result = $this->CakeEmail->getContentTransferEncoding();
  1538
+		$this->assertSame('base64', $result);
  1539
+
  1540
+		$result = $this->CakeEmail->getContentTransferEncoding(false);
  1541
+		$this->assertSame('8bit', $result);
  1542
+	}
  1543
+
  1544
+/**
  1545
+ * Test CakeEmail::transferEncoding
  1546
+ * 
  1547
+ */
  1548
+	public function testTransferEncoding() {
  1549
+		$this->CakeEmail->transferEncoding('quoted-printable');
  1550
+		$result = $this->CakeEmail->_transferEncoding;
  1551
+		$this->assertSame('quoted-printable', $result);
  1552
+
  1553
+		$result = $this->CakeEmail->transferEncoding('base64');
  1554
+		$this->assertInstanceOf('CakeEmail', $result);
  1555
+		$result = $this->CakeEmail->_transferEncoding;
  1556
+		$this->assertSame('base64', $result);
  1557
+
  1558
+		$this->setExpectedException('SocketException');
  1559
+		$this->CakeEmail->transferEncoding('xcode');
  1560
+	}
  1561
+
  1562
+/**
  1563
+ * testMessage method
  1564
+ *
  1565
+ * @return void
  1566
+ */
  1567
+	public function testTransferEncodedMessageBase64() {
  1568
+		$this->CakeEmail->reset();
  1569
+		$this->CakeEmail->transport('debug');
  1570
+		$this->CakeEmail->from('cake@cakephp.org');
  1571
+		$this->CakeEmail->to(array('you@cakephp.org' => 'You'));
  1572
+		$this->CakeEmail->subject('My title');
  1573
+		$this->CakeEmail->config(array('empty'));
  1574
+		$this->CakeEmail->template('default', 'default');
  1575
+		$this->CakeEmail->emailFormat('both');
  1576
+
  1577
+		$this->CakeEmail->transferEncoding('base64');
  1578
+		$result = $this->CakeEmail->send();
  1579
+
  1580
+		$expected = "PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBIVE1MIDQuMDEvL0VOIj4KCjxodG1s\r\n";
  1581
+		$expected .= "Pgo8aGVhZD4KCTx0aXRsZT5FbWFpbHMvaHRtbDwvdGl0bGU+CjwvaGVhZD4KCjxib2R5PgoJPHA+\r\n";
  1582
+		$expected .= "IDwvcD48cD4gPC9wPgoJPHA+VGhpcyBlbWFpbCB3YXMgc2VudCB1c2luZyB0aGUgPGEgaHJlZj0i\r\n";
  1583
+		$expected .= "aHR0cDovL2Nha2VwaHAub3JnIj5DYWtlUEhQIEZyYW1ld29yazwvYT48L3A+CjwvYm9keT4KPC9o\r\n";
  1584
+		$expected .= "dG1sPg==\r\n";
  1585
+		$this->assertSame($expected, $this->CakeEmail->message(CakeEmail::MESSAGE_HTML));
  1586
+
  1587
+		$expected = "CgoKVGhpcyBlbWFpbCB3YXMgc2VudCB1c2luZyB0aGUgQ2FrZVBIUCBGcmFtZXdvcmssIGh0dHA6\r\n";
  1588
+		$expected .= "Ly9jYWtlcGhwLm9yZy4=\r\n";
  1589
+		$this->assertSame($expected, $this->CakeEmail->message(CakeEmail::MESSAGE_TEXT));
  1590
+
  1591
+		$message = $this->CakeEmail->message();
  1592
+
  1593
+		// UTF-8 is 8bit
  1594
+		$this->assertTrue($this->checkContentTransferEncoding($message, 'base64'));
  1595
+		$this->assertContains('Content-Type: text/plain; charset=UTF-8', $message);
  1596
+		$this->assertContains('Content-Type: text/html; charset=UTF-8', $message);
  1597
+	}
  1598
+
  1599
+/**
  1600
+ * testMessage method
  1601
+ *
  1602
+ * @return void
  1603
+ */
  1604
+	public function testTransferEncodedMessageQuotedPrintable() {
  1605
+		if(!function_exists('quoted_printable') && !function_exists('imap_8bit')) {
  1606
+			$this->markTestSkipped('quoted_printable encoding not available');
  1607
+			return;
  1608
+		}
  1609
+
  1610
+		$this->CakeEmail->reset();
  1611
+		$this->CakeEmail->transport('debug');
  1612
+		$this->CakeEmail->from('cake@cakephp.org');
  1613
+		$this->CakeEmail->to(array('you@cakephp.org' => 'You'));
  1614
+		$this->CakeEmail->subject('My title');
  1615
+		$this->CakeEmail->config(array('empty'));
  1616
+		$this->CakeEmail->template('default', 'default');
  1617
+		$this->CakeEmail->emailFormat('both');
  1618
+	
  1619
+		// ISO-8859-1 is 7bit
  1620
+		$this->CakeEmail->charset = 'ISO-8859-1';
  1621
+		$this->CakeEmail->transferEncoding('quoted-printable');
  1622
+		$result = $this->CakeEmail->send();
  1623
+	
  1624
+		$expected = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\">=0A=0A<html>=0A<head>=0A=\r\n";
  1625
+		$expected .= "=09<title>Emails/html</title>=0A</head>=0A=0A<body>=0A=09<p> </p><p> </p>=\r\n";
  1626
+		$expected .= "=0A=09<p>This email was sent using the <a href=3D\"http://cakephp.org\">CakeP=\r\n";
  1627
+		$expected .= "HP Framework</a></p>=0A</body>=0A</html>";
  1628
+		$this->assertSame($expected, $this->CakeEmail->message(CakeEmail::MESSAGE_HTML));
  1629
+	
  1630
+		$expected = "=0A=0A=0AThis email was sent using the CakePHP Framework, http://cakephp.or=\r\n";
  1631
+		$expected .= "g.";
  1632
+		$this->assertSame($expected, $this->CakeEmail->message(CakeEmail::MESSAGE_TEXT));
  1633
+	
  1634
+		$message = $this->CakeEmail->message();
  1635
+	
  1636
+		$this->assertTrue($this->checkContentTransferEncoding($message, 'quoted-printable'));
  1637
+		$this->assertContains('Content-Type: text/plain; charset=ISO-8859-1', $message);
  1638
+		$this->assertContains('Content-Type: text/html; charset=ISO-8859-1', $message);
  1639
+	}
  1640
+
1491 1641
 }
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.