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