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: