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