1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
19:
20: App::uses('Validation', 'Utility');
21: App::uses('Multibyte', 'I18n');
22: App::uses('AbstractTransport', 'Network/Email');
23: App::uses('String', 'Utility');
24: App::uses('View', 'View');
25: App::import('I18n', 'Multibyte');
26:
27: 28: 29: 30: 31: 32: 33: 34:
35: class CakeEmail {
36: 37: 38: 39: 40:
41: const EMAIL_CLIENT = 'CakePHP Email';
42:
43: 44: 45: 46: 47:
48: const LINE_LENGTH_SHOULD = 78;
49:
50: 51: 52: 53: 54:
55: const LINE_LENGTH_MUST = 998;
56:
57: 58: 59: 60: 61:
62: const MESSAGE_HTML = 'html';
63:
64: 65: 66: 67: 68:
69: const MESSAGE_TEXT = 'text';
70:
71: 72: 73: 74: 75:
76: protected $_to = array();
77:
78: 79: 80: 81: 82:
83: protected $_from = array();
84:
85: 86: 87: 88: 89:
90: protected $_sender = array();
91:
92: 93: 94: 95: 96:
97: protected $_replyTo = array();
98:
99: 100: 101: 102: 103:
104: protected $_readReceipt = array();
105:
106: 107: 108: 109: 110: 111: 112: 113:
114: protected $_returnPath = array();
115:
116: 117: 118: 119: 120: 121: 122: 123:
124: protected $_cc = array();
125:
126: 127: 128: 129: 130: 131: 132: 133:
134: protected $_bcc = array();
135:
136: 137: 138: 139: 140:
141: protected $_messageId = true;
142:
143: 144: 145: 146: 147:
148: protected $_subject = '';
149:
150: 151: 152: 153: 154: 155:
156: protected $_headers = array();
157:
158: 159: 160: 161: 162:
163: protected $_layout = 'default';
164:
165: 166: 167: 168: 169:
170: protected $_template = '';
171:
172: 173: 174: 175: 176:
177: protected $_viewRender = 'View';
178:
179: 180: 181: 182: 183:
184: protected $_viewVars = array();
185:
186: 187: 188: 189: 190:
191: protected $_helpers = array('Html');
192:
193: 194: 195: 196: 197:
198: protected $_textMessage = '';
199:
200: 201: 202: 203: 204:
205: protected $_htmlMessage = '';
206:
207: 208: 209: 210: 211:
212: protected $_message = array();
213:
214: 215: 216: 217: 218:
219: protected $_emailFormatAvailable = array('text', 'html', 'both');
220:
221: 222: 223: 224: 225:
226: protected $_emailFormat = 'text';
227:
228: 229: 230: 231: 232:
233: protected $_transportName = 'Mail';
234:
235: 236: 237: 238: 239:
240: protected $_transportClass = null;
241:
242: 243: 244: 245: 246: 247:
248: public $charset = 'utf-8';
249:
250: 251: 252: 253: 254:
255: public $headerCharset = null;
256:
257: 258: 259: 260:
261: public $_appCharset = null;
262:
263: 264: 265: 266: 267: 268: 269:
270: protected $_attachments = array();
271:
272: 273: 274: 275: 276:
277: protected $_boundary = null;
278:
279: 280: 281: 282: 283:
284: protected $_config = array();
285:
286: 287: 288: 289: 290:
291: protected $_charset8bit = array('UTF-8', 'SHIFT_JIS');
292:
293: 294: 295: 296: 297:
298: public function __construct($config = null) {
299: $this->_appCharset = Configure::read('App.encoding');
300: if ($this->_appCharset !== null) {
301: $this->charset = $this->_appCharset;
302: }
303: if ($config) {
304: $this->config($config);
305: }
306: if (empty($this->headerCharset)) {
307: $this->headerCharset = $this->charset;
308: }
309: }
310:
311: 312: 313: 314: 315: 316: 317: 318:
319: public function from($email = null, $name = null) {
320: if ($email === null) {
321: return $this->_from;
322: }
323: return $this->_setEmailSingle('_from', $email, $name, __d('cake_dev', 'From requires only 1 email address.'));
324: }
325:
326: 327: 328: 329: 330: 331: 332: 333:
334: public function sender($email = null, $name = null) {
335: if ($email === null) {
336: return $this->_sender;
337: }
338: return $this->_setEmailSingle('_sender', $email, $name, __d('cake_dev', 'Sender requires only 1 email address.'));
339: }
340:
341: 342: 343: 344: 345: 346: 347: 348:
349: public function replyTo($email = null, $name = null) {
350: if ($email === null) {
351: return $this->_replyTo;
352: }
353: return $this->_setEmailSingle('_replyTo', $email, $name, __d('cake_dev', 'Reply-To requires only 1 email address.'));
354: }
355:
356: 357: 358: 359: 360: 361: 362: 363:
364: public function readReceipt($email = null, $name = null) {
365: if ($email === null) {
366: return $this->_readReceipt;
367: }
368: return $this->_setEmailSingle('_readReceipt', $email, $name, __d('cake_dev', 'Disposition-Notification-To requires only 1 email address.'));
369: }
370:
371: 372: 373: 374: 375: 376: 377: 378:
379: public function returnPath($email = null, $name = null) {
380: if ($email === null) {
381: return $this->_returnPath;
382: }
383: return $this->_setEmailSingle('_returnPath', $email, $name, __d('cake_dev', 'Return-Path requires only 1 email address.'));
384: }
385:
386: 387: 388: 389: 390: 391: 392:
393: public function to($email = null, $name = null) {
394: if ($email === null) {
395: return $this->_to;
396: }
397: return $this->_setEmail('_to', $email, $name);
398: }
399:
400: 401: 402: 403: 404: 405: 406:
407: public function addTo($email, $name = null) {
408: return $this->_addEmail('_to', $email, $name);
409: }
410:
411: 412: 413: 414: 415: 416: 417:
418: public function cc($email = null, $name = null) {
419: if ($email === null) {
420: return $this->_cc;
421: }
422: return $this->_setEmail('_cc', $email, $name);
423: }
424:
425: 426: 427: 428: 429: 430: 431:
432: public function addCc($email, $name = null) {
433: return $this->_addEmail('_cc', $email, $name);
434: }
435:
436: 437: 438: 439: 440: 441: 442:
443: public function bcc($email = null, $name = null) {
444: if ($email === null) {
445: return $this->_bcc;
446: }
447: return $this->_setEmail('_bcc', $email, $name);
448: }
449:
450: 451: 452: 453: 454: 455: 456:
457: public function addBcc($email, $name = null) {
458: return $this->_addEmail('_bcc', $email, $name);
459: }
460:
461: 462: 463: 464: 465: 466: 467: 468: 469:
470: protected function _setEmail($varName, $email, $name) {
471: if (!is_array($email)) {
472: if (!Validation::email($email)) {
473: throw new SocketException(__d('cake_dev', 'Invalid email: "%s"', $email));
474: }
475: if ($name === null) {
476: $name = $email;
477: }
478: $this->{$varName} = array($email => $name);
479: return $this;
480: }
481: $list = array();
482: foreach ($email as $key => $value) {
483: if (is_int($key)) {
484: $key = $value;
485: }
486: if (!Validation::email($key)) {
487: throw new SocketException(__d('cake_dev', 'Invalid email: "%s"', $key));
488: }
489: $list[$key] = $value;
490: }
491: $this->{$varName} = $list;
492: return $this;
493: }
494:
495: 496: 497: 498: 499: 500: 501: 502: 503: 504:
505: protected function _setEmailSingle($varName, $email, $name, $throwMessage) {
506: $current = $this->{$varName};
507: $this->_setEmail($varName, $email, $name);
508: if (count($this->{$varName}) !== 1) {
509: $this->{$varName} = $current;
510: throw new SocketException($throwMessage);
511: }
512: return $this;
513: }
514:
515: 516: 517: 518: 519: 520: 521: 522: 523:
524: protected function _addEmail($varName, $email, $name) {
525: if (!is_array($email)) {
526: if (!Validation::email($email)) {
527: throw new SocketException(__d('cake_dev', 'Invalid email: "%s"', $email));
528: }
529: if ($name === null) {
530: $name = $email;
531: }
532: $this->{$varName}[$email] = $name;
533: return $this;
534: }
535: $list = array();
536: foreach ($email as $key => $value) {
537: if (is_int($key)) {
538: $key = $value;
539: }
540: if (!Validation::email($key)) {
541: throw new SocketException(__d('cake_dev', 'Invalid email: "%s"', $key));
542: }
543: $list[$key] = $value;
544: }
545: $this->{$varName} = array_merge($this->{$varName}, $list);
546: return $this;
547: }
548:
549: 550: 551: 552: 553: 554:
555: public function subject($subject = null) {
556: if ($subject === null) {
557: return $this->_subject;
558: }
559: $this->_subject = $this->_encode((string)$subject);
560: return $this;
561: }
562:
563: 564: 565: 566: 567: 568: 569:
570: public function setHeaders($headers) {
571: if (!is_array($headers)) {
572: throw new SocketException(__d('cake_dev', '$headers should be an array.'));
573: }
574: $this->_headers = $headers;
575: return $this;
576: }
577:
578: 579: 580: 581: 582: 583: 584:
585: public function addHeaders($headers) {
586: if (!is_array($headers)) {
587: throw new SocketException(__d('cake_dev', '$headers should be an array.'));
588: }
589: $this->_headers = array_merge($this->_headers, $headers);
590: return $this;
591: }
592:
593: 594: 595: 596: 597: 598: 599: 600: 601: 602: 603: 604: 605: 606: 607: 608: 609:
610: public function getHeaders($include = array()) {
611: if ($include == array_values($include)) {
612: $include = array_fill_keys($include, true);
613: }
614: $defaults = array_fill_keys(array('from', 'sender', 'replyTo', 'readReceipt', 'returnPath', 'to', 'cc', 'bcc', 'subject'), false);
615: $include += $defaults;
616:
617: $headers = array();
618: $relation = array(
619: 'from' => 'From',
620: 'replyTo' => 'Reply-To',
621: 'readReceipt' => 'Disposition-Notification-To',
622: 'returnPath' => 'Return-Path'
623: );
624: foreach ($relation as $var => $header) {
625: if ($include[$var]) {
626: $var = '_' . $var;
627: $headers[$header] = current($this->_formatAddress($this->{$var}));
628: }
629: }
630: if ($include['sender']) {
631: if (key($this->_sender) === key($this->_from)) {
632: $headers['Sender'] = '';
633: } else {
634: $headers['Sender'] = current($this->_formatAddress($this->_sender));
635: }
636: }
637:
638: foreach (array('to', 'cc', 'bcc') as $var) {
639: if ($include[$var]) {
640: $classVar = '_' . $var;
641: $headers[ucfirst($var)] = implode(', ', $this->_formatAddress($this->{$classVar}));
642: }
643: }
644:
645: $headers += $this->_headers;
646: if (!isset($headers['X-Mailer'])) {
647: $headers['X-Mailer'] = self::EMAIL_CLIENT;
648: }
649: if (!isset($headers['Date'])) {
650: $headers['Date'] = date(DATE_RFC2822);
651: }
652: if ($this->_messageId !== false) {
653: if ($this->_messageId === true) {
654: $headers['Message-ID'] = '<' . str_replace('-', '', String::UUID()) . '@' . env('HTTP_HOST') . '>';
655: } else {
656: $headers['Message-ID'] = $this->_messageId;
657: }
658: }
659:
660: if ($include['subject']) {
661: $headers['Subject'] = $this->_subject;
662: }
663:
664: $headers['MIME-Version'] = '1.0';
665: if (!empty($this->_attachments) || $this->_emailFormat === 'both') {
666: $headers['Content-Type'] = 'multipart/mixed; boundary="' . $this->_boundary . '"';
667: } elseif ($this->_emailFormat === 'text') {
668: $headers['Content-Type'] = 'text/plain; charset=' . $this->charset;
669: } elseif ($this->_emailFormat === 'html') {
670: $headers['Content-Type'] = 'text/html; charset=' . $this->charset;
671: }
672: $headers['Content-Transfer-Encoding'] = $this->_getContentTransferEncoding();
673:
674: return $headers;
675: }
676:
677: 678: 679: 680: 681: 682:
683: protected function _formatAddress($address) {
684: $return = array();
685: foreach ($address as $email => $alias) {
686: if ($email === $alias) {
687: $return[] = $email;
688: } else {
689: $return[] = sprintf('%s <%s>', $this->_encode($alias), $email);
690: }
691: }
692: return $return;
693: }
694:
695: 696: 697: 698: 699: 700: 701:
702: public function template($template = false, $layout = false) {
703: if ($template === false) {
704: return array(
705: 'template' => $this->_template,
706: 'layout' => $this->_layout
707: );
708: }
709: $this->_template = $template;
710: if ($layout !== false) {
711: $this->_layout = $layout;
712: }
713: return $this;
714: }
715:
716: 717: 718: 719: 720: 721:
722: public function viewRender($viewClass = null) {
723: if ($viewClass === null) {
724: return $this->_viewRender;
725: }
726: $this->_viewRender = $viewClass;
727: return $this;
728: }
729:
730: 731: 732: 733: 734: 735:
736: public function viewVars($viewVars = null) {
737: if ($viewVars === null) {
738: return $this->_viewVars;
739: }
740: $this->_viewVars = array_merge($this->_viewVars, (array)$viewVars);
741: return $this;
742: }
743:
744: 745: 746: 747: 748: 749:
750: public function helpers($helpers = null) {
751: if ($helpers === null) {
752: return $this->_helpers;
753: }
754: $this->_helpers = (array)$helpers;
755: return $this;
756: }
757:
758: 759: 760: 761: 762: 763: 764:
765: public function emailFormat($format = null) {
766: if ($format === null) {
767: return $this->_emailFormat;
768: }
769: if (!in_array($format, $this->_emailFormatAvailable)) {
770: throw new SocketException(__d('cake_dev', 'Format not available.'));
771: }
772: $this->_emailFormat = $format;
773: return $this;
774: }
775:
776: 777: 778: 779: 780: 781:
782: public function transport($name = null) {
783: if ($name === null) {
784: return $this->_transportName;
785: }
786: $this->_transportName = (string)$name;
787: $this->_transportClass = null;
788: return $this;
789: }
790:
791: 792: 793: 794: 795: 796:
797: public function transportClass() {
798: if ($this->_transportClass) {
799: return $this->_transportClass;
800: }
801: list($plugin, $transportClassname) = pluginSplit($this->_transportName, true);
802: $transportClassname .= 'Transport';
803: App::uses($transportClassname, $plugin . 'Network/Email');
804: if (!class_exists($transportClassname)) {
805: throw new SocketException(__d('cake_dev', 'Class "%s" not found.', $transportClassname));
806: } elseif (!method_exists($transportClassname, 'send')) {
807: throw new SocketException(__d('cake_dev', 'The "%s" do not have send method.', $transportClassname));
808: }
809:
810: return $this->_transportClass = new $transportClassname();
811: }
812:
813: 814: 815: 816: 817: 818: 819:
820: public function messageId($message = null) {
821: if ($message === null) {
822: return $this->_messageId;
823: }
824: if (is_bool($message)) {
825: $this->_messageId = $message;
826: } else {
827: if (!preg_match('/^\<.+@.+\>$/', $message)) {
828: throw new SocketException(__d('cake_dev', 'Invalid format to Message-ID. The text should be something like "<[email protected]>"'));
829: }
830: $this->_messageId = $message;
831: }
832: return $this;
833: }
834:
835: 836: 837: 838: 839: 840: 841: 842: 843: 844: 845: 846: 847: 848: 849: 850: 851: 852: 853: 854: 855: 856: 857: 858: 859: 860: 861: 862: 863: 864: 865: 866: 867: 868:
869: public function attachments($attachments = null) {
870: if ($attachments === null) {
871: return $this->_attachments;
872: }
873: $attach = array();
874: foreach ((array)$attachments as $name => $fileInfo) {
875: if (!is_array($fileInfo)) {
876: $fileInfo = array('file' => $fileInfo);
877: }
878: if (!isset($fileInfo['file'])) {
879: throw new SocketException(__d('cake_dev', 'File not specified.'));
880: }
881: $fileInfo['file'] = realpath($fileInfo['file']);
882: if ($fileInfo['file'] === false || !file_exists($fileInfo['file'])) {
883: throw new SocketException(__d('cake_dev', 'File not found: "%s"', $fileInfo['file']));
884: }
885: if (is_int($name)) {
886: $name = basename($fileInfo['file']);
887: }
888: if (!isset($fileInfo['mimetype'])) {
889: $fileInfo['mimetype'] = 'application/octet-stream';
890: }
891: $attach[$name] = $fileInfo;
892: }
893: $this->_attachments = $attach;
894: return $this;
895: }
896:
897: 898: 899: 900: 901: 902: 903:
904: public function addAttachments($attachments) {
905: $current = $this->_attachments;
906: $this->attachments($attachments);
907: $this->_attachments = array_merge($current, $this->_attachments);
908: return $this;
909: }
910:
911: 912: 913: 914: 915: 916:
917: public function message($type = null) {
918: switch ($type) {
919: case self::MESSAGE_HTML:
920: return $this->_htmlMessage;
921: case self::MESSAGE_TEXT:
922: return $this->_textMessage;
923: }
924: return $this->_message;
925: }
926:
927: 928: 929: 930: 931: 932:
933: public function config($config = null) {
934: if ($config === null) {
935: return $this->_config;
936: }
937: if (!is_array($config)) {
938: $config = (string)$config;
939: }
940:
941: $this->_applyConfig($config);
942: return $this;
943: }
944:
945: 946: 947: 948: 949: 950:
951: public function send($content = null) {
952: if (empty($this->_from)) {
953: throw new SocketException(__d('cake_dev', 'From is not specified.'));
954: }
955: if (empty($this->_to) && empty($this->_cc) && empty($this->_bcc)) {
956: throw new SocketException(__d('cake_dev', 'You need specify one destination on to, cc or bcc.'));
957: }
958:
959: if (is_array($content)) {
960: $content = implode("\n", $content) . "\n";
961: }
962:
963: $this->_textMessage = $this->_htmlMessage = '';
964: $this->_createBoundary();
965: $this->_message = $this->_render($this->_wrap($content));
966:
967: $contents = $this->transportClass()->send($this);
968: if (!empty($this->_config['log'])) {
969: $level = LOG_DEBUG;
970: if ($this->_config['log'] !== true) {
971: $level = $this->_config['log'];
972: }
973: CakeLog::write($level, PHP_EOL . $contents['headers'] . PHP_EOL . $contents['message']);
974: }
975: return $contents;
976: }
977:
978: 979: 980: 981: 982: 983: 984: 985: 986: 987: 988:
989: public static function deliver($to = null, $subject = null, $message = null, $transportConfig = 'fast', $send = true) {
990: $class = __CLASS__;
991: $instance = new $class($transportConfig);
992: if ($to !== null) {
993: $instance->to($to);
994: }
995: if ($subject !== null) {
996: $instance->subject($subject);
997: }
998: if (is_array($message)) {
999: $instance->viewVars($message);
1000: $message = null;
1001: } elseif ($message === null && array_key_exists('message', $config = $instance->config())) {
1002: $message = $config['message'];
1003: }
1004:
1005: if ($send === true) {
1006: $instance->send($message);
1007: }
1008:
1009: return $instance;
1010: }
1011:
1012: 1013: 1014: 1015: 1016: 1017: 1018:
1019: protected function _applyConfig($config) {
1020: if (is_string($config)) {
1021: if (!class_exists('EmailConfig') && !config('email')) {
1022: throw new ConfigureException(__d('cake_dev', '%s not found.', APP . 'Config' . DS . 'email.php'));
1023: }
1024: $configs = new EmailConfig();
1025: if (!isset($configs->{$config})) {
1026: throw new ConfigureException(__d('cake_dev', 'Unknown email configuration "%s".', $config));
1027: }
1028: $config = $configs->{$config};
1029: }
1030: $this->_config += $config;
1031: if (!empty($config['charset'])) {
1032: $this->charset = $config['charset'];
1033: }
1034: if (!empty($config['headerCharset'])) {
1035: $this->headerCharset = $config['headerCharset'];
1036: }
1037: if (empty($this->headerCharset)) {
1038: $this->headerCharset = $this->charset;
1039: }
1040: $simpleMethods = array(
1041: 'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath', 'cc', 'bcc',
1042: 'messageId', 'subject', 'viewRender', 'viewVars', 'attachments',
1043: 'transport', 'emailFormat'
1044: );
1045: foreach ($simpleMethods as $method) {
1046: if (isset($config[$method])) {
1047: $this->$method($config[$method]);
1048: unset($config[$method]);
1049: }
1050: }
1051: if (isset($config['headers'])) {
1052: $this->setHeaders($config['headers']);
1053: unset($config['headers']);
1054: }
1055: if (array_key_exists('template', $config)) {
1056: $layout = false;
1057: if (array_key_exists('layout', $config)) {
1058: $layout = $config['layout'];
1059: unset($config['layout']);
1060: }
1061: $this->template($config['template'], $layout);
1062: unset($config['template']);
1063: }
1064: $this->transportClass()->config($config);
1065: }
1066:
1067: 1068: 1069: 1070: 1071:
1072: public function reset() {
1073: $this->_to = array();
1074: $this->_from = array();
1075: $this->_sender = array();
1076: $this->_replyTo = array();
1077: $this->_readReceipt = array();
1078: $this->_returnPath = array();
1079: $this->_cc = array();
1080: $this->_bcc = array();
1081: $this->_messageId = true;
1082: $this->_subject = '';
1083: $this->_headers = array();
1084: $this->_layout = 'default';
1085: $this->_template = '';
1086: $this->_viewRender = 'View';
1087: $this->_viewVars = array();
1088: $this->_helpers = array('Html');
1089: $this->_textMessage = '';
1090: $this->_htmlMessage = '';
1091: $this->_message = '';
1092: $this->_emailFormat = 'text';
1093: $this->_transportName = 'Mail';
1094: $this->_transportClass = null;
1095: $this->_attachments = array();
1096: $this->_config = array();
1097: return $this;
1098: }
1099:
1100: 1101: 1102: 1103: 1104: 1105:
1106: protected function _encode($text) {
1107: $internalEncoding = function_exists('mb_internal_encoding');
1108: if ($internalEncoding) {
1109: $restore = mb_internal_encoding();
1110: mb_internal_encoding($this->_appCharset);
1111: }
1112: if (strpos($text, ',') !== false) {
1113: $text = '"' . $text . '"';
1114: }
1115: $return = mb_encode_mimeheader($text, $this->headerCharset, 'B');
1116: if ($internalEncoding) {
1117: mb_internal_encoding($restore);
1118: }
1119: return $return;
1120: }
1121:
1122: 1123: 1124: 1125: 1126: 1127: 1128: 1129:
1130: protected function _encodeString($text, $charset) {
1131: if ($this->_appCharset === $charset || !function_exists('mb_convert_encoding')) {
1132: return $text;
1133: }
1134: return mb_convert_encoding($text, $charset, $this->_appCharset);
1135: }
1136:
1137: 1138: 1139: 1140: 1141: 1142:
1143: protected function _wrap($message) {
1144: $message = str_replace(array("\r\n", "\r"), "\n", $message);
1145: $lines = explode("\n", $message);
1146: $formatted = array();
1147:
1148: foreach ($lines as $line) {
1149: if (empty($line)) {
1150: $formatted[] = '';
1151: continue;
1152: }
1153: if ($line[0] === '.') {
1154: $line = '.' . $line;
1155: }
1156: if (!preg_match('/\<[a-z]/i', $line)) {
1157: $formatted = array_merge($formatted, explode("\n", wordwrap($line, self::LINE_LENGTH_SHOULD, "\n")));
1158: continue;
1159: }
1160:
1161: $tagOpen = false;
1162: $tmpLine = $tag = '';
1163: $tmpLineLength = 0;
1164: for ($i = 0, $count = strlen($line); $i < $count; $i++) {
1165: $char = $line[$i];
1166: if ($tagOpen) {
1167: $tag .= $char;
1168: if ($char === '>') {
1169: $tagLength = strlen($tag);
1170: if ($tagLength + $tmpLineLength < self::LINE_LENGTH_SHOULD) {
1171: $tmpLine .= $tag;
1172: $tmpLineLength += $tagLength;
1173: } else {
1174: if ($tmpLineLength > 0) {
1175: $formatted[] = trim($tmpLine);
1176: $tmpLine = '';
1177: $tmpLineLength = 0;
1178: }
1179: if ($tagLength > self::LINE_LENGTH_SHOULD) {
1180: $formatted[] = $tag;
1181: } else {
1182: $tmpLine = $tag;
1183: $tmpLineLength = $tagLength;
1184: }
1185: }
1186: $tag = '';
1187: $tagOpen = false;
1188: }
1189: continue;
1190: }
1191: if ($char === '<') {
1192: $tagOpen = true;
1193: $tag = '<';
1194: continue;
1195: }
1196: if ($char === ' ' && $tmpLineLength >= self::LINE_LENGTH_SHOULD) {
1197: $formatted[] = $tmpLine;
1198: $tmpLineLength = 0;
1199: continue;
1200: }
1201: $tmpLine .= $char;
1202: $tmpLineLength++;
1203: if ($tmpLineLength === self::LINE_LENGTH_SHOULD) {
1204: $nextChar = $line[$i + 1];
1205: if ($nextChar === ' ' || $nextChar === '<') {
1206: $formatted[] = trim($tmpLine);
1207: $tmpLine = '';
1208: $tmpLineLength = 0;
1209: if ($nextChar === ' ') {
1210: $i++;
1211: }
1212: } else {
1213: $lastSpace = strrpos($tmpLine, ' ');
1214: if ($lastSpace === false) {
1215: continue;
1216: }
1217: $formatted[] = trim(substr($tmpLine, 0, $lastSpace));
1218: $tmpLine = substr($tmpLine, $lastSpace + 1);
1219:
1220: $tmpLineLength = strlen($tmpLine);
1221: }
1222: }
1223: }
1224: if (!empty($tmpLine)) {
1225: $formatted[] = $tmpLine;
1226: }
1227: }
1228: $formatted[] = '';
1229: return $formatted;
1230: }
1231:
1232: 1233: 1234: 1235: 1236:
1237: protected function _createBoundary() {
1238: if (!empty($this->_attachments) || $this->_emailFormat === 'both') {
1239: $this->_boundary = md5(uniqid(time()));
1240: }
1241: }
1242:
1243: 1244: 1245: 1246: 1247: 1248:
1249: protected function _attachFiles($boundary = null) {
1250: if ($boundary === null) {
1251: $boundary = $this->_boundary;
1252: }
1253:
1254: $msg = array();
1255: foreach ($this->_attachments as $filename => $fileInfo) {
1256: if (!empty($fileInfo['contentId'])) {
1257: continue;
1258: }
1259: $data = $this->_readFile($fileInfo['file']);
1260:
1261: $msg[] = '--' . $boundary;
1262: $msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
1263: $msg[] = 'Content-Transfer-Encoding: base64';
1264: $msg[] = 'Content-Disposition: attachment; filename="' . $filename . '"';
1265: $msg[] = '';
1266: $msg[] = $data;
1267: $msg[] = '';
1268: }
1269: return $msg;
1270: }
1271:
1272: 1273: 1274: 1275: 1276: 1277:
1278: protected function _readFile($file) {
1279: $handle = fopen($file, 'rb');
1280: $data = fread($handle, filesize($file));
1281: $data = chunk_split(base64_encode($data)) ;
1282: fclose($handle);
1283: return $data;
1284: }
1285:
1286: 1287: 1288: 1289: 1290: 1291:
1292: protected function _attachInlineFiles($boundary = null) {
1293: if ($boundary === null) {
1294: $boundary = $this->_boundary;
1295: }
1296:
1297: $msg = array();
1298: foreach ($this->_attachments as $filename => $fileInfo) {
1299: if (empty($fileInfo['contentId'])) {
1300: continue;
1301: }
1302: $data = $this->_readFile($fileInfo['file']);
1303:
1304: $msg[] = '--' . $boundary;
1305: $msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
1306: $msg[] = 'Content-Transfer-Encoding: base64';
1307: $msg[] = 'Content-ID: <' . $fileInfo['contentId'] . '>';
1308: $msg[] = 'Content-Disposition: inline; filename="' . $filename . '"';
1309: $msg[] = '';
1310: $msg[] = $data;
1311: $msg[] = '';
1312: }
1313: return $msg;
1314: }
1315:
1316: 1317: 1318: 1319: 1320: 1321:
1322: protected function _render($content) {
1323: $content = implode("\n", $content);
1324: $rendered = $this->_renderTemplates($content);
1325:
1326: $msg = array();
1327:
1328: $contentIds = array_filter((array)Set::classicExtract($this->_attachments, '{s}.contentId'));
1329: $hasInlineAttachments = count($contentIds) > 0;
1330: $hasAttachments = !empty($this->_attachments);
1331: $hasMultipleTypes = count($rendered) > 1;
1332:
1333: $boundary = $relBoundary = $textBoundary = $this->_boundary;
1334:
1335: if ($hasInlineAttachments) {
1336: $msg[] = '--' . $boundary;
1337: $msg[] = 'Content-Type: multipart/related; boundary="rel-' . $boundary . '"';
1338: $msg[] = '';
1339: $relBoundary = $textBoundary = 'rel-' . $boundary;
1340: }
1341:
1342: if ($hasMultipleTypes) {
1343: $msg[] = '--' . $relBoundary;
1344: $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"';
1345: $msg[] = '';
1346: $textBoundary = 'alt-' . $boundary;
1347: }
1348:
1349: if (isset($rendered['text'])) {
1350: if ($textBoundary !== $boundary || $hasAttachments) {
1351: $msg[] = '--' . $textBoundary;
1352: $msg[] = 'Content-Type: text/plain; charset=' . $this->charset;
1353: $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding();
1354: $msg[] = '';
1355: }
1356: $this->_textMessage = $rendered['text'];
1357: $content = explode("\n", $this->_textMessage);
1358: $msg = array_merge($msg, $content);
1359: $msg[] = '';
1360: }
1361:
1362: if (isset($rendered['html'])) {
1363: if ($textBoundary !== $boundary || $hasAttachments) {
1364: $msg[] = '--' . $textBoundary;
1365: $msg[] = 'Content-Type: text/html; charset=' . $this->charset;
1366: $msg[] = 'Content-Transfer-Encoding: ' . $this->_getContentTransferEncoding();
1367: $msg[] = '';
1368: }
1369: $this->_htmlMessage = $rendered['html'];
1370: $content = explode("\n", $this->_htmlMessage);
1371: $msg = array_merge($msg, $content);
1372: $msg[] = '';
1373: }
1374:
1375: if ($hasMultipleTypes) {
1376: $msg[] = '--' . $textBoundary . '--';
1377: $msg[] = '';
1378: }
1379:
1380: if ($hasInlineAttachments) {
1381: $attachments = $this->_attachInlineFiles($relBoundary);
1382: $msg = array_merge($msg, $attachments);
1383: $msg[] = '';
1384: $msg[] = '--' . $relBoundary . '--';
1385: $msg[] = '';
1386: }
1387:
1388: if ($hasAttachments) {
1389: $attachments = $this->_attachFiles($boundary);
1390: $msg = array_merge($msg, $attachments);
1391: }
1392: if ($hasAttachments || $hasMultipleTypes) {
1393: $msg[] = '';
1394: $msg[] = '--' . $boundary . '--';
1395: $msg[] = '';
1396: }
1397: return $msg;
1398: }
1399:
1400: 1401: 1402: 1403: 1404:
1405: protected function _getTypes() {
1406: $types = array($this->_emailFormat);
1407: if ($this->_emailFormat == 'both') {
1408: $types = array('html', 'text');
1409: }
1410: return $types;
1411: }
1412:
1413: 1414: 1415: 1416: 1417: 1418: 1419: 1420:
1421: protected function _renderTemplates($content) {
1422: $types = $this->_getTypes();
1423: $rendered = array();
1424: if (empty($this->_template)) {
1425: foreach ($types as $type) {
1426: $rendered[$type] = $this->_encodeString($content, $this->charset);
1427: }
1428: return $rendered;
1429: }
1430: $viewClass = $this->_viewRender;
1431: if ($viewClass !== 'View') {
1432: list($plugin, $viewClass) = pluginSplit($viewClass, true);
1433: $viewClass .= 'View';
1434: App::uses($viewClass, $plugin . 'View');
1435: }
1436:
1437: $View = new $viewClass(null);
1438: $View->viewVars = $this->_viewVars;
1439: $View->helpers = $this->_helpers;
1440:
1441: list($templatePlugin, $template) = pluginSplit($this->_template);
1442: list($layoutPlugin, $layout) = pluginSplit($this->_layout);
1443: if ($templatePlugin) {
1444: $View->plugin = $templatePlugin;
1445: } elseif ($layoutPlugin) {
1446: $View->plugin = $layoutPlugin;
1447: }
1448:
1449: foreach ($types as $type) {
1450: $View->set('content', $content);
1451: $View->hasRendered = false;
1452: $View->viewPath = $View->layoutPath = 'Emails' . DS . $type;
1453:
1454: $render = $View->render($template, $layout);
1455: $render = str_replace(array("\r\n", "\r"), "\n", $render);
1456: $rendered[$type] = $this->_encodeString($render, $this->charset);
1457: }
1458: return $rendered;
1459: }
1460:
1461: 1462: 1463: 1464: 1465:
1466: protected function _getContentTransferEncoding() {
1467: $charset = strtoupper($this->charset);
1468: if (in_array($charset, $this->_charset8bit)) {
1469: return '8bit';
1470: }
1471: return '7bit';
1472: }
1473: }
1474: