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