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