Issues (2)

src/Transport/SMTP.php (2 issues)

Severity
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   https://www.platine-php.com
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
     * End of line char
64
     */
65
    protected const CRLF = PHP_EOL;
66
67
    /**
68
     * The SMTP socket instance
69
     * @var resource|bool
70
     */
71
    protected $smtp = null;
72
73
    /**
74
     * The SMTP host
75
     * @var string
76
     */
77
    protected string $host;
78
79
    /**
80
     * SMTP server port
81
     * @var int
82
     */
83
    protected int $port = 25;
84
85
    /**
86
     * Whether need use SSL connection
87
     * @var bool
88
     */
89
    protected bool $ssl = false;
90
91
    /**
92
     * Whether need use TLS connection
93
     * @var bool
94
     */
95
    protected bool $tls = false;
96
97
    /**
98
     * The username
99
     * @var string
100
     */
101
    protected string $username = '';
102
103
    /**
104
     * The password
105
     * @var string
106
     */
107
    protected string $password = '';
108
109
    /**
110
     * The instance of message to send
111
     * @var MessageInterface
112
     */
113
    protected MessageInterface $message;
114
115
    /**
116
     * List of all commands send to server
117
     * @var array<int, string>
118
     */
119
    protected array $commands = [];
120
121
    /**
122
     * List of all responses receive from server
123
     * @var array<int, string>
124
     */
125
    protected array $responses = [];
126
127
    /**
128
     * Connection timeout
129
     * @var int
130
     */
131
    protected int $timeout = 30;
132
133
    /**
134
     * Server response timeout
135
     * @var int
136
     */
137
    protected int $responseTimeout = 10;
138
139
    /**
140
     * Create new instance
141
     * @param string $host
142
     * @param int $port
143
     * @param int $timeout
144
     * @param int $responseTimeout
145
     */
146
    public function __construct(
147
        string $host,
148
        int $port = 25,
149
        int $timeout = 30,
150
        int $responseTimeout = 10
151
    ) {
152
        $this->host = $host;
153
        $this->port = $port;
154
        $this->timeout = $timeout;
155
        $this->responseTimeout = $responseTimeout;
156
    }
157
158
    /**
159
     *
160
     * @param int $timeout
161
     * @return $this
162
     */
163
    public function setTimeout(int $timeout): self
164
    {
165
        $this->timeout = $timeout;
166
167
        return $this;
168
    }
169
170
    /**
171
     *
172
     * @param int $responseTimeout
173
     * @return $this
174
     */
175
    public function setResponseTimeout(int $responseTimeout): self
176
    {
177
        $this->responseTimeout = $responseTimeout;
178
        return $this;
179
    }
180
181
182
    /**
183
     * Set TLS connection
184
     * @param bool $status
185
     * @return $this
186
     */
187
    public function tls(bool $status = true): self
188
    {
189
        $this->tls = $status;
190
191
        return $this;
192
    }
193
194
    /**
195
     * Set SSL connection
196
     * @param bool $status
197
     * @return $this
198
     */
199
    public function ssl(bool $status = true): self
200
    {
201
        $this->ssl = $status;
202
203
        return $this;
204
    }
205
206
    /**
207
     * Set authentication information
208
     * @param string $username
209
     * @param string $password
210
     * @return $this
211
     */
212
    public function setAuth(string $username, string $password): self
213
    {
214
        $this->username = $username;
215
        $this->password = $password;
216
217
        return $this;
218
    }
219
220
    /**
221
     * {@inheritedoc}
222
     */
223
    public function send(MessageInterface $message): bool
224
    {
225
        $this->message = $message;
226
227
        $this->connect()
228
              ->ehlo();
229
230
        if ($this->tls) {
231
            $this->starttls()
232
                  ->ehlo();
233
        }
234
235
        $this->authLogin()
236
              ->mailFrom()
237
              ->rcptTo()
238
              ->data()
239
              ->quit();
240
241
        if (is_resource($this->smtp)) {
242
            return fclose($this->smtp);
243
        }
244
245
        return false;
246
    }
247
248
    /**
249
     * Return the list of commands send to server
250
     * @return array<int, string>
251
     */
252
    public function getCommands(): array
253
    {
254
        return $this->commands;
255
    }
256
257
    /**
258
     * Return the list of responses from server
259
     * @return array<int, string>
260
     */
261
    public function getResponses(): array
262
    {
263
        return $this->responses;
264
    }
265
266
        /**
267
     * Connect to server
268
     * @return $this
269
     * @throws SMTPException
270
     * @throws SMTPRetunCodeException
271
     */
272
    protected function connect(): self
273
    {
274
        $host = $this->ssl ? 'ssl://' . $this->host : $this->host;
275
        $this->smtp = @fsockopen(
276
            $host,
277
            $this->port,
278
            $errorNumber,
279
            $errorMessage,
280
            $this->timeout
281
        );
282
283
        if (is_resource($this->smtp) === false) {
284
            throw new SMTPException(sprintf(
285
                'Could not establish SMTP connection to server [%s] error: [%s: %s]',
286
                $host,
287
                $errorNumber,
288
                $errorMessage
289
            ));
290
        }
291
292
        $code = $this->getCode();
293
        if ($code !== 220) {
294
            throw new SMTPRetunCodeException(220, $code, array_pop($this->responses));
295
        }
296
297
        return $this;
298
    }
299
300
    /**
301
     * Start TLS connection
302
     * @return $this
303
     * @throws SMTPRetunCodeException
304
     * @throws SMTPSecureException
305
     */
306
    protected function starttls(): self
307
    {
308
        $code = $this->sendCommand('STARTTLS');
309
        if ($code !== 220) {
310
            throw new SMTPRetunCodeException(220, $code, array_pop($this->responses));
311
        }
312
313
        /**
314
        * STREAM_CRYPTO_METHOD_TLS_CLIENT is quite the mess ...
315
        *
316
        * - On PHP <5.6 it doesn't even mean TLS, but SSL 2.0, and there's no option to use actual TLS
317
        * - On PHP 5.6.0-5.6.6, >=7.2 it means negotiation with any of TLS 1.0, 1.1, 1.2
318
        * - On PHP 5.6.7-7.1.* it means only TLS 1.0
319
        *
320
        * We want the negotiation, so we'll force it below ...
321
        */
322
        if (is_resource($this->smtp)) {
323
            if (
324
                !stream_socket_enable_crypto(
325
                    $this->smtp,
326
                    true,
327
                    STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
328
                    | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
329
                    | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
330
                )
331
            ) {
332
                throw new SMTPSecureException('Start TLS failed to enable crypto');
333
            }
334
        }
335
336
        return $this;
337
    }
338
339
    /**
340
     * Send hello command
341
     * @return $this
342
     * @throws SMTPRetunCodeException
343
     */
344
    protected function ehlo(): self
345
    {
346
        $command = 'EHLO ' . $this->host . self::CRLF;
347
        $code = $this->sendCommand($command);
348
        if ($code !== 250) {
349
            throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
350
        }
351
352
        return $this;
353
    }
354
355
    /**
356
     * Authentication to server
357
     * @return $this
358
     * @throws SMTPRetunCodeException
359
     */
360
    protected function authLogin(): self
361
    {
362
        if (empty($this->username) && empty($this->password)) {
363
            return $this;
364
        }
365
366
        $command = 'AUTH LOGIN' . self::CRLF;
367
        $code = $this->sendCommand($command);
368
        if ($code !== 334) {
369
            throw new SMTPRetunCodeException(334, $code, array_pop($this->responses));
370
        }
371
372
        $command = base64_encode($this->username) . self::CRLF;
373
        $codeUsername = $this->sendCommand($command);
374
        if ($codeUsername !== 334) {
375
            throw new SMTPRetunCodeException(334, $codeUsername, array_pop($this->responses));
376
        }
377
378
        $command = base64_encode($this->password) . self::CRLF;
379
        $codePassword = $this->sendCommand($command);
380
        if ($codePassword !== 235) {
381
            throw new SMTPRetunCodeException(235, $codePassword, array_pop($this->responses));
382
        }
383
384
        return $this;
385
    }
386
387
    /**
388
     * Set From value
389
     * @return $this
390
     * @throws SMTPRetunCodeException
391
     */
392
    protected function mailFrom(): self
393
    {
394
        $command = 'MAIL FROM:' . $this->message->getFrom() . self::CRLF;
395
        $code = $this->sendCommand($command);
396
        if ($code !== 250) {
397
            throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
398
        }
399
400
        return $this;
401
    }
402
403
    /**
404
     * Set recipients
405
     * @return $this
406
     * @throws SMTPRetunCodeException
407
     */
408
    protected function rcptTo(): self
409
    {
410
        $recipients = array_merge(
411
            $this->message->getTo(),
412
            $this->message->getCc(),
413
            $this->message->getBcc()
414
        );
415
416
        foreach ($recipients as $email) {
417
            $command = 'RCPT TO:<' . $email . '>' . self::CRLF;
418
            $code = $this->sendCommand($command);
419
            if ($code !== 250) {
420
                throw new SMTPRetunCodeException(250, $code, array_pop($this->responses));
421
            }
422
        }
423
424
        return $this;
425
    }
426
427
    /**
428
     * Send mail data to server
429
     * @return $this
430
     * @throws SMTPRetunCodeException
431
     */
432
    protected function data(): self
433
    {
434
        $command = 'DATA' . self::CRLF;
435
        $code = $this->sendCommand($command);
436
        if ($code !== 354) {
437
            throw new SMTPRetunCodeException(354, $code, array_pop($this->responses));
438
        }
439
440
        $command = (string) $this->message;
441
        $command .= self::CRLF . '.' . self::CRLF;
442
        $codeMessage = $this->sendCommand($command);
443
        if ($codeMessage !== 250) {
444
            throw new SMTPRetunCodeException(250, $codeMessage, array_pop($this->responses));
445
        }
446
447
        return $this;
448
    }
449
450
    /**
451
     * Disconnect from server
452
     * @return $this
453
     * @throws SMTPRetunCodeException
454
     */
455
    protected function quit(): self
456
    {
457
        $command = 'QUIT' . self::CRLF;
458
        $code = $this->sendCommand($command);
459
        if ($code !== 221) {
460
            throw new SMTPRetunCodeException(221, $code, array_pop($this->responses));
461
        }
462
463
        return $this;
464
    }
465
466
    /**
467
     * Send command to server
468
     * @param string $command
469
     * @return int
470
     */
471
    protected function sendCommand(string $command): int
472
    {
473
        $this->commands[] = $command;
474
        if (is_resource($this->smtp)) {
475
            fputs($this->smtp, $command, strlen($command));
0 ignored issues
show
The call to Platine\Mail\Transport\fputs() has too many arguments starting with strlen($command). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

475
            /** @scrutinizer ignore-call */ 
476
            fputs($this->smtp, $command, strlen($command));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
476
        }
477
        return $this->getCode();
478
    }
479
480
    /**
481
     * Get return code from server
482
     * @return int
483
     * @throws SMTPException
484
     */
485
    protected function getCode(): int
486
    {
487
        if (is_resource($this->smtp)) {
488
            stream_set_timeout($this->smtp, $this->responseTimeout);
489
            while ($str = fgets($this->smtp, 515)) {
0 ignored issues
show
The call to Platine\Mail\Transport\fgets() has too many arguments starting with 515. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

489
            while ($str = /** @scrutinizer ignore-call */ fgets($this->smtp, 515)) {

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
490
                $this->responses[] = $str;
491
492
                if (substr($str, 3, 1) === ' ') {
493
                    $code = substr($str, 0, 3);
494
                    return (int) $code;
495
                }
496
            }
497
        }
498
499
        throw new SMTPException('SMTP Server did not respond with anything I recognized');
500
    }
501
}
502