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