Email.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. <?php
  2. namespace Tools\Mailer;
  3. use Cake\Core\Configure;
  4. use Cake\Mailer\Email as CakeEmail;
  5. use Tools\Utility\Text;
  6. use InvalidArgumentException;
  7. use Tools\Utility\Mime;
  8. use Cake\Log\LogTrait;
  9. use Psr\Log\LogLevel;
  10. class Email extends CakeEmail {
  11. use LogTrait;
  12. protected $_wrapLength = null;
  13. protected $_priority = null;
  14. protected $_error = null;
  15. protected $_debug = null;
  16. protected $_log = [];
  17. public function __construct($config = null) {
  18. if ($config === null) {
  19. $config = 'default';
  20. }
  21. parent::__construct($config);
  22. }
  23. /**
  24. * Change the layout
  25. *
  26. * @param string|bool $layout Layout to use (or false to use none)
  27. * @return self
  28. */
  29. public function layout($layout = false) {
  30. if ($layout !== false) {
  31. $this->_layout = $layout;
  32. }
  33. return $this;
  34. }
  35. /**
  36. * Set/Get wrapLength
  37. *
  38. * @param int|null $length Must not be more than CakeEmail::LINE_LENGTH_MUST
  39. * @return int|self
  40. */
  41. public function wrapLength($length = null) {
  42. if ($length === null) {
  43. return $this->_wrapLength;
  44. }
  45. $this->_wrapLength = $length;
  46. return $this;
  47. }
  48. /**
  49. * Set/Get priority
  50. *
  51. * @param int $priority 1 (highest) to 5 (lowest)
  52. * @return int|self
  53. */
  54. public function priority($priority = null) {
  55. if ($priority === null) {
  56. return $this->_priority;
  57. }
  58. $this->_priority = $priority;
  59. return $this;
  60. }
  61. /**
  62. * Fix line length
  63. *
  64. * @overwrite
  65. *
  66. * @param string $message Message to wrap
  67. * @param int $wrapLength
  68. * @return array Wrapped message
  69. */
  70. protected function _wrap($message, $wrapLength = CakeEmail::LINE_LENGTH_MUST) {
  71. if ($this->_wrapLength !== null) {
  72. $wrapLength = $this->_wrapLength;
  73. }
  74. return parent::_wrap($message, $wrapLength);
  75. }
  76. /**
  77. * EmailLib::resetAndSet()
  78. *
  79. * @return void
  80. */
  81. public function reset() {
  82. parent::reset();
  83. $this->_priority = null;
  84. $this->_wrapLength = null;
  85. $this->_error = null;
  86. $this->_debug = null;
  87. }
  88. /**
  89. * Ovewrite to allow custom enhancements
  90. *
  91. * @param mixed $config
  92. * @return string|null|self
  93. */
  94. public function profile($config = null) {
  95. if ($config === null) {
  96. return $this->_profile;
  97. }
  98. if (!is_array($config)) {
  99. $config = (string)$config;
  100. }
  101. $this->_applyConfig($config);
  102. if ($fromEmail = Configure::read('Config.systemEmail')) {
  103. $fromName = Configure::read('Config.systemName');
  104. } else {
  105. $fromEmail = Configure::read('Config.adminEmail');
  106. $fromName = Configure::read('Config.adminName');
  107. }
  108. if ($fromEmail) {
  109. $this->from($fromEmail, $fromName);
  110. }
  111. if ($xMailer = Configure::read('Config.xMailer')) {
  112. $this->addHeaders(['X-Mailer' => $xMailer]);
  113. }
  114. return $this;
  115. }
  116. /**
  117. * Overwrite to allow mimetype detection
  118. *
  119. * @param mixed $attachments
  120. * @return self
  121. */
  122. public function attachments($attachments = null) {
  123. if ($attachments === null) {
  124. return $this->_attachments;
  125. }
  126. $attach = [];
  127. foreach ((array)$attachments as $name => $fileInfo) {
  128. if (!is_array($fileInfo)) {
  129. $fileInfo = ['file' => $fileInfo];
  130. }
  131. if (!isset($fileInfo['file'])) {
  132. if (!isset($fileInfo['data'])) {
  133. throw new InvalidArgumentException('No file or data specified.');
  134. }
  135. if (is_int($name)) {
  136. throw new InvalidArgumentException('No filename specified.');
  137. }
  138. $fileInfo['data'] = chunk_split(base64_encode($fileInfo['data']), 76, "\r\n");
  139. } else {
  140. $fileName = $fileInfo['file'];
  141. $fileInfo['file'] = realpath($fileInfo['file']);
  142. if ($fileInfo['file'] === false || !file_exists($fileInfo['file'])) {
  143. throw new InvalidArgumentException(sprintf('File not found: "%s"', $fileName));
  144. }
  145. if (is_int($name)) {
  146. $name = basename($fileInfo['file']);
  147. }
  148. }
  149. if (!isset($fileInfo['mimetype'])) {
  150. $ext = pathinfo($name, PATHINFO_EXTENSION);
  151. $fileInfo['mimetype'] = $this->_getMimeByExtension($ext);
  152. }
  153. $attach[$name] = $fileInfo;
  154. }
  155. $this->_attachments = $attach;
  156. return $this;
  157. }
  158. /**
  159. * Add an attachment from file
  160. *
  161. * @param string $file: absolute path
  162. * @param string $name
  163. * @param array $fileInfo
  164. * @return self
  165. */
  166. public function addAttachment($file, $name = null, $fileInfo = []) {
  167. $fileInfo['file'] = $file;
  168. if (!empty($name)) {
  169. $fileInfo = [$name => $fileInfo];
  170. } else {
  171. $fileInfo = [$fileInfo];
  172. }
  173. return $this->addAttachments($fileInfo);
  174. }
  175. /**
  176. * Add an attachment as blob
  177. *
  178. * @param binary $content: blob data
  179. * @param string $filename to attach it
  180. * @param string $mimeType (leave it empty to get mimetype from $filename)
  181. * @param array $fileInfo
  182. * @return self
  183. */
  184. public function addBlobAttachment($content, $filename, $mimeType = null, $fileInfo = []) {
  185. if ($mimeType === null) {
  186. $ext = pathinfo($filename, PATHINFO_EXTENSION);
  187. $mimeType = $this->_getMimeByExtension($ext);
  188. }
  189. $fileInfo['data'] = $content;
  190. $fileInfo['mimetype'] = $mimeType;
  191. $file = [$filename => $fileInfo];
  192. return $this->addAttachments($file);
  193. }
  194. /**
  195. * Add an inline attachment from file
  196. *
  197. * Options:
  198. * - mimetype
  199. * - contentDisposition
  200. *
  201. * @param string $file: absolute path
  202. * @param string $name (optional)
  203. * @param string $contentId (optional)
  204. * @param array $options Options
  205. * @return string|self $contentId or $this
  206. */
  207. public function addEmbeddedAttachment($file, $name = null, $contentId = null, $options = []) {
  208. if (empty($name)) {
  209. $name = basename($file);
  210. }
  211. if ($contentId === null && ($cid = $this->_isEmbeddedAttachment($file, $name))) {
  212. return $cid;
  213. }
  214. $options['file'] = $file;
  215. if (empty($options['mimetype'])) {
  216. $options['mimetype'] = $this->_getMime($file);
  217. }
  218. $options['contentId'] = $contentId ? $contentId : str_replace('-', '', Text::uuid()) . '@' . $this->_domain;
  219. $file = [$name => $options];
  220. $res = $this->addAttachments($file);
  221. if ($contentId === null) {
  222. return $options['contentId'];
  223. }
  224. return $res;
  225. }
  226. /**
  227. * Add an inline attachment as blob
  228. *
  229. * Options:
  230. * - contentDisposition
  231. *
  232. * @param binary $content: blob data
  233. * @param string $filename to attach it
  234. * @param string $mimeType (leave it empty to get mimetype from $filename)
  235. * @param string $contentId (optional)
  236. * @param array $options Options
  237. * @return string|self $contentId or $this
  238. */
  239. public function addEmbeddedBlobAttachment($content, $filename, $mimeType = null, $contentId = null, $options = []) {
  240. if ($mimeType === null) {
  241. $ext = pathinfo($filename, PATHINFO_EXTENSION);
  242. $mimeType = $this->_getMimeByExtension($ext);
  243. }
  244. $options['data'] = $content;
  245. $options['mimetype'] = $mimeType;
  246. $options['contentId'] = $contentId ? $contentId : str_replace('-', '', Text::uuid()) . '@' . $this->_domain;
  247. $file = [$filename => $options];
  248. $res = $this->addAttachments($file);
  249. if ($contentId === null) {
  250. return $options['contentId'];
  251. }
  252. return $res;
  253. }
  254. /**
  255. * Returns if this particular file has already been attached as embedded file with this exact name
  256. * to prevent the same image to overwrite each other and also to only send this image once.
  257. * Allows multiple usage of the same embedded image (using the same cid)
  258. *
  259. * @param string $file
  260. * @param string $name
  261. * @return bool|string Cid of the found file or false if no such attachment can be found
  262. */
  263. protected function _isEmbeddedAttachment($file, $name) {
  264. foreach ($this->_attachments as $filename => $fileInfo) {
  265. if ($filename !== $name) {
  266. continue;
  267. }
  268. if ($fileInfo['file'] === $file) {
  269. return $fileInfo['contentId'];
  270. }
  271. }
  272. return false;
  273. }
  274. /**
  275. * @param $ext
  276. * @param string $default
  277. * @return mixed
  278. */
  279. protected function _getMimeByExtension($ext, $default = 'application/octet-stream') {
  280. if (!isset($this->_Mime)) {
  281. $this->_Mime = new Mime();
  282. }
  283. $mime = $this->_Mime->getMimeTypeByAlias($ext);
  284. if (!$mime) {
  285. $mime = $default;
  286. }
  287. return $mime;
  288. }
  289. /**
  290. * Try to find mimetype by file extension
  291. *
  292. * @param string $ext lowercase (jpg, png, pdf, ...)
  293. * @param string $defaultMimeType
  294. * @return string Mimetype (falls back to `application/octet-stream`)
  295. */
  296. protected function _getMime($filename, $default = 'application/octet-stream') {
  297. if (!isset($this->_Mime)) {
  298. $this->_Mime = new Mime();
  299. }
  300. $mime = $this->_Mime->detectMimeType($filename);
  301. // Some environments falsely return the default too fast, better fallback to extension here
  302. if (!$mime || $mime === $default) {
  303. $ext = pathinfo($filename, PATHINFO_EXTENSION);
  304. $mime = $this->_Mime->getMimeTypeByAlias($ext);
  305. }
  306. return $mime;
  307. }
  308. /**
  309. * Read the file contents and return a base64 version of the file contents.
  310. * Overwrite parent to avoid File class and file_exists to false negative existent
  311. * remove images.
  312. * Also fixes file_get_contents (used via File class) to close the connection again
  313. * after getting remote files. So far it would have kept the connection open in HTTP/1.1.
  314. *
  315. * @param string $path The absolute path to the file to read.
  316. * @return string File contents in base64 encoding
  317. */
  318. protected function _readFile($path) {
  319. $context = stream_context_create(
  320. ['http' => ['header' => 'Connection: close']]);
  321. $content = file_get_contents($path, 0, $context);
  322. if (!$content) {
  323. trigger_error('No content found for ' . $path);
  324. }
  325. return chunk_split(base64_encode($content));
  326. }
  327. /**
  328. * Validate if the email has the required fields necessary to make send() work.
  329. * Assumes layouting (does not check on content to be present or if view/layout files are missing).
  330. *
  331. * @return bool Success
  332. */
  333. public function validates() {
  334. if (!empty($this->_subject) && !empty($this->_to)) {
  335. return true;
  336. }
  337. return false;
  338. }
  339. /**
  340. * Set the body of the mail as we send it.
  341. * Note: the text can be an array, each element will appear as a seperate line in the message body.
  342. *
  343. * Do NOT pass a message if you use $this->set() in combination with templates
  344. *
  345. * @overwrite
  346. * @param string/array: message
  347. * @return bool Success
  348. */
  349. public function send($message = null) {
  350. $this->_log = [
  351. 'to' => $this->_to,
  352. 'from' => $this->_from,
  353. 'sender' => $this->_sender,
  354. 'replyTo' => $this->_replyTo,
  355. 'cc' => $this->_cc,
  356. 'subject' => $this->_subject,
  357. 'bcc' => $this->_bcc,
  358. 'transport' => get_class($this->_transport),
  359. ];
  360. if ($this->_priority) {
  361. $this->_headers['X-Priority'] = $this->_priority;
  362. //$this->_headers['X-MSMail-Priority'] = 'High';
  363. //$this->_headers['Importance'] = 'High';
  364. }
  365. // if not live, just log but do not send any mails //TODO: remove and use Debug Transport!
  366. if (!Configure::read('Config.live')) {
  367. $this->_logEmail();
  368. return true;
  369. }
  370. // Security measure to not sent to the actual addressee in debug mode while email sending is live
  371. if (Configure::read('debug') && Configure::read('Config.live')) {
  372. $adminEmail = Configure::read('Config.adminEmail');
  373. if (!$adminEmail) {
  374. $adminEmail = Configure::read('Config.systemEmail');
  375. }
  376. foreach ($this->_to as $k => $v) {
  377. if ($k === $adminEmail) {
  378. continue;
  379. }
  380. unset($this->_to[$k]);
  381. $this->_to[$adminEmail] = $v;
  382. }
  383. foreach ($this->_cc as $k => $v) {
  384. if ($k === $adminEmail) {
  385. continue;
  386. }
  387. unset($this->_cc[$k]);
  388. $this->_cc[$adminEmail] = $v;
  389. }
  390. foreach ($this->_bcc as $k => $v) {
  391. if ($k === $adminEmail) {
  392. continue;
  393. }
  394. unset($this->_bcc[$k]);
  395. $this->_bcc[] = $v;
  396. }
  397. }
  398. try {
  399. $this->_debug = parent::send($message);
  400. } catch (\Exception $e) {
  401. $this->_error = $e->getMessage();
  402. $this->_error .= ' (line ' . $e->getLine() . ' in ' . $e->getFile() . ')' . PHP_EOL .
  403. $e->getTraceAsString();
  404. // always log report
  405. $this->_logEmail(LogLevel::ERROR);
  406. // log error
  407. $this->log($this->_error, LogLevel::ERROR);
  408. return false;
  409. }
  410. if (!empty($this->_profile['logReport'])) {
  411. $this->_logEmail();
  412. }
  413. return true;
  414. }
  415. /**
  416. * @param string $level
  417. * @return void
  418. */
  419. protected function _logEmail($level = LogLevel::INFO)
  420. {
  421. $content =
  422. $this->_log['transport'] . (!Configure::read('Config.live') ? ' (simulated)' : '')
  423. . ' - ' . 'TO:' . implode(',', array_keys($this->_log['to']))
  424. . '||FROM:' . implode(',', array_keys($this->_log['from']))
  425. . '||REPLY:' . implode(',', array_keys($this->_log['replyTo']))
  426. . '||S:' . $this->_log['subject'];
  427. $this->log($content, $level);
  428. }
  429. /**
  430. * Attach inline/embedded files to the message.
  431. *
  432. * CUSTOM FIX: blob data support
  433. *
  434. * @override
  435. * @param string $boundary Boundary to use. If null, will default to $this->_boundary
  436. * @return array An array of lines to add to the message
  437. */
  438. protected function _attachInlineFiles($boundary = null) {
  439. if ($boundary === null) {
  440. $boundary = $this->_boundary;
  441. }
  442. $msg = [];
  443. foreach ($this->_attachments as $filename => $fileInfo) {
  444. if (empty($fileInfo['contentId'])) {
  445. continue;
  446. }
  447. if (!empty($fileInfo['data'])) {
  448. $data = $fileInfo['data'];
  449. $data = chunk_split(base64_encode($data));
  450. } elseif (!empty($fileInfo['file'])) {
  451. $data = $this->_readFile($fileInfo['file']);
  452. } else {
  453. continue;
  454. }
  455. $msg[] = '--' . $boundary;
  456. $msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
  457. $msg[] = 'Content-Transfer-Encoding: base64';
  458. $msg[] = 'Content-ID: <' . $fileInfo['contentId'] . '>';
  459. $msg[] = 'Content-Disposition: inline; filename="' . $filename . '"';
  460. $msg[] = '';
  461. $msg[] = $data;
  462. $msg[] = '';
  463. }
  464. return $msg;
  465. }
  466. /**
  467. * Attach non-embedded files by adding file contents inside boundaries.
  468. *
  469. * CUSTOM FIX: blob data support
  470. *
  471. * @override
  472. * @param string $boundary Boundary to use. If null, will default to $this->_boundary
  473. * @return array An array of lines to add to the message
  474. */
  475. protected function _attachFiles($boundary = null) {
  476. if ($boundary === null) {
  477. $boundary = $this->_boundary;
  478. }
  479. $msg = [];
  480. foreach ($this->_attachments as $filename => $fileInfo) {
  481. if (!empty($fileInfo['contentId'])) {
  482. continue;
  483. }
  484. if (!empty($fileInfo['data'])) {
  485. $data = $fileInfo['data'];
  486. $data = chunk_split(base64_encode($data));
  487. } elseif (!empty($fileInfo['file'])) {
  488. $data = $this->_readFile($fileInfo['file']);
  489. } else {
  490. continue;
  491. }
  492. $msg[] = '--' . $boundary;
  493. $msg[] = 'Content-Type: ' . $fileInfo['mimetype'];
  494. $msg[] = 'Content-Transfer-Encoding: base64';
  495. if (
  496. !isset($fileInfo['contentDisposition']) ||
  497. $fileInfo['contentDisposition']
  498. ) {
  499. $msg[] = 'Content-Disposition: attachment; filename="' . $filename . '"';
  500. }
  501. $msg[] = '';
  502. $msg[] = $data;
  503. $msg[] = '';
  504. }
  505. return $msg;
  506. }
  507. /**
  508. * Returns the error if existent
  509. *
  510. * @return string
  511. */
  512. public function getError() {
  513. return $this->_error;
  514. }
  515. }