1<?php
2
3declare(strict_types=1);
4
5/**
6 * @copyright Copyright (c) 2016, ownCloud, Inc.
7 *
8 * @author Arne Hamann <kontakt+github@arne.email>
9 * @author Branko Kokanovic <branko@kokanovic.org>
10 * @author Carsten Wiedmann <carsten_sttgt@gmx.de>
11 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
12 * @author Jared Boone <jared.boone@gmail.com>
13 * @author Joas Schilling <coding@schilljs.com>
14 * @author Julius Härtl <jus@bitgrid.net>
15 * @author kevin147147 <kevintamool@gmail.com>
16 * @author Lukas Reschke <lukas@statuscode.ch>
17 * @author Morris Jobke <hey@morrisjobke.de>
18 * @author Roeland Jago Douma <roeland@famdouma.nl>
19 * @author Tekhnee <info@tekhnee.org>
20 *
21 * @license AGPL-3.0
22 *
23 * This code is free software: you can redistribute it and/or modify
24 * it under the terms of the GNU Affero General Public License, version 3,
25 * as published by the Free Software Foundation.
26 *
27 * This program is distributed in the hope that it will be useful,
28 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 * GNU Affero General Public License for more details.
31 *
32 * You should have received a copy of the GNU Affero General Public License, version 3,
33 * along with this program. If not, see <http://www.gnu.org/licenses/>
34 *
35 */
36namespace OC\Mail;
37
38use Egulias\EmailValidator\EmailValidator;
39use Egulias\EmailValidator\Validation\RFCValidation;
40use OCP\Defaults;
41use OCP\EventDispatcher\IEventDispatcher;
42use OCP\IConfig;
43use OCP\IL10N;
44use OCP\ILogger;
45use OCP\IURLGenerator;
46use OCP\L10N\IFactory;
47use OCP\Mail\Events\BeforeMessageSent;
48use OCP\Mail\IAttachment;
49use OCP\Mail\IEMailTemplate;
50use OCP\Mail\IMailer;
51use OCP\Mail\IMessage;
52
53/**
54 * Class Mailer provides some basic functions to create a mail message that can be used in combination with
55 * \OC\Mail\Message.
56 *
57 * Example usage:
58 *
59 * 	$mailer = \OC::$server->getMailer();
60 * 	$message = $mailer->createMessage();
61 * 	$message->setSubject('Your Subject');
62 * 	$message->setFrom(array('cloud@domain.org' => 'ownCloud Notifier'));
63 * 	$message->setTo(array('recipient@domain.org' => 'Recipient'));
64 * 	$message->setBody('The message text', 'text/html');
65 * 	$mailer->send($message);
66 *
67 * This message can then be passed to send() of \OC\Mail\Mailer
68 *
69 * @package OC\Mail
70 */
71class Mailer implements IMailer {
72	/** @var \Swift_Mailer Cached mailer */
73	private $instance = null;
74	/** @var IConfig */
75	private $config;
76	/** @var ILogger */
77	private $logger;
78	/** @var Defaults */
79	private $defaults;
80	/** @var IURLGenerator */
81	private $urlGenerator;
82	/** @var IL10N */
83	private $l10n;
84	/** @var IEventDispatcher */
85	private $dispatcher;
86	/** @var IFactory */
87	private $l10nFactory;
88
89	/**
90	 * @param IConfig $config
91	 * @param ILogger $logger
92	 * @param Defaults $defaults
93	 * @param IURLGenerator $urlGenerator
94	 * @param IL10N $l10n
95	 * @param IEventDispatcher $dispatcher
96	 */
97	public function __construct(IConfig $config,
98						 ILogger $logger,
99						 Defaults $defaults,
100						 IURLGenerator $urlGenerator,
101						 IL10N $l10n,
102						 IEventDispatcher $dispatcher,
103						 IFactory $l10nFactory) {
104		$this->config = $config;
105		$this->logger = $logger;
106		$this->defaults = $defaults;
107		$this->urlGenerator = $urlGenerator;
108		$this->l10n = $l10n;
109		$this->dispatcher = $dispatcher;
110		$this->l10nFactory = $l10nFactory;
111	}
112
113	/**
114	 * Creates a new message object that can be passed to send()
115	 *
116	 * @return IMessage
117	 */
118	public function createMessage(): IMessage {
119		$plainTextOnly = $this->config->getSystemValue('mail_send_plaintext_only', false);
120		return new Message(new \Swift_Message(), $plainTextOnly);
121	}
122
123	/**
124	 * @param string|null $data
125	 * @param string|null $filename
126	 * @param string|null $contentType
127	 * @return IAttachment
128	 * @since 13.0.0
129	 */
130	public function createAttachment($data = null, $filename = null, $contentType = null): IAttachment {
131		return new Attachment(new \Swift_Attachment($data, $filename, $contentType));
132	}
133
134	/**
135	 * @param string $path
136	 * @param string|null $contentType
137	 * @return IAttachment
138	 * @since 13.0.0
139	 */
140	public function createAttachmentFromPath(string $path, $contentType = null): IAttachment {
141		return new Attachment(\Swift_Attachment::fromPath($path, $contentType));
142	}
143
144	/**
145	 * Creates a new email template object
146	 *
147	 * @param string $emailId
148	 * @param array $data
149	 * @return IEMailTemplate
150	 * @since 12.0.0
151	 */
152	public function createEMailTemplate(string $emailId, array $data = []): IEMailTemplate {
153		$class = $this->config->getSystemValue('mail_template_class', '');
154
155		if ($class !== '' && class_exists($class) && is_a($class, EMailTemplate::class, true)) {
156			return new $class(
157				$this->defaults,
158				$this->urlGenerator,
159				$this->l10nFactory,
160				$emailId,
161				$data
162			);
163		}
164
165		return new EMailTemplate(
166			$this->defaults,
167			$this->urlGenerator,
168			$this->l10nFactory,
169			$emailId,
170			$data
171		);
172	}
173
174	/**
175	 * Send the specified message. Also sets the from address to the value defined in config.php
176	 * if no-one has been passed.
177	 *
178	 * @param IMessage|Message $message Message to send
179	 * @return string[] Array with failed recipients. Be aware that this depends on the used mail backend and
180	 * therefore should be considered
181	 * @throws \Exception In case it was not possible to send the message. (for example if an invalid mail address
182	 * has been supplied.)
183	 */
184	public function send(IMessage $message): array {
185		$debugMode = $this->config->getSystemValue('mail_smtpdebug', false);
186
187		if (empty($message->getFrom())) {
188			$message->setFrom([\OCP\Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]);
189		}
190
191		$failedRecipients = [];
192
193		$mailer = $this->getInstance();
194
195		// Enable logger if debug mode is enabled
196		if ($debugMode) {
197			$mailLogger = new \Swift_Plugins_Loggers_ArrayLogger();
198			$mailer->registerPlugin(new \Swift_Plugins_LoggerPlugin($mailLogger));
199		}
200
201
202		$this->dispatcher->dispatchTyped(new BeforeMessageSent($message));
203
204		$mailer->send($message->getSwiftMessage(), $failedRecipients);
205
206		// Debugging logging
207		$logMessage = sprintf('Sent mail to "%s" with subject "%s"', print_r($message->getTo(), true), $message->getSubject());
208		$this->logger->debug($logMessage, ['app' => 'core']);
209		if ($debugMode && isset($mailLogger)) {
210			$this->logger->debug($mailLogger->dump(), ['app' => 'core']);
211		}
212
213		return $failedRecipients;
214	}
215
216	/**
217	 * Checks if an e-mail address is valid
218	 *
219	 * @param string $email Email address to be validated
220	 * @return bool True if the mail address is valid, false otherwise
221	 */
222	public function validateMailAddress(string $email): bool {
223		if ($email === '') {
224			// Shortcut: empty addresses are never valid
225			return false;
226		}
227		$validator = new EmailValidator();
228		$validation = new RFCValidation();
229
230		return $validator->isValid($this->convertEmail($email), $validation);
231	}
232
233	/**
234	 * SwiftMailer does currently not work with IDN domains, this function therefore converts the domains
235	 *
236	 * FIXME: Remove this once SwiftMailer supports IDN
237	 *
238	 * @param string $email
239	 * @return string Converted mail address if `idn_to_ascii` exists
240	 */
241	protected function convertEmail(string $email): string {
242		if (!function_exists('idn_to_ascii') || !defined('INTL_IDNA_VARIANT_UTS46') || strpos($email, '@') === false) {
243			return $email;
244		}
245
246		[$name, $domain] = explode('@', $email, 2);
247		$domain = idn_to_ascii($domain, 0,INTL_IDNA_VARIANT_UTS46);
248		return $name.'@'.$domain;
249	}
250
251	protected function getInstance(): \Swift_Mailer {
252		if (!is_null($this->instance)) {
253			return $this->instance;
254		}
255
256		$transport = null;
257
258		switch ($this->config->getSystemValue('mail_smtpmode', 'smtp')) {
259			case 'sendmail':
260				$transport = $this->getSendMailInstance();
261				break;
262			case 'smtp':
263			default:
264				$transport = $this->getSmtpInstance();
265				break;
266		}
267
268		return new \Swift_Mailer($transport);
269	}
270
271	/**
272	 * Returns the SMTP transport
273	 *
274	 * @return \Swift_SmtpTransport
275	 */
276	protected function getSmtpInstance(): \Swift_SmtpTransport {
277		$transport = new \Swift_SmtpTransport();
278		$transport->setTimeout($this->config->getSystemValue('mail_smtptimeout', 10));
279		$transport->setHost($this->config->getSystemValue('mail_smtphost', '127.0.0.1'));
280		$transport->setPort($this->config->getSystemValue('mail_smtpport', 25));
281		if ($this->config->getSystemValue('mail_smtpauth', false)) {
282			$transport->setUsername($this->config->getSystemValue('mail_smtpname', ''));
283			$transport->setPassword($this->config->getSystemValue('mail_smtppassword', ''));
284			$transport->setAuthMode($this->config->getSystemValue('mail_smtpauthtype', 'LOGIN'));
285		}
286		$smtpSecurity = $this->config->getSystemValue('mail_smtpsecure', '');
287		if (!empty($smtpSecurity)) {
288			$transport->setEncryption($smtpSecurity);
289		}
290		$streamingOptions = $this->config->getSystemValue('mail_smtpstreamoptions', []);
291		if (is_array($streamingOptions) && !empty($streamingOptions)) {
292			$transport->setStreamOptions($streamingOptions);
293		}
294
295		$overwriteCliUrl = parse_url(
296			$this->config->getSystemValueString('overwrite.cli.url', ''),
297			PHP_URL_HOST
298		);
299
300		if (!empty($overwriteCliUrl)) {
301			$transport->setLocalDomain($overwriteCliUrl);
302		}
303
304		return $transport;
305	}
306
307	/**
308	 * Returns the sendmail transport
309	 *
310	 * @return \Swift_SendmailTransport
311	 */
312	protected function getSendMailInstance(): \Swift_SendmailTransport {
313		switch ($this->config->getSystemValue('mail_smtpmode', 'smtp')) {
314			case 'qmail':
315				$binaryPath = '/var/qmail/bin/sendmail';
316				break;
317			default:
318				$sendmail = \OC_Helper::findBinaryPath('sendmail');
319				if ($sendmail === null) {
320					$sendmail = '/usr/sbin/sendmail';
321				}
322				$binaryPath = $sendmail;
323				break;
324		}
325
326		switch ($this->config->getSystemValue('mail_sendmailmode', 'smtp')) {
327			case 'pipe':
328				$binaryParam = ' -t';
329				break;
330			default:
331				$binaryParam = ' -bs';
332				break;
333		}
334
335		return new \Swift_SendmailTransport($binaryPath . $binaryParam);
336	}
337}
338