Email.php 15 KB

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