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 Exception; 23use Fisharebest\Webtrees\Contracts\UserInterface; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Log; 26use Fisharebest\Webtrees\Site; 27use Psr\Http\Message\ServerRequestInterface; 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\Message; 38 39use function assert; 40use function checkdnsrr; 41use function function_exists; 42use function str_replace; 43use function strrchr; 44use function substr; 45 46/** 47 * Send emails. 48 */ 49class EmailService 50{ 51 /** 52 * Send an external email message 53 * Caution! gmail may rewrite the "From" header unless you have added the address to your account. 54 * 55 * @param UserInterface $from 56 * @param UserInterface $to 57 * @param UserInterface $reply_to 58 * @param string $subject 59 * @param string $message_text 60 * @param string $message_html 61 * 62 * @return bool 63 */ 64 public function send(UserInterface $from, UserInterface $to, UserInterface $reply_to, string $subject, string $message_text, string $message_html): bool 65 { 66 try { 67 $message = $this->message($from, $to, $reply_to, $subject, $message_text, $message_html); 68 $transport = $this->transport(); 69 $mailer = new Mailer($transport); 70 $mailer->send($message); 71 } catch (Exception $ex) { 72 Log::addErrorLog('MailService: ' . $ex->getMessage()); 73 74 return false; 75 } 76 77 return true; 78 } 79 80 /** 81 * Create a message 82 * 83 * @param UserInterface $from 84 * @param UserInterface $to 85 * @param UserInterface $reply_to 86 * @param string $subject 87 * @param string $message_text 88 * @param string $message_html 89 * 90 * @return Message 91 */ 92 protected function message(UserInterface $from, UserInterface $to, UserInterface $reply_to, string $subject, string $message_text, string $message_html): Message 93 { 94 // Mail needs MS-DOS line endings 95 $message_text = str_replace("\n", "\r\n", $message_text); 96 $message_html = str_replace("\n", "\r\n", $message_html); 97 98 $message = (new Email()) 99 ->subject($subject) 100 ->from(new Address($from->email(), $from->realName())) 101 ->to(new Address($to->email(), $to->realName())) 102 ->replyTo(new Address($reply_to->email(), $reply_to->realName())) 103 ->html($message_html); 104 105 $dkim_domain = Site::getPreference('DKIM_DOMAIN'); 106 $dkim_selector = Site::getPreference('DKIM_SELECTOR'); 107 $dkim_key = Site::getPreference('DKIM_KEY'); 108 109 if ($dkim_domain !== '' && $dkim_selector !== '' && $dkim_key !== '') { 110 $signer = new DkimSigner($dkim_key, $dkim_domain, $dkim_selector); 111 $options = (new DkimOptions()) 112 ->headerCanon('relaxed') 113 ->bodyCanon('relaxed'); 114 115 return $signer->sign($message, $options->toArray()); 116 } else { 117 // DKIM body hashes don't work with multipart/alternative content. 118 $message->text($message_text); 119 } 120 121 return $message; 122 } 123 124 /** 125 * Create a transport mechanism for sending mail 126 * 127 * @return TransportInterface 128 */ 129 protected function transport(): TransportInterface 130 { 131 switch (Site::getPreference('SMTP_ACTIVE')) { 132 case 'sendmail': 133 // Local sendmail (requires PHP proc_* functions) 134 $request = app(ServerRequestInterface::class); 135 assert($request instanceof ServerRequestInterface); 136 137 $sendmail_command = $request->getAttribute('sendmail_command', '/usr/sbin/sendmail -bs'); 138 139 return new SendmailTransport($sendmail_command); 140 141 case 'external': 142 // SMTP 143 $smtp_helo = Site::getPreference('SMTP_HELO'); 144 $smtp_host = Site::getPreference('SMTP_HOST'); 145 $smtp_port = (int) Site::getPreference('SMTP_PORT'); 146 $smtp_auth = (bool) Site::getPreference('SMTP_AUTH'); 147 $smtp_user = Site::getPreference('SMTP_AUTH_USER'); 148 $smtp_pass = Site::getPreference('SMTP_AUTH_PASS'); 149 $smtp_encr = Site::getPreference('SMTP_SSL') === 'ssl'; 150 151 $transport = new EsmtpTransport($smtp_host, $smtp_port, $smtp_encr); 152 153 $transport->setLocalDomain($smtp_helo); 154 155 if ($smtp_auth) { 156 $transport 157 ->setUsername($smtp_user) 158 ->setPassword($smtp_pass); 159 } 160 161 return $transport; 162 163 default: 164 // For testing 165 return new NullTransport(); 166 } 167 } 168 169 /** 170 * Many mail relays require a valid sender email. 171 * 172 * @param string $email 173 * 174 * @return bool 175 */ 176 public function isValidEmail(string $email): bool 177 { 178 try { 179 $address = new Address($email); 180 } catch (Exception $ex) { 181 return false; 182 } 183 184 // Some web hosts disable checkdnsrr. 185 if (function_exists('checkdnsrr')) { 186 $domain = substr(strrchr($address->getAddress(), '@') ?: '@', 1); 187 return checkdnsrr($domain, 'MX'); 188 } 189 190 return true; 191 } 192 193 /** 194 * A list SSL modes (e.g. for an edit control). 195 * 196 * @return array<string> 197 */ 198 public function mailSslOptions(): array 199 { 200 return [ 201 'none' => I18N::translate('none'), 202 /* I18N: Use SMTP over SSL/TLS, or Implicit TLS - a secure communications protocol */ 203 'ssl' => I18N::translate('SSL/TLS'), 204 /* I18N: Use SMTP with STARTTLS, or Explicit TLS - a secure communications protocol */ 205 'tls' => I18N::translate('STARTTLS'), 206 ]; 207 } 208 209 /** 210 * A list SSL modes (e.g. for an edit control). 211 * 212 * @return array<string> 213 */ 214 public function mailTransportOptions(): array 215 { 216 $options = [ 217 /* I18N: "sendmail" is the name of some mail software */ 218 'sendmail' => I18N::translate('Use sendmail to send messages'), 219 'external' => I18N::translate('Use SMTP to send messages'), 220 ]; 221 222 if (!function_exists('proc_open')) { 223 unset($options['sendmail']); 224 } 225 226 return $options; 227 } 228} 229