1e381f98dSGreg Roach<?php 2e381f98dSGreg Roach 3e381f98dSGreg Roach/** 4e381f98dSGreg Roach * webtrees: online genealogy 55bfc6897SGreg Roach * Copyright (C) 2022 webtrees development team 6e381f98dSGreg Roach * This program is free software: you can redistribute it and/or modify 7e381f98dSGreg Roach * it under the terms of the GNU General Public License as published by 8e381f98dSGreg Roach * the Free Software Foundation, either version 3 of the License, or 9e381f98dSGreg Roach * (at your option) any later version. 10e381f98dSGreg Roach * This program is distributed in the hope that it will be useful, 11e381f98dSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12e381f98dSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13e381f98dSGreg Roach * GNU General Public License for more details. 14e381f98dSGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16e381f98dSGreg Roach */ 17e381f98dSGreg Roach 18e381f98dSGreg Roachdeclare(strict_types=1); 19e381f98dSGreg Roach 20e381f98dSGreg Roachnamespace Fisharebest\Webtrees\Services; 21e381f98dSGreg Roach 22e381f98dSGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface; 23e381f98dSGreg Roachuse Fisharebest\Webtrees\I18N; 24e381f98dSGreg Roachuse Fisharebest\Webtrees\Log; 25e381f98dSGreg Roachuse Fisharebest\Webtrees\Site; 26b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator; 27ef641919SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 28fa5cbab5SGreg Roachuse Symfony\Component\Mailer\Exception\TransportExceptionInterface; 29e1ae561aSJonathan Jaubartuse Symfony\Component\Mailer\Mailer; 30e1ae561aSJonathan Jaubartuse Symfony\Component\Mailer\Transport\NullTransport; 31e1ae561aSJonathan Jaubartuse Symfony\Component\Mailer\Transport\SendmailTransport; 32e1ae561aSJonathan Jaubartuse Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; 33e1ae561aSJonathan Jaubartuse Symfony\Component\Mailer\Transport\TransportInterface; 34e1ae561aSJonathan Jaubartuse Symfony\Component\Mime\Address; 35e1ae561aSJonathan Jaubartuse Symfony\Component\Mime\Crypto\DkimOptions; 36e1ae561aSJonathan Jaubartuse Symfony\Component\Mime\Crypto\DkimSigner; 37e1ae561aSJonathan Jaubartuse Symfony\Component\Mime\Email; 38fa5cbab5SGreg Roachuse Symfony\Component\Mime\Exception\RfcComplianceException; 39e1ae561aSJonathan Jaubartuse Symfony\Component\Mime\Message; 40e381f98dSGreg Roach 4194fc28f5SGreg Roachuse function assert; 4204626e75SGreg Roachuse function checkdnsrr; 43e381f98dSGreg Roachuse function function_exists; 44e381f98dSGreg Roachuse function str_replace; 45e381f98dSGreg Roachuse function strrchr; 46e381f98dSGreg Roachuse function substr; 47e381f98dSGreg Roach 48e381f98dSGreg Roach/** 49e381f98dSGreg Roach * Send emails. 50e381f98dSGreg Roach */ 51e381f98dSGreg Roachclass EmailService 52e381f98dSGreg Roach{ 53e381f98dSGreg Roach /** 54e381f98dSGreg Roach * Send an external email message 55e381f98dSGreg Roach * Caution! gmail may rewrite the "From" header unless you have added the address to your account. 56e381f98dSGreg Roach * 57e381f98dSGreg Roach * @param UserInterface $from 58e381f98dSGreg Roach * @param UserInterface $to 59e381f98dSGreg Roach * @param UserInterface $reply_to 60e381f98dSGreg Roach * @param string $subject 61e381f98dSGreg Roach * @param string $message_text 62e381f98dSGreg Roach * @param string $message_html 63e381f98dSGreg Roach * 64e381f98dSGreg Roach * @return bool 65e381f98dSGreg Roach */ 66e381f98dSGreg Roach public function send(UserInterface $from, UserInterface $to, UserInterface $reply_to, string $subject, string $message_text, string $message_html): bool 67e381f98dSGreg Roach { 68e1ae561aSJonathan Jaubart try { 6905fc85fdSGreg Roach $message = $this->message($from, $to, $reply_to, $subject, $message_text, $message_html); 7005fc85fdSGreg Roach $transport = $this->transport(); 71e1ae561aSJonathan Jaubart $mailer = new Mailer($transport); 7205fc85fdSGreg Roach $mailer->send($message); 73*66fffb93SGreg Roach } catch (RfcComplianceException $ex) { 74*66fffb93SGreg Roach Log::addErrorLog('Cannot create email ' . $ex->getMessage()); 75*66fffb93SGreg Roach 76*66fffb93SGreg Roach return false; 77fa5cbab5SGreg Roach } catch (TransportExceptionInterface $ex) { 78*66fffb93SGreg Roach Log::addErrorLog('Cannot send email: ' . $ex->getMessage()); 7905fc85fdSGreg Roach 8005fc85fdSGreg Roach return false; 8105fc85fdSGreg Roach } 8205fc85fdSGreg Roach 8305fc85fdSGreg Roach return true; 8405fc85fdSGreg Roach } 8505fc85fdSGreg Roach 8605fc85fdSGreg Roach /** 8705fc85fdSGreg Roach * Create a message 8805fc85fdSGreg Roach * 8905fc85fdSGreg Roach * @param UserInterface $from 9005fc85fdSGreg Roach * @param UserInterface $to 9105fc85fdSGreg Roach * @param UserInterface $reply_to 9205fc85fdSGreg Roach * @param string $subject 9305fc85fdSGreg Roach * @param string $message_text 9405fc85fdSGreg Roach * @param string $message_html 9505fc85fdSGreg Roach * 96e1ae561aSJonathan Jaubart * @return Message 9705fc85fdSGreg Roach */ 98e1ae561aSJonathan Jaubart protected function message(UserInterface $from, UserInterface $to, UserInterface $reply_to, string $subject, string $message_text, string $message_html): Message 9905fc85fdSGreg Roach { 100e2c25bffSGreg Roach // Mail needs MS-DOS line endings 101e381f98dSGreg Roach $message_text = str_replace("\n", "\r\n", $message_text); 102e381f98dSGreg Roach $message_html = str_replace("\n", "\r\n", $message_html); 103e381f98dSGreg Roach 104e1ae561aSJonathan Jaubart $message = (new Email()) 105e1ae561aSJonathan Jaubart ->subject($subject) 106e1ae561aSJonathan Jaubart ->from(new Address($from->email(), $from->realName())) 107e1ae561aSJonathan Jaubart ->to(new Address($to->email(), $to->realName())) 108e1ae561aSJonathan Jaubart ->replyTo(new Address($reply_to->email(), $reply_to->realName())) 109e1ae561aSJonathan Jaubart ->html($message_html); 110e381f98dSGreg Roach 111e381f98dSGreg Roach $dkim_domain = Site::getPreference('DKIM_DOMAIN'); 112e381f98dSGreg Roach $dkim_selector = Site::getPreference('DKIM_SELECTOR'); 113e381f98dSGreg Roach $dkim_key = Site::getPreference('DKIM_KEY'); 114e381f98dSGreg Roach 115e381f98dSGreg Roach if ($dkim_domain !== '' && $dkim_selector !== '' && $dkim_key !== '') { 116e1ae561aSJonathan Jaubart $signer = new DkimSigner($dkim_key, $dkim_domain, $dkim_selector); 117e1ae561aSJonathan Jaubart $options = (new DkimOptions()) 118e1ae561aSJonathan Jaubart ->headerCanon('relaxed') 119e1ae561aSJonathan Jaubart ->bodyCanon('relaxed'); 120e381f98dSGreg Roach 121e1ae561aSJonathan Jaubart return $signer->sign($message, $options->toArray()); 12205babb96SGreg Roach } 12305babb96SGreg Roach 124e381f98dSGreg Roach // DKIM body hashes don't work with multipart/alternative content. 125e1ae561aSJonathan Jaubart $message->text($message_text); 126e381f98dSGreg Roach 12705fc85fdSGreg Roach return $message; 128e381f98dSGreg Roach } 129e381f98dSGreg Roach 130e381f98dSGreg Roach /** 131e381f98dSGreg Roach * Create a transport mechanism for sending mail 132e381f98dSGreg Roach * 133e1ae561aSJonathan Jaubart * @return TransportInterface 134e381f98dSGreg Roach */ 135e1ae561aSJonathan Jaubart protected function transport(): TransportInterface 136e381f98dSGreg Roach { 137e381f98dSGreg Roach switch (Site::getPreference('SMTP_ACTIVE')) { 138e381f98dSGreg Roach case 'sendmail': 139e381f98dSGreg Roach // Local sendmail (requires PHP proc_* functions) 140ef641919SGreg Roach $request = app(ServerRequestInterface::class); 141ef641919SGreg Roach assert($request instanceof ServerRequestInterface); 142ef641919SGreg Roach 143b55cbc6bSGreg Roach $sendmail_command = Validator::attributes($request)->string('sendmail_command', '/usr/sbin/sendmail -bs'); 144ef641919SGreg Roach 145e1ae561aSJonathan Jaubart return new SendmailTransport($sendmail_command); 146e381f98dSGreg Roach 147e381f98dSGreg Roach case 'external': 148e381f98dSGreg Roach // SMTP 149e2c25bffSGreg Roach $smtp_helo = Site::getPreference('SMTP_HELO'); 150e381f98dSGreg Roach $smtp_host = Site::getPreference('SMTP_HOST'); 1518a07c98eSGreg Roach $smtp_port = (int) Site::getPreference('SMTP_PORT'); 152e381f98dSGreg Roach $smtp_auth = (bool) Site::getPreference('SMTP_AUTH'); 153e381f98dSGreg Roach $smtp_user = Site::getPreference('SMTP_AUTH_USER'); 154e381f98dSGreg Roach $smtp_pass = Site::getPreference('SMTP_AUTH_PASS'); 155e1ae561aSJonathan Jaubart $smtp_encr = Site::getPreference('SMTP_SSL') === 'ssl'; 156e381f98dSGreg Roach 157e1ae561aSJonathan Jaubart $transport = new EsmtpTransport($smtp_host, $smtp_port, $smtp_encr); 158e381f98dSGreg Roach 159e2c25bffSGreg Roach $transport->setLocalDomain($smtp_helo); 160e381f98dSGreg Roach 161e381f98dSGreg Roach if ($smtp_auth) { 162e381f98dSGreg Roach $transport 163e381f98dSGreg Roach ->setUsername($smtp_user) 164e381f98dSGreg Roach ->setPassword($smtp_pass); 165e381f98dSGreg Roach } 166e381f98dSGreg Roach 167e381f98dSGreg Roach return $transport; 168e381f98dSGreg Roach 169e381f98dSGreg Roach default: 170e381f98dSGreg Roach // For testing 171e1ae561aSJonathan Jaubart return new NullTransport(); 172e381f98dSGreg Roach } 173e381f98dSGreg Roach } 174e381f98dSGreg Roach 175e381f98dSGreg Roach /** 176e381f98dSGreg Roach * Many mail relays require a valid sender email. 177e381f98dSGreg Roach * 178e381f98dSGreg Roach * @param string $email 179e381f98dSGreg Roach * 180e381f98dSGreg Roach * @return bool 181e381f98dSGreg Roach */ 182e381f98dSGreg Roach public function isValidEmail(string $email): bool 183e381f98dSGreg Roach { 184e1ae561aSJonathan Jaubart try { 185e1ae561aSJonathan Jaubart $address = new Address($email); 186fa5cbab5SGreg Roach } catch (RfcComplianceException $ex) { 187ff00880fSGreg Roach return false; 188ff00880fSGreg Roach } 189ff00880fSGreg Roach 19004626e75SGreg Roach // Some web hosts disable checkdnsrr. 191e1ae561aSJonathan Jaubart if (function_exists('checkdnsrr')) { 192e1ae561aSJonathan Jaubart $domain = substr(strrchr($address->getAddress(), '@') ?: '@', 1); 193a9866bf2SGreg Roach return checkdnsrr($domain); 19404626e75SGreg Roach } 19504626e75SGreg Roach 196e1ae561aSJonathan Jaubart return true; 197e381f98dSGreg Roach } 198e381f98dSGreg Roach 199e381f98dSGreg Roach /** 200e381f98dSGreg Roach * A list SSL modes (e.g. for an edit control). 201e381f98dSGreg Roach * 20224f2a3afSGreg Roach * @return array<string> 203e381f98dSGreg Roach */ 204e381f98dSGreg Roach public function mailSslOptions(): array 205e381f98dSGreg Roach { 206e381f98dSGreg Roach return [ 207e381f98dSGreg Roach 'none' => I18N::translate('none'), 208e1ae561aSJonathan Jaubart /* I18N: Use SMTP over SSL/TLS, or Implicit TLS - a secure communications protocol */ 209e1ae561aSJonathan Jaubart 'ssl' => I18N::translate('SSL/TLS'), 210e1ae561aSJonathan Jaubart /* I18N: Use SMTP with STARTTLS, or Explicit TLS - a secure communications protocol */ 211e1ae561aSJonathan Jaubart 'tls' => I18N::translate('STARTTLS'), 212e381f98dSGreg Roach ]; 213e381f98dSGreg Roach } 214e381f98dSGreg Roach 215e381f98dSGreg Roach /** 216e381f98dSGreg Roach * A list SSL modes (e.g. for an edit control). 217e381f98dSGreg Roach * 21824f2a3afSGreg Roach * @return array<string> 219e381f98dSGreg Roach */ 220e381f98dSGreg Roach public function mailTransportOptions(): array 221e381f98dSGreg Roach { 222e381f98dSGreg Roach $options = [ 223e381f98dSGreg Roach /* I18N: "sendmail" is the name of some mail software */ 224e381f98dSGreg Roach 'sendmail' => I18N::translate('Use sendmail to send messages'), 225e381f98dSGreg Roach 'external' => I18N::translate('Use SMTP to send messages'), 226e381f98dSGreg Roach ]; 227e381f98dSGreg Roach 228e381f98dSGreg Roach if (!function_exists('proc_open')) { 229e381f98dSGreg Roach unset($options['sendmail']); 230e381f98dSGreg Roach } 231e381f98dSGreg Roach 232e381f98dSGreg Roach return $options; 233e381f98dSGreg Roach } 234e381f98dSGreg Roach} 235