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
|
|||||
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
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. ![]() |
|||||
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 |
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.