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