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