Elgg  Version 6.3
EmailService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
11 use Elgg\Traits\Loggable;
13 use Laminas\Mail\Exception\ExceptionInterface;
14 use Laminas\Mail\Header\ContentType;
15 use Laminas\Mail\Message as MailMessage;
16 use Laminas\Mail\Transport\TransportInterface;
17 use Laminas\Mime\Exception\InvalidArgumentException;
18 use Laminas\Mime\Message as MimeMessage;
20 use Laminas\Mime\Part;
21 
28 class EmailService {
29 
30  use Loggable;
31 
43  public function __construct(
44  protected Config $config,
45  protected EventsService $events,
46  protected TransportInterface $mailer,
47  protected HtmlFormatter $html_formatter,
48  protected ViewsService $views,
49  protected ImageFetcherService $image_fetcher,
50  protected CssCompiler $css_compiler
51  ) {
52  }
53 
62  public function send(Email $email): bool {
63  $email = $this->events->triggerResults('prepare', 'system:email', [], $email);
64  if (!$email instanceof Email) {
65  $msg = "'prepare','system:email' event handlers should return an instance of " . Email::class;
66  throw new RuntimeException($msg);
67  }
68 
69  $is_valid = $email->getFrom() && !empty($email->getTo());
70  if (!$this->events->triggerResults('validate', 'system:email', ['email' => $email], $is_valid)) {
71  return false;
72  }
73 
74  return $this->transport($email);
75  }
76 
85  public function transport(Email $email): bool {
86  if ($this->events->triggerResults('transport', 'system:email', ['email' => $email], false)) {
87  return true;
88  }
89 
90  // create the e-mail message
91  $message = new MailMessage();
92  $message->setEncoding('UTF-8');
93  $message->setSender($email->getFrom());
94  $message->addFrom($email->getFrom());
95  $message->addTo($email->getTo());
96  $message->addCc($email->getCc());
97  $message->addBcc($email->getBcc());
98 
99  // set headers
100  $headers = [
101  'MIME-Version' => '1.0',
102  'Content-Transfer-Encoding' => '8bit',
103  ];
104  $headers = array_merge($headers, $email->getHeaders());
105 
106  foreach ($headers as $name => $value) {
107  // See #11018
108  // Create a headerline as a concatenated string "name: value"
109  // This is done to force correct class detection for each header type,
110  // which influences the output of the header in the message
111  $message->getHeaders()->addHeaderLine("{$name}: {$value}");
112  }
113 
114  // add the body to the message
115  try {
116  $message = $this->setMessageBody($message, $email);
117  } catch (InvalidArgumentException $e) {
118  $this->getLogger()->error($e->getMessage());
119 
120  return false;
121  }
122 
123  $message->setSubject($this->prepareSubject($email->getSubject()));
124 
125  // allow others to modify the $message content
126  // eg. add html body, add attachments
127  $message = $this->events->triggerResults('zend:message', 'system:email', ['email' => $email], $message);
128 
129  // fix content type header
130  // @see https://github.com/Elgg/Elgg/issues/12555
131  $ct = $message->getHeaders()->get('Content-Type');
132  if ($ct instanceof ContentType) {
133  $ct->addParameter('format', 'flowed');
134  }
135 
136  try {
137  $this->mailer->send($message);
138  } catch (ExceptionInterface $e) {
139  $this->getLogger()->error($e->getMessage());
140 
141  return false;
142  }
143 
144  return true;
145  }
146 
154  protected function prepareSubject(string $subject): string {
156  $subject = html_entity_decode($subject, ENT_QUOTES, 'UTF-8');
157  // Sanitise subject by stripping line endings
158  $subject = preg_replace("/(\r\n|\r|\n)/", ' ', $subject);
159  return trim($subject);
160  }
161 
170  protected function setMessageBody(MailMessage $message, Email $email): MailMessage {
171  // create body
172  $multipart = new MimeMessage();
173  $raw_body = $email->getBody();
174  $message_content_type = '';
175 
176  // add plain text part
177  $plain_text_part = new PlainTextPart($raw_body);
178  $multipart->addPart($plain_text_part);
179 
180  $make_html = (bool) elgg_get_config('email_html_part');
181 
182  if ($make_html) {
183  $multipart->addPart($this->makeHtmlPart($email));
184  $message_content_type = Mime::MULTIPART_ALTERNATIVE;
185  }
186 
187  $body = $multipart;
188 
189  // process attachments
190  $attachments = $email->getAttachments();
191  if (!empty($attachments)) {
192  if ($make_html) {
193  $multipart_content = new Part($multipart->generateMessage());
194  $multipart_content->setType(Mime::MULTIPART_ALTERNATIVE);
195  $multipart_content->setBoundary($multipart->getMime()->boundary());
196 
197  $body = new MimeMessage();
198  $body->addPart($multipart_content);
199  }
200 
201  foreach ($attachments as $attachement) {
202  $body->addPart($attachement);
203  }
204 
205  $message_content_type = Mime::MULTIPART_MIXED;
206  }
207 
208  $message->setBody($body);
209 
210  if (!empty($message_content_type)) {
211  // set correct message content type
212 
213  $headers = $message->getHeaders();
214  foreach ($headers as $header) {
215  if (!$header instanceof ContentType) {
216  continue;
217  }
218 
219  $header->setType($message_content_type);
220  $header->addParameter('boundary', $body->getMime()->boundary());
221  break;
222  }
223  }
224 
225  return $message;
226  }
227 
235  protected function makeHtmlPart(\Elgg\Email $email): Part {
236  $mail_params = $email->getParams();
237  $html_text = elgg_extract('html_message', $mail_params);
238  if ($html_text instanceof Part) {
239  return $html_text;
240  }
241 
242  if (is_string($html_text)) {
243  // html text already provided
244  if (elgg_extract('convert_css', $mail_params, true)) {
245  // still needs to be converted to inline CSS
246  $css = (string) elgg_extract('css', $mail_params);
247  $html_text = $this->html_formatter->inlineCss($html_text, $css);
248  }
249  } else {
250  $html_text = $this->makeHtmlBody([
251  'subject' => $email->getSubject(),
252  'body' => elgg_extract('html_body', $mail_params, $email->getBody()),
253  'email' => $email,
254  ]);
255  }
256 
257  // normalize urls in text
258  $html_text = $this->html_formatter->normalizeUrls($html_text);
259  if (empty($html_text)) {
260  return new HtmlPart($html_text);
261  }
262 
263  $email_html_part_images = elgg_get_config('email_html_part_images');
264  if ($email_html_part_images !== 'base64' && $email_html_part_images !== 'attach') {
265  return new HtmlPart($html_text);
266  }
267 
268  $images = $this->findImages($html_text);
269  if (empty($images)) {
270  return new HtmlPart($html_text);
271  }
272 
273  if ($email_html_part_images === 'base64') {
274  foreach ($images as $url) {
275  // remove wrapping quotes from the url
276  $image_url = substr($url, 1, -1);
277 
278  // get the image contents
279  $image = $this->image_fetcher->getImage($image_url);
280  if (empty($image)) {
281  continue;
282  }
283 
284  // build a valid uri
285  // https://en.wikipedia.org/wiki/Data_URI_scheme
286  $base64image = $image['content-type'] . ';charset=UTF-8;base64,' . base64_encode($image['data']);
287 
288  // build inline image
289  $replacement = str_replace($image_url, "data:{$base64image}", $url);
290 
291  // replace in text
292  $html_text = str_replace($url, $replacement, $html_text);
293  }
294 
295  return new HtmlPart($html_text);
296  }
297 
298  // attach images
299  $attachments = [];
300  foreach ($images as $url) {
301  // remove wrapping quotes from the url
302  $image_url = substr($url, 1, -1);
303 
304  // get the image contents
305  $image = $this->image_fetcher->getImage($image_url);
306  if (empty($image)) {
307  continue;
308  }
309 
310  // Unique ID
311  $uid = uniqid();
312 
313  $attachments[$uid] = $image;
314 
315  // replace url in the text with uid
316  $replacement = str_replace($image_url, "cid:{$uid}", $url);
317 
318  $html_text = str_replace($url, $replacement, $html_text);
319  }
320 
321  // split html body and related images
322  $message = new MimeMessage();
323  $message->addPart(new HtmlPart($html_text));
324 
325  foreach ($attachments as $uid => $image_data) {
326  $attachment = Attachment::factory([
327  'id' => $uid,
328  'content' => $image_data['data'],
329  'type' => $image_data['content-type'],
330  'filename' => $image_data['name'],
331  'encoding' => Mime::ENCODING_BASE64,
332  'disposition' => Mime::DISPOSITION_INLINE,
333  'charset' => 'UTF-8',
334  ]);
335 
336  $message->addPart($attachment);
337  }
338 
339  $part = new Part($message->generateMessage());
340  $part->setType(Mime::MULTIPART_RELATED);
341  $part->setBoundary($message->getMime()->boundary());
342 
343  return $part;
344  }
345 
353  protected function makeHtmlBody(array $options = []): string {
354  $defaults = [
355  'subject' => '',
356  'body' => '',
357  'language' => elgg_get_current_language(),
358  ];
359 
360  $options = array_merge($defaults, $options);
361 
362  $options['body'] = $this->html_formatter->formatBlock($options['body']);
363 
364  // generate HTML mail body
365  $options['body'] = $this->views->renderView('email/elements/body', $options);
366 
367  $css_views = $this->views->renderView('elements/variables.css', $options);
368  $css_views .= $this->views->renderView('email/email.css', $options);
369 
370  $css_compiled = $this->css_compiler->compile($css_views);
371  $minifier = new \MatthiasMullie\Minify\CSS($css_compiled);
372  $css = $minifier->minify();
373 
374  $options['css'] = $css;
375 
376  $html = $this->views->renderView('email/elements/html', $options);
377 
378  return $this->html_formatter->inlineCss($html, $css);
379  }
380 
388  protected function findImages(string $text): array {
389  if (empty($text)) {
390  return [];
391  }
392 
393  // find all matches
394  $matches = [];
395  $pattern = '/\ssrc=([\'"]\S+[\'"])/i';
396 
397  preg_match_all($pattern, $text, $matches);
398 
399  if (empty($matches) || !isset($matches[1])) {
400  return [];
401  }
402 
403  // return all the found image urls
404  return array_unique($matches[1]);
405  }
406 }
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:33
foreach($categories as $key=> $category) $body
Definition: categories.php:35
Compile CSS with CSSCrush.
Definition: CssCompiler.php:16
Fetch external images server side.
Email service.
prepareSubject(string $subject)
Prepare the subject string.
setMessageBody(MailMessage $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.
send(Email $email)
Sends an email.
makeHtmlPart(\Elgg\Email $email)
Make the html part of the e-mail message.
__construct(protected Config $config, protected EventsService $events, protected TransportInterface $mailer, protected HtmlFormatter $html_formatter, protected ViewsService $views, protected ImageFetcherService $image_fetcher, protected CssCompiler $css_compiler)
Constructor.
transport(Email $email)
Transports an email.
Email attachment.
Definition: Attachment.php:11
Html part for email.
Definition: HtmlPart.php:15
Plaintext part for email.
Email message.
Definition: Email.php:13
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.
Support class for MultiPart Mime Messages.
Definition: Mime.php:37
elgg_get_config(string $name, $default=null)
Get an Elgg configuration value.
if($who_can_change_language==='nobody') elseif($who_can_change_language==='admin_only' &&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
foreach($periods as $period) $header
Definition: cron.php:81
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:240
$defaults
Generic entity header upload helper.
Definition: header.php:6
$value
Definition: generic.php:51
$css
Definition: install.css.php:5
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