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