CakeEmail.php 34 KB

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