PageRenderTime 53ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Cake/Network/Email/CakeEmail.php

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