NotificationEmail.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Bridge\Twig\Mime;
  11. use Symfony\Component\ErrorHandler\Exception\FlattenException;
  12. use Symfony\Component\Mime\Header\Headers;
  13. use Symfony\Component\Mime\Part\AbstractPart;
  14. use Twig\Extra\CssInliner\CssInlinerExtension;
  15. use Twig\Extra\Inky\InkyExtension;
  16. use Twig\Extra\Markdown\MarkdownExtension;
  17. /**
  18. * @author Fabien Potencier <fabien@symfony.com>
  19. */
  20. class NotificationEmail extends TemplatedEmail
  21. {
  22. public const IMPORTANCE_URGENT = 'urgent';
  23. public const IMPORTANCE_HIGH = 'high';
  24. public const IMPORTANCE_MEDIUM = 'medium';
  25. public const IMPORTANCE_LOW = 'low';
  26. private $theme = 'default';
  27. private $context = [
  28. 'importance' => self::IMPORTANCE_LOW,
  29. 'content' => '',
  30. 'exception' => false,
  31. 'action_text' => null,
  32. 'action_url' => null,
  33. 'markdown' => false,
  34. 'raw' => false,
  35. 'footer_text' => 'Notification e-mail sent by Symfony',
  36. ];
  37. public function __construct(Headers $headers = null, AbstractPart $body = null)
  38. {
  39. $missingPackages = [];
  40. if (!class_exists(CssInlinerExtension::class)) {
  41. $missingPackages['twig/cssinliner-extra'] = ' CSS Inliner';
  42. }
  43. if (!class_exists(InkyExtension::class)) {
  44. $missingPackages['twig/inky-extra'] = 'Inky';
  45. }
  46. if ($missingPackages) {
  47. throw new \LogicException(sprintf('You cannot use "%s" if the "%s" Twig extension%s not available; try running "%s".', static::class, implode('" and "', $missingPackages), \count($missingPackages) > 1 ? 's are' : ' is', 'composer require '.implode(' ', array_keys($missingPackages))));
  48. }
  49. parent::__construct($headers, $body);
  50. }
  51. /**
  52. * Creates a NotificationEmail instance that is appropriate to send to normal (non-admin) users.
  53. */
  54. public static function asPublicEmail(Headers $headers = null, AbstractPart $body = null): self
  55. {
  56. $email = new static($headers, $body);
  57. $email->context['importance'] = null;
  58. $email->context['footer_text'] = null;
  59. return $email;
  60. }
  61. /**
  62. * @return $this
  63. */
  64. public function markdown(string $content)
  65. {
  66. if (!class_exists(MarkdownExtension::class)) {
  67. throw new \LogicException(sprintf('You cannot use "%s" if the Markdown Twig extension is not available; try running "composer require twig/markdown-extra".', __METHOD__));
  68. }
  69. $this->context['markdown'] = true;
  70. return $this->content($content);
  71. }
  72. /**
  73. * @return $this
  74. */
  75. public function content(string $content, bool $raw = false)
  76. {
  77. $this->context['content'] = $content;
  78. $this->context['raw'] = $raw;
  79. return $this;
  80. }
  81. /**
  82. * @return $this
  83. */
  84. public function action(string $text, string $url)
  85. {
  86. $this->context['action_text'] = $text;
  87. $this->context['action_url'] = $url;
  88. return $this;
  89. }
  90. /**
  91. * @return $this
  92. */
  93. public function importance(string $importance)
  94. {
  95. $this->context['importance'] = $importance;
  96. return $this;
  97. }
  98. /**
  99. * @param \Throwable|FlattenException $exception
  100. *
  101. * @return $this
  102. */
  103. public function exception($exception)
  104. {
  105. if (!$exception instanceof \Throwable && !$exception instanceof FlattenException) {
  106. throw new \LogicException(sprintf('"%s" accepts "%s" or "%s" instances.', __METHOD__, \Throwable::class, FlattenException::class));
  107. }
  108. $exceptionAsString = $this->getExceptionAsString($exception);
  109. $this->context['exception'] = true;
  110. $this->attach($exceptionAsString, 'exception.txt', 'text/plain');
  111. $this->importance(self::IMPORTANCE_URGENT);
  112. if (!$this->getSubject()) {
  113. $this->subject($exception->getMessage());
  114. }
  115. return $this;
  116. }
  117. /**
  118. * @return $this
  119. */
  120. public function theme(string $theme)
  121. {
  122. $this->theme = $theme;
  123. return $this;
  124. }
  125. public function getTextTemplate(): ?string
  126. {
  127. if ($template = parent::getTextTemplate()) {
  128. return $template;
  129. }
  130. return '@email/'.$this->theme.'/notification/body.txt.twig';
  131. }
  132. public function getHtmlTemplate(): ?string
  133. {
  134. if ($template = parent::getHtmlTemplate()) {
  135. return $template;
  136. }
  137. return '@email/'.$this->theme.'/notification/body.html.twig';
  138. }
  139. public function getContext(): array
  140. {
  141. return array_merge($this->context, parent::getContext());
  142. }
  143. public function getPreparedHeaders(): Headers
  144. {
  145. $headers = parent::getPreparedHeaders();
  146. $importance = $this->context['importance'] ?? self::IMPORTANCE_LOW;
  147. $this->priority($this->determinePriority($importance));
  148. if ($this->context['importance']) {
  149. $headers->setHeaderBody('Text', 'Subject', sprintf('[%s] %s', strtoupper($importance), $this->getSubject()));
  150. }
  151. return $headers;
  152. }
  153. private function determinePriority(string $importance): int
  154. {
  155. switch ($importance) {
  156. case self::IMPORTANCE_URGENT:
  157. return self::PRIORITY_HIGHEST;
  158. case self::IMPORTANCE_HIGH:
  159. return self::PRIORITY_HIGH;
  160. case self::IMPORTANCE_MEDIUM:
  161. return self::PRIORITY_NORMAL;
  162. case self::IMPORTANCE_LOW:
  163. default:
  164. return self::PRIORITY_LOW;
  165. }
  166. }
  167. private function getExceptionAsString($exception): string
  168. {
  169. if (class_exists(FlattenException::class)) {
  170. $exception = $exception instanceof FlattenException ? $exception : FlattenException::createFromThrowable($exception);
  171. return $exception->getAsString();
  172. }
  173. $message = \get_class($exception);
  174. if ('' !== $exception->getMessage()) {
  175. $message .= ': '.$exception->getMessage();
  176. }
  177. $message .= ' in '.$exception->getFile().':'.$exception->getLine()."\n";
  178. $message .= "Stack trace:\n".$exception->getTraceAsString()."\n\n";
  179. return rtrim($message);
  180. }
  181. /**
  182. * @internal
  183. */
  184. public function __serialize(): array
  185. {
  186. return [$this->context, parent::__serialize()];
  187. }
  188. /**
  189. * @internal
  190. */
  191. public function __unserialize(array $data): void
  192. {
  193. [$this->context, $parentData] = $data;
  194. parent::__unserialize($parentData);
  195. }
  196. }