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