xref: /webtrees/app/Services/EmailService.php (revision 66fffb9351751d07477bf061bb3e3c0ae1aa12a0)
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