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