EmailLib.php 16 KB

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