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