diff options
author | Côme Chilliet <come.chilliet@nextcloud.com> | 2021-12-20 11:35:06 +0100 |
---|---|---|
committer | Arthur Schiwon <blizzz@arthur-schiwon.de> | 2023-02-02 10:30:06 +0100 |
commit | dde5c46a3eb7c6dcee47795590619ccd393577d2 (patch) | |
tree | 5351227347b92e5d69a49b632ea138e314eea013 /lib/private/Mail | |
parent | fc4e87a2dfc5ff53bc9f15da13f355dd285769a9 (diff) | |
download | nextcloud-server-dde5c46a3eb7c6dcee47795590619ccd393577d2.tar.gz nextcloud-server-dde5c46a3eb7c6dcee47795590619ccd393577d2.zip |
Migrate to Symfony Mailer
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
Diffstat (limited to 'lib/private/Mail')
-rw-r--r-- | lib/private/Mail/Attachment.php | 36 | ||||
-rw-r--r-- | lib/private/Mail/Mailer.php | 172 | ||||
-rw-r--r-- | lib/private/Mail/Message.php | 188 |
3 files changed, 224 insertions, 172 deletions
diff --git a/lib/private/Mail/Attachment.php b/lib/private/Mail/Attachment.php index 12f71df86d9..5bfe0dd0522 100644 --- a/lib/private/Mail/Attachment.php +++ b/lib/private/Mail/Attachment.php @@ -27,6 +27,7 @@ declare(strict_types=1); namespace OC\Mail; use OCP\Mail\IAttachment; +use Symfony\Component\Mime\Email; /** * Class Attachment @@ -35,11 +36,21 @@ use OCP\Mail\IAttachment; * @since 13.0.0 */ class Attachment implements IAttachment { - /** @var \Swift_Mime_Attachment */ - protected $swiftAttachment; + private ?string $body; + private ?string $name; + private ?string $contentType; + private ?string $path; - public function __construct(\Swift_Mime_Attachment $attachment) { - $this->swiftAttachment = $attachment; + public function __construct( + ?string $body, + ?string $name, + ?string $contentType, + ?string $path = null + ) { + $this->body = $body; + $this->name = $name; + $this->contentType = $contentType; + $this->path = $path; } /** @@ -48,7 +59,7 @@ class Attachment implements IAttachment { * @since 13.0.0 */ public function setFilename(string $filename): IAttachment { - $this->swiftAttachment->setFilename($filename); + $this->name = $filename; return $this; } @@ -58,7 +69,7 @@ class Attachment implements IAttachment { * @since 13.0.0 */ public function setContentType(string $contentType): IAttachment { - $this->swiftAttachment->setContentType($contentType); + $this->contentType = $contentType; return $this; } @@ -68,14 +79,15 @@ class Attachment implements IAttachment { * @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/Mailer.php b/lib/private/Mail/Mailer.php index d0c3b04eacb..05ef0bf5139 100644 --- a/lib/private/Mail/Mailer.php +++ b/lib/private/Mail/Mailer.php @@ -50,6 +50,15 @@ use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Mail\IMessage; 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\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\InvalidArgumentException; +use Symfony\Component\Mime\Exception\RfcComplianceException; /** * Class Mailer provides some basic functions to create a mail message that can be used in combination with @@ -70,12 +79,10 @@ use Psr\Log\LoggerInterface; * @package OC\Mail */ class Mailer implements IMailer { - /** @var \Swift_Mailer Cached mailer */ - private $instance = null; + private ?MailerInterface $instance = null; private IConfig $config; private LoggerInterface $logger; - /** @var Defaults */ - private $defaults; + private Defaults $defaults; private IURLGenerator $urlGenerator; private IL10N $l10n; private IEventDispatcher $dispatcher; @@ -100,11 +107,11 @@ class Mailer implements IMailer { /** * Creates a new message object that can be passed to send() * - * @return IMessage + * @return Message */ - public function createMessage(): IMessage { + public function createMessage(): Message { $plainTextOnly = $this->config->getSystemValue('mail_send_plaintext_only', false); - return new Message(new \Swift_Message(), $plainTextOnly); + return new Message(new Email(), $plainTextOnly); } /** @@ -115,7 +122,7 @@ class Mailer implements IMailer { * @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); } /** @@ -125,7 +132,7 @@ class Mailer implements IMailer { * @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); } /** @@ -162,49 +169,82 @@ 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); + 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; + } - $mailer->send($message->getSwiftMessage(), $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; + } + }); + + return $failedRecipients; + } // Debugging logging $logMessage = sprintf('Sent mail to "%s" with subject "%s"', print_r($message->getTo(), true), $message->getSubject()); - if (!empty($failedRecipients)) { - $logMessage .= sprintf(' (failed for "%s")', print_r($failedRecipients, true)); - } $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 + * @deprecated 26.0.0 Implicit validation is done in \OC\Mail\Message::setRecipients + * via \Symfony\Component\Mime\Address::__construct * * @param string $email Email address to be validated * @return bool True if the mail address is valid, false otherwise @@ -217,28 +257,10 @@ class Mailer implements IMailer { $validator = new EmailValidator(); $validation = new RFCValidation(); - return $validator->isValid($this->convertEmail($email), $validation); + return $validator->isValid($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; - } - - [$name, $domain] = explode('@', $email, 2); - $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46); - return $name.'@'.$domain; - } - - protected function getInstance(): \Swift_Mailer { + protected function getInstance(): MailerInterface { if (!is_null($this->instance)) { return $this->instance; } @@ -255,31 +277,47 @@ class Mailer implements IMailer { break; } - return new \Swift_Mailer($transport); + return new SymfonyMailer($transport); } /** * 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)); + 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->getSystemValue('mail_smtphost', '127.0.0.1'), + (int)$this->config->getSystemValue('mail_smtpport', 25), + $mailSmtpsecure, + null, + $this->logger + ); + /** @var SocketStream $stream */ + $stream = $transport->getStream(); + /** @psalm-suppress InternalMethod */ + $stream->setTimeout($this->config->getSystemValue('mail_smtptimeout', 10)); + 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); } + $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( @@ -297,9 +335,9 @@ class Mailer implements IMailer { /** * Returns the sendmail transport * - * @return \Swift_SendmailTransport + * @return SendmailTransport */ - protected function getSendMailInstance(): \Swift_SendmailTransport { + protected function getSendMailInstance(): SendmailTransport { switch ($this->config->getSystemValue('mail_smtpmode', 'smtp')) { case 'qmail': $binaryPath = '/var/qmail/bin/sendmail'; @@ -322,6 +360,6 @@ class Mailer implements IMailer { break; } - return new \Swift_SendmailTransport($binaryPath . $binaryParam); + return new SendmailTransport($binaryPath . $binaryParam, null, $this->logger); } } diff --git a/lib/private/Mail/Message.php b/lib/private/Mail/Message.php index 3313b39e2e2..01beefcc6d6 100644 --- a/lib/private/Mail/Message.php +++ b/lib/private/Mail/Message.php @@ -35,64 +35,67 @@ 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; + private Email $symfonyEmail; + private bool $plainTextOnly; - public function __construct(Swift_Message $swiftMessage, bool $plainTextOnly) { - $this->swiftMessage = $swiftMessage; + private array $to; + private array $from; + private array $replyTo; + private array $cc; + private array $bcc; + + public function __construct(Email $symfonyEmail, bool $plainTextOnly) { + $this->symfonyEmail = $symfonyEmail; $this->plainTextOnly = $plainTextOnly; + $this->to = []; + $this->from = []; + $this->replyTo = []; + $this->cc = []; + $this->bcc = []; } /** - * @param IAttachment $attachment * @return $this * @since 13.0.0 */ public function attach(IAttachment $attachment): IMessage { /** @var Attachment $attachment */ - $this->swiftMessage->attach($attachment->getSwiftAttachment()); + $attachment->attach($this->symfonyEmail); 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) { - $parsableEmail = is_numeric($email) ? $readableName : $email; - if (strpos($parsableEmail, '@') === false) { - $convertedAddresses[$parsableEmail] = $readableName; - continue; - } + if (empty($addresses)) { + return []; + } - [$name, $domain] = explode('@', $parsableEmail, 2); - $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46); + array_walk($addresses, function ($readableName, $email) use (&$convertedAddresses) { if (is_numeric($email)) { - $convertedAddresses[] = $name . '@' . $domain; + $convertedAddresses[] = new Address($readableName); } else { - $convertedAddresses[$name . '@' . $domain] = $readableName; + $convertedAddresses[] = new Address($email, $readableName); } - } + }); return $convertedAddresses; } @@ -106,41 +109,32 @@ 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; } /** @@ -150,19 +144,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; } /** @@ -172,19 +162,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; } /** @@ -194,104 +180,119 @@ 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 intance */ - 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 + * + * @return void + * @throws InvalidArgumentException|RfcComplianceException + */ + public function setRecipients() { + $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 { @@ -311,7 +312,7 @@ class Message implements IMessage { * @return $this */ public function setAutoSubmitted(string $value): IMessage { - $headers = $this->swiftMessage->getHeaders(); + $headers = $this->symfonyEmail->getHeaders(); if ($headers->has(AutoSubmitted::HEADER)) { // if the header already exsists, remove it. @@ -319,6 +320,7 @@ class Message implements IMessage { // 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); } @@ -334,9 +336,9 @@ class Message implements IMessage { * @return string */ public function getAutoSubmitted(): string { - $headers = $this->swiftMessage->getHeaders(); + $headers = $this->symfonyEmail->getHeaders(); return $headers->has(AutoSubmitted::HEADER) ? - $headers->get(AutoSubmitted::HEADER)->toString() : AutoSubmitted::VALUE_NO; + $headers->get(AutoSubmitted::HEADER)->getBodyAsString() : AutoSubmitted::VALUE_NO; } } |