Elgg  Version master
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 $events;
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  EventsService $events,
80  TransportInterface $mailer,
81  HtmlFormatter $html_formatter,
83  ImageFetcherService $image_fetcher,
84  CssCompiler $css_compiler
85  ) {
86  $this->config = $config;
87  $this->events = $events;
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->events->triggerResults('prepare', 'system:email', [], $email);
105  if (!$email instanceof Email) {
106  $msg = "'prepare','system:email' event handlers should return an instance of " . Email::class;
107  throw new RuntimeException($msg);
108  }
109 
110  $is_valid = $email->getFrom() && !empty($email->getTo());
111  if (!$this->events->triggerResults('validate', 'system:email', ['email' => $email], $is_valid)) {
112  return false;
113  }
114 
115  return $this->transport($email);
116  }
117 
126  public function transport(Email $email) {
127 
128  if ($this->events->triggerResults('transport', 'system:email', ['email' => $email], false)) {
129  return true;
130  }
131 
132  // create the e-mail message
133  $message = new MailMessage();
134  $message->setEncoding('UTF-8');
135  $message->setSender($email->getFrom());
136  $message->addFrom($email->getFrom());
137  $message->addTo($email->getTo());
138  $message->addCc($email->getCc());
139  $message->addBcc($email->getBcc());
140 
141  // set headers
142  $headers = [
143  'MIME-Version' => '1.0',
144  'Content-Transfer-Encoding' => '8bit',
145  ];
146  $headers = array_merge($headers, $email->getHeaders());
147 
148  foreach ($headers as $name => $value) {
149  // See #11018
150  // Create a headerline as a concatenated string "name: value"
151  // This is done to force correct class detection for each header type,
152  // which influences the output of the header in the message
153  $message->getHeaders()->addHeaderLine("{$name}: {$value}");
154  }
155 
156  // add the body to the message
157  try {
158  $message = $this->setMessageBody($message, $email);
159  } catch (InvalidArgumentException $e) {
160  $this->getLogger()->error($e->getMessage());
161 
162  return false;
163  }
164 
165  $message->setSubject($this->prepareSubject($email->getSubject()));
166 
167  // allow others to modify the $message content
168  // eg. add html body, add attachments
169  $message = $this->events->triggerResults('zend:message', 'system:email', ['email' => $email], $message);
170 
171  // fix content type header
172  // @see https://github.com/Elgg/Elgg/issues/12555
173  $ct = $message->getHeaders()->get('Content-Type');
174  if ($ct instanceof ContentType) {
175  $ct->addParameter('format', 'flowed');
176  }
177 
178  try {
179  $this->mailer->send($message);
180  } catch (RuntimeException $e) {
181  $this->getLogger()->error($e->getMessage());
182 
183  return false;
184  }
185 
186  return true;
187  }
188 
196  protected function prepareSubject(string $subject): string {
197  $subject = elgg_strip_tags($subject);
198  $subject = html_entity_decode($subject, ENT_QUOTES, 'UTF-8');
199  // Sanitise subject by stripping line endings
200  $subject = preg_replace("/(\r\n|\r|\n)/", ' ', $subject);
201  return trim($subject);
202  }
203 
212  protected function setMessageBody(MailMessage $message, Email $email): MailMessage {
213  // create body
214  $multipart = new MimeMessage();
215  $raw_body = $email->getBody();
216  $message_content_type = '';
217 
218  // add plain text part
219  $plain_text_part = new PlainTextPart($raw_body);
220  $multipart->addPart($plain_text_part);
221 
222  $make_html = (bool) elgg_get_config('email_html_part');
223 
224  if ($make_html) {
225  $multipart->addPart($this->makeHtmlPart($email));
226  $message_content_type = Mime::MULTIPART_ALTERNATIVE;
227  }
228 
229  $body = $multipart;
230 
231  // process attachments
232  $attachments = $email->getAttachments();
233  if (!empty($attachments)) {
234  if ($make_html) {
235  $multipart_content = new Part($multipart->generateMessage());
236  $multipart_content->setType(Mime::MULTIPART_ALTERNATIVE);
237  $multipart_content->setBoundary($multipart->getMime()->boundary());
238 
239  $body = new MimeMessage();
240  $body->addPart($multipart_content);
241  }
242 
243  foreach ($attachments as $attachement) {
244  $body->addPart($attachement);
245  }
246 
247  $message_content_type = Mime::MULTIPART_MIXED;
248  }
249 
250  $message->setBody($body);
251 
252  if (!empty($message_content_type)) {
253  // set correct message content type
254 
255  $headers = $message->getHeaders();
256  foreach ($headers as $header) {
257  if (!$header instanceof ContentType) {
258  continue;
259  }
260 
261  $header->setType($message_content_type);
262  $header->addParameter('boundary', $body->getMime()->boundary());
263  break;
264  }
265  }
266 
267  return $message;
268  }
269 
277  protected function makeHtmlPart(\Elgg\Email $email): Part {
278  $mail_params = $email->getParams();
279  $html_text = elgg_extract('html_message', $mail_params);
280  if ($html_text instanceof Part) {
281  return $html_text;
282  }
283 
284  if (is_string($html_text)) {
285  // html text already provided
286  if (elgg_extract('convert_css', $mail_params, true)) {
287  // still needs to be converted to inline CSS
288  $css = (string) elgg_extract('css', $mail_params);
289  $html_text = $this->html_formatter->inlineCss($html_text, $css);
290  }
291  } else {
292  $html_text = $this->makeHtmlBody([
293  'subject' => $email->getSubject(),
294  'body' => elgg_extract('html_body', $mail_params, $email->getBody()),
295  'email' => $email,
296  ]);
297  }
298 
299  // normalize urls in text
300  $html_text = $this->html_formatter->normalizeUrls($html_text);
301  if (empty($html_text)) {
302  return new HtmlPart($html_text);
303  }
304 
305  $email_html_part_images = elgg_get_config('email_html_part_images');
306  if ($email_html_part_images !== 'base64' && $email_html_part_images !== 'attach') {
307  return new HtmlPart($html_text);
308  }
309 
310  $images = $this->findImages($html_text);
311  if (empty($images)) {
312  return new HtmlPart($html_text);
313  }
314 
315  if ($email_html_part_images === 'base64') {
316  foreach ($images as $url) {
317  // remove wrapping quotes from the url
318  $image_url = substr($url, 1, -1);
319 
320  // get the image contents
321  $image = $this->image_fetcher->getImage($image_url);
322  if (empty($image)) {
323  continue;
324  }
325 
326  // build a valid uri
327  // https://en.wikipedia.org/wiki/Data_URI_scheme
328  $base64image = $image['content-type'] . ';charset=UTF-8;base64,' . base64_encode($image['data']);
329 
330  // build inline image
331  $replacement = str_replace($image_url, "data:{$base64image}", $url);
332 
333  // replace in text
334  $html_text = str_replace($url, $replacement, $html_text);
335  }
336 
337  return new HtmlPart($html_text);
338  }
339 
340  // attach images
341  $attachments = [];
342  foreach ($images as $url) {
343  // remove wrapping quotes from the url
344  $image_url = substr($url, 1, -1);
345 
346  // get the image contents
347  $image = $this->image_fetcher->getImage($image_url);
348  if (empty($image)) {
349  continue;
350  }
351 
352  // Unique ID
353  $uid = uniqid();
354 
355  $attachments[$uid] = $image;
356 
357  // replace url in the text with uid
358  $replacement = str_replace($image_url, "cid:{$uid}", $url);
359 
360  $html_text = str_replace($url, $replacement, $html_text);
361  }
362 
363  // split html body and related images
364  $message = new MimeMessage();
365  $message->addPart(new HtmlPart($html_text));
366 
367  foreach ($attachments as $uid => $image_data) {
368  $attachment = Attachment::factory([
369  'id' => $uid,
370  'content' => $image_data['data'],
371  'type' => $image_data['content-type'],
372  'filename' => $image_data['name'],
373  'encoding' => Mime::ENCODING_BASE64,
374  'disposition' => Mime::DISPOSITION_INLINE,
375  'charset' => 'UTF-8',
376  ]);
377 
378  $message->addPart($attachment);
379  }
380 
381  $part = new Part($message->generateMessage());
382  $part->setType(Mime::MULTIPART_RELATED);
383  $part->setBoundary($message->getMime()->boundary());
384 
385  return $part;
386  }
387 
395  protected function makeHtmlBody(array $options = []): string {
396  $defaults = [
397  'subject' => '',
398  'body' => '',
399  'language' => elgg_get_current_language(),
400  ];
401 
402  $options = array_merge($defaults, $options);
403 
404  $options['body'] = $this->html_formatter->formatBlock($options['body']);
405 
406  // generate HTML mail body
407  $options['body'] = $this->views->renderView('email/elements/body', $options);
408 
409  $css_compiled = $this->css_compiler->compile($this->views->renderView('email/email.css', $options));
410  $minifier = new \MatthiasMullie\Minify\CSS($css_compiled);
411  $css = $minifier->minify();
412 
413  $options['css'] = $css;
414 
415  $html = $this->views->renderView('email/elements/html', $options);
416 
417  return $this->html_formatter->inlineCss($html, $css);
418  }
419 
427  protected function findImages(string $text): array {
428  if (empty($text)) {
429  return [];
430  }
431 
432  // find all matches
433  $matches = [];
434  $pattern = '/\ssrc=([\'"]\S+[\'"])/i';
435 
436  preg_match_all($pattern, $text, $matches);
437 
438  if (empty($matches) || !isset($matches[1])) {
439  return [];
440  }
441 
442  // return all the found image urls
443  return array_unique($matches[1]);
444  }
445 }
elgg_get_current_language()
Get the current system/user language or &#39;en&#39;.
Definition: languages.php:27
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.
elgg_get_config(string $name, $default=null)
Get an Elgg configuration value.
$defaults
Generic entity header upload helper.
Definition: header.php:6
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
Events service.
$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.
__construct(Config $config, EventsService $events, TransportInterface $mailer, HtmlFormatter $html_formatter, ViewsService $views, ImageFetcherService $image_fetcher, CssCompiler $css_compiler)
Constructor.
transport(Email $email)
Transports an email.
$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
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:254
$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:55
getBody()
Returns email body.
Definition: Email.php:255
Compile CSS with CSSCrush.
Definition: CssCompiler.php:16
getLogger()
Returns logger.
Definition: Loggable.php:37
Plaintext part for email.
foreach($periods as $period) $header
Definition: cron.php:51
$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:54
$views
Definition: item.php:17
elgg_strip_tags(string $string, string $allowable_tags=null)
Strip tags and offer plugins the chance.
Definition: output.php:319
getAttachments()
Get all attachments.
Definition: Email.php:348