EmailLib.php 16 KB

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