Elgg  Version 4.3
EmailService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
14 use Laminas\Mail\Message as MailMessage;
16 use Laminas\Mime\Message as MimeMessage;
20 
27 class EmailService {
28 
29  use Loggable;
30 
34  protected $config;
35 
39  protected $hooks;
40 
44  protected $mailer;
45 
49  protected $html_formatter;
50 
54  protected $image_fetcher;
55 
59  protected $views;
60 
64  protected $css_compiler;
65 
77  public function __construct(
79  PluginHooksService $hooks,
80  TransportInterface $mailer,
81  HtmlFormatter $html_formatter,
83  ImageFetcherService $image_fetcher,
84  CssCompiler $css_compiler
85  ) {
86  $this->config = $config;
87  $this->hooks = $hooks;
88  $this->mailer = $mailer;
89  $this->html_formatter = $html_formatter;
90  $this->views = $views;
91  $this->image_fetcher = $image_fetcher;
92  $this->css_compiler = $css_compiler;
93  }
94 
103  public function send(Email $email) {
104  $email = $this->hooks->trigger('prepare', 'system:email', null, $email);
105  if (!$email instanceof Email) {
106  $msg = "'prepare','system:email' hook handlers should return an instance of " . Email::class;
107  throw new RuntimeException($msg);
108  }
109 
110  $hook_params = [
111  'email' => $email,
112  ];
113 
114  $is_valid = $email->getFrom() && !empty($email->getTo());
115  if (!$this->hooks->trigger('validate', 'system:email', $hook_params, $is_valid)) {
116  return false;
117  }
118 
119  return $this->transport($email);
120  }
121 
130  public function transport(Email $email) {
131 
132  if ($this->hooks->trigger('transport', 'system:email', ['email' => $email], false)) {
133  return true;
134  }
135 
136  // create the e-mail message
137  $message = new MailMessage();
138  $message->setEncoding('UTF-8');
139  $message->setSender($email->getFrom());
140  $message->addFrom($email->getFrom());
141  $message->addTo($email->getTo());
142  $message->addCc($email->getCc());
143  $message->addBcc($email->getBcc());
144 
145  // set headers
146  $headers = [
147  'MIME-Version' => '1.0',
148  'Content-Transfer-Encoding' => '8bit',
149  ];
150  $headers = array_merge($headers, $email->getHeaders());
151 
152  foreach ($headers as $name => $value) {
153  // See #11018
154  // Create a headerline as a concatenated string "name: value"
155  // This is done to force correct class detection for each header type,
156  // which influences the output of the header in the message
157  $message->getHeaders()->addHeaderLine("{$name}: {$value}");
158  }
159 
160  // add the body to the message
161  try {
162  $message = $this->setMessageBody($message, $email);
163  } catch (InvalidArgumentException $e) {
164  $this->getLogger()->error($e->getMessage());
165 
166  return false;
167  }
168 
169  $message->setSubject($this->prepareSubject($email->getSubject()));
170 
171  // allow others to modify the $message content
172  // eg. add html body, add attachments
173  $message = $this->hooks->trigger('zend:message', 'system:email', ['email' => $email], $message);
174 
175  // fix content type header
176  // @see https://github.com/Elgg/Elgg/issues/12555
177  $ct = $message->getHeaders()->get('Content-Type');
178  if ($ct instanceof ContentType) {
179  $ct->addParameter('format', 'flowed');
180  }
181 
182  try {
183  $this->mailer->send($message);
184  } catch (RuntimeException $e) {
185  $this->getLogger()->error($e->getMessage());
186 
187  return false;
188  }
189 
190  return true;
191  }
192 
200  protected function prepareSubject(string $subject): string {
201  $subject = elgg_strip_tags($subject);
202  $subject = html_entity_decode($subject, ENT_QUOTES, 'UTF-8');
203  // Sanitise subject by stripping line endings
204  $subject = preg_replace("/(\r\n|\r|\n)/", " ", $subject);
205  return trim($subject);
206  }
207 
216  protected function setMessageBody(MailMessage $message, Email $email): MailMessage {
217  // create body
218  $multipart = new MimeMessage();
219  $raw_body = $email->getBody();
220  $message_content_type = '';
221 
222  // add plain text part
223  $plain_text_part = new PlainTextPart($raw_body);
224  $multipart->addPart($plain_text_part);
225 
226  $make_html = (bool) elgg_get_config('email_html_part');
227 
228  if ($make_html) {
229  $multipart->addPart($this->makeHtmlPart($email));
230  $message_content_type = Mime::MULTIPART_ALTERNATIVE;
231  }
232 
233  $body = $multipart;
234 
235  // process attachments
236  $attachments = $email->getAttachments();
237  if (!empty($attachments)) {
238  if ($make_html) {
239  $multipart_content = new Part($multipart->generateMessage());
240  $multipart_content->setType(Mime::MULTIPART_ALTERNATIVE);
241  $multipart_content->setBoundary($multipart->getMime()->boundary());
242 
243  $body = new MimeMessage();
244  $body->addPart($multipart_content);
245  }
246 
247  foreach ($attachments as $attachement) {
248  $body->addPart($attachement);
249  }
250 
251  $message_content_type = Mime::MULTIPART_MIXED;
252  }
253 
254  $message->setBody($body);
255 
256  if (!empty($message_content_type)) {
257  // set correct message content type
258 
259  $headers = $message->getHeaders();
260  foreach ($headers as $header) {
261  if (!$header instanceof ContentType) {
262  continue;
263  }
264 
265  $header->setType($message_content_type);
266  $header->addParameter('boundary', $body->getMime()->boundary());
267  break;
268  }
269  }
270 
271  return $message;
272  }
273 
281  protected function makeHtmlPart(\Elgg\Email $email): Part {
282  $mail_params = $email->getParams();
283  $html_text = elgg_extract('html_message', $mail_params);
284  if ($html_text instanceof Part) {
285  return $html_text;
286  }
287 
288  if (is_string($html_text)) {
289  // html text already provided
290  if (elgg_extract('convert_css', $mail_params, true)) {
291  // still needs to be converted to inline CSS
292  $css = (string) elgg_extract('css', $mail_params);
293  $html_text = $this->html_formatter->inlineCss($html_text, $css);
294  }
295  } else {
296  $html_text = $this->makeHtmlBody([
297  'subject' => $email->getSubject(),
298  'body' => elgg_extract('html_body', $mail_params, $email->getBody()),
299  'email' => $email,
300  ]);
301  }
302 
303  // normalize urls in text
304  $html_text = $this->html_formatter->normalizeUrls($html_text);
305  if (empty($html_text)) {
306  return new HtmlPart($html_text);
307  }
308 
309  $email_html_part_images = elgg_get_config('email_html_part_images');
310  if ($email_html_part_images !== 'base64' && $email_html_part_images !== 'attach') {
311  return new HtmlPart($html_text);
312  }
313 
314  $images = $this->findImages($html_text);
315  if (empty($images)) {
316  return new HtmlPart($html_text);
317  }
318 
319  if ($email_html_part_images === 'base64') {
320  foreach ($images as $url) {
321  // remove wrapping quotes from the url
322  $image_url = substr($url, 1, -1);
323 
324  // get the image contents
325  $image = $this->image_fetcher->getImage($image_url);
326  if (empty($image)) {
327  continue;
328  }
329 
330  // build a valid uri
331  // https://en.wikipedia.org/wiki/Data_URI_scheme
332  $base64image = $image['content-type'] . ';charset=UTF-8;base64,' . base64_encode($image['data']);
333 
334  // build inline image
335  $replacement = str_replace($image_url, "data:{$base64image}", $url);
336 
337  // replace in text
338  $html_text = str_replace($url, $replacement, $html_text);
339  }
340 
341  return new HtmlPart($html_text);
342  }
343 
344  // attach images
345  $attachments = [];
346  foreach ($images as $url) {
347  // remove wrapping quotes from the url
348  $image_url = substr($url, 1, -1);
349 
350  // get the image contents
351  $image = $this->image_fetcher->getImage($image_url);
352  if (empty($image)) {
353  continue;
354  }
355 
356  // Unique ID
357  $uid = uniqid();
358 
359  $attachments[$uid] = $image;
360 
361  // replace url in the text with uid
362  $replacement = str_replace($image_url, "cid:{$uid}", $url);
363 
364  $html_text = str_replace($url, $replacement, $html_text);
365  }
366 
367  // split html body and related images
368  $message = new MimeMessage();
369  $message->addPart(new HtmlPart($html_text));
370 
371  foreach ($attachments as $uid => $image_data) {
372  $attachment = Attachment::factory([
373  'id' => $uid,
374  'content' => $image_data['data'],
375  'type' => $image_data['content-type'],
376  'filename' => $image_data['name'],
377  'encoding' => Mime::ENCODING_BASE64,
378  'disposition' => Mime::DISPOSITION_INLINE,
379  'charset' => 'UTF-8',
380  ]);
381 
382  $message->addPart($attachment);
383  }
384 
385  $part = new Part($message->generateMessage());
386  $part->setType(Mime::MULTIPART_RELATED);
387  $part->setBoundary($message->getMime()->boundary());
388 
389  return $part;
390  }
391 
399  protected function makeHtmlBody(array $options = []): string {
400  $defaults = [
401  'subject' => '',
402  'body' => '',
403  'language' => elgg_get_current_language(),
404  ];
405 
406  $options = array_merge($defaults, $options);
407 
408  $options['body'] = $this->html_formatter->formatBlock($options['body']);
409 
410  // generate HTML mail body
411  $options['body'] = $this->views->renderView('email/elements/body', $options);
412 
413  $css_compiled = $this->css_compiler->compile($this->views->renderView('email/email.css', $options));
414  $minifier = new \MatthiasMullie\Minify\CSS($css_compiled);
415  $css = $minifier->minify();
416 
417  $options['css'] = $css;
418 
419  $html = $this->views->renderView('email/elements/html', $options);
420 
421  return $this->html_formatter->inlineCss($html, $css);
422  }
423 
431  protected function findImages(string $text): array {
432  if (empty($text)) {
433  return [];
434  }
435 
436  // find all matches
437  $matches = [];
438  $pattern = '/\ssrc=([\'"]\S+[\'"])/i';
439 
440  preg_match_all($pattern, $text, $matches);
441 
442  if (empty($matches) || !isset($matches[1])) {
443  return [];
444  }
445 
446  // return all the found image urls
447  return array_unique($matches[1]);
448  }
449 }
elgg_get_current_language()
Get the current system/user language or "en".
Definition: languages.php:29
getTo()
Returns recipient address.
Definition: Email.php:169
findImages(string $text)
Find img src&#39;s in text.
Exception thrown if an argument is not of the expected type.
Exception thrown if an error which can only be found on runtime occurs.
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
$defaults
elgg_strip_tags($string, $allowable_tags=null)
Strip tags and offer plugins the chance.
Definition: output.php:317
$email
Definition: change_email.php:7
getBcc()
Returns recipient address from bcc.
Definition: Email.php:211
Html part for email.
Definition: HtmlPart.php:15
prepareSubject(string $subject)
Prepare the subject string.
makeHtmlPart(\Elgg\Email $email)
Make the html part of the e-mail message.
transport(Email $email)
Transports an email.
__construct(Config $config, PluginHooksService $hooks, TransportInterface $mailer, HtmlFormatter $html_formatter, ViewsService $views, ImageFetcherService $image_fetcher, CssCompiler $css_compiler)
Constructor.
$options
Elgg admin footer.
Definition: footer.php:6
$value
Definition: generic.php:51
setMessageBody(MailMessage $message, Email $email)
Build the body part of the e-mail message.
$html
Definition: section.php:10
$header
Definition: database.php:26
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
trait Loggable
Enables adding a logger.
Definition: Loggable.php:14
Fetch external images server side.
send(Email $email)
Sends an email.
Email message.
Definition: Email.php:13
getCc()
Returns recipient address from cc.
Definition: Email.php:190
getSubject()
Returns the subject.
Definition: Email.php:234
Views service.
Various helper method for formatting and sanitizing output.
$image
Definition: image_block.php:25
makeHtmlBody(array $options=[])
Create the HTML content for use in a HTML email part.
$css
Definition: install.css.php:5
$body
Definition: useradd.php:59
getBody()
Returns email body.
Definition: Email.php:255
elgg_extract($key, $array, $default=null, $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:547
Compile CSS with CSSCrush.
Definition: CssCompiler.php:16
getLogger()
Returns logger.
Definition: Loggable.php:37
Plaintext part for email.
$attachments
Outputs attachments.
Definition: attachments.php:9
getHeaders()
Returns headers.
Definition: Email.php:310
Email service.
foreach($plugin_guids as $guid) if(empty($deactivated_plugins)) $url
Definition: deactivate.php:39
getFrom()
Returns sender address.
Definition: Email.php:148
$text
Definition: button.php:32
$subject
Definition: useradd.php:58
$views
Definition: item.php:17
elgg_get_config($name, $default=null)
Get an Elgg configuration value.
getAttachments()
Get all attachments.
Definition: Email.php:348