Passed
Push — develop ( dcbf9f...d77f85 )
by nguereza
02:14
created

SMTP::setResponseTimeout()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
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
     * Connection timeout
130
     * @var int
131
     */
132
    protected int $timeout = 30;
133
134
    /**
135
     * Server response timeout
136
     * @var int
137
     */
138
    protected int $responseTimeout = 10;
139
140
    /**
141
     * Create new instance
142
     * @param string $host
143
     * @param int $port
144
     */
145
    public function __construct(
146
        string $host,
147
        int $port = 25,
148
        int $timeout = 30,
149
        int $responseTimeout = 10
150
    ) {
151
        $this->host = $host;
152
        $this->port = $port;
153
        $this->timeout = $timeout;
154
        $this->responseTimeout = $responseTimeout;
155
    }
156
157
    /**
158
     *
159
     * @param int $timeout
160
     * @return $this
161
     */
162
    public function setTimeout(int $timeout): self
163
    {
164
        $this->timeout = $timeout;
165
166
        return $this;
167
    }
168
169
    /**
170
     *
171
     * @param int $responseTimeout
172
     * @return $this
173
     */
174
    public function setResponseTimeout(int $responseTimeout): self
175
    {
176
        $this->responseTimeout = $responseTimeout;
177
        return $this;
178
    }
179
180
181
    /**
182
     * Set TLS connection
183
     * @param bool $status
184
     * @return $this
185
     */
186
    public function tls(bool $status = true): self
187
    {
188
        $this->tls = $status;
189
190
        return $this;
191
    }
192
193
    /**
194
     * Set SSL connection
195
     * @param bool $status
196
     * @return $this
197
     */
198
    public function ssl(bool $status = true): self
199
    {
200
        $this->ssl = $status;
201
202
        return $this;
203
    }
204
205
    /**
206
     * Set authentication information
207
     * @param string $username
208
     * @param string $password
209
     * @return self
210
     */
211
    public function setAuth(string $username, string $password): self
212
    {
213
        $this->username = $username;
214
        $this->password = $password;
215
216
        return $this;
217
    }
218
219
    /**
220
     * {@inheritedoc}
221
     */
222
    public function send(MessageInterface $message): bool
223
    {
224
        $this->message = $message;
225
226
        $this->connect()
227
              ->ehlo();
228
229
        if ($this->tls) {
230
            $this->starttls()
231
                  ->ehlo();
232
        }
233
234
        $this->authLogin()
235
              ->mailFrom()
236
              ->rcptTo()
237
              ->data()
238
              ->quit();
239
240
        if (is_resource($this->smtp)) {
241
            return fclose($this->smtp);
242
        }
243
244
        return false;
245
    }
246
247
    /**
248
     * Return the list of commands send to server
249
     * @return array<int, string>
250
     */
251
    public function getCommands(): array
252
    {
253
        return $this->commands;
254
    }
255
256
    /**
257
     * Return the list of responses from server
258
     * @return array<int, string>
259
     */
260
    public function getResponses(): array
261
    {
262
        return $this->responses;
263
    }
264
265
        /**
266
     * Connect to server
267
     * @return $this
268
     * @throws SMTPException
269
     * @throws SMTPRetunCodeException
270
     */
271
    protected function connect(): self
272
    {
273
        $host = $this->ssl ? 'ssl://' . $this->host : $this->host;
274
        $this->smtp = @fsockopen(
275
            $host,
276
            $this->port,
277
            $errorNumber,
278
            $errorMessage,
279
            $this->timeout
280
        );
281
282
        if (!is_resource($this->smtp)) {
283
            throw new SMTPException(sprintf(
284
                'Could not establish SMTP connection to server [%s] error: [%s: %s]',
285
                $host,
286
                $errorNumber,
287
                $errorMessage
288
            ));
289
        }
290
291
        $code = $this->getCode();
292
        if ($code !== 220) {
293
            throw new SMTPRetunCodeException(220, $code, array_pop($this->responses));
294
        }
295
296
        return $this;
297
    }
298
299
    /**
300
     * Start TLS connection
301
     * @return $this
302
     * @throws SMTPRetunCodeException
303
     * @throws SMTPSecureException
304
     */
305
    protected function starttls(): self
306
    {
307
        $code = $this->sendCommand('STARTTLS');
308
        if ($code !== 220) {
309
            throw new SMTPRetunCodeException(220, $code, array_pop($this->responses));
310
        }
311
312
        /**
313
        * STREAM_CRYPTO_METHOD_TLS_CLIENT is quite the mess ...
314
        *
315
        * - On PHP <5.6 it doesn't even mean TLS, but SSL 2.0, and there's no option to use actual TLS
316
        * - On PHP 5.6.0-5.6.6, >=7.2 it means negotiation with any of TLS 1.0, 1.1, 1.2
317
        * - On PHP 5.6.7-7.1.* it means only TLS 1.0
318
        *
319
        * We want the negotiation, so we'll force it below ...
320
        */
321
        if (is_resource($this->smtp)) {
322
            if (
323
                !stream_socket_enable_crypto(
324
                    $this->smtp,
325
                    true,
326
                    STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
327
                    | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
328
                    | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
329
                )
330
            ) {
331
                throw new SMTPSecureException('Start TLS failed to enable crypto');
332
            }
333
        }
334
335
        return $this;
336
    }
337
338
    /**
339
     * Send hello command
340
     * @return $this
341
     * @throws SMTPRetunCodeException
342
     */
343
    protected function ehlo(): self
344
    {
345
        $command = 'EHLO ' . $this->host . self::CRLF;
346
        $code = $this->sendCommand($command);
347
        if ($code !== 250) {
348
            throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
349
        }
350
351
        return $this;
352
    }
353
354
    /**
355
     * Authentication to server
356
     * @return $this
357
     * @throws SMTPRetunCodeException
358
     */
359
    protected function authLogin(): self
360
    {
361
        if (empty($this->username) && empty($this->password)) {
362
            return $this;
363
        }
364
365
        $command = 'AUTH LOGIN' . self::CRLF;
366
        $code = $this->sendCommand($command);
367
        if ($code !== 334) {
368
            throw new SMTPRetunCodeException(334, $code, array_pop($this->responses));
369
        }
370
371
        $command = base64_encode($this->username) . self::CRLF;
372
        $code = $this->sendCommand($command);
373
        if ($code !== 334) {
374
            throw new SMTPRetunCodeException(334, $code, array_pop($this->responses));
375
        }
376
377
        $command = base64_encode($this->password) . self::CRLF;
378
        $code = $this->sendCommand($command);
379
        if ($code !== 235) {
380
            throw new SMTPRetunCodeException(235, $code, array_pop($this->responses));
381
        }
382
383
        return $this;
384
    }
385
386
    /**
387
     * Set From value
388
     * @return $this
389
     * @throws SMTPRetunCodeException
390
     */
391
    protected function mailFrom(): self
392
    {
393
        $command = 'MAIL FROM:<' . $this->message->getFrom() . '>' . self::CRLF;
394
        $code = $this->sendCommand($command);
395
        if ($code !== 250) {
396
            throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
397
        }
398
399
        return $this;
400
    }
401
402
    /**
403
     * Set recipients
404
     * @return $this
405
     * @throws SMTPRetunCodeException
406
     */
407
    protected function rcptTo(): self
408
    {
409
        $recipients = array_merge(
410
            $this->message->getTo(),
411
            $this->message->getCc(),
412
            $this->message->getBcc()
413
        );
414
415
        foreach ($recipients as $email) {
416
            $command = 'RCPT TO:<' . $email . '>' . self::CRLF;
417
            $code = $this->sendCommand($command);
418
            if ($code !== 250) {
419
                throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
420
            }
421
        }
422
423
        return $this;
424
    }
425
426
    /**
427
     * Send mail data to server
428
     * @return $this
429
     * @throws SMTPRetunCodeException
430
     */
431
    protected function data(): self
432
    {
433
        $command = 'DATA' . self::CRLF;
434
        $code = $this->sendCommand($command);
435
        if ($code !== 354) {
436
            throw new SMTPRetunCodeException(354, $code, array_pop($this->responses));
437
        }
438
439
        $command = (string) $this->message;
440
        $command .= self::CRLF . '.' . self::CRLF;
441
        $code = $this->sendCommand($command);
442
        if ($code !== 250) {
443
            throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
444
        }
445
446
        return $this;
447
    }
448
449
    /**
450
     * Disconnect from server
451
     * @return $this
452
     * @throws SMTPRetunCodeException
453
     */
454
    protected function quit(): self
455
    {
456
        $command = 'QUIT' . self::CRLF;
457
        $code = $this->sendCommand($command);
458
        if ($code !== 221) {
459
            throw new SMTPRetunCodeException(221, $code, array_pop($this->responses));
460
        }
461
462
        return $this;
463
    }
464
465
    /**
466
     * Send command to server
467
     * @param string $command
468
     * @return int
469
     */
470
    protected function sendCommand(string $command): int
471
    {
472
        $this->commands[] = $command;
473
        if (is_resource($this->smtp)) {
474
            fputs($this->smtp, $command, strlen($command));
475
        }
476
        return $this->getCode();
477
    }
478
479
    /**
480
     * Get return code from server
481
     * @return int
482
     * @throws SMTPException
483
     */
484
    protected function getCode(): int
485
    {
486
        if (is_resource($this->smtp)) {
487
            stream_set_timeout($this->smtp, $this->responseTimeout);
488
            while ($str = fgets($this->smtp, 515)) {
489
                $this->responses[] = $str;
490
491
                if (substr($str, 3, 1) === ' ') {
492
                    $code = substr($str, 0, 3);
493
                    return (int) $code;
494
                }
495
            }
496
        }
497
498
        throw new SMTPException('SMTP Server did not respond with anything I recognized');
499
    }
500
}
501