EmailLib.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. <?php
  2. App::uses('CakeEmail', 'Network/Email');
  3. App::uses('CakeLog', 'Log');
  4. App::uses('MimeLib', 'Tools.Lib');
  5. if (!defined('BR')) {
  6. define('BR', '<br />');
  7. }
  8. /**
  9. * Convenience class for internal mailer.
  10. * Adds some useful features and fixes some bugs:
  11. * - enable easier attachment adding (and also from blob)
  12. * - enable embedded images in html mails
  13. * - extensive logging and error tracing
  14. * - create mails with blob attachments (embedded or attached)
  15. * - allow wrapLength to be adjusted
  16. * - Configure::read('Config.x-mailer') can modify the x-mailer
  17. * - basic validation supported
  18. * - allow priority to be set (1 to 5)
  19. *
  20. * @author Mark Scherer
  21. * @license MIT
  22. * @cakephp 2.x
  23. * 2012-03-30 ms
  24. */
  25. class EmailLib extends CakeEmail {
  26. protected $_log = null;
  27. protected $_debug = null;
  28. protected $_error = null;
  29. protected $_wrapLength = null;
  30. protected $_priority = null;
  31. public function __construct($config = null) {
  32. if ($config === null) {
  33. $config = 'default';
  34. }
  35. parent::__construct($config);
  36. $this->resetAndSet();
  37. }
  38. /**
  39. * Quick way to send emails to admin.
  40. * App::uses() + EmailLib::systemEmail()
  41. *
  42. * Note: always go out with default settings (e.g.: SMTP even if debug > 0)
  43. *
  44. * @param string $subject
  45. * @param string $message
  46. * @param string $transportConfig
  47. * @return boolean Success
  48. * 2011-10-31 ms
  49. */
  50. public static function systemEmail($subject, $message = 'System Email', $transportConfig = null) {
  51. $class = __CLASS__;
  52. $instance = new $class($transportConfig);
  53. $instance->from(Configure::read('Config.system_email'), Configure::read('Config.system_name'));
  54. $instance->to(Configure::read('Config.admin_email'), Configure::read('Config.admin_name'));
  55. if ($subject !== null) {
  56. $instance->subject($subject);
  57. }
  58. if (is_array($message)) {
  59. $instance->viewVars($message);
  60. $message = null;
  61. } elseif ($message === null && array_key_exists('message', $config = $instance->config())) {
  62. $message = $config['message'];
  63. }
  64. return $instance->send($message);
  65. }
  66. /**
  67. * Change the layout
  68. *
  69. * @param string $layout Layout to use (or false to use none)
  70. * @return resource EmailLib
  71. * 2011-11-02 ms
  72. */
  73. public function layout($layout = false) {
  74. if ($layout !== false) {
  75. $this->_layout = $layout;
  76. }
  77. return $this;
  78. }
  79. /**
  80. * Add an attachment from file
  81. *
  82. * @param string $file: absolute path
  83. * @param string $filename
  84. * @param array $fileInfo
  85. * @return resource EmailLib
  86. * 2011-11-02 ms
  87. */
  88. public function addAttachment($file, $name = null, $fileInfo = array()) {
  89. $fileInfo['file'] = $file;
  90. if (!empty($name)) {
  91. $fileInfo = array($name => $fileInfo);
  92. } else {
  93. $fileInfo = array($fileInfo);
  94. }
  95. return $this->addAttachments($fileInfo);
  96. }
  97. /**
  98. * Add an attachment as blob
  99. *
  100. * @param binary $content: blob data
  101. * @param string $filename to attach it
  102. * @param string $mimeType (leave it empty to get mimetype from $filename)
  103. * @param array $fileInfo
  104. * @return resource EmailLib
  105. * 2011-11-02 ms
  106. */
  107. public function addBlobAttachment($content, $name, $mimeType = null, $fileInfo = array()) {
  108. $fileInfo['content'] = $content;
  109. $fileInfo['mimetype'] = $mimeType;
  110. $file = array($name => $fileInfo);
  111. return $this->addAttachments($file);
  112. }
  113. /**
  114. * Add an inline attachment from file
  115. *
  116. * @param string $file: absolute path
  117. * @param string $filename (optional)
  118. * @param string $contentId (optional)
  119. * @param array $options
  120. * - mimetype
  121. * - contentDisposition
  122. * @return mixed resource $EmailLib or string $contentId
  123. * 2011-11-02 ms
  124. */
  125. public function addEmbeddedAttachment($file, $name = null, $contentId = null, $options = array()) {
  126. $path = realpath($file);
  127. if (empty($name)) {
  128. $name = basename($file);
  129. }
  130. if ($contentId === null && ($cid = $this->_isEmbeddedAttachment($path, $name))) {
  131. return $cid;
  132. }
  133. $options['file'] = $path;
  134. if (empty($options['mimetype'])) {
  135. $options['mimetype'] = $this->_getMime($file);
  136. }
  137. $options['contentId'] = $contentId ? $contentId : str_replace('-', '', String::uuid()) . '@' . $this->_domain;
  138. $file = array($name => $options);
  139. $res = $this->addAttachments($file);
  140. if ($contentId === null) {
  141. return $options['contentId'];
  142. }
  143. return $res;
  144. }
  145. /**
  146. * Add an inline attachment as blob
  147. *
  148. * @param binary $content: blob data
  149. * @param string $filename to attach it
  150. * @param string $mimeType (leave it empty to get mimetype from $filename)
  151. * @param string $contentId (optional)
  152. * @param array $options
  153. * - contentDisposition
  154. * @return mixed resource $EmailLib or string $contentId
  155. * 2011-11-02 ms
  156. */
  157. public function addEmbeddedBlobAttachment($content, $name, $mimeType = null, $contentId = null, $options = array()) {
  158. $options['content'] = $content;
  159. $options['mimetype'] = $mimeType;
  160. $options['contentId'] = $contentId ? $contentId : str_replace('-', '', String::uuid()) . '@' . $this->_domain;
  161. $file = array($name => $options);
  162. $res = $this->addAttachments($file);
  163. if ($contentId === null) {
  164. return $options['contentId'];
  165. }
  166. return $res;
  167. }
  168. /**
  169. * Returns if this particular file has already been attached as embedded file with this exact name
  170. * to prevent the same image to overwrite each other and also to only send this image once.
  171. * Allows multiple usage of the same embedded image (using the same cid)
  172. *
  173. * @return string cid of the found file or false if no such attachment can be found
  174. */
  175. protected function _isEmbeddedAttachment($file, $name) {
  176. foreach ($this->_attachments as $filename => $fileInfo) {
  177. if ($filename !== $name) {
  178. continue;
  179. }
  180. if ($fileInfo['file'] === $file) {
  181. return $fileInfo['contentId'];
  182. }
  183. }
  184. return false;
  185. }
  186. /**
  187. * Try to determine the mimetype by filename.
  188. * Uses finfo_open() if availble, otherwise guesses it via file extension.
  189. *
  190. * @param string $filename
  191. * @param string Mimetype
  192. */
  193. protected function _getMime($filename) {
  194. if (function_exists('finfo_open')) {
  195. $finfo = finfo_open(FILEINFO_MIME);
  196. $mimetype = finfo_file($finfo, $filename);
  197. finfo_close($finfo);
  198. } else {
  199. //TODO: improve
  200. $ext = pathinfo($filename, PATHINFO_EXTENSION);
  201. $mimetype = $this->_getMimeByExtension($ext);
  202. }
  203. return $mimetype;
  204. }
  205. /**
  206. * Try to find mimetype by file extension
  207. *
  208. * @param string $ext lowercase (jpg, png, pdf, ...)
  209. * @param string $defaultMimeType
  210. * @return string Mimetype (falls back to `application/octet-stream`)
  211. * 2012-04-17 ms
  212. */
  213. protected function _getMimeByExtension($ext, $default = 'application/octet-stream') {
  214. if (!isset($this->_Mime)) {
  215. $this->_Mime = new MimeLib();
  216. }
  217. $mime = $this->_Mime->getMimeType($ext);
  218. if (!$mime) {
  219. $mime = $default;
  220. }
  221. return $mime;
  222. }
  223. /**
  224. * Validate if the email has the required fields necessary to make send() work.
  225. * Assumes layouting (does not check on content to be present or if view/layout files are missing).
  226. *
  227. * @return boolean Success
  228. */
  229. public function validates() {
  230. if (!empty($this->_subject) && !empty($this->_to)) {
  231. return true;
  232. }
  233. return false;
  234. }
  235. /**
  236. * Attach inline/embedded files to the message.
  237. *
  238. * CUSTOM FIX: blob data support
  239. *
  240. * @override
  241. * @param string $boundary Boundary to use. If null, will default to $this->_boundary
  242. * @return array An array of lines to add to the message
  243. */
  244. protected function _attachInlineFiles($boundary = null) {
  245. if ($boundary === null) {
  246. $boundary = $this->_boundary;
  247. }
  248. $msg = array();
  249. foreach ($this->_attachments as $filename => $fileInfo) {
  250. if (empty($fileInfo['contentId'])) {
  251. continue;
  252. }
  253. if (!empty($fileInfo['content'])) {
  254. $data = $fileInfo['content'];
  255. $data = chunk_split(base64_encode($data));
  256. } elseif (!empty($fileInfo['file'])) {
  257. $data = $this->_readFile($fileInfo['file']);
  258. } else {
  259. continue;
  260. }
  261. $msg[] = '--' . $boundary;
  262. $msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
  263. $msg[] = 'Content-Transfer-Encoding: base64';
  264. $msg[] = 'Content-ID: <' . $fileInfo['contentId'] . '>';
  265. $msg[] = 'Content-Disposition: inline; filename="' . $filename . '"';
  266. $msg[] = '';
  267. $msg[] = $data;
  268. $msg[] = '';
  269. }
  270. return $msg;
  271. }
  272. /**
  273. * Attach non-embedded files by adding file contents inside boundaries.
  274. *
  275. * CUSTOM FIX: blob data support
  276. *
  277. * @override
  278. * @param string $boundary Boundary to use. If null, will default to $this->_boundary
  279. * @return array An array of lines to add to the message
  280. */
  281. protected function _attachFiles($boundary = null) {
  282. if ($boundary === null) {
  283. $boundary = $this->_boundary;
  284. }
  285. $msg = array();
  286. foreach ($this->_attachments as $filename => $fileInfo) {
  287. if (!empty($fileInfo['contentId'])) {
  288. continue;
  289. }
  290. if (!empty($fileInfo['content'])) {
  291. $data = $fileInfo['content'];
  292. $data = chunk_split(base64_encode($data));
  293. } elseif (!empty($fileInfo['file'])) {
  294. $data = $this->_readFile($fileInfo['file']);
  295. } else {
  296. continue;
  297. }
  298. $msg[] = '--' . $boundary;
  299. $msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
  300. $msg[] = 'Content-Transfer-Encoding: base64';
  301. if (
  302. !isset($fileInfo['contentDisposition']) ||
  303. $fileInfo['contentDisposition']
  304. ) {
  305. $msg[] = 'Content-Disposition: attachment; filename="' . $filename . '"';
  306. }
  307. $msg[] = '';
  308. $msg[] = $data;
  309. $msg[] = '';
  310. }
  311. return $msg;
  312. }
  313. /**
  314. * Add attachments to the email message
  315. *
  316. * CUSTOM FIX: blob data support
  317. *
  318. * Attachments can be defined in a few forms depending on how much control you need:
  319. *
  320. * Attach a single file:
  321. *
  322. * {{{
  323. * $email->attachments('path/to/file');
  324. * }}}
  325. *
  326. * Attach a file with a different filename:
  327. *
  328. * {{{
  329. * $email->attachments(array('custom_name.txt' => 'path/to/file.txt'));
  330. * }}}
  331. *
  332. * Attach a file and specify additional properties:
  333. *
  334. * {{{
  335. * $email->attachments(array('custom_name.png' => array(
  336. * 'file' => 'path/to/file',
  337. * 'mimetype' => 'image/png',
  338. * 'contentId' => 'abc123'
  339. * ));
  340. * }}}
  341. *
  342. * The `contentId` key allows you to specify an inline attachment. In your email text, you
  343. * can use `<img src="cid:abc123" />` to display the image inline.
  344. *
  345. * @override
  346. * @param mixed $attachments String with the filename or array with filenames
  347. * @return mixed Either the array of attachments when getting or $this when setting.
  348. * @throws SocketException
  349. */
  350. public function attachments($attachments = null) {
  351. if ($attachments === null) {
  352. return $this->_attachments;
  353. }
  354. $attach = array();
  355. foreach ((array)$attachments as $name => $fileInfo) {
  356. if (!is_array($fileInfo)) {
  357. $fileInfo = array('file' => $fileInfo);
  358. }
  359. if (empty($fileInfo['content'])) {
  360. if (!isset($fileInfo['file'])) {
  361. throw new SocketException(__d('cake_dev', 'File not specified.'));
  362. }
  363. $fileInfo['file'] = realpath($fileInfo['file']);
  364. if ($fileInfo['file'] === false || !file_exists($fileInfo['file'])) {
  365. throw new SocketException(__d('cake_dev', 'File not found: "%s"', $fileInfo['file']));
  366. }
  367. if (is_int($name)) {
  368. $name = basename($fileInfo['file']);
  369. }
  370. }
  371. if (empty($fileInfo['mimetype'])) {
  372. $ext = pathinfo($name, PATHINFO_EXTENSION);
  373. $fileInfo['mimetype'] = $this->_getMimeByExtension($ext);
  374. }
  375. $attach[$name] = $fileInfo;
  376. }
  377. $this->_attachments = $attach;
  378. return $this;
  379. }
  380. /**
  381. * Apply the config to an instance
  382. *
  383. * @overwrite
  384. * @param CakeEmail $obj CakeEmail
  385. * @param array $config
  386. * @return void
  387. * @throws ConfigureException When configuration file cannot be found, or is missing
  388. * the named config.
  389. */
  390. protected function _applyConfig($config) {
  391. if (is_string($config)) {
  392. if (!class_exists('EmailConfig') && !config('email')) {
  393. throw new ConfigureException(__d('cake_dev', '%s not found.', APP . 'Config' . DS . 'email.php'));
  394. }
  395. $configs = new EmailConfig();
  396. if (!isset($configs->{$config})) {
  397. throw new ConfigureException(__d('cake_dev', 'Unknown email configuration "%s".', $config));
  398. }
  399. $config = $configs->{$config};
  400. }
  401. $this->_config += $config;
  402. if (!empty($config['charset'])) {
  403. $this->charset = $config['charset'];
  404. }
  405. if (!empty($config['headerCharset'])) {
  406. $this->headerCharset = $config['headerCharset'];
  407. }
  408. if (empty($this->headerCharset)) {
  409. $this->headerCharset = $this->charset;
  410. }
  411. $simpleMethods = array(
  412. 'from', 'sender', 'to', 'replyTo', 'readReceipt', 'returnPath', 'cc', 'bcc',
  413. 'messageId', 'domain', 'subject', 'viewRender', 'viewVars', 'attachments',
  414. 'transport', 'emailFormat', 'theme', 'helpers'
  415. );
  416. foreach ($simpleMethods as $method) {
  417. if (isset($config[$method])) {
  418. $this->$method($config[$method]);
  419. unset($config[$method]);
  420. }
  421. }
  422. if (isset($config['headers'])) {
  423. $this->setHeaders($config['headers']);
  424. unset($config['headers']);
  425. }
  426. if (array_key_exists('template', $config)) {
  427. $layout = false;
  428. if (array_key_exists('layout', $config)) {
  429. $layout = $config['layout'];
  430. unset($config['layout']);
  431. }
  432. $this->template($config['template'], $layout);
  433. unset($config['template']);
  434. }
  435. $this->transportClass()->config($config);
  436. }
  437. /**
  438. * Set the body of the mail as we send it.
  439. * Note: the text can be an array, each element will appear as a seperate line in the message body.
  440. *
  441. * Do NOT pass a message if you use $this->set() in combination with templates
  442. *
  443. * @overwrite
  444. * @param string/array: message
  445. * @return boolean Success
  446. */
  447. public function send($message = null) {
  448. $this->_log = array(
  449. 'to' => $this->_to,
  450. 'from' => $this->_from,
  451. 'sender' => $this->_sender,
  452. 'replyTo' => $this->_replyTo,
  453. 'cc' => $this->_cc,
  454. 'subject' => $this->_subject,
  455. 'bcc' => $this->_bcc,
  456. 'transport' => $this->_transportName
  457. );
  458. if ($this->_priority) {
  459. $this->_headers['X-Priority'] = $this->_priority;
  460. //$this->_headers['X-MSMail-Priority'] = 'High';
  461. //$this->_headers['Importance'] = 'High';
  462. }
  463. try {
  464. $this->_debug = parent::send($message);
  465. } catch (Exception $e) {
  466. $this->_error = $e->getMessage();
  467. $this->_error .= ' (line '.$e->getLine().' in '.$e->getFile().')'.PHP_EOL.$e->getTraceAsString();
  468. if (!empty($this->_config['report'])) {
  469. $this->_logEmail();
  470. }
  471. return false;
  472. }
  473. if (!empty($this->_config['report'])) {
  474. $this->_logEmail();
  475. }
  476. return true;
  477. }
  478. /**
  479. * Allow modifications of the message
  480. *
  481. * @param string $text
  482. * @return string Text
  483. */
  484. protected function _prepMessage($text) {
  485. return $text;
  486. }
  487. /**
  488. * Returns the error if existent
  489. *
  490. * @return string
  491. */
  492. public function getError() {
  493. return $this->_error;
  494. }
  495. /**
  496. * Returns the debug content returned by send()
  497. *
  498. * @return string
  499. */
  500. public function getDebug() {
  501. return $this->_debug;
  502. }
  503. /**
  504. * Set/Get wrapLength
  505. *
  506. * @param integer $length Must not be more than CakeEmail::LINE_LENGTH_MUST
  507. * @return integer|CakeEmail
  508. */
  509. public function wrapLength($length = null) {
  510. if ($length === null) {
  511. return $this->_wrapLength;
  512. }
  513. $this->_wrapLength = $length;
  514. return $this;
  515. }
  516. /**
  517. * Set/Get priority
  518. *
  519. * @param integer $priority 1 (highest) to 5 (lowest)
  520. * @return integer|CakeEmail
  521. */
  522. public function priority($priority = null) {
  523. if ($priority === null) {
  524. return $this->_priority;
  525. }
  526. $this->_priority = $priority;
  527. return $this;
  528. }
  529. /**
  530. * Fix line length
  531. *
  532. * @overwrite
  533. * @param string $message Message to wrap
  534. * @return array Wrapped message
  535. */
  536. protected function _wrap($message, $wrapLength = CakeEmail::LINE_LENGTH_MUST) {
  537. if ($this->_wrapLength !== null) {
  538. $wrapLength = $this->_wrapLength;
  539. }
  540. return parent::_wrap($message, $wrapLength);
  541. }
  542. /**
  543. * Logs Email to type email
  544. *
  545. * @return void
  546. */
  547. protected function _logEmail($append = null) {
  548. $res = $this->_log['transport'].
  549. ' - '.'TO:'.implode(',', array_keys($this->_log['to'])).
  550. '||FROM:'.implode(',', array_keys($this->_log['from'])).
  551. '||REPLY:'.implode(',', array_keys($this->_log['replyTo'])).
  552. '||S:'.$this->_log['subject'];
  553. $type = 'email';
  554. if (!empty($this->_error)) {
  555. $type = 'email_error';
  556. $res .= '||ERROR:' . $this->_error;
  557. }
  558. if ($append) {
  559. $res .= '||'.$append;
  560. }
  561. CakeLog::write($type, $res);
  562. }
  563. /**
  564. * EmailLib::resetAndSet()
  565. *
  566. * @return void
  567. */
  568. public function resetAndSet() {
  569. //$this->reset();
  570. $this->_to = array();
  571. $this->_cc = array();
  572. $this->_bcc = array();
  573. $this->_messageId = true;
  574. $this->_subject = '';
  575. $this->_headers = array();
  576. $this->_viewVars = array();
  577. $this->_textMessage = '';
  578. $this->_htmlMessage = '';
  579. $this->_message = '';
  580. $this->_attachments = array();
  581. $this->_error = null;
  582. $this->_debug = null;
  583. if ($fromEmail = Configure::read('Config.system_email')) {
  584. $fromName = Configure::read('Config.system_name');
  585. } else {
  586. $fromEmail = Configure::read('Config.admin_email');
  587. $fromName = Configure::read('Config.admin_name');
  588. }
  589. $this->from($fromEmail, $fromName);
  590. if ($xMailer = Configure::read('Config.x-mailer')) {
  591. $this->addHeaders(array('X-Mailer' => $xMailer));
  592. }
  593. //$this->_errors = array();
  594. //$this->charset($this->charset);
  595. //$this->sendAs($this->sendAs);
  596. //$this->layout($this->_layout);
  597. //$this->delivery($this->deliveryMethod);
  598. }
  599. }