1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Services; 21 22use Fisharebest\Webtrees\Contracts\UserInterface; 23use Fisharebest\Webtrees\I18N; 24use Fisharebest\Webtrees\Log; 25use Fisharebest\Webtrees\Site; 26use Psr\Http\Message\ServerRequestInterface; 27use Symfony\Component\Mailer\Exception\TransportExceptionInterface; 28use Symfony\Component\Mailer\Mailer; 29use Symfony\Component\Mailer\Transport\NullTransport; 30use Symfony\Component\Mailer\Transport\SendmailTransport; 31use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; 32use Symfony\Component\Mailer\Transport\TransportInterface; 33use Symfony\Component\Mime\Address; 34use Symfony\Component\Mime\Crypto\DkimOptions; 35use Symfony\Component\Mime\Crypto\DkimSigner; 36use Symfony\Component\Mime\Email; 37use Symfony\Component\Mime\Exception\RfcComplianceException; 38use Symfony\Component\Mime\Message; 39 40use function assert; 41use function checkdnsrr; 42use function function_exists; 43use function str_replace; 44use function strrchr; 45use function substr; 46 47/** 48 * Send emails. 49 */ 50class EmailService 51{ 52 /** 53 * Send an external email message 54 * Caution! gmail may rewrite the "From" header unless you have added the address to your account. 55 * 56 * @param UserInterface $from 57 * @param UserInterface $to 58 * @param UserInterface $reply_to 59 * @param string $subject 60 * @param string $message_text 61 * @param string $message_html 62 * 63 * @return bool 64 */ 65 public function send(UserInterface $from, UserInterface $to, UserInterface $reply_to, string $subject, string $message_text, string $message_html): bool 66 { 67 try { 68 $message = $this->message($from, $to, $reply_to, $subject, $message_text, $message_html); 69 $transport = $this->transport(); 70 $mailer = new Mailer($transport); 71 $mailer->send($message); 72 } catch (TransportExceptionInterface $ex) { 73 Log::addErrorLog('MailService: ' . $ex->getMessage()); 74 75 return false; 76 } 77 78 return true; 79 } 80 81 /** 82 * Create a message 83 * 84 * @param UserInterface $from 85 * @param UserInterface $to 86 * @param UserInterface $reply_to 87 * @param string $subject 88 * @param string $message_text 89 * @param string $message_html 90 * 91 * @return Message 92 */ 93 protected function message(UserInterface $from, UserInterface $to, UserInterface $reply_to, string $subject, string $message_text, string $message_html): Message 94 { 95 // Mail needs MS-DOS line endings 96 $message_text = str_replace("\n", "\r\n", $message_text); 97 $message_html = str_replace("\n", "\r\n", $message_html); 98 99 $message = (new Email()) 100 ->subject($subject) 101 ->from(new Address($from->email(), $from->realName())) 102 ->to(new Address($to->email(), $to->realName())) 103 ->replyTo(new Address($reply_to->email(), $reply_to->realName())) 104 ->html($message_html); 105 106 $dkim_domain = Site::getPreference('DKIM_DOMAIN'); 107 $dkim_selector = Site::getPreference('DKIM_SELECTOR'); 108 $dkim_key = Site::getPreference('DKIM_KEY'); 109 110 if ($dkim_domain !== '' && $dkim_selector !== '' && $dkim_key !== '') { 111 $signer = new DkimSigner($dkim_key, $dkim_domain, $dkim_selector); 112 $options = (new DkimOptions()) 113 ->headerCanon('relaxed') 114 ->bodyCanon('relaxed'); 115 116 return $signer->sign($message, $options->toArray()); 117 } 118 119 // DKIM body hashes don't work with multipart/alternative content. 120 $message->text($message_text); 121 122 return $message; 123 } 124 125 /** 126 * Create a transport mechanism for sending mail 127 * 128 * @return TransportInterface 129 */ 130 protected function transport(): TransportInterface 131 { 132 switch (Site::getPreference('SMTP_ACTIVE')) { 133 case 'sendmail': 134 // Local sendmail (requires PHP proc_* functions) 135 $request = app(ServerRequestInterface::class); 136 assert($request instanceof ServerRequestInterface); 137 138 $sendmail_command = $request->getAttribute('sendmail_command', '/usr/sbin/sendmail -bs'); 139 140 return new SendmailTransport($sendmail_command); 141 142 case 'external': 143 // SMTP 144 $smtp_helo = Site::getPreference('SMTP_HELO'); 145 $smtp_host = Site::getPreference('SMTP_HOST'); 146 $smtp_port = (int) Site::getPreference('SMTP_PORT'); 147 $smtp_auth = (bool) Site::getPreference('SMTP_AUTH'); 148 $smtp_user = Site::getPreference('SMTP_AUTH_USER'); 149 $smtp_pass = Site::getPreference('SMTP_AUTH_PASS'); 150 $smtp_encr = Site::getPreference('SMTP_SSL') === 'ssl'; 151 152 $transport = new EsmtpTransport($smtp_host, $smtp_port, $smtp_encr); 153 154 $transport->setLocalDomain($smtp_helo); 155 156 if ($smtp_auth) { 157 $transport 158 ->setUsername($smtp_user) 159 ->setPassword($smtp_pass); 160 } 161 162 return $transport; 163 164 default: 165 // For testing 166 return new NullTransport(); 167 } 168 } 169 170 /** 171 * Many mail relays require a valid sender email. 172 * 173 * @param string $email 174 * 175 * @return bool 176 */ 177 public function isValidEmail(string $email): bool 178 { 179 try { 180 $address = new Address($email); 181 } catch (RfcComplianceException $ex) { 182 return false; 183 } 184 185 // Some web hosts disable checkdnsrr. 186 if (function_exists('checkdnsrr')) { 187 $domain = substr(strrchr($address->getAddress(), '@') ?: '@', 1); 188 return checkdnsrr($domain, 'MX'); 189 } 190 191 return true; 192 } 193 194 /** 195 * A list SSL modes (e.g. for an edit control). 196 * 197 * @return array<string> 198 */ 199 public function mailSslOptions(): array 200 { 201 return [ 202 'none' => I18N::translate('none'), 203 /* I18N: Use SMTP over SSL/TLS, or Implicit TLS - a secure communications protocol */ 204 'ssl' => I18N::translate('SSL/TLS'), 205 /* I18N: Use SMTP with STARTTLS, or Explicit TLS - a secure communications protocol */ 206 'tls' => I18N::translate('STARTTLS'), 207 ]; 208 } 209 210 /** 211 * A list SSL modes (e.g. for an edit control). 212 * 213 * @return array<string> 214 */ 215 public function mailTransportOptions(): array 216 { 217 $options = [ 218 /* I18N: "sendmail" is the name of some mail software */ 219 'sendmail' => I18N::translate('Use sendmail to send messages'), 220 'external' => I18N::translate('Use SMTP to send messages'), 221 ]; 222 223 if (!function_exists('proc_open')) { 224 unset($options['sendmail']); 225 } 226 227 return $options; 228 } 229} 230