Email.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. <?php
  2. namespace Grav\Plugin\Email;
  3. use Grav\Common\Config\Config;
  4. use Grav\Common\Grav;
  5. use Grav\Common\Language\Language;
  6. use Grav\Common\Markdown\Parsedown;
  7. use Grav\Common\Twig\Twig;
  8. use Grav\Framework\Form\Interfaces\FormInterface;
  9. use \Monolog\Logger;
  10. use \Monolog\Handler\StreamHandler;
  11. class Email
  12. {
  13. /**
  14. * @var \Swift_Transport
  15. */
  16. protected $mailer;
  17. /**
  18. * @var \Swift_Plugins_LoggerPlugin
  19. */
  20. protected $logger;
  21. protected $queue_path;
  22. /**
  23. * Returns true if emails have been enabled in the system.
  24. *
  25. * @return bool
  26. */
  27. public static function enabled()
  28. {
  29. return Grav::instance()['config']->get('plugins.email.mailer.engine') !== 'none';
  30. }
  31. /**
  32. * Returns true if debugging on emails has been enabled.
  33. *
  34. * @return bool
  35. */
  36. public static function debug()
  37. {
  38. return Grav::instance()['config']->get('plugins.email.debug') == 'true';
  39. }
  40. /**
  41. * Creates an email message.
  42. *
  43. * @param string $subject
  44. * @param string $body
  45. * @param string $contentType
  46. * @param string $charset
  47. * @return \Swift_Message
  48. */
  49. public function message($subject = null, $body = null, $contentType = null, $charset = null)
  50. {
  51. return new \Swift_Message($subject, $body, $contentType, $charset);
  52. }
  53. /**
  54. * Creates an attachment.
  55. *
  56. * @param string $data
  57. * @param string $filename
  58. * @param string $contentType
  59. * @return \Swift_Attachment
  60. */
  61. public function attachment($data = null, $filename = null, $contentType = null)
  62. {
  63. return new \Swift_Attachment($data, $filename, $contentType);
  64. }
  65. /**
  66. * Creates an embedded attachment.
  67. *
  68. * @param string $data
  69. * @param string $filename
  70. * @param string $contentType
  71. * @return \Swift_EmbeddedFile
  72. */
  73. public function embedded($data = null, $filename = null, $contentType = null)
  74. {
  75. return new \Swift_EmbeddedFile($data, $filename, $contentType);
  76. }
  77. /**
  78. * Creates an image attachment.
  79. *
  80. * @param string $data
  81. * @param string $filename
  82. * @param string $contentType
  83. * @return \Swift_Image
  84. */
  85. public function image($data = null, $filename = null, $contentType = null)
  86. {
  87. return new \Swift_Image($data, $filename, $contentType);
  88. }
  89. /**
  90. * Send email.
  91. *
  92. * @param \Swift_Message $message
  93. * @param array|null $failedRecipients
  94. * @return int
  95. */
  96. public function send($message, &$failedRecipients = null)
  97. {
  98. $mailer = $this->getMailer();
  99. $result = $mailer ? $mailer->send($message, $failedRecipients) : 0;
  100. // Check if emails and debugging are both enabled.
  101. if ($mailer && $this->debug()) {
  102. $log = new Logger('email');
  103. $locator = Grav::instance()['locator'];
  104. $log_file = $locator->findResource('log://email.log', true, true);
  105. $log->pushHandler(new StreamHandler($log_file, Logger::DEBUG));
  106. // Append the SwiftMailer log to the log.
  107. $log->addDebug($this->getLogs());
  108. }
  109. return $result;
  110. }
  111. /**
  112. * Build e-mail message.
  113. *
  114. * @param array $params
  115. * @param array $vars
  116. * @return \Swift_Message
  117. */
  118. public function buildMessage(array $params, array $vars = [])
  119. {
  120. /** @var Twig $twig */
  121. $twig = Grav::instance()['twig'];
  122. /** @var Config $config */
  123. $config = Grav::instance()['config'];
  124. /** @var Language $language */
  125. $language = Grav::instance()['language'];
  126. // Extend parameters with defaults.
  127. $params += [
  128. 'bcc' => $config->get('plugins.email.bcc', []),
  129. 'body' => $config->get('plugins.email.body', '{% include "forms/data.html.twig" %}'),
  130. 'cc' => $config->get('plugins.email.cc', []),
  131. 'cc_name' => $config->get('plugins.email.cc_name'),
  132. 'charset' => $config->get('plugins.email.charset', 'utf-8'),
  133. 'from' => $config->get('plugins.email.from'),
  134. 'from_name' => $config->get('plugins.email.from_name'),
  135. 'content_type' => $config->get('plugins.email.content_type', 'text/html'),
  136. 'reply_to' => $config->get('plugins.email.reply_to', []),
  137. 'reply_to_name' => $config->get('plugins.email.reply_to_name'),
  138. 'subject' => !empty($vars['form']) && $vars['form'] instanceof FormInterface ? $vars['form']->page()->title() : null,
  139. 'to' => $config->get('plugins.email.to'),
  140. 'to_name' => $config->get('plugins.email.to_name'),
  141. 'process_markdown' => false,
  142. 'template' => false
  143. ];
  144. // Create message object.
  145. $message = $this->message();
  146. if (!$params['to']) {
  147. throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_TO_ADDRESS'));
  148. }
  149. if (!$params['from']) {
  150. throw new \RuntimeException($language->translate('PLUGIN_EMAIL.PLEASE_CONFIGURE_A_FROM_ADDRESS'));
  151. }
  152. // make email configuration available to templates
  153. $vars += [
  154. 'email' => $params,
  155. ];
  156. // Process parameters.
  157. foreach ($params as $key => $value) {
  158. switch ($key) {
  159. case 'body':
  160. if (is_string($value)) {
  161. if (strpos($value, '{{') !== false || strpos($value, '{%') !== false) {
  162. $body = $twig->processString($value, $vars);
  163. } else {
  164. $body = $value;
  165. }
  166. if ($params['process_markdown'] && $params['content_type'] === 'text/html') {
  167. $parsedown = new Parsedown();
  168. $body = $parsedown->text($body);
  169. }
  170. if ($params['template']) {
  171. $body = $twig->processTemplate($params['template'], ['content' => $body] + $vars);
  172. }
  173. $content_type = !empty($params['content_type']) ? $twig->processString($params['content_type'], $vars) : null;
  174. $charset = !empty($params['charset']) ? $twig->processString($params['charset'], $vars) : null;
  175. $message->setBody($body, $content_type, $charset);
  176. } elseif (is_array($value)) {
  177. foreach ($value as $body_part) {
  178. $body_part += [
  179. 'charset' => $params['charset'],
  180. 'content_type' => $params['content_type'],
  181. ];
  182. $body = !empty($body_part['body']) ? $twig->processString($body_part['body'], $vars) : null;
  183. if ($params['process_markdown'] && $body_part['content_type'] === 'text/html') {
  184. $parsedown = new Parsedown();
  185. $body = $parsedown->text($body);
  186. }
  187. if (isset($body_part['template'])) {
  188. $body = $twig->processTemplate($body_part['template'], ['content' => $body] + $vars);
  189. }
  190. $content_type = !empty($body_part['content_type']) ? $twig->processString($body_part['content_type'], $vars) : null;
  191. $charset = !empty($body_part['charset']) ? $twig->processString($body_part['charset'], $vars) : null;
  192. if (!$message->getBody()) {
  193. $message->setBody($body, $content_type, $charset);
  194. }
  195. else {
  196. $message->addPart($body, $content_type, $charset);
  197. }
  198. }
  199. }
  200. break;
  201. case 'subject':
  202. $message->setSubject($twig->processString($language->translate($value), $vars));
  203. break;
  204. case 'to':
  205. if (is_string($value) && !empty($params['to_name'])) {
  206. $value = [
  207. 'mail' => $twig->processString($value, $vars),
  208. 'name' => $twig->processString($params['to_name'], $vars),
  209. ];
  210. }
  211. foreach ($this->parseAddressValue($value, $vars) as $address) {
  212. $message->addTo($address->mail, $address->name);
  213. }
  214. break;
  215. case 'cc':
  216. if (is_string($value) && !empty($params['cc_name'])) {
  217. $value = [
  218. 'mail' => $twig->processString($value, $vars),
  219. 'name' => $twig->processString($params['cc_name'], $vars),
  220. ];
  221. }
  222. foreach ($this->parseAddressValue($value, $vars) as $address) {
  223. $message->addCc($address->mail, $address->name);
  224. }
  225. break;
  226. case 'bcc':
  227. foreach ($this->parseAddressValue($value, $vars) as $address) {
  228. $message->addBcc($address->mail, $address->name);
  229. }
  230. break;
  231. case 'from':
  232. if (is_string($value) && !empty($params['from_name'])) {
  233. $value = [
  234. 'mail' => $twig->processString($value, $vars),
  235. 'name' => $twig->processString($params['from_name'], $vars),
  236. ];
  237. }
  238. foreach ($this->parseAddressValue($value, $vars) as $address) {
  239. $message->addFrom($address->mail, $address->name);
  240. }
  241. break;
  242. case 'reply_to':
  243. if (is_string($value) && !empty($params['reply_to_name'])) {
  244. $value = [
  245. 'mail' => $twig->processString($value, $vars),
  246. 'name' => $twig->processString($params['reply_to_name'], $vars),
  247. ];
  248. }
  249. foreach ($this->parseAddressValue($value, $vars) as $address) {
  250. $message->addReplyTo($address->mail, $address->name);
  251. }
  252. break;
  253. }
  254. }
  255. return $message;
  256. }
  257. /**
  258. * Return parsed e-mail address value.
  259. *
  260. * @param string|string[] $value
  261. * @param array $vars
  262. * @return array
  263. */
  264. public function parseAddressValue($value, array $vars = [])
  265. {
  266. $parsed = [];
  267. /** @var Twig $twig */
  268. $twig = Grav::instance()['twig'];
  269. // Single e-mail address string
  270. if (is_string($value)) {
  271. $parsed[] = (object) [
  272. 'mail' => $twig->processString($value, $vars),
  273. 'name' => null,
  274. ];
  275. }
  276. else {
  277. // Cast value as array
  278. $value = (array) $value;
  279. // Single e-mail address array
  280. if (!empty($value['mail'])) {
  281. $parsed[] = (object) [
  282. 'mail' => $twig->processString($value['mail'], $vars),
  283. 'name' => !empty($value['name']) ? $twig->processString($value['name'], $vars) : NULL,
  284. ];
  285. }
  286. // Multiple addresses (either as strings or arrays)
  287. elseif (!(empty($value['mail']) && !empty($value['name']))) {
  288. foreach ($value as $y => $itemx) {
  289. $addresses = $this->parseAddressValue($itemx, $vars);
  290. if (($address = reset($addresses))) {
  291. $parsed[] = $address;
  292. }
  293. }
  294. }
  295. }
  296. return $parsed;
  297. }
  298. /**
  299. * Return debugging logs if enabled
  300. *
  301. * @return string
  302. */
  303. public function getLogs()
  304. {
  305. if ($this->debug()) {
  306. return $this->logger->dump();
  307. }
  308. return '';
  309. }
  310. /**
  311. * @internal
  312. * @return null|\Swift_Mailer
  313. */
  314. protected function getMailer()
  315. {
  316. if (!$this->enabled()) {
  317. return null;
  318. }
  319. if (!$this->mailer) {
  320. /** @var Config $config */
  321. $config = Grav::instance()['config'];
  322. $queue_enabled = $config->get('plugins.email.queue.enabled');
  323. $transport = $queue_enabled === true ? $this->getQueue() : $this->getTransport();
  324. // Create the Mailer using your created Transport
  325. $this->mailer = new \Swift_Mailer($transport);
  326. // Register the logger if we're debugging.
  327. if ($this->debug()) {
  328. $this->logger = new \Swift_Plugins_Loggers_ArrayLogger();
  329. $this->mailer->registerPlugin(new \Swift_Plugins_LoggerPlugin($this->logger));
  330. }
  331. }
  332. return $this->mailer;
  333. }
  334. protected static function getQueuePath()
  335. {
  336. $queue_path = Grav::instance()['locator']->findResource('user://data', true) . '/email-queue';
  337. if (!file_exists($queue_path)) {
  338. mkdir($queue_path);
  339. }
  340. return $queue_path;
  341. }
  342. protected static function getQueue()
  343. {
  344. $queue_path = static::getQueuePath();
  345. $spool = new \Swift_FileSpool($queue_path);
  346. $transport = new \Swift_SpoolTransport($spool);
  347. return $transport;
  348. }
  349. public static function flushQueue()
  350. {
  351. $grav = Grav::instance();
  352. $grav['debugger']->enabled(false);
  353. $config = $grav['config']->get('plugins.email.queue');
  354. try {
  355. $queue = static::getQueue();
  356. $spool = $queue->getSpool();
  357. $spool->setMessageLimit($config['flush_msg_limit']);
  358. $spool->setTimeLimit($config['flush_time_limit']);
  359. $failures = [];
  360. $result = $spool->flushQueue(static::getTransport(), $failures);
  361. return $result . ' messages flushed from queue...';
  362. } catch (\Exception $e) {
  363. $grav['log']->error($e->getMessage());
  364. return $e->getMessage();
  365. }
  366. }
  367. public static function clearQueueFailures()
  368. {
  369. $grav = Grav::instance();
  370. $grav['debugger']->enabled(false);
  371. $preferences = \Swift_Preferences::getInstance();
  372. $preferences->setTempDir(sys_get_temp_dir());
  373. /** @var \Swift_Transport $transport */
  374. $transport = static::getTransport();
  375. if (!$transport->isStarted()) {
  376. $transport->start();
  377. }
  378. $queue_path = static::getQueuePath();
  379. foreach (new \GlobIterator($queue_path . '/*.sending') as $file) {
  380. $final_message = $file->getPathname();
  381. /** @var \Swift_Message $message */
  382. $message = unserialize(file_get_contents($final_message));
  383. echo(sprintf(
  384. 'Retrying "%s" to "%s"',
  385. $message->getSubject(),
  386. implode(', ', array_keys($message->getTo()))
  387. ) . "\n");
  388. try {
  389. $clean = static::cloneMessage($message);
  390. $transport->send($clean);
  391. echo("sent!\n");
  392. // DOn't want to trip up any errors from sending too fast
  393. sleep(1);
  394. } catch (\Swift_TransportException $e) {
  395. echo("ERROR: Send failed - deleting spooled message\n");
  396. }
  397. // Remove the file
  398. unlink($final_message);
  399. }
  400. }
  401. /**
  402. * Clean copy a message
  403. *
  404. * @param \Swift_Message $message
  405. */
  406. public static function cloneMessage($message)
  407. {
  408. $clean = new \Swift_Message();
  409. $clean->setBoundary($message->getBoundary());
  410. $clean->setBcc($message->getBcc());
  411. $clean->setBody($message->getBody());
  412. $clean->setCharset($message->getCharset());
  413. $clean->setChildren($message->getChildren());
  414. $clean->setContentType($message->getContentType());
  415. $clean->setCc($message->getCc());
  416. $clean->setDate($message->getDate());
  417. $clean->setDescription($message->getDescription());
  418. $clean->setEncoder($message->getEncoder());
  419. $clean->setFormat($message->getFormat());
  420. $clean->setFrom($message->getFrom());
  421. $clean->setId($message->getId());
  422. $clean->setMaxLineLength($message->getMaxLineLength());
  423. $clean->setPriority($message->getPriority());
  424. $clean->setReplyTo($message->getReplyTo());
  425. $clean->setReturnPath($message->getReturnPath());
  426. $clean->setSender($message->getSender());
  427. $clean->setSubject($message->getSubject());
  428. $clean->setTo($message->getTo());
  429. $clean->setAuthMode($message->getAuthMode());
  430. return $clean;
  431. }
  432. protected static function getTransport()
  433. {
  434. /** @var Config $config */
  435. $config = Grav::instance()['config'];
  436. $engine = $config->get('plugins.email.mailer.engine');
  437. // Create the Transport and initialize it.
  438. switch ($engine) {
  439. case 'smtp':
  440. $transport = new \Swift_SmtpTransport();
  441. $options = $config->get('plugins.email.mailer.smtp');
  442. if (!empty($options['server'])) {
  443. $transport->setHost($options['server']);
  444. }
  445. if (!empty($options['port'])) {
  446. $transport->setPort($options['port']);
  447. }
  448. if (!empty($options['encryption']) && $options['encryption'] !== 'none') {
  449. $transport->setEncryption($options['encryption']);
  450. }
  451. if (!empty($options['user'])) {
  452. $transport->setUsername($options['user']);
  453. }
  454. if (!empty($options['password'])) {
  455. $transport->setPassword($options['password']);
  456. }
  457. if (!empty($options['auth_mode'])) {
  458. $transport->setAuthMode($options['auth_mode']);
  459. }
  460. break;
  461. case 'sendmail':
  462. default:
  463. $options = $config->get('plugins.email.mailer.sendmail');
  464. $bin = !empty($options['bin']) ? $options['bin'] : '/usr/sbin/sendmail';
  465. $transport = new \Swift_SendmailTransport($bin);
  466. break;
  467. }
  468. return $transport;
  469. }
  470. }