1e381f98dSGreg Roach<?php 2e381f98dSGreg Roach 3e381f98dSGreg Roach/** 4e381f98dSGreg Roach * webtrees: online genealogy 589f7189bSGreg Roach * Copyright (C) 2021 webtrees development team 6e381f98dSGreg Roach * This program is free software: you can redistribute it and/or modify 7e381f98dSGreg Roach * it under the terms of the GNU General Public License as published by 8e381f98dSGreg Roach * the Free Software Foundation, either version 3 of the License, or 9e381f98dSGreg Roach * (at your option) any later version. 10e381f98dSGreg Roach * This program is distributed in the hope that it will be useful, 11e381f98dSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12e381f98dSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13e381f98dSGreg Roach * GNU General Public License for more details. 14e381f98dSGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16e381f98dSGreg Roach */ 17e381f98dSGreg Roach 18e381f98dSGreg Roachdeclare(strict_types=1); 19e381f98dSGreg Roach 20e381f98dSGreg Roachnamespace Fisharebest\Webtrees\Services; 21e381f98dSGreg Roach 22e381f98dSGreg Roachuse Exception; 23e381f98dSGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface; 24e381f98dSGreg Roachuse Fisharebest\Webtrees\I18N; 25e381f98dSGreg Roachuse Fisharebest\Webtrees\Log; 26e381f98dSGreg Roachuse Fisharebest\Webtrees\Site; 27ef641919SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 28e381f98dSGreg Roachuse Swift_Mailer; 29e381f98dSGreg Roachuse Swift_Message; 30e381f98dSGreg Roachuse Swift_NullTransport; 31e381f98dSGreg Roachuse Swift_SendmailTransport; 32e381f98dSGreg Roachuse Swift_Signers_DKIMSigner; 33e381f98dSGreg Roachuse Swift_SmtpTransport; 34e381f98dSGreg Roachuse Swift_Transport; 35e381f98dSGreg Roachuse Throwable; 36e381f98dSGreg Roach 3794fc28f5SGreg Roachuse function assert; 3804626e75SGreg Roachuse function checkdnsrr; 39e381f98dSGreg Roachuse function filter_var; 40e381f98dSGreg Roachuse function function_exists; 41e381f98dSGreg Roachuse function gethostbyaddr; 42e381f98dSGreg Roachuse function gethostbyname; 43e381f98dSGreg Roachuse function gethostname; 44e381f98dSGreg Roachuse function str_replace; 45e381f98dSGreg Roachuse function strrchr; 46e381f98dSGreg Roachuse function substr; 47e381f98dSGreg Roach 48e381f98dSGreg Roachuse const FILTER_VALIDATE_DOMAIN; 49e381f98dSGreg Roachuse const FILTER_VALIDATE_EMAIL; 50e381f98dSGreg Roach 51e381f98dSGreg Roach/** 52e381f98dSGreg Roach * Send emails. 53e381f98dSGreg Roach */ 54e381f98dSGreg Roachclass EmailService 55e381f98dSGreg Roach{ 56e381f98dSGreg Roach /** 57e381f98dSGreg Roach * Send an external email message 58e381f98dSGreg Roach * Caution! gmail may rewrite the "From" header unless you have added the address to your account. 59e381f98dSGreg Roach * 60e381f98dSGreg Roach * @param UserInterface $from 61e381f98dSGreg Roach * @param UserInterface $to 62e381f98dSGreg Roach * @param UserInterface $reply_to 63e381f98dSGreg Roach * @param string $subject 64e381f98dSGreg Roach * @param string $message_text 65e381f98dSGreg Roach * @param string $message_html 66e381f98dSGreg Roach * 67e381f98dSGreg Roach * @return bool 68e381f98dSGreg Roach */ 69e381f98dSGreg Roach public function send(UserInterface $from, UserInterface $to, UserInterface $reply_to, string $subject, string $message_text, string $message_html): bool 70e381f98dSGreg Roach { 71e381f98dSGreg Roach // Mail needs MSDOS line endings 72e381f98dSGreg Roach $message_text = str_replace("\n", "\r\n", $message_text); 73e381f98dSGreg Roach $message_html = str_replace("\n", "\r\n", $message_html); 74e381f98dSGreg Roach 75e381f98dSGreg Roach // Special accounts do not have an email address. Use the system one. 76e381f98dSGreg Roach $from_email = $from->email() ?: $this->senderEmail(); 77e381f98dSGreg Roach $reply_to_email = $reply_to->email() ?: $this->senderEmail(); 78e381f98dSGreg Roach 7918db2ab6SGreg Roach try { 80e381f98dSGreg Roach $message = (new Swift_Message()) 81e381f98dSGreg Roach ->setSubject($subject) 82e381f98dSGreg Roach ->setFrom($from_email, $from->realName()) 83e381f98dSGreg Roach ->setTo($to->email(), $to->realName()) 84e381f98dSGreg Roach ->setBody($message_html, 'text/html'); 85e381f98dSGreg Roach 86e381f98dSGreg Roach if ($from_email !== $reply_to_email) { 87e381f98dSGreg Roach $message->setReplyTo($reply_to_email, $reply_to->realName()); 88e381f98dSGreg Roach } 89e381f98dSGreg Roach 90e381f98dSGreg Roach $dkim_domain = Site::getPreference('DKIM_DOMAIN'); 91e381f98dSGreg Roach $dkim_selector = Site::getPreference('DKIM_SELECTOR'); 92e381f98dSGreg Roach $dkim_key = Site::getPreference('DKIM_KEY'); 93e381f98dSGreg Roach 94e381f98dSGreg Roach if ($dkim_domain !== '' && $dkim_selector !== '' && $dkim_key !== '') { 95e381f98dSGreg Roach $signer = new Swift_Signers_DKIMSigner($dkim_key, $dkim_domain, $dkim_selector); 96e381f98dSGreg Roach $signer 97e381f98dSGreg Roach ->setHeaderCanon('relaxed') 98e381f98dSGreg Roach ->setBodyCanon('relaxed'); 99e381f98dSGreg Roach 100e381f98dSGreg Roach $message->attachSigner($signer); 101e381f98dSGreg Roach } else { 102e381f98dSGreg Roach // DKIM body hashes don't work with multipart/alternative content. 103e381f98dSGreg Roach $message->addPart($message_text, 'text/plain'); 104e381f98dSGreg Roach } 105e381f98dSGreg Roach 106e381f98dSGreg Roach $mailer = new Swift_Mailer($this->transport()); 107e381f98dSGreg Roach 108e381f98dSGreg Roach $mailer->send($message); 109e381f98dSGreg Roach } catch (Exception $ex) { 110e381f98dSGreg Roach Log::addErrorLog('MailService: ' . $ex->getMessage()); 111e381f98dSGreg Roach 112e381f98dSGreg Roach return false; 113e381f98dSGreg Roach } 114e381f98dSGreg Roach 115e381f98dSGreg Roach return true; 116e381f98dSGreg Roach } 117e381f98dSGreg Roach 118e381f98dSGreg Roach /** 119e381f98dSGreg Roach * Create a transport mechanism for sending mail 120e381f98dSGreg Roach * 121e381f98dSGreg Roach * @return Swift_Transport 122e381f98dSGreg Roach */ 123e381f98dSGreg Roach private function transport(): Swift_Transport 124e381f98dSGreg Roach { 125e381f98dSGreg Roach switch (Site::getPreference('SMTP_ACTIVE')) { 126e381f98dSGreg Roach case 'sendmail': 127e381f98dSGreg Roach // Local sendmail (requires PHP proc_* functions) 128ef641919SGreg Roach $request = app(ServerRequestInterface::class); 129ef641919SGreg Roach assert($request instanceof ServerRequestInterface); 130ef641919SGreg Roach 1314bb3a6faSGreg Roach $sendmail_command = $request->getAttribute('sendmail_command', '/usr/sbin/sendmail -bs'); 132ef641919SGreg Roach 133ef641919SGreg Roach return new Swift_SendmailTransport($sendmail_command); 134e381f98dSGreg Roach 135e381f98dSGreg Roach case 'external': 136e381f98dSGreg Roach // SMTP 137e381f98dSGreg Roach $smtp_host = Site::getPreference('SMTP_HOST'); 138e381f98dSGreg Roach $smtp_port = (int) Site::getPreference('SMTP_PORT', '25'); 139e381f98dSGreg Roach $smtp_auth = (bool) Site::getPreference('SMTP_AUTH'); 140e381f98dSGreg Roach $smtp_user = Site::getPreference('SMTP_AUTH_USER'); 141e381f98dSGreg Roach $smtp_pass = Site::getPreference('SMTP_AUTH_PASS'); 142e381f98dSGreg Roach $smtp_encr = Site::getPreference('SMTP_SSL'); 143e381f98dSGreg Roach 14483ad74b0SGreg Roach if ($smtp_encr === 'none') { 14583ad74b0SGreg Roach $smtp_encr = null; 14683ad74b0SGreg Roach } 14783ad74b0SGreg Roach 148e381f98dSGreg Roach $transport = new Swift_SmtpTransport($smtp_host, $smtp_port, $smtp_encr); 149e381f98dSGreg Roach 150e381f98dSGreg Roach $transport->setLocalDomain($this->localDomain()); 151e381f98dSGreg Roach 152e381f98dSGreg Roach if ($smtp_auth) { 153e381f98dSGreg Roach $transport 154e381f98dSGreg Roach ->setUsername($smtp_user) 155e381f98dSGreg Roach ->setPassword($smtp_pass); 156e381f98dSGreg Roach } 157e381f98dSGreg Roach 158e381f98dSGreg Roach return $transport; 159e381f98dSGreg Roach 160e381f98dSGreg Roach default: 161e381f98dSGreg Roach // For testing 162e381f98dSGreg Roach return new Swift_NullTransport(); 163e381f98dSGreg Roach } 164e381f98dSGreg Roach } 165e381f98dSGreg Roach 166e381f98dSGreg Roach /** 167e381f98dSGreg Roach * Where are we sending mail from? 168e381f98dSGreg Roach * 169e381f98dSGreg Roach * @return string 170e381f98dSGreg Roach */ 171e381f98dSGreg Roach public function localDomain(): string 172e381f98dSGreg Roach { 173e381f98dSGreg Roach $local_domain = Site::getPreference('SMTP_HELO'); 174e381f98dSGreg Roach 175e381f98dSGreg Roach try { 176e381f98dSGreg Roach // Look ourself up using DNS. 177e381f98dSGreg Roach $default = gethostbyaddr(gethostbyname(gethostname())); 178e381f98dSGreg Roach } catch (Throwable $ex) { 179e381f98dSGreg Roach $default = 'localhost'; 180e381f98dSGreg Roach } 181e381f98dSGreg Roach 182e381f98dSGreg Roach return $local_domain ?: $default; 183e381f98dSGreg Roach } 184e381f98dSGreg Roach 185e381f98dSGreg Roach /** 186e381f98dSGreg Roach * Who are we sending mail from? 187e381f98dSGreg Roach * 188e381f98dSGreg Roach * @return string 189e381f98dSGreg Roach */ 190e381f98dSGreg Roach public function senderEmail(): string 191e381f98dSGreg Roach { 192e381f98dSGreg Roach $sender = Site::getPreference('SMTP_FROM_NAME'); 193e381f98dSGreg Roach $default = 'no-reply@' . $this->localDomain(); 194e381f98dSGreg Roach 195e381f98dSGreg Roach return $sender ?: $default; 196e381f98dSGreg Roach } 197e381f98dSGreg Roach 198e381f98dSGreg Roach /** 199e381f98dSGreg Roach * Many mail relays require a valid sender email. 200e381f98dSGreg Roach * 201e381f98dSGreg Roach * @param string $email 202e381f98dSGreg Roach * 203e381f98dSGreg Roach * @return bool 204e381f98dSGreg Roach */ 205e381f98dSGreg Roach public function isValidEmail(string $email): bool 206e381f98dSGreg Roach { 207ff00880fSGreg Roach $at_domain = strrchr($email, '@'); 208ff00880fSGreg Roach 209ff00880fSGreg Roach if ($at_domain === false) { 210ff00880fSGreg Roach return false; 211ff00880fSGreg Roach } 212ff00880fSGreg Roach 213ff00880fSGreg Roach $domain = substr($at_domain, 1); 214e381f98dSGreg Roach 21523945a1eSGreg Roach $email_valid = filter_var($email, FILTER_VALIDATE_EMAIL) !== false; 21623945a1eSGreg Roach $domain_valid = filter_var($domain, FILTER_VALIDATE_DOMAIN) !== false; 217e381f98dSGreg Roach 21804626e75SGreg Roach // Some web hosts disable checkdnsrr. 21904626e75SGreg Roach if ($domain_valid && function_exists('checkdnsrr')) { 22004626e75SGreg Roach $domain_valid = checkdnsrr($domain); 22104626e75SGreg Roach } 22204626e75SGreg Roach 22323945a1eSGreg Roach return $email_valid && $domain_valid; 224e381f98dSGreg Roach } 225e381f98dSGreg Roach 226e381f98dSGreg Roach /** 227e381f98dSGreg Roach * A list SSL modes (e.g. for an edit control). 228e381f98dSGreg Roach * 229*24f2a3afSGreg Roach * @return array<string> 230e381f98dSGreg Roach */ 231e381f98dSGreg Roach public function mailSslOptions(): array 232e381f98dSGreg Roach { 233e381f98dSGreg Roach return [ 234e381f98dSGreg Roach 'none' => I18N::translate('none'), 235e381f98dSGreg Roach /* I18N: Secure Sockets Layer - a secure communications protocol*/ 236e381f98dSGreg Roach 'ssl' => I18N::translate('ssl'), 237e381f98dSGreg Roach /* I18N: Transport Layer Security - a secure communications protocol */ 238e381f98dSGreg Roach 'tls' => I18N::translate('tls'), 239e381f98dSGreg Roach ]; 240e381f98dSGreg Roach } 241e381f98dSGreg Roach 242e381f98dSGreg Roach /** 243e381f98dSGreg Roach * A list SSL modes (e.g. for an edit control). 244e381f98dSGreg Roach * 245*24f2a3afSGreg Roach * @return array<string> 246e381f98dSGreg Roach */ 247e381f98dSGreg Roach public function mailTransportOptions(): array 248e381f98dSGreg Roach { 249e381f98dSGreg Roach $options = [ 250e381f98dSGreg Roach /* I18N: "sendmail" is the name of some mail software */ 251e381f98dSGreg Roach 'sendmail' => I18N::translate('Use sendmail to send messages'), 252e381f98dSGreg Roach 'external' => I18N::translate('Use SMTP to send messages'), 253e381f98dSGreg Roach ]; 254e381f98dSGreg Roach 255e381f98dSGreg Roach if (!function_exists('proc_open')) { 256e381f98dSGreg Roach unset($options['sendmail']); 257e381f98dSGreg Roach } 258e381f98dSGreg Roach 259e381f98dSGreg Roach return $options; 260e381f98dSGreg Roach } 261e381f98dSGreg Roach} 262