Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <?php
  2. namespace Grav\Plugin\Email;
  3. use Grav\Common\Config\Config;
  4. use Grav\Common\Grav;
  5. use Grav\Common\Utils;
  6. use Grav\Common\Language\Language;
  7. use Grav\Common\Markdown\Parsedown;
  8. use Grav\Common\Twig\Twig;
  9. use Grav\Framework\Form\Interfaces\FormInterface;
  10. use \Monolog\Logger;
  11. use \Monolog\Handler\StreamHandler;
  12. use RocketTheme\Toolbox\Event\Event;
  13. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  14. use Symfony\Component\Mailer\Envelope;
  15. use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
  16. use Symfony\Component\Mailer\Header\MetadataHeader;
  17. use Symfony\Component\Mailer\Header\TagHeader;
  18. use Symfony\Component\Mailer\Mailer;
  19. use Symfony\Component\Mailer\Transport;
  20. use Symfony\Component\Mailer\Transport\TransportInterface;
  21. use Symfony\Component\Mime\Address;
  22. class Email
  23. {
  24. /** @var Mailer */
  25. protected $mailer;
  26. /** @var TransportInterface */
  27. protected $transport;
  28. protected $log;
  29. public function __construct()
  30. {
  31. $this->initMailer();
  32. $this->initLog();
  33. }
  34. /**
  35. * Returns true if emails have been enabled in the system.
  36. *
  37. * @return bool
  38. */
  39. public static function enabled(): bool
  40. {
  41. return Grav::instance()['config']->get('plugins.email.mailer.engine') !== 'none';
  42. }
  43. /**
  44. * Returns true if debugging on emails has been enabled.
  45. *
  46. * @return bool
  47. */
  48. public static function debug(): bool
  49. {
  50. return Grav::instance()['config']->get('plugins.email.debug') == 'true';
  51. }
  52. /**
  53. * Creates an email message.
  54. *
  55. * @param string|null $subject
  56. * @param string|null $body
  57. * @param string|null $contentType
  58. * @param string|null $charset @deprecated
  59. * @return Message
  60. */
  61. public function message(string $subject = null, string $body = null, string $contentType = null, string $charset = null): Message
  62. {
  63. $message = new Message();
  64. $message->subject($subject);
  65. if ($contentType === 'text/html') {
  66. $message->html($body);
  67. } else {
  68. $message->text($body);
  69. }
  70. return $message;
  71. }
  72. /**
  73. * Send email.
  74. *
  75. * @param Message $message
  76. * @param Envelope|null $envelope
  77. * @return int
  78. */
  79. public function send(Message $message, Envelope $envelope = null): int
  80. {
  81. $status = '🛑 ';
  82. $sent_msg = null;
  83. $debug = null;
  84. try {
  85. $sent_msg = $this->transport->send($message->getEmail(), $envelope);
  86. $return = 1;
  87. $status = '✅';
  88. $debug = $sent_msg->getDebug();
  89. } catch (TransportExceptionInterface $e) {
  90. $return = 0;
  91. $status .= $e->getMessage();
  92. $debug = $e->getDebug();
  93. }
  94. if ($this->debug()) {
  95. $log_msg = "Email sent to %s at %s -> %s\n%s";
  96. $to = $this->jsonifyRecipients($message->getEmail()->getTo());
  97. $msg = sprintf($log_msg, $to, date('Y-m-d H:i:s'), $status, $debug);
  98. $this->log->addInfo($msg);
  99. }
  100. return $return;
  101. }
  102. /**
  103. * Build e-mail message.
  104. *
  105. * @param array $params
  106. * @param array $vars
  107. * @return Message
  108. */
  109. public function buildMessage(array $params, array $vars = []): Message
  110. {
  111. /** @var Twig $twig */
  112. $twig = Grav::instance()['twig'];
  113. $twig->init();
  114. /** @var Config $config */
  115. $config = Grav::instance()['config'];
  116. /** @var Language $language */
  117. $language = Grav::instance()['language'];
  118. // Create message object.
  119. $message = new Message();
  120. $headers = $message->getEmail()->getHeaders();
  121. $email = $message->getEmail();
  122. // Extend parameters with defaults.
  123. $defaults = [
  124. 'bcc' => $config->get('plugins.email.bcc', []),
  125. 'bcc_name' => $config->get('plugins.email.bcc_name'),
  126. 'body' => $config->get('plugins.email.body', '{% include "forms/data.html.twig" %}'),
  127. 'cc' => $config->get('plugins.email.cc', []),
  128. 'cc_name' => $config->get('plugins.email.cc_name'),
  129. 'charset' => $config->get('plugins.email.charset', 'utf-8'),
  130. 'from' => $config->get('plugins.email.from'),
  131. 'from_name' => $config->get('plugins.email.from_name'),
  132. 'content_type' => $config->get('plugins.email.content_type', 'text/html'),
  133. 'reply_to' => $config->get('plugins.email.reply_to', []),
  134. 'reply_to_name' => $config->get('plugins.email.reply_to_name'),
  135. 'subject' => !empty($vars['form']) && $vars['form'] instanceof FormInterface ? $vars['form']->page()->title() : null,
  136. 'to' => $config->get('plugins.email.to'),
  137. 'to_name' => $config->get('plugins.email.to_name'),
  138. 'process_markdown' => false,
  139. 'template' => false,
  140. 'message' => $message
  141. ];
  142. foreach ($defaults as $key => $value) {
  143. if (!key_exists($key, $params)) {
  144. $params[$key] = $value;
  145. }
  146. }
  147. if (!$params['to']) {
  148. throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_TO_ADDRESS'));
  149. }
  150. if (!$params['from']) {
  151. throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_FROM_ADDRESS'));
  152. }
  153. // make email configuration available to templates
  154. $vars += [
  155. 'email' => $params,
  156. ];
  157. $params = $this->processParams($params, $vars);
  158. // Process parameters.
  159. foreach ($params as $key => $value) {
  160. switch ($key) {
  161. case 'body':
  162. if (is_string($value)) {
  163. $this->processBody($message, $params, $vars, $twig, $value);
  164. } elseif (is_array($value)) {
  165. foreach ($value as $body_part) {
  166. $params_part = $params;
  167. if (isset($body_part['content_type'])) {
  168. $params_part['content_type'] = $body_part['content_type'];
  169. }
  170. if (isset($body_part['template'])) {
  171. $params_part['template'] = $body_part['template'];
  172. }
  173. if (isset($body_part['body'])) {
  174. $this->processBody($message, $params_part, $vars, $twig, $body_part['body']);
  175. }
  176. }
  177. }
  178. break;
  179. case 'subject':
  180. if ($value) {
  181. $message->subject($language->translate($value));
  182. }
  183. break;
  184. case 'to':
  185. case 'from':
  186. case 'cc':
  187. case 'bcc':
  188. case 'reply_to':
  189. if ($recipients = $this->processRecipients($key, $params)) {
  190. $key = $key === 'reply_to' ? 'replyTo' : $key;
  191. $email->$key(...$recipients);
  192. }
  193. break;
  194. case 'tags':
  195. foreach ((array) $value as $tag) {
  196. if (is_string($tag)) {
  197. $headers->add(new TagHeader($tag));
  198. }
  199. }
  200. break;
  201. case 'metadata':
  202. foreach ((array) $value as $k => $v) {
  203. if (is_string($k) && is_string($v)) {
  204. $headers->add(new MetadataHeader($k, $v));
  205. }
  206. }
  207. break;
  208. }
  209. }
  210. return $message;
  211. }
  212. /**
  213. * @param string $type
  214. * @param array $params
  215. * @return array
  216. */
  217. protected function processRecipients(string $type, array $params): array
  218. {
  219. if (array_key_exists($type, $params) && $params[$type] === null) {
  220. return [];
  221. }
  222. $recipients = $params[$type] ?? Grav::instance()['config']->get('plugins.email.'.$type) ?? [];
  223. $list = [];
  224. if (!empty($recipients)) {
  225. if (is_array($recipients)) {
  226. if (Utils::isAssoc($recipients) || (count($recipients) ===2 && $this->isValidEmail($recipients[0]) && !$this->isValidEmail($recipients[1]))) {
  227. $list[] = $this->createAddress($recipients);
  228. } else {
  229. foreach ($recipients as $recipient) {
  230. $list[] = $this->createAddress($recipient);
  231. }
  232. }
  233. } else {
  234. if (is_string($recipients) && Utils::contains($recipients, ',')) {
  235. $recipients = array_map('trim', explode(',', $recipients));
  236. foreach ($recipients as $recipient) {
  237. $list[] = $this->createAddress($recipient);
  238. }
  239. } else {
  240. if (!Utils::contains($recipients, ['<','>']) && (isset($params[$type."_name"]))) {
  241. $recipients = [$recipients, $params[$type."_name"]];
  242. }
  243. $list[] = $this->createAddress($recipients);
  244. }
  245. }
  246. }
  247. return $list;
  248. }
  249. /**
  250. * @param $data
  251. * @return Address
  252. */
  253. protected function createAddress($data): Address
  254. {
  255. if (is_string($data)) {
  256. preg_match('/^(.*)\<(.*)\>$/', $data, $matches);
  257. if (isset($matches[2])) {
  258. $email = trim($matches[2]);
  259. $name = trim($matches[1]);
  260. } else {
  261. $email = $data;
  262. $name = '';
  263. }
  264. } elseif (Utils::isAssoc($data)) {
  265. $first_key = array_key_first($data);
  266. if (filter_var($first_key, FILTER_VALIDATE_EMAIL)) {
  267. $email = $first_key;
  268. $name = $data[$first_key];
  269. } else {
  270. $email = $data['email'] ?? $data['mail'] ?? $data['address'] ?? '';
  271. $name = $data['name'] ?? $data['fullname'] ?? '';
  272. }
  273. } else {
  274. $email = $data[0] ?? '';
  275. $name = $data[1] ?? '';
  276. }
  277. return new Address($email, $name);
  278. }
  279. /**
  280. * @return null|Mailer
  281. * @internal
  282. */
  283. protected function initMailer(): ?Mailer
  284. {
  285. if (!$this->enabled()) {
  286. return null;
  287. }
  288. if (!$this->mailer) {
  289. $this->transport = $this->getTransport();
  290. // Create the Mailer using your created Transport
  291. $this->mailer = new Mailer($this->transport);
  292. }
  293. return $this->mailer;
  294. }
  295. /**
  296. * @return void
  297. * @throws \Exception
  298. */
  299. protected function initLog()
  300. {
  301. $log_file = Grav::instance()['locator']->findResource('log://email.log', true, true);
  302. $this->log = new Logger('email');
  303. /** @var UniformResourceLocator $locator */
  304. $this->log->pushHandler(new StreamHandler($log_file, Logger::DEBUG));
  305. }
  306. /**
  307. * @param array $params
  308. * @param array $vars
  309. * @return array
  310. */
  311. protected function processParams(array $params, array $vars = []): array
  312. {
  313. $twig = Grav::instance()['twig'];
  314. array_walk_recursive($params, function(&$value) use ($twig, $vars) {
  315. if (is_string($value)) {
  316. $value = $twig->processString($value, $vars);
  317. }
  318. });
  319. return $params;
  320. }
  321. /**
  322. * @param $message
  323. * @param $params
  324. * @param $vars
  325. * @param $twig
  326. * @param $body
  327. * @return void
  328. */
  329. protected function processBody($message, $params, $vars, $twig, $body)
  330. {
  331. if ($params['process_markdown'] && $params['content_type'] === 'text/html') {
  332. $body = (new Parsedown())->text($body);
  333. }
  334. if ($params['template']) {
  335. $body = $twig->processTemplate($params['template'], ['content' => $body] + $vars);
  336. }
  337. $content_type = !empty($params['content_type']) ? $twig->processString($params['content_type'], $vars) : null;
  338. if ($content_type === 'text/html') {
  339. $message->html($body);
  340. } else {
  341. $message->text($body);
  342. }
  343. }
  344. /**
  345. * @return TransportInterface
  346. */
  347. protected static function getTransport(): Transport\TransportInterface
  348. {
  349. /** @var Config $config */
  350. $config = Grav::instance()['config'];
  351. $engine = $config->get('plugins.email.mailer.engine');
  352. $dsn = 'null://default';
  353. // Create the Transport and initialize it.
  354. switch ($engine) {
  355. case 'smtps':
  356. case 'smtp':
  357. $options = $config->get('plugins.email.mailer.smtp');
  358. $dsn = $engine . '://';
  359. $auth = '';
  360. if (isset($options['encryption']) && $options['encryption'] === 'none') {
  361. $options['options']['verify_peer'] = 0;
  362. }
  363. if (isset($options['user'])) {
  364. $auth .= urlencode($options['user']);
  365. }
  366. if (isset($options['password'])) {
  367. $auth .= ':'. urlencode($options['password']);
  368. }
  369. if (!empty($auth)) {
  370. $dsn .= "$auth@";
  371. }
  372. if (isset($options['server'])) {
  373. $dsn .= urlencode($options['server']);
  374. }
  375. if (isset($options['port'])) {
  376. $dsn .= ":{$options['port']}";
  377. }
  378. if (isset($options['options'])) {
  379. $dsn .= '?' . http_build_query($options['options']);
  380. }
  381. break;
  382. case 'mail':
  383. case 'native':
  384. $dsn = 'native://default';
  385. break;
  386. case 'sendmail':
  387. $dsn = 'sendmail://default';
  388. $bin = $config->get('plugins.email.mailer.sendmail.bin');
  389. if (isset($bin)) {
  390. $dsn .= '?command=' . urlencode($bin);
  391. }
  392. break;
  393. default:
  394. $e = new Event(['engine' => $engine, ]);
  395. Grav::instance()->fireEvent('onEmailTransportDsn', $e);
  396. if (isset($e['dsn'])) {
  397. $dsn = $e['dsn'];
  398. }
  399. break;
  400. }
  401. if ($dsn instanceof TransportInterface) {
  402. $transport = $dsn;
  403. } else {
  404. $transport = Transport::fromDsn($dsn) ;
  405. }
  406. return $transport;
  407. }
  408. /**
  409. * @param array $recipients
  410. * @return string
  411. */
  412. protected function jsonifyRecipients(array $recipients): string
  413. {
  414. $json = [];
  415. foreach ($recipients as $recipient) {
  416. $json[] = str_replace('"', "", $recipient->toString());
  417. }
  418. return json_encode($json);
  419. }
  420. protected function isValidEmail($email): bool
  421. {
  422. return is_string($email) && filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
  423. }
  424. /**
  425. * @return void
  426. * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
  427. */
  428. public static function flushQueue() {}
  429. /**
  430. * @return void
  431. * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
  432. */
  433. public static function clearQueueFailures() {}
  434. /**
  435. * Creates an attachment.
  436. *
  437. * @param string $data
  438. * @param string $filename
  439. * @param string $contentType
  440. * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
  441. * @return void
  442. */
  443. public function attachment($data = null, $filename = null, $contentType = null) {}
  444. /**
  445. * Creates an embedded attachment.
  446. *
  447. * @param string $data
  448. * @param string $filename
  449. * @param string $contentType
  450. * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
  451. * @return void
  452. */
  453. public function embedded($data = null, $filename = null, $contentType = null) {}
  454. /**
  455. * Creates an image attachment.
  456. *
  457. * @param string $data
  458. * @param string $filename
  459. * @param string $contentType
  460. * @deprecated 4.0 Switched from Swiftmailer to Symfony/Mailer - No longer supported
  461. * @return void
  462. */
  463. public function image($data = null, $filename = null, $contentType = null) {}
  464. }