aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Mail
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Mail')
-rw-r--r--lib/private/Mail/Attachment.php56
-rw-r--r--lib/private/Mail/EMailTemplate.php262
-rw-r--r--lib/private/Mail/Mailer.php350
-rw-r--r--lib/private/Mail/Message.php267
-rw-r--r--lib/private/Mail/Provider/Manager.php255
5 files changed, 725 insertions, 465 deletions
diff --git a/lib/private/Mail/Attachment.php b/lib/private/Mail/Attachment.php
index 1f88c875565..2a5246c0019 100644
--- a/lib/private/Mail/Attachment.php
+++ b/lib/private/Mail/Attachment.php
@@ -3,31 +3,13 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2017 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OC\Mail;
use OCP\Mail\IAttachment;
+use Symfony\Component\Mime\Email;
/**
* Class Attachment
@@ -36,48 +18,46 @@ use OCP\Mail\IAttachment;
* @since 13.0.0
*/
class Attachment implements IAttachment {
-
- /** @var \Swift_Mime_Attachment */
- protected $swiftAttachment;
-
- public function __construct(\Swift_Mime_Attachment $attachment) {
- $this->swiftAttachment = $attachment;
+ public function __construct(
+ private ?string $body,
+ private ?string $name,
+ private ?string $contentType,
+ private ?string $path = null,
+ ) {
}
/**
- * @param string $filename
* @return $this
* @since 13.0.0
*/
public function setFilename(string $filename): IAttachment {
- $this->swiftAttachment->setFilename($filename);
+ $this->name = $filename;
return $this;
}
/**
- * @param string $contentType
* @return $this
* @since 13.0.0
*/
public function setContentType(string $contentType): IAttachment {
- $this->swiftAttachment->setContentType($contentType);
+ $this->contentType = $contentType;
return $this;
}
/**
- * @param string $body
* @return $this
* @since 13.0.0
*/
public function setBody(string $body): IAttachment {
- $this->swiftAttachment->setBody($body);
+ $this->body = $body;
return $this;
}
- /**
- * @return \Swift_Mime_Attachment
- */
- public function getSwiftAttachment(): \Swift_Mime_Attachment {
- return $this->swiftAttachment;
+ public function attach(Email $symfonyEmail): void {
+ if ($this->path !== null) {
+ $symfonyEmail->attachFromPath($this->path, $this->name, $this->contentType);
+ } else {
+ $symfonyEmail->attach($this->body, $this->name, $this->contentType);
+ }
}
}
diff --git a/lib/private/Mail/EMailTemplate.php b/lib/private/Mail/EMailTemplate.php
index d5bd27007df..a327109cc12 100644
--- a/lib/private/Mail/EMailTemplate.php
+++ b/lib/private/Mail/EMailTemplate.php
@@ -3,40 +3,9 @@
declare(strict_types=1);
/**
- * @copyright 2017, Morris Jobke <hey@morrisjobke.de>
- * @copyright 2017, Lukas Reschke <lukas@statuscode.ch>
- *
- * @author Bjoern Schiessle <bjoern@schiessle.org>
- * @author brad2014 <brad2014@users.noreply.github.com>
- * @author Brad Rubenstein <brad@wbr.tech>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Liam JACK <liamjack@users.noreply.github.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author medcloud <42641918+medcloud@users.noreply.github.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Tomasz Paluszkiewicz <tomasz.paluszkiewicz@gmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
-
namespace OC\Mail;
use OCP\Defaults;
@@ -53,33 +22,19 @@ use OCP\Mail\IEMailTemplate;
* @package OC\Mail
*/
class EMailTemplate implements IEMailTemplate {
- /** @var Defaults */
- protected $themingDefaults;
- /** @var IURLGenerator */
- protected $urlGenerator;
- /** @var IFactory */
- protected $l10nFactory;
- /** @var string */
- protected $emailId;
- /** @var array */
- protected $data;
-
- /** @var string */
- protected $subject = '';
- /** @var string */
- protected $htmlBody = '';
- /** @var string */
- protected $plainBody = '';
- /** @var bool indicated if the footer is added */
- protected $headerAdded = false;
- /** @var bool indicated if the body is already opened */
- protected $bodyOpened = false;
- /** @var bool indicated if there is a list open in the body */
- protected $bodyListOpened = false;
- /** @var bool indicated if the footer is added */
- protected $footerAdded = false;
-
- protected $head = <<<EOF
+ protected string $subject = '';
+ protected string $htmlBody = '';
+ protected string $plainBody = '';
+ /** indicated if the header is added */
+ protected bool $headerAdded = false;
+ /** indicated if the body is already opened */
+ protected bool $bodyOpened = false;
+ /** indicated if there is a list open in the body */
+ protected bool $bodyListOpened = false;
+ /** indicated if the footer is added */
+ protected bool $footerAdded = false;
+
+ protected string $head = <<<EOF
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" style="-webkit-font-smoothing:antialiased;background:#fff!important">
<head>
@@ -97,7 +52,7 @@ class EMailTemplate implements IEMailTemplate {
<center data-parsed="" style="min-width:580px;width:100%">
EOF;
- protected $tail = <<<EOF
+ protected string $tail = <<<EOF
</center>
</td>
</tr>
@@ -109,7 +64,7 @@ EOF;
EOF;
- protected $header = <<<EOF
+ protected string $header = <<<EOF
<table align="center" class="wrapper header float-center" style="Margin:0 auto;background:#fff;border-collapse:collapse;border-spacing:0;float:none;margin:0 auto;padding:0;text-align:center;vertical-align:top;width:100%%">
<tr style="padding:0;text-align:left;vertical-align:top">
<td class="wrapper-inner" style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:1.3;margin:0;padding:20px;text-align:left;vertical-align:top;word-wrap:break-word">
@@ -121,7 +76,7 @@ EOF;
<tbody>
<tr style="padding:0;text-align:left;vertical-align:top">
<center data-parsed="" style="background-color:%s;min-width:175px;max-height:175px; padding:35px 0px;border-radius:200px">
- <img class="logo float-center" src="%s" alt="%s" align="center" style="-ms-interpolation-mode:bicubic;clear:both;display:block;float:none;margin:0 auto;outline:0;text-align:center;text-decoration:none;max-height:105px;max-width:105px;width:auto;height:auto">
+ <img class="logo float-center" src="%s" alt="%s" align="center" style="-ms-interpolation-mode:bicubic;clear:both;display:block;float:none;margin:0 auto;outline:0;text-align:center;text-decoration:none;max-height:105px;max-width:105px;width:auto;height:auto"%s>
</center>
</tr>
</tbody>
@@ -142,7 +97,7 @@ EOF;
</table>
EOF;
- protected $heading = <<<EOF
+ protected string $heading = <<<EOF
<table align="center" class="container main-heading float-center" style="Margin:0 auto;background:0 0!important;border-collapse:collapse;border-spacing:0;float:none;margin:0 auto;padding:0;text-align:center;vertical-align:top;width:580px">
<tbody>
<tr style="padding:0;text-align:left;vertical-align:top">
@@ -161,7 +116,7 @@ EOF;
</table>
EOF;
- protected $bodyBegin = <<<EOF
+ protected string $bodyBegin = <<<EOF
<table align="center" class="wrapper content float-center" style="Margin:0 auto;border-collapse:collapse;border-spacing:0;float:none;margin:0 auto;padding:0;text-align:center;vertical-align:top;width:100%">
<tr style="padding:0;text-align:left;vertical-align:top">
<td class="wrapper-inner" style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:1.3;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
@@ -171,7 +126,7 @@ EOF;
<td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:1.3;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
EOF;
- protected $bodyText = <<<EOF
+ protected string $bodyText = <<<EOF
<table class="row description" style="border-collapse:collapse;border-spacing:0;display:table;padding:0;position:relative;text-align:left;vertical-align:top;width:100%%">
<tbody>
<tr style="padding:0;text-align:left;vertical-align:top">
@@ -191,7 +146,7 @@ EOF;
EOF;
// note: listBegin (like bodyBegin) is not processed through sprintf, so "%" is not escaped as "%%". (bug #12151)
- protected $listBegin = <<<EOF
+ protected string $listBegin = <<<EOF
<table class="row description" style="border-collapse:collapse;border-spacing:0;display:table;padding:0;position:relative;text-align:left;vertical-align:top;width:100%">
<tbody>
<tr style="padding:0;text-align:left;vertical-align:top">
@@ -199,7 +154,7 @@ EOF;
<table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%">
EOF;
- protected $listItem = <<<EOF
+ protected string $listItem = <<<EOF
<tr style="padding:0;text-align:left;vertical-align:top">
<td style="Margin:0;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;text-align:left;width:15px;">
<p class="text-left" style="Margin:0;Margin-bottom:10px;color:#777;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;margin-bottom:10px;padding:0;padding-left:10px;text-align:left">%s</p>
@@ -211,7 +166,7 @@ EOF;
</tr>
EOF;
- protected $listEnd = <<<EOF
+ protected string $listEnd = <<<EOF
</table>
</th>
</tr>
@@ -219,7 +174,7 @@ EOF;
</table>
EOF;
- protected $buttonGroup = <<<EOF
+ protected string $buttonGroup = <<<EOF
<table class="spacer" style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%">
<tbody>
<tr style="padding:0;text-align:left;vertical-align:top">
@@ -235,32 +190,46 @@ EOF;
<tr style="padding:0;text-align:left;vertical-align:top">
<th style="Margin:0;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;text-align:left">
<center data-parsed="" style="min-width:490px;width:100%%">
- <table class="button btn default primary float-center" style="Margin:0 0 30px 0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;margin:0 0 30px 0;margin-right:15px;max-height:60px;max-width:200px;padding:0;text-align:center;vertical-align:top;width:auto;background:%1\$s;background-color:%1\$s;color:#fefefe;">
- <tr style="padding:0;text-align:left;vertical-align:top">
- <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:1.3;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
- <table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%">
+ <!--[if (gte mso 9)|(IE)]>
+ <table>
+ <tr>
+ <td>
+ <![endif]-->
+ <table class="button btn default primary float-center" style="Margin:0 0 30px 0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;margin:0 0 30px 0;margin-right:15px;border-radius:8px;max-width:300px;padding:0;text-align:center;vertical-align:top;width:auto;background:%1\$s;background-color:%1\$s;color:#fefefe;">
<tr style="padding:0;text-align:left;vertical-align:top">
- <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border:0 solid %2\$s;border-collapse:collapse!important;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:1.3;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
- <a href="%3\$s" style="Margin:0;border:0 solid %4\$s;border-radius:2px;color:%5\$s;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:1.3;margin:0;padding:10px 25px 10px 25px;text-align:left;outline:1px solid %6\$s;text-decoration:none">%7\$s</a>
+ <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
+ <table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%">
+ <tr style="padding:0;text-align:left;vertical-align:top">
+ <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border:0 solid %2\$s;border-collapse:collapse!important;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
+ <a href="%3\$s" style="Margin:0;border:0 solid %4\$s;color:%5\$s;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:normal;margin:0;padding:8px;text-align:left;outline:1px solid %6\$s;text-decoration:none">%7\$s</a>
+ </td>
+ </tr>
+ </table>
</td>
</tr>
</table>
+ <!--[if (gte mso 9)|(IE)]>
</td>
- </tr>
- </table>
- <table class="button btn default secondary float-center" style="Margin:0 0 30px 0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;margin:0 0 30px 0;max-height:40px;max-width:200px;padding:0;text-align:center;vertical-align:top;width:auto">
- <tr style="padding:0;text-align:left;vertical-align:top">
- <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:1.3;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
- <table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%">
+ <td>
+ <![endif]-->
+ <table class="button btn default secondary float-center" style="Margin:0 0 30px 0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;background-color: #ccc;margin:0 0 30px 0;max-height:40px;max-width:300px;padding:1px;border-radius:8px;text-align:center;vertical-align:top;width:auto">
<tr style="padding:0;text-align:left;vertical-align:top">
- <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;background:#777;border:0 solid #777;border-collapse:collapse!important;color:#fefefe;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:1.3;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
- <a href="%8\$s" style="Margin:0;background-color:#fff;border:0 solid #777;border-radius:2px;color:#6C6C6C!important;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:1.3;margin:0;outline:1px solid #CBCBCB;padding:10px 25px 10px 25px;text-align:left;text-decoration:none">%9\$s</a>
+ <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
+ <table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%">
+ <tr style="padding:0;text-align:left;vertical-align:top">
+ <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border:0 solid #777;border-collapse:collapse!important;color:#fefefe;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
+ <a href="%8\$s" style="Margin:0;background-color:#fff;border:0 solid #777;color:#6C6C6C!important;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:normal;margin:0;border-radius: 7px;padding:8px;text-align:left;text-decoration:none">%9\$s</a>
+ </td>
+ </tr>
+ </table>
</td>
</tr>
</table>
+ <!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
+ <![endif]-->
</center>
</th>
<th class="expander" style="Margin:0;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0!important;text-align:left;visibility:hidden;width:0"></th>
@@ -272,7 +241,7 @@ EOF;
</table>
EOF;
- protected $button = <<<EOF
+ protected string $button = <<<EOF
<table class="spacer" style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%">
<tbody>
<tr style="padding:0;text-align:left;vertical-align:top">
@@ -288,13 +257,13 @@ EOF;
<tr style="padding:0;text-align:left;vertical-align:top">
<th style="Margin:0;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;line-height:1.3;margin:0;padding:0;text-align:left">
<center data-parsed="" style="min-width:490px;width:100%%">
- <table class="button btn default primary float-center" style="Margin:0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;margin:0;max-height:60px;padding:0;text-align:center;vertical-align:top;width:auto;background:%1\$s;color:#fefefe;background-color:%1\$s;">
+ <table class="button btn default primary float-center" style="Margin:0;border-collapse:collapse;border-spacing:0;display:inline-block;float:none;margin:0;border-radius:8px;padding:0;text-align:center;vertical-align:top;width:auto;background:%1\$s;color:#fefefe;background-color:%1\$s;">
<tr style="padding:0;text-align:left;vertical-align:top">
- <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:1.3;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
+ <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border-collapse:collapse!important;color:#0a0a0a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
<table style="border-collapse:collapse;border-spacing:0;padding:0;text-align:left;vertical-align:top;width:100%%">
<tr style="padding:0;text-align:left;vertical-align:top">
- <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border:0 solid %2\$s;border-collapse:collapse!important;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:1.3;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
- <a href="%3\$s" style="Margin:0;border:0 solid %4\$s;border-radius:2px;color:%5\$s;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:1.3;margin:0;padding:10px 25px 10px 25px;text-align:left;outline:1px solid %5\$s;text-decoration:none">%7\$s</a>
+ <td style="-moz-hyphens:auto;-webkit-hyphens:auto;Margin:0;border:0 solid %2\$s;border-collapse:collapse!important;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:400;hyphens:auto;line-height:normal;margin:0;padding:0;text-align:left;vertical-align:top;word-wrap:break-word">
+ <a href="%3\$s" style="Margin:0;border:0 solid %4\$s;color:%5\$s;display:inline-block;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;font-weight:regular;line-height:normal;margin:0;padding:8px;text-align:left;outline:1px solid %5\$s;text-decoration:none">%7\$s</a>
</td>
</tr>
</table>
@@ -312,7 +281,7 @@ EOF;
</table>
EOF;
- protected $bodyEnd = <<<EOF
+ protected string $bodyEnd = <<<EOF
</td>
</tr>
@@ -323,7 +292,7 @@ EOF;
</table>
EOF;
- protected $footer = <<<EOF
+ protected string $footer = <<<EOF
<table class="spacer float-center" style="Margin:0 auto;border-collapse:collapse;border-spacing:0;float:none;margin:0 auto;padding:0;text-align:center;vertical-align:top;width:100%%">
<tbody>
<tr style="padding:0;text-align:left;vertical-align:top">
@@ -349,49 +318,51 @@ EOF;
</table>
EOF;
- public function __construct(Defaults $themingDefaults,
- IURLGenerator $urlGenerator,
- IFactory $l10nFactory,
- $emailId,
- array $data) {
- $this->themingDefaults = $themingDefaults;
- $this->urlGenerator = $urlGenerator;
- $this->l10nFactory = $l10nFactory;
+ public function __construct(
+ protected Defaults $themingDefaults,
+ protected IURLGenerator $urlGenerator,
+ protected IFactory $l10nFactory,
+ protected ?int $logoWidth,
+ protected ?int $logoHeight,
+ protected string $emailId,
+ protected array $data,
+ ) {
$this->htmlBody .= $this->head;
- $this->emailId = $emailId;
- $this->data = $data;
}
/**
* Sets the subject of the email
- *
- * @param string $subject
*/
- public function setSubject(string $subject) {
+ public function setSubject(string $subject): void {
$this->subject = $subject;
}
/**
* Adds a header to the email
*/
- public function addHeader() {
+ public function addHeader(): void {
if ($this->headerAdded) {
return;
}
$this->headerAdded = true;
+ $logoSizeDimensions = '';
+ if ($this->logoWidth && $this->logoHeight) {
+ // Provide a logo size when we have the dimensions so that it displays nicely in Outlook
+ $logoSizeDimensions = ' width="' . $this->logoWidth . '" height="' . $this->logoHeight . '"';
+ }
+
$logoUrl = $this->urlGenerator->getAbsoluteURL($this->themingDefaults->getLogo(false));
- $this->htmlBody .= vsprintf($this->header, [$this->themingDefaults->getColorPrimary(), $logoUrl, $this->themingDefaults->getName()]);
+ $this->htmlBody .= vsprintf($this->header, [$this->themingDefaults->getDefaultColorPrimary(), $logoUrl, $this->themingDefaults->getName(), $logoSizeDimensions]);
}
/**
* Adds a heading to the email
*
- * @param string $title
* @param string|bool $plainTitle Title that is used in the plain text email
- * if empty the $title is used, if false none will be used
+ * if empty the $title is used, if false none will be used
*/
- public function addHeading(string $title, $plainTitle = '') {
+ public function addHeading(string $title, $plainTitle = ''): void {
if ($this->footerAdded) {
return;
}
@@ -408,7 +379,7 @@ EOF;
/**
* Open the HTML body when it is not already
*/
- protected function ensureBodyIsOpened() {
+ protected function ensureBodyIsOpened(): void {
if ($this->bodyOpened) {
return;
}
@@ -422,9 +393,9 @@ EOF;
*
* @param string $text Note: When $plainText falls back to this, HTML is automatically escaped in the HTML email
* @param string|bool $plainText Text that is used in the plain text email
- * if empty the $text is used, if false none will be used
+ * if empty the $text is used, if false none will be used
*/
- public function addBodyText(string $text, $plainText = '') {
+ public function addBodyText(string $text, $plainText = ''): void {
if ($this->footerAdded) {
return;
}
@@ -449,19 +420,26 @@ EOF;
* @param string $metaInfo Note: When $plainMetaInfo falls back to this, HTML is automatically escaped in the HTML email
* @param string $icon Absolute path, must be 16*16 pixels
* @param string|bool $plainText Text that is used in the plain text email
- * if empty or true the $text is used, if false none will be used
+ * if empty or true the $text is used, if false none will be used
* @param string|bool $plainMetaInfo Meta info that is used in the plain text email
- * if empty or true the $metaInfo is used, if false none will be used
- * @param integer plainIndent If > 0, Indent plainText by this amount.
+ * if empty or true the $metaInfo is used, if false none will be used
+ * @param integer $plainIndent plainIndent If > 0, Indent plainText by this amount.
* @since 12.0.0
*/
- public function addBodyListItem(string $text, string $metaInfo = '', string $icon = '', $plainText = '', $plainMetaInfo = '', $plainIndent = 0) {
+ public function addBodyListItem(
+ string $text,
+ string $metaInfo = '',
+ string $icon = '',
+ $plainText = '',
+ $plainMetaInfo = '',
+ $plainIndent = 0,
+ ): void {
$this->ensureBodyListOpened();
if ($plainText === '' || $plainText === true) {
$plainText = $text;
$text = htmlspecialchars($text);
- $text = str_replace("\n", "<br/>", $text); // convert newlines to HTML breaks
+ $text = str_replace("\n", '<br/>', $text); // convert newlines to HTML breaks
}
if ($plainMetaInfo === '' || $plainMetaInfo === true) {
$plainMetaInfo = $metaInfo;
@@ -498,14 +476,14 @@ EOF;
*/
/** @var string $label */
$label = ($plainMetaInfo !== false)? $plainMetaInfo : '';
- $this->plainBody .= sprintf("%${plainIndent}s %s\n",
+ $this->plainBody .= sprintf("%{$plainIndent}s %s\n",
$label,
str_replace("\n", "\n" . str_repeat(' ', $plainIndent + 1), $plainText));
}
}
}
- protected function ensureBodyListOpened() {
+ protected function ensureBodyListOpened(): void {
if ($this->bodyListOpened) {
return;
}
@@ -515,7 +493,7 @@ EOF;
$this->htmlBody .= $this->listBegin;
}
- protected function ensureBodyListClosed() {
+ protected function ensureBodyListClosed(): void {
if (!$this->bodyListOpened) {
return;
}
@@ -534,12 +512,14 @@ EOF;
* @param string $plainTextLeft Text of left button that is used in the plain text version - if unset the $textLeft is used
* @param string $plainTextRight Text of right button that is used in the plain text version - if unset the $textRight is used
*/
- public function addBodyButtonGroup(string $textLeft,
- string $urlLeft,
- string $textRight,
- string $urlRight,
- string $plainTextLeft = '',
- string $plainTextRight = '') {
+ public function addBodyButtonGroup(
+ string $textLeft,
+ string $urlLeft,
+ string $textRight,
+ string $urlRight,
+ string $plainTextLeft = '',
+ string $plainTextRight = '',
+ ): void {
if ($this->footerAdded) {
return;
}
@@ -556,8 +536,8 @@ EOF;
$this->ensureBodyIsOpened();
$this->ensureBodyListClosed();
- $color = $this->themingDefaults->getColorPrimary();
- $textColor = $this->themingDefaults->getTextColorPrimary();
+ $color = $this->themingDefaults->getDefaultColorPrimary();
+ $textColor = $this->themingDefaults->getDefaultTextColorPrimary();
$this->htmlBody .= vsprintf($this->buttonGroup, [$color, $color, $urlLeft, $color, $textColor, $textColor, $textLeft, $urlRight, $textRight]);
$this->plainBody .= PHP_EOL . $plainTextLeft . ': ' . $urlLeft . PHP_EOL;
@@ -569,12 +549,12 @@ EOF;
*
* @param string $text Text of button; Note: When $plainText falls back to this, HTML is automatically escaped in the HTML email
* @param string $url URL of button
- * @param string $plainText Text of button in plain text version
- * if empty the $text is used, if false none will be used
+ * @param string|false $plainText Text of button in plain text version
+ * if empty the $text is used, if false none will be used
*
* @since 12.0.0
*/
- public function addBodyButton(string $text, string $url, $plainText = '') {
+ public function addBodyButton(string $text, string $url, $plainText = ''): void {
if ($this->footerAdded) {
return;
}
@@ -587,8 +567,8 @@ EOF;
$text = htmlspecialchars($text);
}
- $color = $this->themingDefaults->getColorPrimary();
- $textColor = $this->themingDefaults->getTextColorPrimary();
+ $color = $this->themingDefaults->getDefaultColorPrimary();
+ $textColor = $this->themingDefaults->getDefaultTextColorPrimary();
$this->htmlBody .= vsprintf($this->button, [$color, $color, $url, $color, $textColor, $textColor, $text]);
if ($plainText !== false) {
@@ -601,7 +581,7 @@ EOF;
/**
* Close the HTML body when it is open
*/
- protected function ensureBodyIsClosed() {
+ protected function ensureBodyIsClosed(): void {
if (!$this->bodyOpened) {
return;
}
@@ -617,10 +597,14 @@ EOF;
*
* @param string $text If the text is empty the default "Name - Slogan<br>This is an automatically sent email" will be used
*/
- public function addFooter(string $text = '', ?string $lang = null) {
+ public function addFooter(string $text = '', ?string $lang = null): void {
if ($text === '') {
$l10n = $this->l10nFactory->get('lib', $lang);
- $text = $this->themingDefaults->getName() . ' - ' . $this->themingDefaults->getSlogan($lang) . '<br>' . $l10n->t('This is an automatically sent email, please do not reply.');
+ $slogan = $this->themingDefaults->getSlogan($lang);
+ if ($slogan !== '') {
+ $slogan = ' - ' . $slogan;
+ }
+ $text = $this->themingDefaults->getName() . $slogan . '<br>' . $l10n->t('This is an automatically sent email, please do not reply.');
}
if ($this->footerAdded) {
@@ -638,8 +622,6 @@ EOF;
/**
* Returns the rendered email subject as string
- *
- * @return string
*/
public function renderSubject(): string {
return $this->subject;
@@ -647,8 +629,6 @@ EOF;
/**
* Returns the rendered HTML email as string
- *
- * @return string
*/
public function renderHtml(): string {
if (!$this->footerAdded) {
@@ -661,8 +641,6 @@ EOF;
/**
* Returns the rendered plain text email as string
- *
- * @return string
*/
public function renderText(): string {
if (!$this->footerAdded) {
diff --git a/lib/private/Mail/Mailer.php b/lib/private/Mail/Mailer.php
index 2e996dea502..bdc4d6760e0 100644
--- a/lib/private/Mail/Mailer.php
+++ b/lib/private/Mail/Mailer.php
@@ -1,55 +1,38 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arne Hamann <kontakt+github@arne.email>
- * @author Branko Kokanovic <branko@kokanovic.org>
- * @author Carsten Wiedmann <carsten_sttgt@gmx.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jared Boone <jared.boone@gmail.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author kevin147147 <kevintamool@gmail.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Tekhnee <info@tekhnee.org>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OC\Mail;
use Egulias\EmailValidator\EmailValidator;
+use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
use Egulias\EmailValidator\Validation\RFCValidation;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IBinaryFinder;
use OCP\IConfig;
use OCP\IL10N;
-use OCP\ILogger;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
+use OCP\Mail\Events\BeforeMessageSent;
use OCP\Mail\IAttachment;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
-use OCP\Mail\Events\BeforeMessageSent;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
+use Symfony\Component\Mailer\Mailer as SymfonyMailer;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Mailer\Transport\NullTransport;
+use Symfony\Component\Mailer\Transport\SendmailTransport;
+use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
+use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
* Class Mailer provides some basic functions to create a mail message that can be used in combination with
@@ -57,7 +40,7 @@ use OCP\Mail\Events\BeforeMessageSent;
*
* Example usage:
*
- * $mailer = \OC::$server->getMailer();
+ * $mailer = \OC::$server->get(\OCP\Mail\IMailer::class);
* $message = $mailer->createMessage();
* $message->setSubject('Your Subject');
* $message->setFrom(array('cloud@domain.org' => 'ownCloud Notifier'));
@@ -70,94 +53,91 @@ use OCP\Mail\Events\BeforeMessageSent;
* @package OC\Mail
*/
class Mailer implements IMailer {
- /** @var \Swift_Mailer Cached mailer */
- private $instance = null;
- /** @var IConfig */
- private $config;
- /** @var ILogger */
- private $logger;
- /** @var Defaults */
- private $defaults;
- /** @var IURLGenerator */
- private $urlGenerator;
- /** @var IL10N */
- private $l10n;
- /** @var IEventDispatcher */
- private $dispatcher;
- /** @var IFactory */
- private $l10nFactory;
-
- /**
- * @param IConfig $config
- * @param ILogger $logger
- * @param Defaults $defaults
- * @param IURLGenerator $urlGenerator
- * @param IL10N $l10n
- * @param IEventDispatcher $dispatcher
- */
- public function __construct(IConfig $config,
- ILogger $logger,
- Defaults $defaults,
- IURLGenerator $urlGenerator,
- IL10N $l10n,
- IEventDispatcher $dispatcher,
- IFactory $l10nFactory) {
- $this->config = $config;
- $this->logger = $logger;
- $this->defaults = $defaults;
- $this->urlGenerator = $urlGenerator;
- $this->l10n = $l10n;
- $this->dispatcher = $dispatcher;
- $this->l10nFactory = $l10nFactory;
+ // Do not move this block or change it's content without contacting the release crew
+ public const DEFAULT_DIMENSIONS = '252x120';
+ // Do not move this block or change it's content without contacting the release crew
+
+ public const MAX_LOGO_SIZE = 105;
+
+ private ?MailerInterface $instance = null;
+
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private Defaults $defaults,
+ private IURLGenerator $urlGenerator,
+ private IL10N $l10n,
+ private IEventDispatcher $dispatcher,
+ private IFactory $l10nFactory,
+ ) {
}
/**
* Creates a new message object that can be passed to send()
- *
- * @return IMessage
*/
- public function createMessage(): IMessage {
- $plainTextOnly = $this->config->getSystemValue('mail_send_plaintext_only', false);
- return new Message(new \Swift_Message(), $plainTextOnly);
+ public function createMessage(): Message {
+ $plainTextOnly = $this->config->getSystemValueBool('mail_send_plaintext_only', false);
+ return new Message(new Email(), $plainTextOnly);
}
/**
* @param string|null $data
* @param string|null $filename
* @param string|null $contentType
- * @return IAttachment
* @since 13.0.0
*/
public function createAttachment($data = null, $filename = null, $contentType = null): IAttachment {
- return new Attachment(new \Swift_Attachment($data, $filename, $contentType));
+ return new Attachment($data, $filename, $contentType);
}
/**
- * @param string $path
* @param string|null $contentType
- * @return IAttachment
* @since 13.0.0
*/
public function createAttachmentFromPath(string $path, $contentType = null): IAttachment {
- return new Attachment(\Swift_Attachment::fromPath($path, $contentType));
+ return new Attachment(null, null, $contentType, $path);
}
/**
* Creates a new email template object
*
- * @param string $emailId
- * @param array $data
- * @return IEMailTemplate
* @since 12.0.0
*/
public function createEMailTemplate(string $emailId, array $data = []): IEMailTemplate {
- $class = $this->config->getSystemValue('mail_template_class', '');
+ $logoDimensions = $this->config->getAppValue('theming', 'logoDimensions', self::DEFAULT_DIMENSIONS);
+ if (str_contains($logoDimensions, 'x')) {
+ [$width, $height] = explode('x', $logoDimensions);
+ $width = (int)$width;
+ $height = (int)$height;
+
+ if ($width > self::MAX_LOGO_SIZE || $height > self::MAX_LOGO_SIZE) {
+ if ($width === $height) {
+ $logoWidth = self::MAX_LOGO_SIZE;
+ $logoHeight = self::MAX_LOGO_SIZE;
+ } elseif ($width > $height) {
+ $logoWidth = self::MAX_LOGO_SIZE;
+ $logoHeight = (int)(($height / $width) * self::MAX_LOGO_SIZE);
+ } else {
+ $logoWidth = (int)(($width / $height) * self::MAX_LOGO_SIZE);
+ $logoHeight = self::MAX_LOGO_SIZE;
+ }
+ } else {
+ $logoWidth = $width;
+ $logoHeight = $height;
+ }
+ } else {
+ $logoWidth = $logoHeight = null;
+ }
+
+ $class = $this->config->getSystemValueString('mail_template_class', '');
if ($class !== '' && class_exists($class) && is_a($class, EMailTemplate::class, true)) {
return new $class(
$this->defaults,
$this->urlGenerator,
$this->l10nFactory,
+ $logoWidth,
+ $logoHeight,
$emailId,
$data
);
@@ -167,6 +147,8 @@ class Mailer implements IMailer {
$this->defaults,
$this->urlGenerator,
$this->l10nFactory,
+ $logoWidth,
+ $logoHeight,
$emailId,
$data
);
@@ -176,47 +158,80 @@ class Mailer implements IMailer {
* Send the specified message. Also sets the from address to the value defined in config.php
* if no-one has been passed.
*
- * @param IMessage|Message $message Message to send
- * @return string[] Array with failed recipients. Be aware that this depends on the used mail backend and
- * therefore should be considered
- * @throws \Exception In case it was not possible to send the message. (for example if an invalid mail address
- * has been supplied.)
+ * If sending failed, the recipients that failed will be returned (to, cc and bcc).
+ * Will output additional debug info if 'mail_smtpdebug' => 'true' is set in config.php
+ *
+ * @param IMessage $message Message to send
+ * @return string[] $failedRecipients
*/
public function send(IMessage $message): array {
- $debugMode = $this->config->getSystemValue('mail_smtpdebug', false);
+ $debugMode = $this->config->getSystemValueBool('mail_smtpdebug', false);
+
+ if (!($message instanceof Message)) {
+ throw new \InvalidArgumentException('Object not of type ' . Message::class);
+ }
if (empty($message->getFrom())) {
$message->setFrom([\OCP\Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]);
}
- $failedRecipients = [];
-
$mailer = $this->getInstance();
- // Enable logger if debug mode is enabled
- if ($debugMode) {
- $mailLogger = new \Swift_Plugins_Loggers_ArrayLogger();
- $mailer->registerPlugin(new \Swift_Plugins_LoggerPlugin($mailLogger));
- }
+ $this->dispatcher->dispatchTyped(new BeforeMessageSent($message));
+ try {
+ $message->setRecipients();
+ } catch (\InvalidArgumentException|RfcComplianceException $e) {
+ $logMessage = sprintf(
+ 'Could not send mail to "%s" with subject "%s" as validation for address failed',
+ print_r(array_merge($message->getTo(), $message->getCc(), $message->getBcc()), true),
+ $message->getSubject()
+ );
+ $this->logger->debug($logMessage, ['app' => 'core', 'exception' => $e]);
+ $recipients = array_merge($message->getTo(), $message->getCc(), $message->getBcc());
+ $failedRecipients = [];
+
+ array_walk($recipients, function ($value, $key) use (&$failedRecipients) {
+ if (is_numeric($key)) {
+ $failedRecipients[] = $value;
+ } else {
+ $failedRecipients[] = $key;
+ }
+ });
- $this->dispatcher->dispatchTyped(new BeforeMessageSent($message));
+ return $failedRecipients;
+ }
+
+ try {
+ $mailer->send($message->getSymfonyEmail());
+ } catch (TransportExceptionInterface $e) {
+ $logMessage = sprintf('Sending mail to "%s" with subject "%s" failed', print_r($message->getTo(), true), $message->getSubject());
+ $this->logger->debug($logMessage, ['app' => 'core', 'exception' => $e]);
+ if ($debugMode) {
+ $this->logger->debug($e->getDebug(), ['app' => 'core']);
+ }
+ $recipients = array_merge($message->getTo(), $message->getCc(), $message->getBcc());
+ $failedRecipients = [];
+
+ array_walk($recipients, function ($value, $key) use (&$failedRecipients) {
+ if (is_numeric($key)) {
+ $failedRecipients[] = $value;
+ } else {
+ $failedRecipients[] = $key;
+ }
+ });
- $mailer->send($message->getSwiftMessage(), $failedRecipients);
+ return $failedRecipients;
+ }
// Debugging logging
$logMessage = sprintf('Sent mail to "%s" with subject "%s"', print_r($message->getTo(), true), $message->getSubject());
$this->logger->debug($logMessage, ['app' => 'core']);
- if ($debugMode && isset($mailLogger)) {
- $this->logger->debug($mailLogger->dump(), ['app' => 'core']);
- }
- return $failedRecipients;
+ return [];
}
/**
- * Checks if an e-mail address is valid
- *
* @param string $email Email address to be validated
* @return bool True if the mail address is valid, false otherwise
*/
@@ -225,38 +240,23 @@ class Mailer implements IMailer {
// Shortcut: empty addresses are never valid
return false;
}
- $validator = new EmailValidator();
- $validation = new RFCValidation();
- return $validator->isValid($this->convertEmail($email), $validation);
- }
-
- /**
- * SwiftMailer does currently not work with IDN domains, this function therefore converts the domains
- *
- * FIXME: Remove this once SwiftMailer supports IDN
- *
- * @param string $email
- * @return string Converted mail address if `idn_to_ascii` exists
- */
- protected function convertEmail(string $email): string {
- if (!function_exists('idn_to_ascii') || !defined('INTL_IDNA_VARIANT_UTS46') || strpos($email, '@') === false) {
- return $email;
- }
+ $strictMailCheck = $this->config->getAppValue('core', 'enforce_strict_email_check', 'yes') === 'yes';
+ $validator = new EmailValidator();
+ $validation = $strictMailCheck ? new NoRFCWarningsValidation() : new RFCValidation();
- list($name, $domain) = explode('@', $email, 2);
- $domain = idn_to_ascii($domain, 0,INTL_IDNA_VARIANT_UTS46);
- return $name.'@'.$domain;
+ return $validator->isValid($email, $validation);
}
- protected function getInstance(): \Swift_Mailer {
+ protected function getInstance(): MailerInterface {
if (!is_null($this->instance)) {
return $this->instance;
}
- $transport = null;
-
- switch ($this->config->getSystemValue('mail_smtpmode', 'smtp')) {
+ switch ($this->config->getSystemValueString('mail_smtpmode', 'smtp')) {
+ case 'null':
+ $transport = new NullTransport();
+ break;
case 'sendmail':
$transport = $this->getSendMailInstance();
break;
@@ -266,31 +266,58 @@ class Mailer implements IMailer {
break;
}
- return new \Swift_Mailer($transport);
+ $this->instance = new SymfonyMailer($transport);
+
+ return $this->instance;
}
/**
* Returns the SMTP transport
*
- * @return \Swift_SmtpTransport
+ * Only supports ssl/tls
+ * starttls is not enforcable with Symfony Mailer but might be available
+ * via the automatic config (Symfony Mailer internal)
+ *
+ * @return EsmtpTransport
*/
- protected function getSmtpInstance(): \Swift_SmtpTransport {
- $transport = new \Swift_SmtpTransport();
- $transport->setTimeout($this->config->getSystemValue('mail_smtptimeout', 10));
- $transport->setHost($this->config->getSystemValue('mail_smtphost', '127.0.0.1'));
- $transport->setPort($this->config->getSystemValue('mail_smtpport', 25));
- if ($this->config->getSystemValue('mail_smtpauth', false)) {
- $transport->setUsername($this->config->getSystemValue('mail_smtpname', ''));
- $transport->setPassword($this->config->getSystemValue('mail_smtppassword', ''));
- $transport->setAuthMode($this->config->getSystemValue('mail_smtpauthtype', 'LOGIN'));
- }
- $smtpSecurity = $this->config->getSystemValue('mail_smtpsecure', '');
- if (!empty($smtpSecurity)) {
- $transport->setEncryption($smtpSecurity);
+ protected function getSmtpInstance(): EsmtpTransport {
+ // either null or true - if nothing is passed, let the symfony mailer figure out the configuration by itself
+ $mailSmtpsecure = ($this->config->getSystemValue('mail_smtpsecure', null) === 'ssl') ? true : null;
+ $transport = new EsmtpTransport(
+ $this->config->getSystemValueString('mail_smtphost', '127.0.0.1'),
+ $this->config->getSystemValueInt('mail_smtpport', 25),
+ $mailSmtpsecure,
+ null,
+ $this->logger
+ );
+ /** @var SocketStream $stream */
+ $stream = $transport->getStream();
+ /** @psalm-suppress InternalMethod */
+ $stream->setTimeout($this->config->getSystemValueInt('mail_smtptimeout', 10));
+
+ if ($this->config->getSystemValueBool('mail_smtpauth', false)) {
+ $transport->setUsername($this->config->getSystemValueString('mail_smtpname', ''));
+ $transport->setPassword($this->config->getSystemValueString('mail_smtppassword', ''));
}
+
$streamingOptions = $this->config->getSystemValue('mail_smtpstreamoptions', []);
if (is_array($streamingOptions) && !empty($streamingOptions)) {
- $transport->setStreamOptions($streamingOptions);
+ /** @psalm-suppress InternalMethod */
+ $currentStreamingOptions = $stream->getStreamOptions();
+
+ $currentStreamingOptions = array_merge_recursive($currentStreamingOptions, $streamingOptions);
+
+ /** @psalm-suppress InternalMethod */
+ $stream->setStreamOptions($currentStreamingOptions);
+ }
+
+ $overwriteCliUrl = parse_url(
+ $this->config->getSystemValueString('overwrite.cli.url', ''),
+ PHP_URL_HOST
+ );
+
+ if (!empty($overwriteCliUrl)) {
+ $transport->setLocalDomain($overwriteCliUrl);
}
return $transport;
@@ -299,31 +326,30 @@ class Mailer implements IMailer {
/**
* Returns the sendmail transport
*
- * @return \Swift_SendmailTransport
+ * @return SendmailTransport
*/
- protected function getSendMailInstance(): \Swift_SendmailTransport {
- switch ($this->config->getSystemValue('mail_smtpmode', 'smtp')) {
+ protected function getSendMailInstance(): SendmailTransport {
+ switch ($this->config->getSystemValueString('mail_smtpmode', 'smtp')) {
case 'qmail':
$binaryPath = '/var/qmail/bin/sendmail';
break;
default:
- $sendmail = \OC_Helper::findBinaryPath('sendmail');
- if ($sendmail === null) {
+ $sendmail = \OCP\Server::get(IBinaryFinder::class)->findBinaryPath('sendmail');
+ if ($sendmail === false) {
+ // fallback (though not sure what good it'll do)
$sendmail = '/usr/sbin/sendmail';
+ $this->logger->debug('sendmail binary search failed, using fallback ' . $sendmail, ['app' => 'core']);
}
$binaryPath = $sendmail;
break;
}
- switch ($this->config->getSystemValue('mail_sendmailmode', 'smtp')) {
- case 'pipe':
- $binaryParam = ' -t';
- break;
- default:
- $binaryParam = ' -bs';
- break;
- }
+ $binaryParam = match ($this->config->getSystemValueString('mail_sendmailmode', 'smtp')) {
+ 'pipe' => ' -t -i',
+ default => ' -bs',
+ };
- return new \Swift_SendmailTransport($binaryPath . $binaryParam);
+ $this->logger->debug('Using sendmail binary: ' . $binaryPath, ['app' => 'core']);
+ return new SendmailTransport($binaryPath . $binaryParam, null, $this->logger);
}
}
diff --git a/lib/private/Mail/Message.php b/lib/private/Mail/Message.php
index bd7c01bd358..523a4836760 100644
--- a/lib/private/Mail/Message.php
+++ b/lib/private/Mail/Message.php
@@ -1,94 +1,87 @@
<?php
declare(strict_types=1);
-
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Arne Hamann <kontakt+github@arne.email>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Jared Boone <jared.boone@gmail.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OC\Mail;
+use OCP\Mail\Headers\AutoSubmitted;
use OCP\Mail\IAttachment;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMessage;
-use Swift_Message;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\Mime\Email;
+use Symfony\Component\Mime\Exception\InvalidArgumentException;
+use Symfony\Component\Mime\Exception\RfcComplianceException;
/**
- * Class Message provides a wrapper around SwiftMail
+ * Class Message provides a wrapper around Symfony\Component\Mime\Email (Used to be around SwiftMail)
*
* @package OC\Mail
*/
class Message implements IMessage {
- /** @var Swift_Message */
- private $swiftMessage;
- /** @var bool */
- private $plainTextOnly;
-
- public function __construct(Swift_Message $swiftMessage, bool $plainTextOnly) {
- $this->swiftMessage = $swiftMessage;
- $this->plainTextOnly = $plainTextOnly;
+ private array $to = [];
+ private array $from = [];
+ private array $replyTo = [];
+ private array $cc = [];
+ private array $bcc = [];
+
+ public function __construct(
+ private Email $symfonyEmail,
+ private bool $plainTextOnly,
+ ) {
}
/**
- * @param IAttachment $attachment
- * @return $this
* @since 13.0.0
+ * @return $this
*/
public function attach(IAttachment $attachment): IMessage {
/** @var Attachment $attachment */
- $this->swiftMessage->attach($attachment->getSwiftAttachment());
+ $attachment->attach($this->symfonyEmail);
+ return $this;
+ }
+
+ /**
+ * Can be used to "attach content inline" as message parts with specific MIME type and encoding.
+ * {@inheritDoc}
+ * @since 26.0.0
+ */
+ public function attachInline(string $body, string $name, ?string $contentType = null): IMessage {
+ # To be sure this works with iCalendar messages, we encode with 8bit instead of
+ # quoted-printable encoding. We save the current encoder, replace the current
+ # encoder with an 8bit encoder and after we've finished, we reset the encoder
+ # to the previous one. Originally intended to be added after the message body,
+ # as it is curently unknown if all mail clients handle this properly if added
+ # before.
+ $this->symfonyEmail->embed($body, $name, $contentType);
return $this;
}
/**
- * SwiftMailer does currently not work with IDN domains, this function therefore converts the domains
- * FIXME: Remove this once SwiftMailer supports IDN
+ * Converts the [['displayName' => 'email'], ['displayName2' => 'email2']] arrays to valid Adresses
*
- * @param array $addresses Array of mail addresses, key will get converted
- * @return array Converted addresses if `idn_to_ascii` exists
+ * @param array $addresses Array of mail addresses
+ * @return Address[]
+ * @throws RfcComplianceException|InvalidArgumentException
*/
protected function convertAddresses(array $addresses): array {
- if (!function_exists('idn_to_ascii') || !defined('INTL_IDNA_VARIANT_UTS46')) {
- return $addresses;
- }
-
$convertedAddresses = [];
- foreach ($addresses as $email => $readableName) {
- if (!is_numeric($email)) {
- list($name, $domain) = explode('@', $email, 2);
- $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
- $convertedAddresses[$name.'@'.$domain] = $readableName;
+ if (empty($addresses)) {
+ return [];
+ }
+
+ array_walk($addresses, function ($readableName, $email) use (&$convertedAddresses) {
+ if (is_numeric($email)) {
+ $convertedAddresses[] = new Address($readableName);
} else {
- list($name, $domain) = explode('@', $readableName, 2);
- $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
- $convertedAddresses[$email] = $name.'@'.$domain;
+ $convertedAddresses[] = new Address($email, $readableName);
}
- }
+ });
return $convertedAddresses;
}
@@ -102,41 +95,31 @@ class Message implements IMessage {
* @return $this
*/
public function setFrom(array $addresses): IMessage {
- $addresses = $this->convertAddresses($addresses);
-
- $this->swiftMessage->setFrom($addresses);
+ $this->from = $addresses;
return $this;
}
/**
* Get the from address of this message.
- *
- * @return array
*/
public function getFrom(): array {
- return $this->swiftMessage->getFrom() ?? [];
+ return $this->from;
}
/**
* Set the Reply-To address of this message
- *
- * @param array $addresses
* @return $this
*/
public function setReplyTo(array $addresses): IMessage {
- $addresses = $this->convertAddresses($addresses);
-
- $this->swiftMessage->setReplyTo($addresses);
+ $this->replyTo = $addresses;
return $this;
}
/**
* Returns the Reply-To address of this message
- *
- * @return string
*/
- public function getReplyTo(): string {
- return $this->swiftMessage->getReplyTo();
+ public function getReplyTo(): array {
+ return $this->replyTo;
}
/**
@@ -146,19 +129,15 @@ class Message implements IMessage {
* @return $this
*/
public function setTo(array $recipients): IMessage {
- $recipients = $this->convertAddresses($recipients);
-
- $this->swiftMessage->setTo($recipients);
+ $this->to = $recipients;
return $this;
}
/**
* Get the to address of this message.
- *
- * @return array
*/
public function getTo(): array {
- return $this->swiftMessage->getTo() ?? [];
+ return $this->to;
}
/**
@@ -168,19 +147,15 @@ class Message implements IMessage {
* @return $this
*/
public function setCc(array $recipients): IMessage {
- $recipients = $this->convertAddresses($recipients);
-
- $this->swiftMessage->setCc($recipients);
+ $this->cc = $recipients;
return $this;
}
/**
* Get the cc address of this message.
- *
- * @return array
*/
public function getCc(): array {
- return $this->swiftMessage->getCc() ?? [];
+ return $this->cc;
}
/**
@@ -190,104 +165,114 @@ class Message implements IMessage {
* @return $this
*/
public function setBcc(array $recipients): IMessage {
- $recipients = $this->convertAddresses($recipients);
-
- $this->swiftMessage->setBcc($recipients);
+ $this->bcc = $recipients;
return $this;
}
/**
* Get the Bcc address of this message.
- *
- * @return array
*/
public function getBcc(): array {
- return $this->swiftMessage->getBcc() ?? [];
+ return $this->bcc;
}
/**
- * Set the subject of this message.
- *
- * @param string $subject
- * @return IMessage
+ * @return $this
*/
public function setSubject(string $subject): IMessage {
- $this->swiftMessage->setSubject($subject);
+ $this->symfonyEmail->subject($subject);
return $this;
}
/**
* Get the from subject of this message.
- *
- * @return string
*/
public function getSubject(): string {
- return $this->swiftMessage->getSubject();
+ return $this->symfonyEmail->getSubject() ?? '';
}
/**
- * Set the plain-text body of this message.
- *
- * @param string $body
* @return $this
*/
public function setPlainBody(string $body): IMessage {
- $this->swiftMessage->setBody($body);
+ $this->symfonyEmail->text($body);
return $this;
}
/**
* Get the plain body of this message.
- *
- * @return string
*/
public function getPlainBody(): string {
- return $this->swiftMessage->getBody();
+ /** @var string $body */
+ $body = $this->symfonyEmail->getTextBody() ?? '';
+ return $body;
}
/**
- * Set the HTML body of this message. Consider also sending a plain-text body instead of only an HTML one.
- *
- * @param string $body
* @return $this
*/
- public function setHtmlBody($body) {
+ public function setHtmlBody(string $body): IMessage {
if (!$this->plainTextOnly) {
- $this->swiftMessage->addPart($body, 'text/html');
+ $this->symfonyEmail->html($body);
}
return $this;
}
/**
- * Get's the underlying SwiftMessage
- * @param Swift_Message $swiftMessage
+ * Set the underlying Email instance
*/
- public function setSwiftMessage(Swift_Message $swiftMessage): void {
- $this->swiftMessage = $swiftMessage;
+ public function setSymfonyEmail(Email $symfonyEmail): void {
+ $this->symfonyEmail = $symfonyEmail;
}
/**
- * Get's the underlying SwiftMessage
- * @return Swift_Message
+ * Get the underlying Email instance
*/
- public function getSwiftMessage(): Swift_Message {
- return $this->swiftMessage;
+ public function getSymfonyEmail(): Email {
+ return $this->symfonyEmail;
}
/**
- * @param string $body
- * @param string $contentType
* @return $this
*/
- public function setBody($body, $contentType) {
+ public function setBody(string $body, string $contentType): IMessage {
if (!$this->plainTextOnly || $contentType !== 'text/html') {
- $this->swiftMessage->setBody($body, $contentType);
+ if ($contentType === 'text/html') {
+ $this->symfonyEmail->html($body);
+ } else {
+ $this->symfonyEmail->text($body);
+ }
}
return $this;
}
/**
- * @param IEMailTemplate $emailTemplate
+ * Set the recipients on the symphony email
+ *
+ * Since
+ *
+ * setTo
+ * setFrom
+ * setReplyTo
+ * setCc
+ * setBcc
+ *
+ * could throw a \Symfony\Component\Mime\Exception\RfcComplianceException
+ * or a \Symfony\Component\Mime\Exception\InvalidArgumentException
+ * we wrap the calls here. We then have the validation errors all in one place and can
+ * throw shortly before \OC\Mail\Mailer::send
+ *
+ * @throws InvalidArgumentException|RfcComplianceException
+ */
+ public function setRecipients(): void {
+ $this->symfonyEmail->to(...$this->convertAddresses($this->getTo()));
+ $this->symfonyEmail->from(...$this->convertAddresses($this->getFrom()));
+ $this->symfonyEmail->replyTo(...$this->convertAddresses($this->getReplyTo()));
+ $this->symfonyEmail->cc(...$this->convertAddresses($this->getCc()));
+ $this->symfonyEmail->bcc(...$this->convertAddresses($this->getBcc()));
+ }
+
+ /**
* @return $this
*/
public function useTemplate(IEMailTemplate $emailTemplate): IMessage {
@@ -298,4 +283,40 @@ class Message implements IMessage {
}
return $this;
}
+
+ /**
+ * Add the Auto-Submitted header to the email, preventing most automated
+ * responses to automated messages.
+ *
+ * @param AutoSubmitted::VALUE_* $value (one of AutoSubmitted::VALUE_NO, AutoSubmitted::VALUE_AUTO_GENERATED, AutoSubmitted::VALUE_AUTO_REPLIED)
+ * @return $this
+ */
+ public function setAutoSubmitted(string $value): IMessage {
+ $headers = $this->symfonyEmail->getHeaders();
+
+ if ($headers->has(AutoSubmitted::HEADER)) {
+ // if the header already exsists, remove it.
+ // the value can be modified with some implementations
+ // of the interface \Swift_Mime_Header, however the
+ // interface doesn't, and this makes the static-code
+ // analysis unhappy.
+ // @todo check if symfony mailer can modify the autosubmitted header
+ $headers->remove(AutoSubmitted::HEADER);
+ }
+
+ $headers->addTextHeader(AutoSubmitted::HEADER, $value);
+
+ return $this;
+ }
+
+ /**
+ * Get the current value of the Auto-Submitted header. Defaults to "no"
+ * which is equivalent to the header not existing at all
+ */
+ public function getAutoSubmitted(): string {
+ $headers = $this->symfonyEmail->getHeaders();
+
+ return $headers->has(AutoSubmitted::HEADER)
+ ? $headers->get(AutoSubmitted::HEADER)->getBodyAsString() : AutoSubmitted::VALUE_NO;
+ }
}
diff --git a/lib/private/Mail/Provider/Manager.php b/lib/private/Mail/Provider/Manager.php
new file mode 100644
index 00000000000..f162d30b834
--- /dev/null
+++ b/lib/private/Mail/Provider/Manager.php
@@ -0,0 +1,255 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Mail\Provider;
+
+use OC\AppFramework\Bootstrap\Coordinator;
+use OCP\Mail\Provider\IManager;
+use OCP\Mail\Provider\IProvider;
+use OCP\Mail\Provider\IService;
+use Psr\Container\ContainerInterface;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+class Manager implements IManager {
+
+ protected ?array $providersCollection = null;
+
+ public function __construct(
+ private Coordinator $coordinator,
+ private ContainerInterface $container,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * Determine if any mail providers are registered
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function has(): bool {
+
+ // return true if collection has any providers
+ return !empty($this->providers());
+
+ }
+
+ /**
+ * Retrieve a count of how many mail providers are registered
+ *
+ * @since 30.0.0
+ *
+ * @return int
+ */
+ public function count(): int {
+
+ // return count of providers in collection
+ return count($this->providers());
+
+ }
+
+ /**
+ * Retrieve which mail providers are registered
+ *
+ * @since 30.0.0
+ *
+ * @return array<string,string> collection of provider id and label ['jmap' => 'JMap Connector']
+ */
+ public function types(): array {
+
+ // construct types collection
+ $types = [];
+ // extract id and name from providers collection
+ foreach ($this->providers() as $entry) {
+ $types[$entry->id()] = $entry->label();
+ }
+ // return types collection
+ return $types;
+
+ }
+
+ /**
+ * Retrieve all registered mail providers
+ *
+ * @since 30.0.0
+ *
+ * @return array<string,IProvider> collection of provider id and object ['jmap' => IProviderObject]
+ */
+ public function providers(): array {
+
+ // evaluate if we already have a cached collection of providers and return the collection if we do
+ if (is_array($this->providersCollection)) {
+ return $this->providersCollection;
+ }
+ // retrieve server registration context
+ $context = $this->coordinator->getRegistrationContext();
+ // evaluate if registration context was returned
+ if ($context === null) {
+ return [];
+ }
+ // initilize cached collection
+ $this->providersCollection = [];
+ // iterate through all registered mail providers
+ foreach ($context->getMailProviders() as $entry) {
+ try {
+ /** @var IProvider $provider */
+ // object provider
+ $provider = $this->container->get($entry->getService());
+ // add provider to cache collection
+ $this->providersCollection[$provider->id()] = $provider;
+ } catch (Throwable $e) {
+ $this->logger->error(
+ 'Could not load mail provider ' . $entry->getService() . ': ' . $e->getMessage(),
+ ['exception' => $e]
+ );
+ }
+ }
+ // return mail provider collection
+ return $this->providersCollection;
+
+ }
+
+ /**
+ * Retrieve a provider with a specific id
+ *
+ * @since 30.0.0
+ *
+ * @param string $providerId provider id
+ *
+ * @return IProvider|null
+ */
+ public function findProviderById(string $providerId): ?IProvider {
+
+ // evaluate if we already have a cached collection of providers
+ if (!is_array($this->providersCollection)) {
+ $this->providers();
+ }
+
+ if (isset($this->providersCollection[$providerId])) {
+ return $this->providersCollection[$providerId];
+ }
+ // return null if provider was not found
+ return null;
+
+ }
+
+ /**
+ * Retrieve all services for all registered mail providers
+ *
+ * @since 30.0.0
+ *
+ * @param string $userId user id
+ *
+ * @return array<string,array<string,IService>> collection of provider id, service id and object ['jmap' => ['Service1' => IServiceObject]]
+ */
+ public function services(string $userId): array {
+
+ // initilize collection
+ $services = [];
+ // retrieve and iterate through mail providers
+ foreach ($this->providers() as $entry) {
+ // retrieve collection of services
+ $mailServices = $entry->listServices($userId);
+ // evaluate if mail services collection is not empty and add results to services collection
+ if (!empty($mailServices)) {
+ $services[$entry->id()] = $mailServices;
+ }
+ }
+ // return collection
+ return $services;
+
+ }
+
+ /**
+ * Retrieve a service with a specific id
+ *
+ * @since 30.0.0
+ *
+ * @param string $userId user id
+ * @param string $serviceId service id
+ * @param string $providerId provider id
+ *
+ * @return IService|null returns service object or null if none found
+ */
+ public function findServiceById(string $userId, string $serviceId, ?string $providerId = null): ?IService {
+
+ // evaluate if provider id was specified
+ if ($providerId !== null) {
+ // find provider
+ $provider = $this->findProviderById($providerId);
+ // evaluate if provider was found
+ if ($provider instanceof IProvider) {
+ // find service with specific id
+ $service = $provider->findServiceById($userId, $serviceId);
+ // evaluate if mail service was found
+ if ($service instanceof IService) {
+ return $service;
+ }
+ }
+ } else {
+ // retrieve and iterate through mail providers
+ foreach ($this->providers() as $provider) {
+ // find service with specific id
+ $service = $provider->findServiceById($userId, $serviceId);
+ // evaluate if mail service was found
+ if ($service instanceof IService) {
+ return $service;
+ }
+ }
+ }
+
+ // return null if no match was found
+ return null;
+
+ }
+
+ /**
+ * Retrieve a service for a specific mail address
+ * returns first service with specific primary address
+ *
+ * @since 30.0.0
+ *
+ * @param string $userId user id
+ * @param string $address mail address (e.g. test@example.com)
+ * @param string $providerId provider id
+ *
+ * @return IService|null returns service object or null if none found
+ */
+ public function findServiceByAddress(string $userId, string $address, ?string $providerId = null): ?IService {
+
+ // evaluate if provider id was specified
+ if ($providerId !== null) {
+ // find provider
+ $provider = $this->findProviderById($providerId);
+ // evaluate if provider was found
+ if ($provider instanceof IProvider) {
+ // find service with specific mail address
+ $service = $provider->findServiceByAddress($userId, $address);
+ // evaluate if mail service was found
+ if ($service instanceof IService) {
+ return $service;
+ }
+ }
+ } else {
+ // retrieve and iterate through mail providers
+ foreach ($this->providers() as $provider) {
+ // find service with specific mail address
+ $service = $provider->findServiceByAddress($userId, $address);
+ // evaluate if mail service was found
+ if ($service instanceof IService) {
+ return $service;
+ }
+ }
+ }
+ // return null if no match was found
+ return null;
+
+ }
+}