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