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