Email.php 18 KB

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