CakeEmail.php 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366
  1. <?php
  2. /**
  3. * Cake E-Mail
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2011, 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-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package cake.libs
  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.libs
  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();
  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 object
  210. */
  211. protected $_transportClass = null;
  212. /**
  213. * charset the email is sent in
  214. *
  215. * @var string
  216. */
  217. public $charset = 'utf-8';
  218. /**
  219. * List of files that should be attached to the email.
  220. *
  221. * Only absolute paths
  222. *
  223. * @var array
  224. */
  225. protected $_attachments = array();
  226. /**
  227. * If set, boundary to use for multipart mime messages
  228. *
  229. * @var string
  230. */
  231. protected $_boundary = null;
  232. /**
  233. * Configuration to transport
  234. *
  235. * @var mixed
  236. */
  237. protected $_config = 'default';
  238. /**
  239. * Constructor
  240. *
  241. */
  242. public function __construct() {
  243. $charset = Configure::read('App.encoding');
  244. if ($charset !== null) {
  245. $this->charset = $charset;
  246. }
  247. }
  248. /**
  249. * From
  250. *
  251. * @param mixed $email
  252. * @param string $name
  253. * @return mixed
  254. * @throws SocketException
  255. */
  256. public function from($email = null, $name = null) {
  257. if ($email === null) {
  258. return $this->_from;
  259. }
  260. return $this->_setEmailSingle('_from', $email, $name, __d('cake', 'From requires only 1 email address.'));
  261. }
  262. /**
  263. * Sender
  264. *
  265. * @param mixed $email
  266. * @param string $name
  267. * @return mixed
  268. * @throws SocketException
  269. */
  270. public function sender($email = null, $name = null) {
  271. if ($email === null) {
  272. return $this->_sender;
  273. }
  274. return $this->_setEmailSingle('_sender', $email, $name, __d('cake', 'Sender requires only 1 email address.'));
  275. }
  276. /**
  277. * Reply-To
  278. *
  279. * @param mixed $email
  280. * @param string $name
  281. * @return mixed
  282. * @throws SocketException
  283. */
  284. public function replyTo($email = null, $name = null) {
  285. if ($email === null) {
  286. return $this->_replyTo;
  287. }
  288. return $this->_setEmailSingle('_replyTo', $email, $name, __d('cake', 'Reply-To requires only 1 email address.'));
  289. }
  290. /**
  291. * Read Receipt (Disposition-Notification-To header)
  292. *
  293. * @param mixed $email
  294. * @param string $name
  295. * @return mixed
  296. * @throws SocketException
  297. */
  298. public function readReceipt($email = null, $name = null) {
  299. if ($email === null) {
  300. return $this->_readReceipt;
  301. }
  302. return $this->_setEmailSingle('_readReceipt', $email, $name, __d('cake', 'Disposition-Notification-To requires only 1 email address.'));
  303. }
  304. /**
  305. * Return Path
  306. *
  307. * @param mixed $email
  308. * @param string $name
  309. * @return mixed
  310. * @throws SocketException
  311. */
  312. public function returnPath($email = null, $name = null) {
  313. if ($email === null) {
  314. return $this->_returnPath;
  315. }
  316. return $this->_setEmailSingle('_returnPath', $email, $name, __d('cake', 'Return-Path requires only 1 email address.'));
  317. }
  318. /**
  319. * To
  320. *
  321. * @param mixed $email Null to get, String with email, Array with email as key, name as value or email as value (without name)
  322. * @param string $name
  323. * @return mixed
  324. */
  325. public function to($email = null, $name = null) {
  326. if ($email === null) {
  327. return $this->_to;
  328. }
  329. return $this->_setEmail('_to', $email, $name);
  330. }
  331. /**
  332. * Add To
  333. *
  334. * @param mixed $email String with email, Array with email as key, name as value or email as value (without name)
  335. * @param string $name
  336. * @return object $this
  337. */
  338. public function addTo($email, $name = null) {
  339. return $this->_addEmail('_to', $email, $name);
  340. }
  341. /**
  342. * Cc
  343. *
  344. * @param mixed $email String with email, Array with email as key, name as value or email as value (without name)
  345. * @param string $name
  346. * @return mixed
  347. */
  348. public function cc($email = null, $name = null) {
  349. if ($email === null) {
  350. return $this->_cc;
  351. }
  352. return $this->_setEmail('_cc', $email, $name);
  353. }
  354. /**
  355. * Add Cc
  356. *
  357. * @param mixed $email String with email, Array with email as key, name as value or email as value (without name)
  358. * @param string $name
  359. * @return object $this
  360. */
  361. public function addCc($email, $name = null) {
  362. return $this->_addEmail('_cc', $email, $name);
  363. }
  364. /**
  365. * Bcc
  366. *
  367. * @param mixed $email String with email, Array with email as key, name as value or email as value (without name)
  368. * @param string $name
  369. * @return mixed
  370. */
  371. public function bcc($email = null, $name = null) {
  372. if ($email === null) {
  373. return $this->_bcc;
  374. }
  375. return $this->_setEmail('_bcc', $email, $name);
  376. }
  377. /**
  378. * Add Bcc
  379. *
  380. * @param mixed $email String with email, Array with email as key, name as value or email as value (without name)
  381. * @param string $name
  382. * @return object $this
  383. */
  384. public function addBcc($email, $name = null) {
  385. return $this->_addEmail('_bcc', $email, $name);
  386. }
  387. /**
  388. * Set email
  389. *
  390. * @param string $varName
  391. * @param mixed $email
  392. * @param mixed $name
  393. * @return object $this
  394. * @throws SocketException
  395. */
  396. protected function _setEmail($varName, $email, $name) {
  397. if (!is_array($email)) {
  398. if (!Validation::email($email)) {
  399. throw new SocketException(__d('cake', 'Invalid email: "%s"', $email));
  400. }
  401. if ($name === null) {
  402. $name = $email;
  403. }
  404. $this->{$varName} = array($email => $name);
  405. return $this;
  406. }
  407. $list = array();
  408. foreach ($email as $key => $value) {
  409. if (is_int($key)) {
  410. $key = $value;
  411. }
  412. if (!Validation::email($key)) {
  413. throw new SocketException(__d('cake', 'Invalid email: "%s"', $key));
  414. }
  415. $list[$key] = $value;
  416. }
  417. $this->{$varName} = $list;
  418. return $this;
  419. }
  420. /**
  421. * Set only 1 email
  422. *
  423. * @param string $varName
  424. * @param mixed $email
  425. * @param string $name
  426. * @param string $throwMessage
  427. * @return object $this
  428. * @throws SocketExpceiton
  429. */
  430. protected function _setEmailSingle($varName, $email, $name, $throwMessage) {
  431. $current = $this->{$varName};
  432. $this->_setEmail($varName, $email, $name);
  433. if (count($this->{$varName}) !== 1) {
  434. $this->{$varName} = $current;
  435. throw new SocketException($throwMessage);
  436. }
  437. return $this;
  438. }
  439. /**
  440. * Add email
  441. *
  442. * @param string $varName
  443. * @param mixed $email
  444. * @param mixed $name
  445. * @return object $this
  446. */
  447. protected function _addEmail($varName, $email, $name) {
  448. if (!is_array($email)) {
  449. if (!Validation::email($email)) {
  450. throw new SocketException(__d('cake', 'Invalid email: "%s"', $email));
  451. }
  452. if ($name === null) {
  453. $name = $email;
  454. }
  455. $this->{$varName}[$email] = $name;
  456. return $this;
  457. }
  458. $list = array();
  459. foreach ($email as $key => $value) {
  460. if (is_int($key)) {
  461. $key = $value;
  462. }
  463. if (!Validation::email($key)) {
  464. throw new SocketException(__d('cake', 'Invalid email: "%s"', $key));
  465. }
  466. $list[$key] = $value;
  467. }
  468. $this->{$varName} = array_merge($this->{$varName}, $list);
  469. return $this;
  470. }
  471. /**
  472. * Set Subject
  473. *
  474. * @param string $subject
  475. * @return mixed
  476. */
  477. public function subject($subject = null) {
  478. if ($subject === null) {
  479. return $this->_subject;
  480. }
  481. $this->_subject = $this->_encode((string)$subject);
  482. return $this;
  483. }
  484. /**
  485. * Sets headers for the message
  486. *
  487. * @param array Associative array containing headers to be set.
  488. * @return object $this
  489. * @throws SocketException
  490. */
  491. public function setHeaders($headers) {
  492. if (!is_array($headers)) {
  493. throw new SocketException(__d('cake', '$headers should be an array.'));
  494. }
  495. $this->_headers = $headers;
  496. return $this;
  497. }
  498. /**
  499. * Add header for the message
  500. *
  501. * @param array $headers
  502. * @return mixed $this
  503. * @throws SocketException
  504. */
  505. public function addHeaders($headers) {
  506. if (!is_array($headers)) {
  507. throw new SocketException(__d('cake', '$headers should be an array.'));
  508. }
  509. $this->_headers = array_merge($this->_headers, $headers);
  510. return $this;
  511. }
  512. /**
  513. * Get list of headers
  514. *
  515. * ### Includes:
  516. *
  517. * - `from`
  518. * - `replyTo`
  519. * - `readReceipt`
  520. * - `returnPath`
  521. * - `to`
  522. * - `cc`
  523. * - `bcc`
  524. * - `subject`
  525. *
  526. * @param array $include
  527. * @return array
  528. */
  529. public function getHeaders($include = array()) {
  530. $defaults = array(
  531. 'from' => false,
  532. 'sender' => false,
  533. 'replyTo' => false,
  534. 'readReceipt' => false,
  535. 'returnPath' => false,
  536. 'to' => false,
  537. 'cc' => false,
  538. 'bcc' => false,
  539. 'subject' => false
  540. );
  541. $include += $defaults;
  542. $headers = array();
  543. $relation = array(
  544. 'from' => 'From',
  545. 'replyTo' => 'Reply-To',
  546. 'readReceipt' => 'Disposition-Notification-To',
  547. 'returnPath' => 'Return-Path'
  548. );
  549. foreach ($relation as $var => $header) {
  550. if ($include[$var]) {
  551. $var = '_' . $var;
  552. $headers[$header] = current($this->_formatAddress($this->{$var}));
  553. }
  554. }
  555. if ($include['sender']) {
  556. if (key($this->_sender) === key($this->_from)) {
  557. $headers['Sender'] = '';
  558. } else {
  559. $headers['Sender'] = current($this->_formatAddress($this->_sender));
  560. }
  561. }
  562. foreach (array('to', 'cc', 'bcc') as $var) {
  563. if ($include[$var]) {
  564. $classVar = '_' . $var;
  565. $headers[ucfirst($var)] = implode(', ', $this->_formatAddress($this->{$classVar}));
  566. }
  567. }
  568. $headers += $this->_headers;
  569. if (!isset($headers['X-Mailer'])) {
  570. $headers['X-Mailer'] = self::EMAIL_CLIENT;
  571. }
  572. if (!isset($headers['Date'])) {
  573. $headers['Date'] = date(DATE_RFC2822);
  574. }
  575. if ($this->_messageId !== false) {
  576. if ($this->_messageId === true) {
  577. $headers['Message-ID'] = '<' . String::UUID() . '@' . env('HTTP_HOST') . '>';
  578. } else {
  579. $headers['Message-ID'] = $this->_messageId;
  580. }
  581. }
  582. if ($include['subject']) {
  583. $headers['Subject'] = $this->_subject;
  584. }
  585. $headers['MIME-Version'] = '1.0';
  586. if (!empty($this->_attachments)) {
  587. $headers['Content-Type'] = 'multipart/mixed; boundary="' . $this->_boundary . '"';
  588. $headers[] = 'This part of the E-mail should never be seen. If';
  589. $headers[] = 'you are reading this, consider upgrading your e-mail';
  590. $headers[] = 'client to a MIME-compatible client.';
  591. } elseif ($this->_emailFormat === 'text') {
  592. $headers['Content-Type'] = 'text/plain; charset=' . $this->charset;
  593. } elseif ($this->_emailFormat === 'html') {
  594. $headers['Content-Type'] = 'text/html; charset=' . $this->charset;
  595. } elseif ($this->_emailFormat === 'both') {
  596. $headers['Content-Type'] = 'multipart/alternative; boundary="alt-' . $this->_boundary . '"';
  597. }
  598. $headers['Content-Transfer-Encoding'] = '7bit';
  599. return $headers;
  600. }
  601. /**
  602. * Format addresses
  603. *
  604. * @param array $address
  605. * @return array
  606. */
  607. protected function _formatAddress($address) {
  608. $return = array();
  609. foreach ($address as $email => $alias) {
  610. if ($email === $alias) {
  611. $return[] = $email;
  612. } else {
  613. $return[] = sprintf('%s <%s>', $this->_encode($alias), $email);
  614. }
  615. }
  616. return $return;
  617. }
  618. /**
  619. * Template and layout
  620. *
  621. * @param mixed $template Template name or null to not use
  622. * @param mixed $layout Layout name or null to not use
  623. * @return mixed
  624. */
  625. public function template($template = false, $layout = false) {
  626. if ($template === false) {
  627. return array(
  628. 'template' => $this->_template,
  629. 'layout' => $this->_layout
  630. );
  631. }
  632. $this->_template = $template;
  633. if ($layout !== false) {
  634. $this->_layout = $layout;
  635. }
  636. return $this;
  637. }
  638. /**
  639. * View class for render
  640. *
  641. * @param string $viewClass
  642. * @return mixed
  643. */
  644. public function viewRender($viewClass = null) {
  645. if ($viewClass === null) {
  646. return $this->_viewRender;
  647. }
  648. $this->_viewRender = $viewClass;
  649. return $this;
  650. }
  651. /**
  652. * Variables to be set on render
  653. *
  654. * @param array $viewVars
  655. * @return mixed
  656. */
  657. public function viewVars($viewVars = null) {
  658. if ($viewVars === null) {
  659. return $this->_viewVars;
  660. }
  661. $this->_viewVars = array_merge($this->_viewVars, (array)$viewVars);
  662. return $this;
  663. }
  664. /**
  665. * Helpers to be used in render
  666. *
  667. * @param array $helpers
  668. * @return mixed
  669. */
  670. public function helpers($helpers = null) {
  671. if ($helpers === null) {
  672. return $this->_helpers;
  673. }
  674. $this->_helpers = (array)$helpers;
  675. return $this;
  676. }
  677. /**
  678. * Email format
  679. *
  680. * @param string $format
  681. * @return mixed
  682. * @throws SocketException
  683. */
  684. public function emailFormat($format = null) {
  685. if ($format === null) {
  686. return $this->_emailFormat;
  687. }
  688. if (!in_array($format, $this->_emailFormatAvailable)) {
  689. throw new SocketException(__d('cake', 'Format not available.'));
  690. }
  691. $this->_emailFormat = $format;
  692. return $this;
  693. }
  694. /**
  695. * Transport name
  696. *
  697. * @param string $name
  698. * @return mixed
  699. */
  700. public function transport($name = null) {
  701. if ($name === null) {
  702. return $this->_transportName;
  703. }
  704. $this->_transportName = (string)$name;
  705. $this->_transportClass = null;
  706. return $this;
  707. }
  708. /**
  709. * Return the transport class
  710. *
  711. * @return object
  712. * @throws SocketException
  713. */
  714. public function transportClass() {
  715. if ($this->_transportClass) {
  716. return $this->_transportClass;
  717. }
  718. list($plugin, $transportClassname) = pluginSplit($this->_transportName, true);
  719. $transportClassname .= 'Transport';
  720. App::uses($transportClassname, $plugin . 'Network/Email');
  721. if (!class_exists($transportClassname)) {
  722. throw new SocketException(__d('cake', 'Class "%s" not found.', $transportClassname));
  723. } elseif (!method_exists($transportClassname, 'send')) {
  724. throw new SocketException(__d('cake', 'The "%s" do not have send method.', $transportClassname));
  725. }
  726. return $this->_transportClass = new $transportClassname();
  727. }
  728. /**
  729. * Message-ID
  730. *
  731. * @param mixed $message True to generate a new Message-ID, False to ignore (not send in email), String to set as Message-ID
  732. * @return mixed
  733. * @throws SocketException
  734. */
  735. public function messageId($message = null) {
  736. if ($message === null) {
  737. return $this->_messageId;
  738. }
  739. if (is_bool($message)) {
  740. $this->_messageId = $message;
  741. } else {
  742. if (!preg_match('/^\<.+@.+\>$/', $message)) {
  743. throw new SocketException(__d('cake', 'Invalid format to Message-ID. The text should be something like "<uuid@server.com>"'));
  744. }
  745. $this->_messageId = $message;
  746. }
  747. return $this;
  748. }
  749. /**
  750. * Attachments
  751. *
  752. * @param mixed $attachments String with the filename or array with filenames
  753. * @return mixed
  754. * @throws SocketException
  755. */
  756. public function attachments($attachments = null) {
  757. if ($attachments === null) {
  758. return $this->_attachments;
  759. }
  760. $attach = array();
  761. foreach ((array)$attachments as $name => $fileInfo) {
  762. if (!is_array($fileInfo)) {
  763. $fileInfo = array('file' => $fileInfo);
  764. }
  765. if (!isset($fileInfo['file'])) {
  766. throw new SocketException(__d('cake', 'File not specified.'));
  767. }
  768. $fileInfo['file'] = realpath($fileInfo['file']);
  769. if ($fileInfo['file'] === false || !file_exists($fileInfo['file'])) {
  770. throw new SocketException(__d('cake', 'File not found: "%s"', $fileInfo['file']));
  771. }
  772. if (is_int($name)) {
  773. $name = basename($fileInfo['file']);
  774. }
  775. if (!isset($fileInfo['mimetype'])) {
  776. $fileInfo['mimetype'] = 'application/octet-stream';
  777. }
  778. $attach[$name] = $fileInfo;
  779. }
  780. $this->_attachments = $attach;
  781. return $this;
  782. }
  783. /**
  784. * Add attachments
  785. *
  786. * @param mixed $attachments String with the filename or array with filenames
  787. * @return object $this
  788. * @throws SocketException
  789. */
  790. public function addAttachments($attachments) {
  791. $current = $this->_attachments;
  792. $this->attachments($attachments);
  793. $this->_attachments = array_merge($current, $this->_attachments);
  794. return $this;
  795. }
  796. /**
  797. * Get generated message (used by transport classes)
  798. *
  799. * @param mixed $type Use MESSAGE_* constants or null to return the full message as array
  800. * @return mixed String if have type, array if type is null
  801. */
  802. public function message($type = null) {
  803. switch ($type) {
  804. case self::MESSAGE_HTML:
  805. return $this->_htmlMessage;
  806. case self::MESSAGE_TEXT:
  807. return $this->_textMessage;
  808. }
  809. return $this->_message;
  810. }
  811. /**
  812. * Configuration to use when send email
  813. *
  814. * @param mixed $config String with configuration name (from email.php), array with config or null to return current config
  815. * @return mixed
  816. */
  817. public function config($config = null) {
  818. if ($config === null) {
  819. return $this->_config;
  820. }
  821. if (is_array($config)) {
  822. $this->_config = $config;
  823. } else {
  824. $this->_config = (string)$config;
  825. }
  826. if ($this->_transportClass) {
  827. $this->_transportClass->config($this->_config);
  828. }
  829. return $this;
  830. }
  831. /**
  832. * Send an email using the specified content, template and layout
  833. *
  834. * @return boolean Success
  835. * @throws SocketException
  836. */
  837. public function send($content = null) {
  838. if (is_string($this->_config)) {
  839. if (!config('email')) {
  840. throw new SocketException(__d('cake', '%s not found.', APP . 'Config' . DS . 'email.php'));
  841. }
  842. $configs = new EmailConfig();
  843. if (!isset($configs->{$this->_config})) {
  844. throw new SocketException(__d('cake', 'Unknown email configuration "%s".', $this->_config));
  845. }
  846. $config = $configs->{$this->_config};
  847. if (isset($config['transport'])) {
  848. $this->transport($config['transport']);
  849. }
  850. } else {
  851. $config = $this->_config;
  852. }
  853. if (empty($this->_from)) {
  854. if (!empty($config['from'])) {
  855. $this->from($config['from']);
  856. } else {
  857. throw new SocketException(__d('cake', 'From is not specified.'));
  858. }
  859. }
  860. if (empty($this->_to) && empty($this->_cc) && empty($this->_bcc)) {
  861. throw new SocketException(__d('cake', 'You need specify one destination on to, cc or bcc.'));
  862. }
  863. if (is_array($content)) {
  864. $content = implode("\n", $content) . "\n";
  865. }
  866. $this->_textMessage = $this->_htmlMessage = '';
  867. if ($content !== null) {
  868. if ($this->_emailFormat === 'text') {
  869. $this->_textMessage = $content;
  870. } elseif ($this->_emailFormat === 'html') {
  871. $this->_htmlMessage = $content;
  872. } elseif ($this->_emailFormat === 'both') {
  873. $this->_textMessage = $this->_htmlMessage = $content;
  874. }
  875. }
  876. $this->_createBoundary();
  877. $message = $this->_wrap($content);
  878. if (empty($this->_template)) {
  879. $message = $this->_formatMessage($message);
  880. } else {
  881. $message = $this->_render($message);
  882. }
  883. $message[] = '';
  884. $this->_message = $message;
  885. if (!empty($this->_attachments)) {
  886. $this->_attachFiles();
  887. $this->_message[] = '';
  888. $this->_message[] = '--' . $this->_boundary . '--';
  889. $this->_message[] = '';
  890. }
  891. $transport = $this->transportClass();
  892. $transport->config($config);
  893. return $transport->send($this);
  894. }
  895. /**
  896. * Static method to fast create an instance of CakeEmail
  897. *
  898. * @param mixed $to Address to send (see CakeEmail::to()). If null, will try to use 'to' from transport config
  899. * @param mixed $subject String of subject or null to use 'subject' from transport config
  900. * @param mixed $message String with message or array with variables to be used in render
  901. * @param mixed $transportConfig String to use config from EmailConfig or array with configs
  902. * @param boolean $send Send the email or just return the instance pre-configured
  903. * @return object Instance of CakeEmail
  904. */
  905. public static function deliver($to = null, $subject = null, $message = null, $transportConfig = 'fast', $send = true) {
  906. $class = __CLASS__;
  907. $instance = new $class();
  908. if (is_string($transportConfig)) {
  909. if (!config('email')) {
  910. throw new SocketException(__d('cake', '%s not found.', APP . 'Config' . DS . 'email.php'));
  911. }
  912. $configs = new EmailConfig();
  913. if (!isset($configs->{$transportConfig})) {
  914. throw new SocketException(__d('cake', 'Unknown email configuration "%s".', $transportConfig));
  915. }
  916. $transportConfig = $configs->{$transportConfig};
  917. }
  918. self::_applyConfig($instance, $transportConfig);
  919. if ($to !== null) {
  920. $instance->to($to);
  921. }
  922. if ($subject !== null) {
  923. $instance->subject($subject);
  924. }
  925. if (is_array($message)) {
  926. $instance->viewVars($message);
  927. $message = null;
  928. } elseif ($message === null && isset($config['message'])) {
  929. $message = $config['message'];
  930. }
  931. if ($send === true) {
  932. $instance->send($message);
  933. }
  934. return $instance;
  935. }
  936. /**
  937. * Apply the config to an instance
  938. *
  939. * @param object $obj CakeEmail
  940. * @param array $config
  941. * @return void
  942. */
  943. protected static function _applyConfig(CakeEmail $obj, $config) {
  944. $simpleMethods = array(
  945. 'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath', 'cc', 'bcc',
  946. 'messageId', 'subject', 'viewRender', 'viewVars', 'attachments',
  947. 'transport', 'emailFormat'
  948. );
  949. foreach ($simpleMethods as $method) {
  950. if (isset($config[$method])) {
  951. $obj->$method($config[$method]);
  952. unset($config[$method]);
  953. }
  954. }
  955. if (isset($config['headers'])) {
  956. $obj->setHeaders($config['headers']);
  957. unset($config['headers']);
  958. }
  959. if (array_key_exists('template', $config)) {
  960. $layout = false;
  961. if (array_key_exists('layout', $config)) {
  962. $layout = $config['layout'];
  963. unset($config['layout']);
  964. }
  965. $obj->template($config['template'], $layout);
  966. unset($config['template']);
  967. }
  968. $obj->config($config);
  969. }
  970. /**
  971. * Reset all EmailComponent internal variables to be able to send out a new email.
  972. *
  973. * @return object $this
  974. */
  975. public function reset() {
  976. $this->_to = array();
  977. $this->_from = array();
  978. $this->_sender = array();
  979. $this->_replyTo = array();
  980. $this->_readReceipt = array();
  981. $this->_returnPath = array();
  982. $this->_cc = array();
  983. $this->_bcc = array();
  984. $this->_messageId = true;
  985. $this->_subject = '';
  986. $this->_headers = array();
  987. $this->_layout = 'default';
  988. $this->_template = '';
  989. $this->_viewRender = 'View';
  990. $this->_viewVars = array();
  991. $this->_helpers = array();
  992. $this->_textMessage = '';
  993. $this->_htmlMessage = '';
  994. $this->_message = '';
  995. $this->_emailFormat = 'text';
  996. $this->_transportName = 'Mail';
  997. $this->_transportClass = null;
  998. $this->_attachments = array();
  999. $this->_config = 'default';
  1000. return $this;
  1001. }
  1002. /**
  1003. * Encode the specified string using the current charset
  1004. *
  1005. * @param string $text String to encode
  1006. * @return string Encoded string
  1007. */
  1008. protected function _encode($text) {
  1009. $internalEncoding = function_exists('mb_internal_encoding');
  1010. if ($internalEncoding) {
  1011. $restore = mb_internal_encoding();
  1012. mb_internal_encoding($this->charset);
  1013. }
  1014. $return = mb_encode_mimeheader($text, $this->charset, 'B');
  1015. if ($internalEncoding) {
  1016. mb_internal_encoding($restore);
  1017. }
  1018. return $return;
  1019. }
  1020. /**
  1021. * Wrap the message to follow the RFC 2822 - 2.1.1
  1022. *
  1023. * @param string $message Message to wrap
  1024. * @return array Wrapped message
  1025. */
  1026. protected function _wrap($message) {
  1027. $message = str_replace(array("\r\n", "\r"), "\n", $message);
  1028. $lines = explode("\n", $message);
  1029. $formatted = array();
  1030. foreach ($lines as $line) {
  1031. if (empty($line)) {
  1032. $formatted[] = '';
  1033. continue;
  1034. }
  1035. if ($line[0] === '.') {
  1036. $line = '.' . $line;
  1037. }
  1038. if (!preg_match('/\<[a-z]/i', $line)) {
  1039. $formatted = array_merge($formatted, explode("\n", wordwrap($line, self::LINE_LENGTH_SHOULD, "\n")));
  1040. continue;
  1041. }
  1042. $tagOpen = false;
  1043. $tmpLine = $tag = '';
  1044. $tmpLineLength = 0;
  1045. for ($i = 0, $count = strlen($line); $i < $count; $i++) {
  1046. $char = $line[$i];
  1047. if ($tagOpen) {
  1048. $tag .= $char;
  1049. if ($char === '>') {
  1050. $tagLength = strlen($tag);
  1051. if ($tagLength + $tmpLineLength < self::LINE_LENGTH_SHOULD) {
  1052. $tmpLine .= $tag;
  1053. $tmpLineLength += $tagLength;
  1054. } else {
  1055. if ($tmpLineLength > 0) {
  1056. $formatted[] = trim($tmpLine);
  1057. $tmpLine = '';
  1058. $tmpLineLength = 0;
  1059. }
  1060. if ($tagLength > self::LINE_LENGTH_SHOULD) {
  1061. $formatted[] = $tag;
  1062. } else {
  1063. $tmpLine = $tag;
  1064. $tmpLineLength = $tagLength;
  1065. }
  1066. }
  1067. $tag = '';
  1068. $tagOpen = false;
  1069. }
  1070. continue;
  1071. }
  1072. if ($char === '<') {
  1073. $tagOpen = true;
  1074. $tag = '<';
  1075. continue;
  1076. }
  1077. if ($char === ' ' && $tmpLineLength >= self::LINE_LENGTH_SHOULD) {
  1078. $formatted[] = $tmpLine;
  1079. $tmpLineLength = 0;
  1080. continue;
  1081. }
  1082. $tmpLine .= $char;
  1083. $tmpLineLength++;
  1084. if ($tmpLineLength === self::LINE_LENGTH_SHOULD) {
  1085. $nextChar = $line[$i + 1];
  1086. if ($nextChar === ' ' || $nextChar === '<') {
  1087. $formatted[] = trim($tmpLine);
  1088. $tmpLine = '';
  1089. $tmpLineLength = 0;
  1090. if ($nextChar === ' ') {
  1091. $i++;
  1092. }
  1093. } else {
  1094. $lastSpace = strrpos($tmpLine, ' ');
  1095. if ($lastSpace === false) {
  1096. continue;
  1097. }
  1098. $formatted[] = trim(substr($tmpLine, 0, $lastSpace));
  1099. $tmpLine = substr($tmpLine, $lastSpace + 1);
  1100. $tmpLineLength = strlen($tmpLine);
  1101. }
  1102. }
  1103. }
  1104. if (!empty($tmpLine)) {
  1105. $formatted[] = $tmpLine;
  1106. }
  1107. }
  1108. $formatted[] = '';
  1109. return $formatted;
  1110. }
  1111. /**
  1112. * Create unique boundary identifier
  1113. *
  1114. * @return void
  1115. */
  1116. protected function _createBoundary() {
  1117. if (!empty($this->_attachments) || $this->_emailFormat === 'both') {
  1118. $this->_boundary = md5(uniqid(time()));
  1119. }
  1120. }
  1121. /**
  1122. * Attach files by adding file contents inside boundaries.
  1123. *
  1124. * @return void
  1125. */
  1126. protected function _attachFiles() {
  1127. foreach ($this->_attachments as $filename => $fileInfo) {
  1128. $handle = fopen($fileInfo['file'], 'rb');
  1129. $data = fread($handle, filesize($fileInfo['file']));
  1130. $data = chunk_split(base64_encode($data)) ;
  1131. fclose($handle);
  1132. $this->_message[] = '--' . $this->_boundary;
  1133. $this->_message[] = 'Content-Type: ' . $fileInfo['mimetype'];
  1134. $this->_message[] = 'Content-Transfer-Encoding: base64';
  1135. if (empty($fileInfo['contentId'])) {
  1136. $this->_message[] = 'Content-Disposition: attachment; filename="' . $filename . '"';
  1137. } else {
  1138. $this->_message[] = 'Content-ID: <' . $fileInfo['contentId'] . '>';
  1139. $this->_message[] = 'Content-Disposition: inline; filename="' . $filename . '"';
  1140. }
  1141. $this->_message[] = '';
  1142. $this->_message[] = $data;
  1143. $this->_message[] = '';
  1144. }
  1145. }
  1146. /**
  1147. * Format the message by seeing if it has attachments.
  1148. *
  1149. * @param array $message Message to format
  1150. * @return array
  1151. */
  1152. protected function _formatMessage($message) {
  1153. if (!empty($this->_attachments)) {
  1154. $prefix = array('--' . $this->_boundary);
  1155. if ($this->_emailFormat === 'text') {
  1156. $prefix[] = 'Content-Type: text/plain; charset=' . $this->charset;
  1157. } elseif ($this->_emailFormat === 'html') {
  1158. $prefix[] = 'Content-Type: text/html; charset=' . $this->charset;
  1159. } elseif ($this->_emailFormat === 'both') {
  1160. $prefix[] = 'Content-Type: multipart/alternative; boundary="alt-' . $this->_boundary . '"';
  1161. }
  1162. $prefix[] = 'Content-Transfer-Encoding: 7bit';
  1163. $prefix[] = '';
  1164. $message = array_merge($prefix, $message);
  1165. }
  1166. return $message;
  1167. }
  1168. /**
  1169. * Render the contents using the current layout and template.
  1170. *
  1171. * @param string $content Content to render
  1172. * @return array Email ready to be sent
  1173. * @access private
  1174. */
  1175. protected function _render($content) {
  1176. $viewClass = $this->_viewRender;
  1177. if ($viewClass !== 'View') {
  1178. list($plugin, $viewClass) = pluginSplit($viewClass, true);
  1179. $viewClass .= 'View';
  1180. App::uses($viewClass, $plugin . 'View');
  1181. }
  1182. $View = new $viewClass(null);
  1183. $View->viewVars = $this->_viewVars;
  1184. $View->helpers = $this->_helpers;
  1185. $msg = array();
  1186. list($templatePlugin, $template) = pluginSplit($this->_template, true);
  1187. list($layoutPlugin, $layout) = pluginSplit($this->_layout, true);
  1188. if (!empty($templatePlugin)) {
  1189. $View->plugin = rtrim($templatePlugin, '.');
  1190. } elseif (!empty($layoutPlugin)) {
  1191. $View->plugin = rtrim($layoutPlugin, '.');
  1192. }
  1193. $content = implode("\n", $content);
  1194. if ($this->_emailFormat === 'both') {
  1195. $originalContent = $content;
  1196. if (!empty($this->_attachments)) {
  1197. $msg[] = '--' . $this->_boundary;
  1198. $msg[] = 'Content-Type: multipart/alternative; boundary="alt-' . $this->_boundary . '"';
  1199. $msg[] = '';
  1200. }
  1201. $msg[] = '--alt-' . $this->_boundary;
  1202. $msg[] = 'Content-Type: text/plain; charset=' . $this->charset;
  1203. $msg[] = 'Content-Transfer-Encoding: 7bit';
  1204. $msg[] = '';
  1205. $View->viewPath = $View->layoutPath = 'Emails' . DS . 'text';
  1206. $View->viewVars['content'] = $originalContent;
  1207. $this->_textMessage = str_replace(array("\r\n", "\r"), "\n", $View->render($template, $layout));
  1208. $content = explode("\n", $this->_textMessage);
  1209. $msg = array_merge($msg, $content);
  1210. $msg[] = '';
  1211. $msg[] = '--alt-' . $this->_boundary;
  1212. $msg[] = 'Content-Type: text/html; charset=' . $this->charset;
  1213. $msg[] = 'Content-Transfer-Encoding: 7bit';
  1214. $msg[] = '';
  1215. $View->viewPath = $View->layoutPath = 'Emails' . DS . 'html';
  1216. $View->viewVars['content'] = $originalContent;
  1217. $View->hasRendered = false;
  1218. $this->_htmlMessage = str_replace(array("\r\n", "\r"), "\n", $View->render($template, $layout));
  1219. $content = explode("\n", $this->_htmlMessage);
  1220. $msg = array_merge($msg, $content);
  1221. $msg[] = '';
  1222. $msg[] = '--alt-' . $this->_boundary . '--';
  1223. $msg[] = '';
  1224. return $msg;
  1225. }
  1226. if (!empty($this->_attachments)) {
  1227. if ($this->_emailFormat === 'html') {
  1228. $msg[] = '';
  1229. $msg[] = '--' . $this->_boundary;
  1230. $msg[] = 'Content-Type: text/html; charset=' . $this->charset;
  1231. $msg[] = 'Content-Transfer-Encoding: 7bit';
  1232. $msg[] = '';
  1233. } else {
  1234. $msg[] = '--' . $this->_boundary;
  1235. $msg[] = 'Content-Type: text/plain; charset=' . $this->charset;
  1236. $msg[] = 'Content-Transfer-Encoding: 7bit';
  1237. $msg[] = '';
  1238. }
  1239. }
  1240. $View->viewPath = $View->layoutPath = 'Emails' . DS . $this->_emailFormat;
  1241. $View->viewVars['content'] = $content;
  1242. $rendered = $View->render($template, $layout);
  1243. $content = explode("\n", $rendered);
  1244. if ($this->_emailFormat === 'html') {
  1245. $this->_htmlMessage = $rendered;
  1246. } else {
  1247. $this->_textMessage = $rendered;
  1248. }
  1249. return array_merge($msg, $content);
  1250. }
  1251. }