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 Swift_Mailer; 29use Swift_Message; 30use Swift_NullTransport; 31use Swift_SendmailTransport; 32use Swift_Signers_DKIMSigner; 33use Swift_SmtpTransport; 34use Swift_Transport; 35 36use function assert; 37use function checkdnsrr; 38use function filter_var; 39use function function_exists; 40use function str_replace; 41use function strrchr; 42use function substr; 43 44use const FILTER_VALIDATE_DOMAIN; 45use const FILTER_VALIDATE_EMAIL; 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 $message = $this->message($from, $to, $reply_to, $subject, $message_text, $message_html); 68 $transport = $this->transport(); 69 $mailer = new Swift_Mailer($transport); 70 71 try { 72 $mailer->send($message); 73 } catch (Exception $ex) { 74 Log::addErrorLog('MailService: ' . $ex->getMessage()); 75 76 return false; 77 } 78 79 return true; 80 } 81 82 /** 83 * Create a message 84 * 85 * @param UserInterface $from 86 * @param UserInterface $to 87 * @param UserInterface $reply_to 88 * @param string $subject 89 * @param string $message_text 90 * @param string $message_html 91 * 92 * @return Swift_Message 93 */ 94 protected function message(UserInterface $from, UserInterface $to, UserInterface $reply_to, string $subject, string $message_text, string $message_html): Swift_Message 95 { 96 // Mail needs MS-DOS line endings 97 $message_text = str_replace("\n", "\r\n", $message_text); 98 $message_html = str_replace("\n", "\r\n", $message_html); 99 100 $message = (new Swift_Message()) 101 ->setSubject($subject) 102 ->setFrom($from->email(), $from->realName()) 103 ->setTo($to->email(), $to->realName()) 104 ->setReplyTo($reply_to->email(), $reply_to->realName()) 105 ->setBody($message_html, 'text/html'); 106 107 $dkim_domain = Site::getPreference('DKIM_DOMAIN'); 108 $dkim_selector = Site::getPreference('DKIM_SELECTOR'); 109 $dkim_key = Site::getPreference('DKIM_KEY'); 110 111 if ($dkim_domain !== '' && $dkim_selector !== '' && $dkim_key !== '') { 112 $signer = new Swift_Signers_DKIMSigner($dkim_key, $dkim_domain, $dkim_selector); 113 $signer 114 ->setHeaderCanon('relaxed') 115 ->setBodyCanon('relaxed'); 116 117 $message->attachSigner($signer); 118 } else { 119 // DKIM body hashes don't work with multipart/alternative content. 120 $message->addPart($message_text, 'text/plain'); 121 } 122 123 return $message; 124 } 125 126 /** 127 * Create a transport mechanism for sending mail 128 * 129 * @return Swift_Transport 130 */ 131 protected function transport(): Swift_Transport 132 { 133 switch (Site::getPreference('SMTP_ACTIVE')) { 134 case 'sendmail': 135 // Local sendmail (requires PHP proc_* functions) 136 $request = app(ServerRequestInterface::class); 137 assert($request instanceof ServerRequestInterface); 138 139 $sendmail_command = $request->getAttribute('sendmail_command', '/usr/sbin/sendmail -bs'); 140 141 return new Swift_SendmailTransport($sendmail_command); 142 143 case 'external': 144 // SMTP 145 $smtp_helo = Site::getPreference('SMTP_HELO'); 146 $smtp_host = Site::getPreference('SMTP_HOST'); 147 $smtp_port = (int) Site::getPreference('SMTP_PORT'); 148 $smtp_auth = (bool) Site::getPreference('SMTP_AUTH'); 149 $smtp_user = Site::getPreference('SMTP_AUTH_USER'); 150 $smtp_pass = Site::getPreference('SMTP_AUTH_PASS'); 151 $smtp_encr = Site::getPreference('SMTP_SSL'); 152 153 if ($smtp_encr === 'none') { 154 $smtp_encr = null; 155 } 156 157 $transport = new Swift_SmtpTransport($smtp_host, $smtp_port, $smtp_encr); 158 159 $transport->setLocalDomain($smtp_helo); 160 161 if ($smtp_auth) { 162 $transport 163 ->setUsername($smtp_user) 164 ->setPassword($smtp_pass); 165 } 166 167 return $transport; 168 169 default: 170 // For testing 171 return new Swift_NullTransport(); 172 } 173 } 174 175 /** 176 * Many mail relays require a valid sender email. 177 * 178 * @param string $email 179 * 180 * @return bool 181 */ 182 public function isValidEmail(string $email): bool 183 { 184 $at_domain = strrchr($email, '@'); 185 186 if ($at_domain === false) { 187 return false; 188 } 189 190 $domain = substr($at_domain, 1); 191 192 $email_valid = filter_var($email, FILTER_VALIDATE_EMAIL) !== false; 193 $domain_valid = filter_var($domain, FILTER_VALIDATE_DOMAIN) !== false; 194 195 // Some web hosts disable checkdnsrr. 196 if ($domain_valid && function_exists('checkdnsrr')) { 197 $domain_valid = checkdnsrr($domain); 198 } 199 200 return $email_valid && $domain_valid; 201 } 202 203 /** 204 * A list SSL modes (e.g. for an edit control). 205 * 206 * @return array<string> 207 */ 208 public function mailSslOptions(): array 209 { 210 return [ 211 'none' => I18N::translate('none'), 212 /* I18N: Secure Sockets Layer - a secure communications protocol*/ 213 'ssl' => I18N::translate('ssl'), 214 /* I18N: Transport Layer Security - a secure communications protocol */ 215 'tls' => I18N::translate('tls'), 216 ]; 217 } 218 219 /** 220 * A list SSL modes (e.g. for an edit control). 221 * 222 * @return array<string> 223 */ 224 public function mailTransportOptions(): array 225 { 226 $options = [ 227 /* I18N: "sendmail" is the name of some mail software */ 228 'sendmail' => I18N::translate('Use sendmail to send messages'), 229 'external' => I18N::translate('Use SMTP to send messages'), 230 ]; 231 232 if (!function_exists('proc_open')) { 233 unset($options['sendmail']); 234 } 235 236 return $options; 237 } 238} 239