Mailer.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2016, ownCloud, Inc.
  5. *
  6. * @author Arne Hamann <kontakt+github@arne.email>
  7. * @author Branko Kokanovic <branko@kokanovic.org>
  8. * @author Carsten Wiedmann <carsten_sttgt@gmx.de>
  9. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  10. * @author Jared Boone <jared.boone@gmail.com>
  11. * @author Joas Schilling <coding@schilljs.com>
  12. * @author Julius Härtl <jus@bitgrid.net>
  13. * @author kevin147147 <kevintamool@gmail.com>
  14. * @author Lukas Reschke <lukas@statuscode.ch>
  15. * @author Morris Jobke <hey@morrisjobke.de>
  16. * @author Roeland Jago Douma <roeland@famdouma.nl>
  17. * @author Tekhnee <info@tekhnee.org>
  18. *
  19. * @license AGPL-3.0
  20. *
  21. * This code is free software: you can redistribute it and/or modify
  22. * it under the terms of the GNU Affero General Public License, version 3,
  23. * as published by the Free Software Foundation.
  24. *
  25. * This program is distributed in the hope that it will be useful,
  26. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  27. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  28. * GNU Affero General Public License for more details.
  29. *
  30. * You should have received a copy of the GNU Affero General Public License, version 3,
  31. * along with this program. If not, see <http://www.gnu.org/licenses/>
  32. *
  33. */
  34. namespace OC\Mail;
  35. use Egulias\EmailValidator\EmailValidator;
  36. use Egulias\EmailValidator\Validation\NoRFCWarningsValidation;
  37. use Egulias\EmailValidator\Validation\RFCValidation;
  38. use OCP\Defaults;
  39. use OCP\EventDispatcher\IEventDispatcher;
  40. use OCP\IBinaryFinder;
  41. use OCP\IConfig;
  42. use OCP\IL10N;
  43. use OCP\IURLGenerator;
  44. use OCP\L10N\IFactory;
  45. use OCP\Mail\Events\BeforeMessageSent;
  46. use OCP\Mail\IAttachment;
  47. use OCP\Mail\IEMailTemplate;
  48. use OCP\Mail\IMailer;
  49. use OCP\Mail\IMessage;
  50. use Psr\Log\LoggerInterface;
  51. use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
  52. use Symfony\Component\Mailer\Mailer as SymfonyMailer;
  53. use Symfony\Component\Mailer\MailerInterface;
  54. use Symfony\Component\Mailer\Transport\SendmailTransport;
  55. use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
  56. use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
  57. use Symfony\Component\Mime\Email;
  58. use Symfony\Component\Mime\Exception\RfcComplianceException;
  59. /**
  60. * Class Mailer provides some basic functions to create a mail message that can be used in combination with
  61. * \OC\Mail\Message.
  62. *
  63. * Example usage:
  64. *
  65. * $mailer = \OC::$server->getMailer();
  66. * $message = $mailer->createMessage();
  67. * $message->setSubject('Your Subject');
  68. * $message->setFrom(array('cloud@domain.org' => 'ownCloud Notifier'));
  69. * $message->setTo(array('recipient@domain.org' => 'Recipient'));
  70. * $message->setBody('The message text', 'text/html');
  71. * $mailer->send($message);
  72. *
  73. * This message can then be passed to send() of \OC\Mail\Mailer
  74. *
  75. * @package OC\Mail
  76. */
  77. class Mailer implements IMailer {
  78. private ?MailerInterface $instance = null;
  79. public function __construct(
  80. private IConfig $config,
  81. private LoggerInterface $logger,
  82. private Defaults $defaults,
  83. private IURLGenerator $urlGenerator,
  84. private IL10N $l10n,
  85. private IEventDispatcher $dispatcher,
  86. private IFactory $l10nFactory,
  87. ) {
  88. }
  89. /**
  90. * Creates a new message object that can be passed to send()
  91. */
  92. public function createMessage(): Message {
  93. $plainTextOnly = $this->config->getSystemValueBool('mail_send_plaintext_only', false);
  94. return new Message(new Email(), $plainTextOnly);
  95. }
  96. /**
  97. * @param string|null $data
  98. * @param string|null $filename
  99. * @param string|null $contentType
  100. * @since 13.0.0
  101. */
  102. public function createAttachment($data = null, $filename = null, $contentType = null): IAttachment {
  103. return new Attachment($data, $filename, $contentType);
  104. }
  105. /**
  106. * @param string|null $contentType
  107. * @since 13.0.0
  108. */
  109. public function createAttachmentFromPath(string $path, $contentType = null): IAttachment {
  110. return new Attachment(null, null, $contentType, $path);
  111. }
  112. /**
  113. * Creates a new email template object
  114. *
  115. * @since 12.0.0
  116. */
  117. public function createEMailTemplate(string $emailId, array $data = []): IEMailTemplate {
  118. $class = $this->config->getSystemValueString('mail_template_class', '');
  119. if ($class !== '' && class_exists($class) && is_a($class, EMailTemplate::class, true)) {
  120. return new $class(
  121. $this->defaults,
  122. $this->urlGenerator,
  123. $this->l10nFactory,
  124. $emailId,
  125. $data
  126. );
  127. }
  128. return new EMailTemplate(
  129. $this->defaults,
  130. $this->urlGenerator,
  131. $this->l10nFactory,
  132. $emailId,
  133. $data
  134. );
  135. }
  136. /**
  137. * Send the specified message. Also sets the from address to the value defined in config.php
  138. * if no-one has been passed.
  139. *
  140. * If sending failed, the recipients that failed will be returned (to, cc and bcc).
  141. * Will output additional debug info if 'mail_smtpdebug' => 'true' is set in config.php
  142. *
  143. * @param IMessage $message Message to send
  144. * @return string[] $failedRecipients
  145. */
  146. public function send(IMessage $message): array {
  147. $debugMode = $this->config->getSystemValueBool('mail_smtpdebug', false);
  148. if (!($message instanceof Message)) {
  149. throw new \InvalidArgumentException('Object not of type ' . Message::class);
  150. }
  151. if (empty($message->getFrom())) {
  152. $message->setFrom([\OCP\Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]);
  153. }
  154. $mailer = $this->getInstance();
  155. $this->dispatcher->dispatchTyped(new BeforeMessageSent($message));
  156. try {
  157. $message->setRecipients();
  158. } catch (\InvalidArgumentException|RfcComplianceException $e) {
  159. $logMessage = sprintf(
  160. 'Could not send mail to "%s" with subject "%s" as validation for address failed',
  161. print_r(array_merge($message->getTo(), $message->getCc(), $message->getBcc()), true),
  162. $message->getSubject()
  163. );
  164. $this->logger->debug($logMessage, ['app' => 'core', 'exception' => $e]);
  165. $recipients = array_merge($message->getTo(), $message->getCc(), $message->getBcc());
  166. $failedRecipients = [];
  167. array_walk($recipients, function ($value, $key) use (&$failedRecipients) {
  168. if (is_numeric($key)) {
  169. $failedRecipients[] = $value;
  170. } else {
  171. $failedRecipients[] = $key;
  172. }
  173. });
  174. return $failedRecipients;
  175. }
  176. try {
  177. $mailer->send($message->getSymfonyEmail());
  178. } catch (TransportExceptionInterface $e) {
  179. $logMessage = sprintf('Sending mail to "%s" with subject "%s" failed', print_r($message->getTo(), true), $message->getSubject());
  180. $this->logger->debug($logMessage, ['app' => 'core', 'exception' => $e]);
  181. if ($debugMode) {
  182. $this->logger->debug($e->getDebug(), ['app' => 'core']);
  183. }
  184. $recipients = array_merge($message->getTo(), $message->getCc(), $message->getBcc());
  185. $failedRecipients = [];
  186. array_walk($recipients, function ($value, $key) use (&$failedRecipients) {
  187. if (is_numeric($key)) {
  188. $failedRecipients[] = $value;
  189. } else {
  190. $failedRecipients[] = $key;
  191. }
  192. });
  193. return $failedRecipients;
  194. }
  195. // Debugging logging
  196. $logMessage = sprintf('Sent mail to "%s" with subject "%s"', print_r($message->getTo(), true), $message->getSubject());
  197. $this->logger->debug($logMessage, ['app' => 'core']);
  198. return [];
  199. }
  200. /**
  201. * @deprecated 26.0.0 Implicit validation is done in \OC\Mail\Message::setRecipients
  202. * via \Symfony\Component\Mime\Address::__construct
  203. *
  204. * @param string $email Email address to be validated
  205. * @return bool True if the mail address is valid, false otherwise
  206. */
  207. public function validateMailAddress(string $email): bool {
  208. if ($email === '') {
  209. // Shortcut: empty addresses are never valid
  210. return false;
  211. }
  212. $strictMailCheck = $this->config->getAppValue('core', 'enforce_strict_email_check', 'no') === 'yes';
  213. $validator = new EmailValidator();
  214. $validation = $strictMailCheck ? new NoRFCWarningsValidation() : new RFCValidation();
  215. return $validator->isValid($email, $validation);
  216. }
  217. protected function getInstance(): MailerInterface {
  218. if (!is_null($this->instance)) {
  219. return $this->instance;
  220. }
  221. $transport = null;
  222. switch ($this->config->getSystemValueString('mail_smtpmode', 'smtp')) {
  223. case 'sendmail':
  224. $transport = $this->getSendMailInstance();
  225. break;
  226. case 'smtp':
  227. default:
  228. $transport = $this->getSmtpInstance();
  229. break;
  230. }
  231. return new SymfonyMailer($transport);
  232. }
  233. /**
  234. * Returns the SMTP transport
  235. *
  236. * Only supports ssl/tls
  237. * starttls is not enforcable with Symfony Mailer but might be available
  238. * via the automatic config (Symfony Mailer internal)
  239. *
  240. * @return EsmtpTransport
  241. */
  242. protected function getSmtpInstance(): EsmtpTransport {
  243. // either null or true - if nothing is passed, let the symfony mailer figure out the configuration by itself
  244. $mailSmtpsecure = ($this->config->getSystemValue('mail_smtpsecure', null) === 'ssl') ? true : null;
  245. $transport = new EsmtpTransport(
  246. $this->config->getSystemValueString('mail_smtphost', '127.0.0.1'),
  247. $this->config->getSystemValueInt('mail_smtpport', 25),
  248. $mailSmtpsecure,
  249. null,
  250. $this->logger
  251. );
  252. /** @var SocketStream $stream */
  253. $stream = $transport->getStream();
  254. /** @psalm-suppress InternalMethod */
  255. $stream->setTimeout($this->config->getSystemValueInt('mail_smtptimeout', 10));
  256. if ($this->config->getSystemValueBool('mail_smtpauth', false)) {
  257. $transport->setUsername($this->config->getSystemValueString('mail_smtpname', ''));
  258. $transport->setPassword($this->config->getSystemValueString('mail_smtppassword', ''));
  259. }
  260. $streamingOptions = $this->config->getSystemValue('mail_smtpstreamoptions', []);
  261. if (is_array($streamingOptions) && !empty($streamingOptions)) {
  262. /** @psalm-suppress InternalMethod */
  263. $currentStreamingOptions = $stream->getStreamOptions();
  264. $currentStreamingOptions = array_merge_recursive($currentStreamingOptions, $streamingOptions);
  265. /** @psalm-suppress InternalMethod */
  266. $stream->setStreamOptions($currentStreamingOptions);
  267. }
  268. $overwriteCliUrl = parse_url(
  269. $this->config->getSystemValueString('overwrite.cli.url', ''),
  270. PHP_URL_HOST
  271. );
  272. if (!empty($overwriteCliUrl)) {
  273. $transport->setLocalDomain($overwriteCliUrl);
  274. }
  275. return $transport;
  276. }
  277. /**
  278. * Returns the sendmail transport
  279. *
  280. * @return SendmailTransport
  281. */
  282. protected function getSendMailInstance(): SendmailTransport {
  283. switch ($this->config->getSystemValueString('mail_smtpmode', 'smtp')) {
  284. case 'qmail':
  285. $binaryPath = '/var/qmail/bin/sendmail';
  286. break;
  287. default:
  288. $sendmail = \OCP\Server::get(IBinaryFinder::class)->findBinaryPath('sendmail');
  289. if ($sendmail === null) {
  290. $sendmail = '/usr/sbin/sendmail';
  291. }
  292. $binaryPath = $sendmail;
  293. break;
  294. }
  295. $binaryParam = match ($this->config->getSystemValueString('mail_sendmailmode', 'smtp')) {
  296. 'pipe' => ' -t -i',
  297. default => ' -bs',
  298. };
  299. return new SendmailTransport($binaryPath . $binaryParam, null, $this->logger);
  300. }
  301. }