Completed
Push — develop ( 778c00...2ca406 )
by Greg
24:21 queued 15:55
created

EmailService::senderEmail()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 6
rs 10
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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Services;
21
22
use Exception;
23
use Fisharebest\Webtrees\Contracts\UserInterface;
24
use Fisharebest\Webtrees\I18N;
25
use Fisharebest\Webtrees\Log;
26
use Fisharebest\Webtrees\Site;
27
use Fisharebest\Webtrees\SiteUser;
28
use Psr\Http\Message\ServerRequestInterface;
29
use Swift_Mailer;
30
use Swift_Message;
31
use Swift_NullTransport;
32
use Swift_SendmailTransport;
33
use Swift_Signers_DKIMSigner;
34
use Swift_SmtpTransport;
35
use Swift_Transport;
36
37
use function assert;
38
use function checkdnsrr;
39
use function filter_var;
40
use function function_exists;
41
use function str_replace;
42
use function strrchr;
43
use function substr;
44
45
use const FILTER_VALIDATE_DOMAIN;
46
use const FILTER_VALIDATE_EMAIL;
47
48
/**
49
 * Send emails.
50
 */
51
class 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