Passed
Push — master ( 149810...e381f9 )
by Greg
06:20
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) 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
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 Swift_Mailer;
28
use Swift_Message;
29
use Swift_NullTransport;
30
use Swift_SendmailTransport;
31
use Swift_Signers_DKIMSigner;
32
use Swift_SmtpTransport;
33
use Swift_Transport;
34
use Throwable;
35
36
use function filter_var;
37
use function function_exists;
38
use function gethostbyaddr;
39
use function gethostbyname;
40
use function gethostname;
41
use function getmxrr;
42
use function str_replace;
43
use function strrchr;
44
use function substr;
45
46
use const FILTER_VALIDATE_DOMAIN;
47
use const FILTER_VALIDATE_EMAIL;
48
49
/**
50
 * Send emails.
51
 */
52
class 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
                $transport = new Swift_SmtpTransport($smtp_host, $smtp_port, $smtp_encr);
138
139
                $transport->setLocalDomain($this->localDomain());
140
141
                if ($smtp_auth) {
142
                    $transport
143
                        ->setUsername($smtp_user)
144
                        ->setPassword($smtp_pass);
145
                }
146
147
                return $transport;
148
149
            default:
150
                // For testing
151
                return new Swift_NullTransport();
152
        }
153
    }
154
155
    /**
156
     * Where are we sending mail from?
157
     *
158
     * @return string
159
     */
160
    public function localDomain(): string
161
    {
162
        $local_domain = Site::getPreference('SMTP_HELO');
163
164
        try {
165
            // Look ourself up using DNS.
166
            $default = gethostbyaddr(gethostbyname(gethostname()));
167
        } catch (Throwable $ex) {
168
            $default = 'localhost';
169
        }
170
171
        return $local_domain ?: $default;
172
    }
173
174
    /**
175
     * Who are we sending mail from?
176
     *
177
     * @return string
178
     */
179
    public function senderEmail(): string
180
    {
181
        $sender  = Site::getPreference('SMTP_FROM_NAME');
182
        $default = 'no-reply@' . $this->localDomain();
183
184
        return $sender ?: $default;
185
    }
186
187
    /**
188
     * Many mail relays require a valid sender email.
189
     *
190
     * @param string $email
191
     *
192
     * @return bool
193
     */
194
    public function isValidEmail(string $email): bool
195
    {
196
        if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
197
            return false;
198
        }
199
200
        $domain = substr(strrchr($email, '@'), 1);
201
202
        if (filter_var($domain, FILTER_VALIDATE_DOMAIN) === false) {
203
            return false;
204
        }
205
206
        return getmxrr($domain, $mxhosts);
207
    }
208
209
    /**
210
     * A list SSL modes (e.g. for an edit control).
211
     *
212
     * @return string[]
213
     */
214
    public function mailSslOptions(): array
215
    {
216
        return [
217
            'none' => I18N::translate('none'),
218
            /* I18N: Secure Sockets Layer - a secure communications protocol*/
219
            'ssl'  => I18N::translate('ssl'),
220
            /* I18N: Transport Layer Security - a secure communications protocol */
221
            'tls'  => I18N::translate('tls'),
222
        ];
223
    }
224
225
    /**
226
     * A list SSL modes (e.g. for an edit control).
227
     *
228
     * @return string[]
229
     */
230
    public function mailTransportOptions(): array
231
    {
232
        $options = [
233
            /* I18N: "sendmail" is the name of some mail software */
234
            'sendmail' => I18N::translate('Use sendmail to send messages'),
235
            'external' => I18N::translate('Use SMTP to send messages'),
236
        ];
237
238
        if (!function_exists('proc_open')) {
239
            unset($options['sendmail']);
240
        }
241
242
        return $options;
243
    }
244
}
245