Passed
Push — develop ( c34963...58e492 )
by nguereza
02:34
created

SMTP::sendCommand()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Platine Mail
5
 *
6
 * Platine Mail provides a flexible and powerful PHP email sender
7
 *  with support of SMTP, Native Mail, sendmail, etc transport.
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Mail
12
 * Copyright (c) 2015, Sonia Marquette
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file SMTP.php
35
 *
36
 *  The SMTP transport class
37
 *
38
 *  @package    Platine\Mail\Transport
39
 *  @author Platine Developers Team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   http://www.iacademy.cf
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Mail\Transport;
50
51
use Platine\Mail\Exception\SMTPException;
52
use Platine\Mail\Exception\SMTPRetunCodeException;
53
use Platine\Mail\Exception\SMTPSecureException;
54
use Platine\Mail\MessageInterface;
55
56
/**
57
 * Class SMTP
58
 * @package Platine\Mail\Transport
59
 */
60
class SMTP implements TransportInterface
61
{
62
63
    /**
64
     * End of line char
65
     */
66
    protected const CRLF = PHP_EOL;
67
68
    /**
69
     * The SMTP socket instance
70
     * @var resource|false
71
     */
72
    protected $smtp = null;
73
74
    /**
75
     * The SMTP host
76
     * @var string
77
     */
78
    protected string $host;
79
80
    /**
81
     * SMTP server port
82
     * @var int
83
     */
84
    protected int $port = 25;
85
86
    /**
87
     * Whether need use SSL connection
88
     * @var bool
89
     */
90
    protected bool $ssl = false;
91
92
    /**
93
     * Whether need use TLS connection
94
     * @var bool
95
     */
96
    protected bool $tls = false;
97
98
    /**
99
     * The username
100
     * @var string
101
     */
102
    protected string $username = '';
103
104
    /**
105
     * The password
106
     * @var string
107
     */
108
    protected string $password = '';
109
110
    /**
111
     * The instance of message to send
112
     * @var MessageInterface
113
     */
114
    protected MessageInterface $message;
115
116
    /**
117
     * List of all commands send to server
118
     * @var array<int, string>
119
     */
120
    protected array $commands = [];
121
122
    /**
123
     * List of all responses receive from server
124
     * @var array<int, string>
125
     */
126
    protected array $responses = [];
127
128
    /**
129
     * Create new instance
130
     * @param string $host
131
     * @param int $port
132
     */
133
    public function __construct(string $host, int $port = 25)
134
    {
135
        $this->host = $host;
136
        $this->port = $port;
137
    }
138
139
    /**
140
     * Set TLS connection
141
     * @param bool $status
142
     * @return $this
143
     */
144
    public function tls(bool $status = true): self
145
    {
146
        $this->tls = $status;
147
148
        return $this;
149
    }
150
151
    /**
152
     * Set SSL connection
153
     * @param bool $status
154
     * @return $this
155
     */
156
    public function ssl(bool $status = true): self
157
    {
158
        $this->ssl = $status;
159
160
        return $this;
161
    }
162
163
    /**
164
     * Set authentication information
165
     * @param string $username
166
     * @param string $password
167
     * @return self
168
     */
169
    public function setAuth(string $username, string $password): self
170
    {
171
        $this->username = $username;
172
        $this->password = $password;
173
174
        return $this;
175
    }
176
177
    /**
178
     * {@inheritedoc}
179
     */
180
    public function send(MessageInterface $message): bool
181
    {
182
        $this->message = $message;
183
184
        $this->connect()
185
              ->ehlo();
186
187
        if ($this->tls) {
188
            $this->starttls()
189
                  ->ehlo();
190
        }
191
192
        $this->authLogin()
193
              ->mailFrom()
194
              ->rcptTo()
195
              ->data()
196
              ->quit();
197
198
        if (is_resource($this->smtp)) {
199
            return fclose($this->smtp);
200
        }
201
202
        return false;
203
    }
204
205
    /**
206
     * Return the list of commands send to server
207
     * @return array<int, string>
208
     */
209
    public function getCommands(): array
210
    {
211
        return $this->commands;
212
    }
213
214
    /**
215
     * Return the list of responses from server
216
     * @return array<int, string>
217
     */
218
    public function getResponses(): array
219
    {
220
        return $this->responses;
221
    }
222
223
        /**
224
     * Connect to server
225
     * @return $this
226
     * @throws SMTPException
227
     * @throws SMTPRetunCodeException
228
     */
229
    protected function connect(): self
230
    {
231
        $host = $this->ssl ? 'ssl://' . $this->host : $this->host;
232
        $this->smtp = @fsockopen($host, $this->port);
233
234
        if (!is_resource($this->smtp)) {
235
            throw new SMTPException('Could not establish SMTP connection to server');
236
        }
237
238
        $code = $this->getCode();
239
        if ($code !== 220) {
240
            throw new SMTPRetunCodeException(220, $code, array_pop($this->responses));
241
        }
242
243
        return $this;
244
    }
245
246
    /**
247
     * Start TLS connection
248
     * @return $this
249
     * @throws SMTPRetunCodeException
250
     * @throws SMTPSecureException
251
     */
252
    protected function starttls(): self
253
    {
254
        $code = $this->sendCommand('STARTTLS');
255
        if ($code !== 220) {
256
            throw new SMTPRetunCodeException(220, $code, array_pop($this->responses));
257
        }
258
259
        /**
260
        * STREAM_CRYPTO_METHOD_TLS_CLIENT is quite the mess ...
261
        *
262
        * - On PHP <5.6 it doesn't even mean TLS, but SSL 2.0, and there's no option to use actual TLS
263
        * - On PHP 5.6.0-5.6.6, >=7.2 it means negotiation with any of TLS 1.0, 1.1, 1.2
264
        * - On PHP 5.6.7-7.1.* it means only TLS 1.0
265
        *
266
        * We want the negotiation, so we'll force it below ...
267
        */
268
        if (is_resource($this->smtp)) {
269
            if (
270
                !stream_socket_enable_crypto(
271
                    $this->smtp,
272
                    true,
273
                    STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
274
                    | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
275
                    | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
276
                )
277
            ) {
278
                throw new SMTPSecureException('Start TLS failed to enable crypto');
279
            }
280
        }
281
282
        return $this;
283
    }
284
285
    /**
286
     * Send hello command
287
     * @return $this
288
     * @throws SMTPRetunCodeException
289
     */
290
    protected function ehlo(): self
291
    {
292
        $command = 'EHLO ' . $this->host . self::CRLF;
293
        $code = $this->sendCommand($command);
294
        if ($code !== 250) {
295
            throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
296
        }
297
298
        return $this;
299
    }
300
301
    /**
302
     * Authentication to server
303
     * @return $this
304
     * @throws SMTPRetunCodeException
305
     */
306
    protected function authLogin(): self
307
    {
308
        if (empty($this->username) && empty($this->password)) {
309
            return $this;
310
        }
311
312
        $command = 'AUTH LOGIN' . self::CRLF;
313
        $code = $this->sendCommand($command);
314
        if ($code !== 334) {
315
            throw new SMTPRetunCodeException(334, $code, array_pop($this->responses));
316
        }
317
318
        $command = base64_encode($this->username) . self::CRLF;
319
        $code = $this->sendCommand($command);
320
        if ($code !== 334) {
321
            throw new SMTPRetunCodeException(334, $code, array_pop($this->responses));
322
        }
323
324
        $command = base64_encode($this->password) . self::CRLF;
325
        $code = $this->sendCommand($command);
326
        if ($code !== 235) {
327
            throw new SMTPRetunCodeException(235, $code, array_pop($this->responses));
328
        }
329
330
        return $this;
331
    }
332
333
    /**
334
     * Set From value
335
     * @return $this
336
     * @throws SMTPRetunCodeException
337
     */
338
    protected function mailFrom(): self
339
    {
340
        $command = 'MAIL FROM:<' . $this->message->getFrom() . '>' . self::CRLF;
341
        $code = $this->sendCommand($command);
342
        if ($code !== 250) {
343
            throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
344
        }
345
346
        return $this;
347
    }
348
349
    /**
350
     * Set recipients
351
     * @return $this
352
     * @throws SMTPRetunCodeException
353
     */
354
    protected function rcptTo(): self
355
    {
356
        $recipients = array_merge(
357
            $this->message->getTo(),
358
            $this->message->getCc(),
359
            $this->message->getBcc()
360
        );
361
362
        foreach ($recipients as $email) {
363
            $command = 'RCPT TO:<' . $email . '>' . self::CRLF;
364
            $code = $this->sendCommand($command);
365
            if ($code !== 250) {
366
                throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
367
            }
368
        }
369
370
        return $this;
371
    }
372
373
    /**
374
     * Send mail data to server
375
     * @return $this
376
     * @throws SMTPRetunCodeException
377
     */
378
    protected function data(): self
379
    {
380
        $command = 'DATA' . self::CRLF;
381
        $code = $this->sendCommand($command);
382
        if ($code !== 354) {
383
            throw new SMTPRetunCodeException(354, $code, array_pop($this->responses));
384
        }
385
386
        $command = (string) $this->message;
387
        $command .= self::CRLF . self::CRLF . '.' . self::CRLF;
388
        $code = $this->sendCommand($command);
389
        if ($code !== 250) {
390
            throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
391
        }
392
393
        return $this;
394
    }
395
396
    /**
397
     * Disconnect from server
398
     * @return $this
399
     * @throws SMTPRetunCodeException
400
     */
401
    protected function quit(): self
402
    {
403
        $command = 'QUIT' . self::CRLF;
404
        $code = $this->sendCommand($command);
405
        if ($code !== 221) {
406
            throw new SMTPRetunCodeException(221, $code, array_pop($this->responses));
407
        }
408
409
        return $this;
410
    }
411
412
    /**
413
     * Get return code from server
414
     * @return int
415
     * @throws SMTPException
416
     */
417
    protected function getCode(): int
418
    {
419
        if (is_resource($this->smtp)) {
420
            while ($str = fgets($this->smtp, 515)) {
421
                $this->responses[] = $str;
422
423
                if (substr($str, 3, 1) === ' ') {
424
                    $code = substr($str, 0, 3);
425
                    return (int) $code;
426
                }
427
            }
428
        }
429
430
        throw new SMTPException('SMTP Server did not respond with anything I recognized');
431
    }
432
433
    /**
434
     * Send command to server
435
     * @param string $command
436
     * @return int
437
     */
438
    protected function sendCommand(string $command): int
439
    {
440
        $this->commands[] = $command;
441
        if (is_resource($this->smtp)) {
442
            fputs($this->smtp, $command, strlen($command));
443
        }
444
        return $this->getCode();
445
    }
446
}
447