1<?php
2
3/**
4 * PHPMailer RFC821 SMTP email transport class.
5 * PHP Version 5.5.
6 *
7 * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
8 *
9 * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
10 * @author Jim Jagielski (jimjag) <jimjag@gmail.com>
11 * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
12 * @author Brent R. Matzelle (original founder)
13 * @copyright 2012 - 2020 Marcus Bointon
14 * @copyright 2010 - 2012 Jim Jagielski
15 * @copyright 2004 - 2009 Andy Prevost
16 * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
17 * @note This program is distributed in the hope that it will be useful - WITHOUT
18 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 * FITNESS FOR A PARTICULAR PURPOSE.
20 */
21
22namespace PHPMailer\PHPMailer;
23
24/**
25 * PHPMailer RFC821 SMTP email transport class.
26 * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
27 *
28 * @author Chris Ryan
29 * @author Marcus Bointon <phpmailer@synchromedia.co.uk>
30 */
31class SMTP
32{
33 /**
34 * The PHPMailer SMTP version number.
35 *
36 * @var string
37 */
38 const VERSION = '7.0.0';
39
40 /**
41 * SMTP line break constant.
42 *
43 * @var string
44 */
45 const LE = "\r\n";
46
47 /**
48 * The SMTP port to use if one is not specified.
49 *
50 * @var int
51 */
52 const DEFAULT_PORT = 25;
53
54 /**
55 * The SMTPs port to use if one is not specified.
56 *
57 * @var int
58 */
59 const DEFAULT_SECURE_PORT = 465;
60
61 /**
62 * The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
63 * *excluding* a trailing CRLF break.
64 *
65 * @see https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3.1.6
66 *
67 * @var int
68 */
69 const MAX_LINE_LENGTH = 998;
70
71 /**
72 * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
73 * *including* a trailing CRLF line break.
74 *
75 * @see https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3.1.5
76 *
77 * @var int
78 */
79 const MAX_REPLY_LENGTH = 512;
80
81 /**
82 * Debug level for no output.
83 *
84 * @var int
85 */
86 const DEBUG_OFF = 0;
87
88 /**
89 * Debug level to show client -> server messages.
90 *
91 * @var int
92 */
93 const DEBUG_CLIENT = 1;
94
95 /**
96 * Debug level to show client -> server and server -> client messages.
97 *
98 * @var int
99 */
100 const DEBUG_SERVER = 2;
101
102 /**
103 * Debug level to show connection status, client -> server and server -> client messages.
104 *
105 * @var int
106 */
107 const DEBUG_CONNECTION = 3;
108
109 /**
110 * Debug level to show all messages.
111 *
112 * @var int
113 */
114 const DEBUG_LOWLEVEL = 4;
115
116 /**
117 * Debug output level.
118 * Options:
119 * * self::DEBUG_OFF (`0`) No debug output, default
120 * * self::DEBUG_CLIENT (`1`) Client commands
121 * * self::DEBUG_SERVER (`2`) Client commands and server responses
122 * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
123 * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
124 *
125 * @var int
126 */
127 public $do_debug = self::DEBUG_OFF;
128
129 /**
130 * How to handle debug output.
131 * Options:
132 * * `echo` Output plain-text as-is, appropriate for CLI
133 * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
134 * * `error_log` Output to error log as configured in php.ini
135 * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
136 *
137 * ```php
138 * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
139 * ```
140 *
141 * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
142 * level output is used:
143 *
144 * ```php
145 * $mail->Debugoutput = new myPsr3Logger;
146 * ```
147 *
148 * @var string|callable|\Psr\Log\LoggerInterface
149 */
150 public $Debugoutput = 'echo';
151
152 /**
153 * Whether to use VERP.
154 *
155 * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
156 * @see https://www.postfix.org/VERP_README.html Info on VERP
157 *
158 * @var bool
159 */
160 public $do_verp = false;
161
162 /**
163 * Whether to use SMTPUTF8.
164 *
165 * @see https://www.rfc-editor.org/rfc/rfc6531
166 *
167 * @var bool
168 */
169 public $do_smtputf8 = false;
170
171 /**
172 * The timeout value for connection, in seconds.
173 * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
174 * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
175 *
176 * @see https://www.rfc-editor.org/rfc/rfc2821#section-4.5.3.2
177 *
178 * @var int
179 */
180 public $Timeout = 300;
181
182 /**
183 * How long to wait for commands to complete, in seconds.
184 * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
185 *
186 * @var int
187 */
188 public $Timelimit = 300;
189
190 /**
191 * Patterns to extract an SMTP transaction id from reply to a DATA command.
192 * The first capture group in each regex will be used as the ID.
193 * MS ESMTP returns the message ID, which may not be correct for internal tracking.
194 *
195 * @var string[]
196 */
197 protected $smtp_transaction_id_patterns = [
198 'exim' => '/[\d]{3} OK id=(.*)/',
199 'sendmail' => '/[\d]{3} 2\.0\.0 (.*) Message/',
200 'postfix' => '/[\d]{3} 2\.0\.0 Ok: queued as (.*)/',
201 'Microsoft_ESMTP' => '/[0-9]{3} 2\.[\d]\.0 (.*)@(?:.*) Queued mail for delivery/',
202 'Amazon_SES' => '/[\d]{3} Ok (.*)/',
203 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
204 'CampaignMonitor' => '/[\d]{3} 2\.0\.0 OK:([a-zA-Z\d]{48})/',
205 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
206 'ZoneMTA' => '/[\d]{3} Message queued as (.*)/',
207 'Mailjet' => '/[\d]{3} OK queued as (.*)/',
208 'Gsmtp' => '/[\d]{3} 2\.0\.0 OK (.*) - gsmtp/',
209 ];
210
211 /**
212 * Allowed SMTP XCLIENT attributes.
213 * Must be allowed by the SMTP server. EHLO response is not checked.
214 *
215 * @see https://www.postfix.org/XCLIENT_README.html
216 *
217 * @var array
218 */
219 public static $xclient_allowed_attributes = [
220 'NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'LOGIN', 'DESTADDR', 'DESTPORT'
221 ];
222
223 /**
224 * The last transaction ID issued in response to a DATA command,
225 * if one was detected.
226 *
227 * @var string|bool|null
228 */
229 protected $last_smtp_transaction_id;
230
231 /**
232 * The socket for the server connection.
233 *
234 * @var ?resource
235 */
236 protected $smtp_conn;
237
238 /**
239 * Error information, if any, for the last SMTP command.
240 *
241 * @var array
242 */
243 protected $error = [
244 'error' => '',
245 'detail' => '',
246 'smtp_code' => '',
247 'smtp_code_ex' => '',
248 ];
249
250 /**
251 * The reply the server sent to us for HELO.
252 * If null, no HELO string has yet been received.
253 *
254 * @var string|null
255 */
256 protected $helo_rply;
257
258 /**
259 * The set of SMTP extensions sent in reply to EHLO command.
260 * Indexes of the array are extension names.
261 * Value at index 'HELO' or 'EHLO' (according to command that was sent)
262 * represents the server name. In case of HELO it is the only element of the array.
263 * Other values can be boolean TRUE or an array containing extension options.
264 * If null, no HELO/EHLO string has yet been received.
265 *
266 * @var array|null
267 */
268 protected $server_caps;
269
270 /**
271 * The most recent reply received from the server.
272 *
273 * @var string
274 */
275 protected $last_reply = '';
276
277 /**
278 * Output debugging info via a user-selected method.
279 *
280 * @param string $str Debug string to output
281 * @param int $level The debug level of this message; see DEBUG_* constants
282 *
283 * @see SMTP::$Debugoutput
284 * @see SMTP::$do_debug
285 */
286 protected function edebug($str, $level = 0)
287 {
288 if ($level > $this->do_debug) {
289 return;
290 }
291 //Is this a PSR-3 logger?
292 if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
293 //Remove trailing line breaks potentially added by calls to SMTP::client_send()
294 $this->Debugoutput->debug(rtrim($str, "\r\n"));
295
296 return;
297 }
298 //Avoid clash with built-in function names
299 if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
300 call_user_func($this->Debugoutput, $str, $level);
301
302 return;
303 }
304 switch ($this->Debugoutput) {
305 case 'error_log':
306 //Don't output, just log
307 /** @noinspection ForgottenDebugOutputInspection */
308 error_log($str);
309 break;
310 case 'html':
311 //Cleans up output a bit for a better looking, HTML-safe output
312 echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
313 preg_replace('/[\r\n]+/', '', $str),
314 ENT_QUOTES,
315 'UTF-8'
316 ), "<br>\n";
317 break;
318 case 'echo':
319 default:
320 //Normalize line breaks
321 $str = preg_replace('/\r\n|\r/m', "\n", $str);
322 echo gmdate('Y-m-d H:i:s'),
323 "\t",
324 //Trim trailing space
325 trim(
326 //Indent for readability, except for trailing break
327 str_replace(
328 "\n",
329 "\n \t ",
330 trim($str)
331 )
332 ),
333 "\n";
334 }
335 }
336
337 /**
338 * Connect to an SMTP server.
339 *
340 * @param string $host SMTP server IP or host name
341 * @param int $port The port number to connect to
342 * @param int $timeout How long to wait for the connection to open
343 * @param array $options An array of options for stream_context_create()
344 *
345 * @return bool
346 */
347 public function connect($host, $port = null, $timeout = 30, $options = [])
348 {
349 //Clear errors to avoid confusion
350 $this->setError('');
351 //Make sure we are __not__ connected
352 if ($this->connected()) {
353 //Already connected, generate error
354 $this->setError('Already connected to a server');
355
356 return false;
357 }
358 if (empty($port)) {
359 $port = self::DEFAULT_PORT;
360 }
361 //Connect to the SMTP server
362 $this->edebug(
363 "Connection: opening to $host:$port, timeout=$timeout, options=" .
364 (count($options) > 0 ? var_export($options, true) : 'array()'),
365 self::DEBUG_CONNECTION
366 );
367
368 $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
369
370 if ($this->smtp_conn === false) {
371 //Error info already set inside `getSMTPConnection()`
372 return false;
373 }
374
375 $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
376
377 //Get any announcement
378 $this->last_reply = $this->get_lines();
379 $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
380 $responseCode = (int)substr($this->last_reply, 0, 3);
381 if ($responseCode === 220) {
382 return true;
383 }
384 //Anything other than a 220 response means something went wrong
385 //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
386 //https://www.rfc-editor.org/rfc/rfc5321#section-3.1
387 if ($responseCode === 554) {
388 $this->quit();
389 }
390 //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
391 $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
392 $this->close();
393 return false;
394 }
395
396 /**
397 * Create connection to the SMTP server.
398 *
399 * @param string $host SMTP server IP or host name
400 * @param int $port The port number to connect to
401 * @param int $timeout How long to wait for the connection to open
402 * @param array $options An array of options for stream_context_create()
403 *
404 * @return false|resource
405 */
406 protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
407 {
408 static $streamok;
409 //This is enabled by default since 5.0.0 but some providers disable it
410 //Check this once and cache the result
411 if (null === $streamok) {
412 $streamok = function_exists('stream_socket_client');
413 }
414
415 $errno = 0;
416 $errstr = '';
417 if ($streamok) {
418 $socket_context = stream_context_create($options);
419 set_error_handler(function () {
420 call_user_func_array([$this, 'errorHandler'], func_get_args());
421 });
422 $connection = stream_socket_client(
423 $host . ':' . $port,
424 $errno,
425 $errstr,
426 $timeout,
427 STREAM_CLIENT_CONNECT,
428 $socket_context
429 );
430 } else {
431 //Fall back to fsockopen which should work in more places, but is missing some features
432 $this->edebug(
433 'Connection: stream_socket_client not available, falling back to fsockopen',
434 self::DEBUG_CONNECTION
435 );
436 set_error_handler(function () {
437 call_user_func_array([$this, 'errorHandler'], func_get_args());
438 });
439 $connection = fsockopen(
440 $host,
441 $port,
442 $errno,
443 $errstr,
444 $timeout
445 );
446 }
447 restore_error_handler();
448
449 //Verify we connected properly
450 if (!is_resource($connection)) {
451 $this->setError(
452 'Failed to connect to server',
453 '',
454 (string) $errno,
455 $errstr
456 );
457 $this->edebug(
458 'SMTP ERROR: ' . $this->error['error']
459 . ": $errstr ($errno)",
460 self::DEBUG_CLIENT
461 );
462
463 return false;
464 }
465
466 //SMTP server can take longer to respond, give longer timeout for first read
467 //Windows does not have support for this timeout function
468 if (strpos(PHP_OS, 'WIN') !== 0) {
469 $max = (int)ini_get('max_execution_time');
470 //Don't bother if unlimited, or if set_time_limit is disabled
471 if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
472 @set_time_limit($timeout);
473 }
474 stream_set_timeout($connection, $timeout, 0);
475 }
476
477 return $connection;
478 }
479
480 /**
481 * Initiate a TLS (encrypted) session.
482 *
483 * @return bool
484 */
485 public function startTLS()
486 {
487 if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
488 return false;
489 }
490
491 //Allow the best TLS version(s) we can
492 $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
493
494 //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
495 //so add them back in manually if we can
496 if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
497 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
498 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
499 }
500
501 //Begin encrypted connection
502 set_error_handler(function () {
503 call_user_func_array([$this, 'errorHandler'], func_get_args());
504 });
505 $crypto_ok = stream_socket_enable_crypto(
506 $this->smtp_conn,
507 true,
508 $crypto_method
509 );
510 restore_error_handler();
511
512 return (bool) $crypto_ok;
513 }
514
515 /**
516 * Perform SMTP authentication.
517 * Must be run after hello().
518 *
519 * @see hello()
520 *
521 * @param string $username The user name
522 * @param string $password The password
523 * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
524 * @param OAuthTokenProvider $OAuth An optional OAuthTokenProvider instance for XOAUTH2 authentication
525 *
526 * @return bool True if successfully authenticated
527 */
528 public function authenticate(
529 $username,
530 $password,
531 $authtype = null,
532 $OAuth = null
533 ) {
534 if (!$this->server_caps) {
535 $this->setError('Authentication is not allowed before HELO/EHLO');
536
537 return false;
538 }
539
540 if (array_key_exists('EHLO', $this->server_caps)) {
541 //SMTP extensions are available; try to find a proper authentication method
542 if (!array_key_exists('AUTH', $this->server_caps)) {
543 $this->setError('Authentication is not allowed at this stage');
544 //'at this stage' means that auth may be allowed after the stage changes
545 //e.g. after STARTTLS
546
547 return false;
548 }
549
550 $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
551 $this->edebug(
552 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
553 self::DEBUG_LOWLEVEL
554 );
555
556 //If we have requested a specific auth type, check the server supports it before trying others
557 if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
558 $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
559 $authtype = null;
560 }
561
562 if (empty($authtype)) {
563 //If no auth mechanism is specified, attempt to use these, in this order
564 //Try CRAM-MD5 first as it's more secure than the others
565 foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
566 if (in_array($method, $this->server_caps['AUTH'], true)) {
567 $authtype = $method;
568 break;
569 }
570 }
571 if (empty($authtype)) {
572 $this->setError('No supported authentication methods found');
573
574 return false;
575 }
576 $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
577 }
578
579 if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
580 $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
581
582 return false;
583 }
584 } elseif (empty($authtype)) {
585 $authtype = 'LOGIN';
586 }
587 switch ($authtype) {
588 case 'PLAIN':
589 //Start authentication
590 if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
591 return false;
592 }
593 //Send encoded username and password
594 if (
595 //Format from https://www.rfc-editor.org/rfc/rfc4616#section-2
596 //We skip the first field (it's forgery), so the string starts with a null byte
597 !$this->sendCommand(
598 'User & Password',
599 base64_encode("\0" . $username . "\0" . $password),
600 235
601 )
602 ) {
603 return false;
604 }
605 break;
606 case 'LOGIN':
607 //Start authentication
608 if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
609 return false;
610 }
611 if (!$this->sendCommand('Username', base64_encode($username), 334)) {
612 return false;
613 }
614 if (!$this->sendCommand('Password', base64_encode($password), 235)) {
615 return false;
616 }
617 break;
618 case 'CRAM-MD5':
619 //Start authentication
620 if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
621 return false;
622 }
623 //Get the challenge
624 $challenge = base64_decode(substr($this->last_reply, 4));
625
626 //Build the response
627 $response = $username . ' ' . $this->hmac($challenge, $password);
628
629 //send encoded credentials
630 return $this->sendCommand('Username', base64_encode($response), 235);
631 case 'XOAUTH2':
632 //The OAuth instance must be set up prior to requesting auth.
633 if (null === $OAuth) {
634 return false;
635 }
636 $oauth = $OAuth->getOauth64();
637 /*
638 * An SMTP command line can have a maximum length of 512 bytes, including the command name,
639 * so the base64-encoded OAUTH token has a maximum length of:
640 * 512 - 13 (AUTH XOAUTH2) - 2 (CRLF) = 497 bytes
641 * If the token is longer than that, the command and the token must be sent separately as described in
642 * https://www.rfc-editor.org/rfc/rfc4954#section-4
643 */
644 if ($oauth === '') {
645 //Sending an empty auth token is legitimate, but it must be encoded as '='
646 //to indicate it's not a 2-part command
647 if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 =', 235)) {
648 return false;
649 }
650 } elseif (strlen($oauth) <= 497) {
651 //Authenticate using a token in the initial-response part
652 if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
653 return false;
654 }
655 } else {
656 //The token is too long, so we need to send it in two parts.
657 //Send the auth command without a token and expect a 334
658 if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2', 334)) {
659 return false;
660 }
661 //Send the token
662 if (!$this->sendCommand('OAuth TOKEN', $oauth, [235, 334])) {
663 return false;
664 }
665 //If the server answers with 334, send an empty line and wait for a 235
666 if (
667 substr($this->last_reply, 0, 3) === '334'
668 && $this->sendCommand('AUTH End', '', 235)
669 ) {
670 return false;
671 }
672 }
673 break;
674 default:
675 $this->setError("Authentication method \"$authtype\" is not supported");
676
677 return false;
678 }
679
680 return true;
681 }
682
683 /**
684 * Calculate an MD5 HMAC hash.
685 * Works like hash_hmac('md5', $data, $key)
686 * in case that function is not available.
687 *
688 * @param string $data The data to hash
689 * @param string $key The key to hash with
690 *
691 * @return string
692 */
693 protected function hmac($data, $key)
694 {
695 if (function_exists('hash_hmac')) {
696 return hash_hmac('md5', $data, $key);
697 }
698
699 //The following borrowed from
700 //https://www.php.net/manual/en/function.mhash.php#27225
701
702 //RFC 2104 HMAC implementation for php.
703 //Creates an md5 HMAC.
704 //Eliminates the need to install mhash to compute a HMAC
705 //by Lance Rushing
706
707 $bytelen = 64; //byte length for md5
708 if (strlen($key) > $bytelen) {
709 $key = pack('H*', md5($key));
710 }
711 $key = str_pad($key, $bytelen, chr(0x00));
712 $ipad = str_pad('', $bytelen, chr(0x36));
713 $opad = str_pad('', $bytelen, chr(0x5c));
714 $k_ipad = $key ^ $ipad;
715 $k_opad = $key ^ $opad;
716
717 return md5($k_opad . pack('H*', md5($k_ipad . $data)));
718 }
719
720 /**
721 * Check connection state.
722 *
723 * @return bool True if connected
724 */
725 public function connected()
726 {
727 if (is_resource($this->smtp_conn)) {
728 $sock_status = stream_get_meta_data($this->smtp_conn);
729 if ($sock_status['eof']) {
730 //The socket is valid but we are not connected
731 $this->edebug(
732 'SMTP NOTICE: EOF caught while checking if connected',
733 self::DEBUG_CLIENT
734 );
735 $this->close();
736
737 return false;
738 }
739
740 return true; //everything looks good
741 }
742
743 return false;
744 }
745
746 /**
747 * Close the socket and clean up the state of the class.
748 * Don't use this function without first trying to use QUIT.
749 *
750 * @see quit()
751 */
752 public function close()
753 {
754 $this->server_caps = null;
755 $this->helo_rply = null;
756 if (is_resource($this->smtp_conn)) {
757 //Close the connection and cleanup
758 fclose($this->smtp_conn);
759 $this->smtp_conn = null; //Makes for cleaner serialization
760 $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
761 }
762 }
763
764 /**
765 * Send an SMTP DATA command.
766 * Issues a data command and sends the msg_data to the server,
767 * finalizing the mail transaction. $msg_data is the message
768 * that is to be sent with the headers. Each header needs to be
769 * on a single line followed by a <CRLF> with the message headers
770 * and the message body being separated by an additional <CRLF>.
771 * Implements RFC 821: DATA <CRLF>.
772 *
773 * @param string $msg_data Message data to send
774 *
775 * @return bool
776 */
777 public function data($msg_data)
778 {
779 //This will use the standard timelimit
780 if (!$this->sendCommand('DATA', 'DATA', 354)) {
781 return false;
782 }
783
784 /* The server is ready to accept data!
785 * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
786 * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
787 * smaller lines to fit within the limit.
788 * We will also look for lines that start with a '.' and prepend an additional '.'.
789 * NOTE: this does not count towards line-length limit.
790 */
791
792 //Normalize line breaks before exploding
793 $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
794
795 /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
796 * of the first line (':' separated) does not contain a space then it _should_ be a header, and we will
797 * process all lines before a blank line as headers.
798 */
799
800 $field = substr($lines[0], 0, strpos($lines[0], ':'));
801 $in_headers = false;
802 if (!empty($field) && strpos($field, ' ') === false) {
803 $in_headers = true;
804 }
805
806 foreach ($lines as $line) {
807 $lines_out = [];
808 if ($in_headers && $line === '') {
809 $in_headers = false;
810 }
811 //Break this line up into several smaller lines if it's too long
812 //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
813 while (isset($line[self::MAX_LINE_LENGTH])) {
814 //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
815 //so as to avoid breaking in the middle of a word
816 $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
817 //Deliberately matches both false and 0
818 if (!$pos) {
819 //No nice break found, add a hard break
820 $pos = self::MAX_LINE_LENGTH - 1;
821 $lines_out[] = substr($line, 0, $pos);
822 $line = substr($line, $pos);
823 } else {
824 //Break at the found point
825 $lines_out[] = substr($line, 0, $pos);
826 //Move along by the amount we dealt with
827 $line = substr($line, $pos + 1);
828 }
829 //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
830 if ($in_headers) {
831 $line = "\t" . $line;
832 }
833 }
834 $lines_out[] = $line;
835
836 //Send the lines to the server
837 foreach ($lines_out as $line_out) {
838 //Dot-stuffing as per RFC5321 section 4.5.2
839 //https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
840 if (!empty($line_out) && $line_out[0] === '.') {
841 $line_out = '.' . $line_out;
842 }
843 $this->client_send($line_out . static::LE, 'DATA');
844 }
845 }
846
847 //Message data has been sent, complete the command
848 //Increase timelimit for end of DATA command
849 $savetimelimit = $this->Timelimit;
850 $this->Timelimit *= 2;
851 $result = $this->sendCommand('DATA END', '.', 250);
852 $this->recordLastTransactionID();
853 //Restore timelimit
854 $this->Timelimit = $savetimelimit;
855
856 return $result;
857 }
858
859 /**
860 * Send an SMTP HELO or EHLO command.
861 * Used to identify the sending server to the receiving server.
862 * This makes sure that client and server are in a known state.
863 * Implements RFC 821: HELO <SP> <domain> <CRLF>
864 * and RFC 2821 EHLO.
865 *
866 * @param string $host The host name or IP to connect to
867 *
868 * @return bool
869 */
870 public function hello($host = '')
871 {
872 //Try extended hello first (RFC 2821)
873 if ($this->sendHello('EHLO', $host)) {
874 return true;
875 }
876
877 //Some servers shut down the SMTP service here (RFC 5321)
878 if (substr($this->helo_rply, 0, 3) == '421') {
879 return false;
880 }
881
882 return $this->sendHello('HELO', $host);
883 }
884
885 /**
886 * Send an SMTP HELO or EHLO command.
887 * Low-level implementation used by hello().
888 *
889 * @param string $hello The HELO string
890 * @param string $host The hostname to say we are
891 *
892 * @return bool
893 *
894 * @see hello()
895 */
896 protected function sendHello($hello, $host)
897 {
898 $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
899 $this->helo_rply = $this->last_reply;
900 if ($noerror) {
901 $this->parseHelloFields($hello);
902 } else {
903 $this->server_caps = null;
904 }
905
906 return $noerror;
907 }
908
909 /**
910 * Parse a reply to HELO/EHLO command to discover server extensions.
911 * In case of HELO, the only parameter that can be discovered is a server name.
912 *
913 * @param string $type `HELO` or `EHLO`
914 */
915 protected function parseHelloFields($type)
916 {
917 $this->server_caps = [];
918 $lines = explode("\n", $this->helo_rply);
919
920 foreach ($lines as $n => $s) {
921 //First 4 chars contain response code followed by - or space
922 $s = trim(substr($s, 4));
923 if (empty($s)) {
924 continue;
925 }
926 $fields = explode(' ', $s);
927 if (!empty($fields)) {
928 if (!$n) {
929 $name = $type;
930 $fields = $fields[0];
931 } else {
932 $name = array_shift($fields);
933 switch ($name) {
934 case 'SIZE':
935 $fields = ($fields ? $fields[0] : 0);
936 break;
937 case 'AUTH':
938 if (!is_array($fields)) {
939 $fields = [];
940 }
941 break;
942 default:
943 $fields = true;
944 }
945 }
946 $this->server_caps[$name] = $fields;
947 }
948 }
949 }
950
951 /**
952 * Send an SMTP MAIL command.
953 * Starts a mail transaction from the email address specified in
954 * $from. Returns true if successful or false otherwise. If True
955 * the mail transaction is started and then one or more recipient
956 * commands may be called followed by a data command.
957 * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF> and
958 * two extensions, namely XVERP and SMTPUTF8.
959 *
960 * The server's EHLO response is not checked. If use of either
961 * extensions is enabled even though the server does not support
962 * that, mail submission will fail.
963 *
964 * XVERP is documented at https://www.postfix.org/VERP_README.html
965 * and SMTPUTF8 is specified in RFC 6531.
966 *
967 * @param string $from Source address of this message
968 *
969 * @return bool
970 */
971 public function mail($from)
972 {
973 $useVerp = ($this->do_verp ? ' XVERP' : '');
974 $useSmtputf8 = ($this->do_smtputf8 ? ' SMTPUTF8' : '');
975
976 return $this->sendCommand(
977 'MAIL FROM',
978 'MAIL FROM:<' . $from . '>' . $useSmtputf8 . $useVerp,
979 250
980 );
981 }
982
983 /**
984 * Send an SMTP QUIT command.
985 * Closes the socket if there is no error or the $close_on_error argument is true.
986 * Implements from RFC 821: QUIT <CRLF>.
987 *
988 * @param bool $close_on_error Should the connection close if an error occurs?
989 *
990 * @return bool
991 */
992 public function quit($close_on_error = true)
993 {
994 $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
995 $err = $this->error; //Save any error
996 if ($noerror || $close_on_error) {
997 $this->close();
998 $this->error = $err; //Restore any error from the quit command
999 }
1000
1001 return $noerror;
1002 }
1003
1004 /**
1005 * Send an SMTP RCPT command.
1006 * Sets the TO argument to $toaddr.
1007 * Returns true if the recipient was accepted false if it was rejected.
1008 * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
1009 *
1010 * @param string $address The address the message is being sent to
1011 * @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
1012 * or DELAY. If you specify NEVER all other notifications are ignored.
1013 *
1014 * @return bool
1015 */
1016 public function recipient($address, $dsn = '')
1017 {
1018 if (empty($dsn)) {
1019 $rcpt = 'RCPT TO:<' . $address . '>';
1020 } else {
1021 $dsn = strtoupper($dsn);
1022 $notify = [];
1023
1024 if (strpos($dsn, 'NEVER') !== false) {
1025 $notify[] = 'NEVER';
1026 } else {
1027 foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
1028 if (strpos($dsn, $value) !== false) {
1029 $notify[] = $value;
1030 }
1031 }
1032 }
1033
1034 $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
1035 }
1036
1037 return $this->sendCommand(
1038 'RCPT TO',
1039 $rcpt,
1040 [250, 251]
1041 );
1042 }
1043
1044 /**
1045 * Send SMTP XCLIENT command to server and check its return code.
1046 *
1047 * @return bool True on success
1048 */
1049 public function xclient(array $vars)
1050 {
1051 $xclient_options = "";
1052 foreach ($vars as $key => $value) {
1053 if (in_array($key, SMTP::$xclient_allowed_attributes)) {
1054 $xclient_options .= " {$key}={$value}";
1055 }
1056 }
1057 if (!$xclient_options) {
1058 return true;
1059 }
1060 return $this->sendCommand('XCLIENT', 'XCLIENT' . $xclient_options, 250);
1061 }
1062
1063 /**
1064 * Send an SMTP RSET command.
1065 * Abort any transaction that is currently in progress.
1066 * Implements RFC 821: RSET <CRLF>.
1067 *
1068 * @return bool True on success
1069 */
1070 public function reset()
1071 {
1072 return $this->sendCommand('RSET', 'RSET', 250);
1073 }
1074
1075 /**
1076 * Send a command to an SMTP server and check its return code.
1077 *
1078 * @param string $command The command name - not sent to the server
1079 * @param string $commandstring The actual command to send
1080 * @param int|array $expect One or more expected integer success codes
1081 *
1082 * @return bool True on success
1083 */
1084 protected function sendCommand($command, $commandstring, $expect)
1085 {
1086 if (!$this->connected()) {
1087 $this->setError("Called $command without being connected");
1088
1089 return false;
1090 }
1091 //Reject line breaks in all commands
1092 if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
1093 $this->setError("Command '$command' contained line breaks");
1094
1095 return false;
1096 }
1097 $this->client_send($commandstring . static::LE, $command);
1098
1099 $this->last_reply = $this->get_lines();
1100 //Fetch SMTP code and possible error code explanation
1101 $matches = [];
1102 if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
1103 $code = (int) $matches[1];
1104 $code_ex = (count($matches) > 2 ? $matches[2] : null);
1105 //Cut off error code from each response line
1106 $detail = preg_replace(
1107 "/{$code}[ -]" .
1108 ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
1109 '',
1110 $this->last_reply
1111 );
1112 } else {
1113 //Fall back to simple parsing if regex fails
1114 $code = (int) substr($this->last_reply, 0, 3);
1115 $code_ex = null;
1116 $detail = substr($this->last_reply, 4);
1117 }
1118
1119 $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
1120
1121 if (!in_array($code, (array) $expect, true)) {
1122 $this->setError(
1123 "$command command failed",
1124 $detail,
1125 $code,
1126 $code_ex
1127 );
1128 $this->edebug(
1129 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
1130 self::DEBUG_CLIENT
1131 );
1132
1133 return false;
1134 }
1135
1136 //Don't clear the error store when using keepalive
1137 if ($command !== 'RSET') {
1138 $this->setError('');
1139 }
1140
1141 return true;
1142 }
1143
1144 /**
1145 * Send an SMTP SAML command.
1146 * Starts a mail transaction from the email address specified in $from.
1147 * Returns true if successful or false otherwise. If True
1148 * the mail transaction is started and then one or more recipient
1149 * commands may be called followed by a data command. This command
1150 * will send the message to the users terminal if they are logged
1151 * in and send them an email.
1152 * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
1153 *
1154 * @param string $from The address the message is from
1155 *
1156 * @return bool
1157 */
1158 public function sendAndMail($from)
1159 {
1160 return $this->sendCommand('SAML', "SAML FROM:$from", 250);
1161 }
1162
1163 /**
1164 * Send an SMTP VRFY command.
1165 *
1166 * @param string $name The name to verify
1167 *
1168 * @return bool
1169 */
1170 public function verify($name)
1171 {
1172 return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
1173 }
1174
1175 /**
1176 * Send an SMTP NOOP command.
1177 * Used to keep keep-alives alive, doesn't actually do anything.
1178 *
1179 * @return bool
1180 */
1181 public function noop()
1182 {
1183 return $this->sendCommand('NOOP', 'NOOP', 250);
1184 }
1185
1186 /**
1187 * Send an SMTP TURN command.
1188 * This is an optional command for SMTP that this class does not support.
1189 * This method is here to make the RFC821 Definition complete for this class
1190 * and _may_ be implemented in future.
1191 * Implements from RFC 821: TURN <CRLF>.
1192 *
1193 * @return bool
1194 */
1195 public function turn()
1196 {
1197 $this->setError('The SMTP TURN command is not implemented');
1198 $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
1199
1200 return false;
1201 }
1202
1203 /**
1204 * Send raw data to the server.
1205 *
1206 * @param string $data The data to send
1207 * @param string $command Optionally, the command this is part of, used only for controlling debug output
1208 *
1209 * @return int|bool The number of bytes sent to the server or false on error
1210 */
1211 public function client_send($data, $command = '')
1212 {
1213 //If SMTP transcripts are left enabled, or debug output is posted online
1214 //it can leak credentials, so hide credentials in all but lowest level
1215 if (
1216 self::DEBUG_LOWLEVEL > $this->do_debug &&
1217 in_array($command, ['User & Password', 'Username', 'Password'], true)
1218 ) {
1219 $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
1220 } else {
1221 $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
1222 }
1223 set_error_handler(function () {
1224 call_user_func_array([$this, 'errorHandler'], func_get_args());
1225 });
1226 $result = fwrite($this->smtp_conn, $data);
1227 restore_error_handler();
1228
1229 return $result;
1230 }
1231
1232 /**
1233 * Get the latest error.
1234 *
1235 * @return array
1236 */
1237 public function getError()
1238 {
1239 return $this->error;
1240 }
1241
1242 /**
1243 * Get SMTP extensions available on the server.
1244 *
1245 * @return array|null
1246 */
1247 public function getServerExtList()
1248 {
1249 return $this->server_caps;
1250 }
1251
1252 /**
1253 * Get metadata about the SMTP server from its HELO/EHLO response.
1254 * The method works in three ways, dependent on argument value and current state:
1255 * 1. HELO/EHLO has not been sent - returns null and populates $this->error.
1256 * 2. HELO has been sent -
1257 * $name == 'HELO': returns server name
1258 * $name == 'EHLO': returns boolean false
1259 * $name == any other string: returns null and populates $this->error
1260 * 3. EHLO has been sent -
1261 * $name == 'HELO'|'EHLO': returns the server name
1262 * $name == any other string: if extension $name exists, returns True
1263 * or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
1264 *
1265 * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
1266 *
1267 * @return string|bool|null
1268 */
1269 public function getServerExt($name)
1270 {
1271 if (!$this->server_caps) {
1272 $this->setError('No HELO/EHLO was sent');
1273
1274 return null;
1275 }
1276
1277 if (!array_key_exists($name, $this->server_caps)) {
1278 if ('HELO' === $name) {
1279 return $this->server_caps['EHLO'];
1280 }
1281 if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
1282 return false;
1283 }
1284 $this->setError('HELO handshake was used; No information about server extensions available');
1285
1286 return null;
1287 }
1288
1289 return $this->server_caps[$name];
1290 }
1291
1292 /**
1293 * Get the last reply from the server.
1294 *
1295 * @return string
1296 */
1297 public function getLastReply()
1298 {
1299 return $this->last_reply;
1300 }
1301
1302 /**
1303 * Read the SMTP server's response.
1304 * Either before eof or socket timeout occurs on the operation.
1305 * With SMTP we can tell if we have more lines to read if the
1306 * 4th character is '-' symbol. If it is a space then we don't
1307 * need to read anything else.
1308 *
1309 * @return string
1310 */
1311 protected function get_lines()
1312 {
1313 //If the connection is bad, give up straight away
1314 if (!is_resource($this->smtp_conn)) {
1315 return '';
1316 }
1317 $data = '';
1318 $endtime = 0;
1319 stream_set_timeout($this->smtp_conn, $this->Timeout);
1320 if ($this->Timelimit > 0) {
1321 $endtime = time() + $this->Timelimit;
1322 }
1323 $selR = [$this->smtp_conn];
1324 $selW = null;
1325 while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1326 //Must pass vars in here as params are by reference
1327 //solution for signals inspired by https://github.com/symfony/symfony/pull/6540
1328 set_error_handler(function () {
1329 call_user_func_array([$this, 'errorHandler'], func_get_args());
1330 });
1331 $n = stream_select($selR, $selW, $selW, $this->Timelimit);
1332 restore_error_handler();
1333
1334 if ($n === false) {
1335 $message = $this->getError()['detail'];
1336
1337 $this->edebug(
1338 'SMTP -> get_lines(): select failed (' . $message . ')',
1339 self::DEBUG_LOWLEVEL
1340 );
1341
1342 //stream_select returns false when the `select` system call is interrupted
1343 //by an incoming signal, try the select again
1344 if (
1345 stripos($message, 'interrupted system call') !== false ||
1346 (
1347 // on applications with a different locale than english, the message above is not found because
1348 // it's translated. So we also check for the SOCKET_EINTR constant which is defined under
1349 // Windows and UNIX-like platforms (if available on the platform).
1350 defined('SOCKET_EINTR') &&
1351 stripos($message, 'stream_select(): Unable to select [' . SOCKET_EINTR . ']') !== false
1352 )
1353 ) {
1354 $this->edebug(
1355 'SMTP -> get_lines(): retrying stream_select',
1356 self::DEBUG_LOWLEVEL
1357 );
1358 $this->setError('');
1359 continue;
1360 }
1361
1362 break;
1363 }
1364
1365 if (!$n) {
1366 $this->edebug(
1367 'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
1368 self::DEBUG_LOWLEVEL
1369 );
1370 break;
1371 }
1372
1373 //Deliberate noise suppression - errors are handled afterwards
1374 $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
1375 $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
1376 $data .= $str;
1377 //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
1378 //or 4th character is a space or a line break char, we are done reading, break the loop.
1379 //String array access is a significant micro-optimisation over strlen
1380 if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
1381 break;
1382 }
1383 //Timed-out? Log and break
1384 $info = stream_get_meta_data($this->smtp_conn);
1385 if ($info['timed_out']) {
1386 $this->edebug(
1387 'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
1388 self::DEBUG_LOWLEVEL
1389 );
1390 break;
1391 }
1392 //Now check if reads took too long
1393 if ($endtime && time() > $endtime) {
1394 $this->edebug(
1395 'SMTP -> get_lines(): timelimit reached (' .
1396 $this->Timelimit . ' sec)',
1397 self::DEBUG_LOWLEVEL
1398 );
1399 break;
1400 }
1401 }
1402
1403 return $data;
1404 }
1405
1406 /**
1407 * Enable or disable VERP address generation.
1408 *
1409 * @param bool $enabled
1410 */
1411 public function setVerp($enabled = false)
1412 {
1413 $this->do_verp = $enabled;
1414 }
1415
1416 /**
1417 * Get VERP address generation mode.
1418 *
1419 * @return bool
1420 */
1421 public function getVerp()
1422 {
1423 return $this->do_verp;
1424 }
1425
1426 /**
1427 * Enable or disable use of SMTPUTF8.
1428 *
1429 * @param bool $enabled
1430 */
1431 public function setSMTPUTF8($enabled = false)
1432 {
1433 $this->do_smtputf8 = $enabled;
1434 }
1435
1436 /**
1437 * Get SMTPUTF8 use.
1438 *
1439 * @return bool
1440 */
1441 public function getSMTPUTF8()
1442 {
1443 return $this->do_smtputf8;
1444 }
1445
1446 /**
1447 * Set error messages and codes.
1448 *
1449 * @param string $message The error message
1450 * @param string $detail Further detail on the error
1451 * @param string $smtp_code An associated SMTP error code
1452 * @param string $smtp_code_ex Extended SMTP code
1453 */
1454 protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1455 {
1456 $this->error = [
1457 'error' => $message,
1458 'detail' => $detail,
1459 'smtp_code' => $smtp_code,
1460 'smtp_code_ex' => $smtp_code_ex,
1461 ];
1462 }
1463
1464 /**
1465 * Set debug output method.
1466 *
1467 * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
1468 */
1469 public function setDebugOutput($method = 'echo')
1470 {
1471 $this->Debugoutput = $method;
1472 }
1473
1474 /**
1475 * Get debug output method.
1476 *
1477 * @return string
1478 */
1479 public function getDebugOutput()
1480 {
1481 return $this->Debugoutput;
1482 }
1483
1484 /**
1485 * Set debug output level.
1486 *
1487 * @param int $level
1488 */
1489 public function setDebugLevel($level = 0)
1490 {
1491 $this->do_debug = $level;
1492 }
1493
1494 /**
1495 * Get debug output level.
1496 *
1497 * @return int
1498 */
1499 public function getDebugLevel()
1500 {
1501 return $this->do_debug;
1502 }
1503
1504 /**
1505 * Set SMTP timeout.
1506 *
1507 * @param int $timeout The timeout duration in seconds
1508 */
1509 public function setTimeout($timeout = 0)
1510 {
1511 $this->Timeout = $timeout;
1512 }
1513
1514 /**
1515 * Get SMTP timeout.
1516 *
1517 * @return int
1518 */
1519 public function getTimeout()
1520 {
1521 return $this->Timeout;
1522 }
1523
1524 /**
1525 * Reports an error number and string.
1526 *
1527 * @param int $errno The error number returned by PHP
1528 * @param string $errmsg The error message returned by PHP
1529 * @param string $errfile The file the error occurred in
1530 * @param int $errline The line number the error occurred on
1531 */
1532 protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1533 {
1534 $notice = 'Connection failed.';
1535 $this->setError(
1536 $notice,
1537 $errmsg,
1538 (string) $errno
1539 );
1540 $this->edebug(
1541 "$notice Error #$errno: $errmsg [$errfile line $errline]",
1542 self::DEBUG_CONNECTION
1543 );
1544 }
1545
1546 /**
1547 * Extract and return the ID of the last SMTP transaction based on
1548 * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1549 * Relies on the host providing the ID in response to a DATA command.
1550 * If no reply has been received yet, it will return null.
1551 * If no pattern was matched, it will return false.
1552 *
1553 * @return bool|string|null
1554 */
1555 protected function recordLastTransactionID()
1556 {
1557 $reply = $this->getLastReply();
1558
1559 if (empty($reply)) {
1560 $this->last_smtp_transaction_id = null;
1561 } else {
1562 $this->last_smtp_transaction_id = false;
1563 foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1564 $matches = [];
1565 if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1566 $this->last_smtp_transaction_id = trim($matches[1]);
1567 break;
1568 }
1569 }
1570 }
1571
1572 return $this->last_smtp_transaction_id;
1573 }
1574
1575 /**
1576 * Get the queue/transaction ID of the last SMTP transaction
1577 * If no reply has been received yet, it will return null.
1578 * If no pattern was matched, it will return false.
1579 *
1580 * @return bool|string|null
1581 *
1582 * @see recordLastTransactionID()
1583 */
1584 public function getLastTransactionID()
1585 {
1586 return $this->last_smtp_transaction_id;
1587 }
1588}
1589