Elgg  Version master
EmailService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
8 use Elgg\Traits\Loggable;
10 use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
11 use Symfony\Component\Mailer\MailerInterface;
12 use Symfony\Component\Mime\Email as SymfonyEmail;
13 
20 class EmailService {
21 
22  use Loggable;
23 
34  public function __construct(
35  protected Config $config,
36  protected EventsService $events,
37  protected MailerInterface $mailer,
38  protected HtmlFormatter $html_formatter,
39  protected ViewsService $views,
40  protected ImageFetcherService $image_fetcher
41  ) {
42  }
43 
52  public function send(Email $email): bool {
53  $email = $this->events->triggerResults('prepare', 'system:email', [], $email);
54  if (!$email instanceof Email) {
55  $msg = "'prepare','system:email' event handlers should return an instance of " . Email::class;
56  throw new RuntimeException($msg);
57  }
58 
59  $is_valid = $email->getFrom() && !empty($email->getTo());
60  if (!$this->events->triggerResults('validate', 'system:email', ['email' => $email], $is_valid)) {
61  return false;
62  }
63 
64  return $this->transport($email);
65  }
66 
74  public function transport(Email $email): bool {
75  if ($this->events->triggerResults('transport', 'system:email', ['email' => $email], false)) {
76  return true;
77  }
78 
79  // create the e-mail message
80  $message = new SymfonyEmail();
81  $message->sender($email->getSender());
82  $message->addFrom($email->getFrom());
83  $message->addTo(...$email->getTo());
84  $message->addCc(...$email->getCc());
85  $message->addBcc(...$email->getBcc());
86 
87  // set headers
88  $headers = [
89  'MIME-Version' => '1.0',
90  'Content-Transfer-Encoding' => '8bit',
91  ];
92  $headers = array_merge($headers, $email->getHeaders());
93 
94  foreach ($headers as $name => $value) {
95  $message->getHeaders()->addHeader($name, $value);
96  }
97 
98  // add the body to the message
99  $this->setMessageBody($message, $email);
100  $message->subject($this->prepareSubject($email->getSubject()));
101 
102  // allow others to modify the $message content
103  // eg. add HTML body, add attachments
104  $message = $this->events->triggerResults('message', 'system:email', ['email' => $email], $message);
105 
106  try {
107  $this->mailer->send($message);
108  } catch (TransportExceptionInterface $e) {
109  $this->getLogger()->error($e->getMessage());
110 
111  return false;
112  }
113 
114  return true;
115  }
116 
124  protected function prepareSubject(string $subject): string {
126  $subject = html_entity_decode($subject, ENT_QUOTES, 'UTF-8');
127  // Sanitize subject by stripping line endings
128  $subject = preg_replace("/(\r\n|\r|\n)/", ' ', $subject);
129  return trim($subject);
130  }
131 
140  protected function setMessageBody(SymfonyEmail $message, Email $email): void {
141  // add plaintext body part
142  $plain_text = $email->getBody();
143  $plain_text = elgg_strip_tags($plain_text);
144  $plain_text = html_entity_decode($plain_text, ENT_QUOTES, 'UTF-8');
145  $plain_text = wordwrap($plain_text);
146 
147  $message->text($plain_text);
148  $this->addHtmlPart($message, $email);
149 
150  $attachments = $email->getAttachments();
151  foreach ($attachments as $attachment) {
152  $message->addPart($attachment);
153  }
154  }
155 
164  protected function addHtmlPart(SymfonyEmail $message, \Elgg\Email $email): void {
165  if (!$this->config->email_html_part) {
166  return;
167  }
168 
169  $mail_params = $email->getParams();
170 
171  $html_text = elgg_extract('html_message', $mail_params);
172  if (is_string($html_text)) {
173  // HTML text already provided
174  if (elgg_extract('convert_css', $mail_params, true)) {
175  // still needs to be converted to inline CSS
176  $css = (string) elgg_extract('css', $mail_params);
177  $html_text = $this->html_formatter->inlineCss($html_text, $css);
178  }
179  } else {
180  $html_text = $this->makeHtmlBody([
181  'subject' => $email->getSubject(),
182  'body' => elgg_extract('html_body', $mail_params, $email->getBody()),
183  'email' => $email,
184  ]);
185  }
186 
187  // normalize urls in text
188  $html_text = $this->html_formatter->normalizeUrls($html_text);
189  if (empty($html_text)) {
190  return;
191  }
192 
193  $email_html_part_images = $this->config->email_html_part_images;
194  if ($email_html_part_images !== 'base64' && $email_html_part_images !== 'attach') {
195  $message->html($html_text);
196  return;
197  }
198 
199  $images = $this->findImages($html_text);
200  if (empty($images)) {
201  $message->html($html_text);
202  return;
203  }
204 
205  if ($email_html_part_images === 'base64') {
206  foreach ($images as $url) {
207  // remove wrapping quotes from the url
208  $image_url = substr($url, 1, -1);
209 
210  // get the image contents
211  $image = $this->image_fetcher->getImage($image_url);
212  if (empty($image)) {
213  continue;
214  }
215 
216  // build a valid uri
217  // https://en.wikipedia.org/wiki/Data_URI_scheme
218  $base64image = $image['content-type'] . ';charset=UTF-8;base64,' . base64_encode($image['data']);
219 
220  // build inline image
221  $replacement = str_replace($image_url, "data:{$base64image}", $url);
222 
223  // replace in text
224  $html_text = str_replace($url, $replacement, $html_text);
225  }
226 
227  $message->html($html_text);
228  return;
229  }
230 
231  // attach images
232  $attachments = [];
233  foreach ($images as $url) {
234  // remove wrapping quotes from the url
235  $image_url = substr($url, 1, -1);
236 
237  // get the image contents
238  $image = $this->image_fetcher->getImage($image_url);
239  if (empty($image)) {
240  continue;
241  }
242 
243  // Unique ID
244  $uid = uniqid() . '@elgg-image';
245 
246  $attachments[$uid] = $image;
247 
248  // replace url in the text with uid
249  $replacement = str_replace($image_url, "cid:{$uid}", $url);
250 
251  $html_text = str_replace($url, $replacement, $html_text);
252  }
253 
254  // split HTML body and related images
255  foreach ($attachments as $uid => $image_data) {
256  $inline_image = Attachment::factory([
257  'content' => $image_data['data'],
258  'type' => $image_data['content-type'],
259  'filename' => $image_data['name'],
260  'id' => $uid,
261  ]);
262 
263  $message->addPart($inline_image->asInline());
264  }
265 
266  $message->html($html_text);
267  }
268 
276  protected function makeHtmlBody(array $options = []): string {
277  $defaults = [
278  'subject' => '',
279  'body' => '',
280  'language' => elgg_get_current_language(),
281  ];
282 
283  $options = array_merge($defaults, $options);
284 
285  $options['body'] = $this->html_formatter->formatBlock($options['body']);
286 
287  // generate HTML mail body
288  $options['body'] = $this->views->renderView('email/elements/body', $options);
289 
290  $css_views = $this->views->renderView('elements/variables.css', $options);
291  $css_views .= $this->views->renderView('email/email.css', $options);
292 
293  $minifier = new \MatthiasMullie\Minify\CSS($css_views);
294  $css = $minifier->minify();
295 
296  $options['css'] = $css;
297 
298  $html = $this->views->renderView('email/elements/html', $options);
299 
300  return $this->html_formatter->inlineCss($html, $css);
301  }
302 
310  protected function findImages(string $text): array {
311  if (empty($text)) {
312  return [];
313  }
314 
315  // find all matches
316  $matches = [];
317  $pattern = '/\ssrc=([\'"]\S+[\'"])/i';
318 
319  preg_match_all($pattern, $text, $matches);
320 
321  if (empty($matches) || !isset($matches[1])) {
322  return [];
323  }
324 
325  // return all the found image urls
326  return array_unique($matches[1]);
327  }
328 }
getLogger()
Returns logger.
Definition: Loggable.php:37
$email
Definition: change_email.php:7
if(! $user||! $user->canDelete()) $name
Definition: delete.php:22
$attachments
Outputs attachments.
Definition: attachments.php:9
$text
Definition: button.php:26
Fetch external images server side.
Email service.
prepareSubject(string $subject)
Prepare the subject string.
setMessageBody(SymfonyEmail $message, Email $email)
Build the body part of the e-mail message.
makeHtmlBody(array $options=[])
Create the HTML content for use in a HTML email part.
findImages(string $text)
Find img src's in text.
__construct(protected Config $config, protected EventsService $events, protected MailerInterface $mailer, protected HtmlFormatter $html_formatter, protected ViewsService $views, protected ImageFetcherService $image_fetcher)
Constructor.
send(Email $email)
Sends an email.
addHtmlPart(SymfonyEmail $message, \Elgg\Email $email)
Add the HTML part to the e-mail message.
transport(Email $email)
Transports an email.
Email attachment.
Definition: Attachment.php:11
Email message.
Definition: Email.php:14
Events service.
Exception thrown if an error which can only be found on runtime occurs.
Views service.
Various helper methods for formatting and sanitizing output.
if($who_can_change_language==='nobody') elseif($who_can_change_language==='admin_only' &&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
foreach($plugin_guids as $guid) if(empty($deactivated_plugins)) $url
Definition: deactivate.php:39
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
$subject
HTML body of an email.
Definition: body.php:11
$views
Definition: item.php:17
$image
Definition: image_block.php:26
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:246
$defaults
Generic entity header upload helper.
Definition: header.php:6
$value
Definition: generic.php:51
elgg_get_current_language()
Get the current system/user language or 'en'.
Definition: languages.php:27
$headers
Definition: section.php:21
elgg_strip_tags(string $string, ?string $allowable_tags=null)
Strip tags and offer plugins the chance.
Definition: output.php:323
$html
A wrapper to render a section of the page shell.
Definition: section.php:9
if(parse_url(elgg_get_site_url(), PHP_URL_PATH) !=='/') if(file_exists(elgg_get_root_path() . 'robots.txt'))
Set robots.txt.
Definition: robots.php:10