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