diff options
Diffstat (limited to 'lib/private/Mail')
-rw-r--r-- | lib/private/Mail/Attachment.php | 56 | ||||
-rw-r--r-- | lib/private/Mail/EMailTemplate.php | 262 | ||||
-rw-r--r-- | lib/private/Mail/Mailer.php | 350 | ||||
-rw-r--r-- | lib/private/Mail/Message.php | 267 | ||||
-rw-r--r-- | lib/private/Mail/Provider/Manager.php | 255 |
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; + + } +} |