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