PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 10.1.4
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v10.1.4
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
acymailing / back / Libraries / Mailer / Smtp.php

Smtp.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 10.1.4, at back/Libraries/Mailer/Smtp.php

1,515 lines 47.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
636 //Start authentication
637 if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 '.$oauth, 235)) {
638 return false;
639 }
640 break;
641 default:
642 $this->setError("Authentication method \"$authtype\" is not supported");
643
644 return false;
645 }
646
647 return true;
648 }
649
650 /**
651 * Calculate an MD5 HMAC hash.
652 * Works like hash_hmac('md5', $data, $key)
653 * in case that function is not available.
654 *
655 * @param string $data The data to hash
656 * @param string $key The key to hash with
657 *
658 * @return string
659 */
660 protected function hmac($data, $key)
661 {
662 if (function_exists('hash_hmac')) {
663 return hash_hmac('md5', $data, $key);
664 }
665
666 //The following borrowed from
667 //https://www.php.net/manual/en/function.mhash.php#27225
668
669 //RFC 2104 HMAC implementation for php.
670 //Creates an md5 HMAC.
671 //Eliminates the need to install mhash to compute a HMAC
672 //by Lance Rushing
673
674 $bytelen = 64; //byte length for md5
675 if (strlen($key) > $bytelen) {
676 $key = pack('H*', md5($key));
677 }
678 $key = str_pad($key, $bytelen, chr(0x00));
679 $ipad = str_pad('', $bytelen, chr(0x36));
680 $opad = str_pad('', $bytelen, chr(0x5c));
681 $k_ipad = $key ^ $ipad;
682 $k_opad = $key ^ $opad;
683
684 return md5($k_opad.pack('H*', md5($k_ipad.$data)));
685 }
686
687 /**
688 * Check connection state.
689 *
690 * @return bool True if connected
691 */
692 public function connected()
693 {
694 if (is_resource($this->smtp_conn)) {
695 $sock_status = stream_get_meta_data($this->smtp_conn);
696 if ($sock_status['eof']) {
697 //The socket is valid but we are not connected
698 $this->edebug(
699 'SMTP NOTICE: EOF caught while checking if connected',
700 self::DEBUG_CLIENT
701 );
702 $this->close();
703
704 return false;
705 }
706
707 return true; //everything looks good
708 }
709
710 return false;
711 }
712
713 /**
714 * Close the socket and clean up the state of the class.
715 * Don't use this function without first trying to use QUIT.
716 *
717 * @see quit()
718 */
719 public function close()
720 {
721 $this->server_caps = null;
722 $this->helo_rply = null;
723 if (is_resource($this->smtp_conn)) {
724 //Close the connection and cleanup
725 fclose($this->smtp_conn);
726 $this->smtp_conn = null; //Makes for cleaner serialization
727 $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
728 }
729 }
730
731 /**
732 * Send an SMTP DATA command.
733 * Issues a data command and sends the msg_data to the server,
734 * finalizing the mail transaction. $msg_data is the message
735 * that is to be sent with the headers. Each header needs to be
736 * on a single line followed by a <CRLF> with the message headers
737 * and the message body being separated by an additional <CRLF>.
738 * Implements RFC 821: DATA <CRLF>.
739 *
740 * @param string $msg_data Message data to send
741 *
742 * @return bool
743 */
744 public function data($msg_data)
745 {
746 //This will use the standard timelimit
747 if (!$this->sendCommand('DATA', 'DATA', 354)) {
748 return false;
749 }
750
751 /* The server is ready to accept data!
752 * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
753 * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
754 * smaller lines to fit within the limit.
755 * We will also look for lines that start with a '.' and prepend an additional '.'.
756 * NOTE: this does not count towards line-length limit.
757 */
758
759 //Normalize line breaks before exploding
760 $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
761
762 /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
763 * of the first line (':' separated) does not contain a space then it _should_ be a header, and we will
764 * process all lines before a blank line as headers.
765 */
766
767 $field = substr($lines[0], 0, strpos($lines[0], ':'));
768 $in_headers = false;
769 if (!empty($field) && strpos($field, ' ') === false) {
770 $in_headers = true;
771 }
772
773 foreach ($lines as $line) {
774 $lines_out = [];
775 if ($in_headers && $line === '') {
776 $in_headers = false;
777 }
778 //Break this line up into several smaller lines if it's too long
779 //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
780 while (isset($line[self::MAX_LINE_LENGTH])) {
781 //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
782 //so as to avoid breaking in the middle of a word
783 $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
784 //Deliberately matches both false and 0
785 if (!$pos) {
786 //No nice break found, add a hard break
787 $pos = self::MAX_LINE_LENGTH - 1;
788 $lines_out[] = substr($line, 0, $pos);
789 $line = substr($line, $pos);
790 } else {
791 //Break at the found point
792 $lines_out[] = substr($line, 0, $pos);
793 //Move along by the amount we dealt with
794 $line = substr($line, $pos + 1);
795 }
796 //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
797 if ($in_headers) {
798 $line = "\t".$line;
799 }
800 }
801 $lines_out[] = $line;
802
803 //Send the lines to the server
804 foreach ($lines_out as $line_out) {
805 //Dot-stuffing as per RFC5321 section 4.5.2
806 //https://tools.ietf.org/html/rfc5321#section-4.5.2
807 if (!empty($line_out) && $line_out[0] === '.') {
808 $line_out = '.'.$line_out;
809 }
810 $this->client_send($line_out.static::LE, 'DATA');
811 }
812 }
813
814 //Message data has been sent, complete the command
815 //Increase timelimit for end of DATA command
816 $savetimelimit = $this->Timelimit;
817 $this->Timelimit *= 2;
818 $result = $this->sendCommand('DATA END', '.', 250);
819 $this->recordLastTransactionID();
820 //Restore timelimit
821 $this->Timelimit = $savetimelimit;
822
823 return $result;
824 }
825
826 /**
827 * Send an SMTP HELO or EHLO command.
828 * Used to identify the sending server to the receiving server.
829 * This makes sure that client and server are in a known state.
830 * Implements RFC 821: HELO <SP> <domain> <CRLF>
831 * and RFC 2821 EHLO.
832 *
833 * @param string $host The host name or IP to connect to
834 *
835 * @return bool
836 */
837 public function hello($host = '')
838 {
839 //Try extended hello first (RFC 2821)
840 if ($this->sendHello('EHLO', $host)) {
841 return true;
842 }
843
844 //Some servers shut down the SMTP service here (RFC 5321)
845 if (substr($this->helo_rply, 0, 3) == '421') {
846 return false;
847 }
848
849 return $this->sendHello('HELO', $host);
850 }
851
852 /**
853 * Send an SMTP HELO or EHLO command.
854 * Low-level implementation used by hello().
855 *
856 * @param string $hello The HELO string
857 * @param string $host The hostname to say we are
858 *
859 * @return bool
860 *
861 * @see hello()
862 */
863 protected function sendHello($hello, $host)
864 {
865 $noerror = $this->sendCommand($hello, $hello.' '.$host, 250);
866 $this->helo_rply = $this->last_reply;
867 if ($noerror) {
868 $this->parseHelloFields($hello);
869 } else {
870 $this->server_caps = null;
871 }
872
873 return $noerror;
874 }
875
876 /**
877 * Parse a reply to HELO/EHLO command to discover server extensions.
878 * In case of HELO, the only parameter that can be discovered is a server name.
879 *
880 * @param string $type `HELO` or `EHLO`
881 */
882 protected function parseHelloFields($type)
883 {
884 $this->server_caps = [];
885 $lines = explode("\n", $this->helo_rply);
886
887 foreach ($lines as $n => $s) {
888 //First 4 chars contain response code followed by - or space
889 $s = trim(substr($s, 4));
890 if (empty($s)) {
891 continue;
892 }
893 $fields = explode(' ', $s);
894 if (!empty($fields)) {
895 if (!$n) {
896 $name = $type;
897 $fields = $fields[0];
898 } else {
899 $name = array_shift($fields);
900 switch ($name) {
901 case 'SIZE':
902 $fields = ($fields ? $fields[0] : 0);
903 break;
904 case 'AUTH':
905 if (!is_array($fields)) {
906 $fields = [];
907 }
908 break;
909 default:
910 $fields = true;
911 }
912 }
913 $this->server_caps[$name] = $fields;
914 }
915 }
916 }
917
918 /**
919 * Send an SMTP MAIL command.
920 * Starts a mail transaction from the email address specified in
921 * $from. Returns true if successful or false otherwise. If True
922 * the mail transaction is started and then one or more recipient
923 * commands may be called followed by a data command.
924 * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
925 *
926 * @param string $from Source address of this message
927 *
928 * @return bool
929 */
930 public function mail($from)
931 {
932 $useVerp = ($this->do_verp ? ' XVERP' : '');
933
934 return $this->sendCommand(
935 'MAIL FROM',
936 'MAIL FROM:<'.$from.'>'.$useVerp,
937 250
938 );
939 }
940
941 /**
942 * Send an SMTP QUIT command.
943 * Closes the socket if there is no error or the $close_on_error argument is true.
944 * Implements from RFC 821: QUIT <CRLF>.
945 *
946 * @param bool $close_on_error Should the connection close if an error occurs?
947 *
948 * @return bool
949 */
950 public function quit($close_on_error = true)
951 {
952 $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
953 $err = $this->error; //Save any error
954 if ($noerror || $close_on_error) {
955 $this->close();
956 $this->error = $err; //Restore any error from the quit command
957 }
958
959 return $noerror;
960 }
961
962 /**
963 * Send an SMTP RCPT command.
964 * Sets the TO argument to $toaddr.
965 * Returns true if the recipient was accepted false if it was rejected.
966 * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
967 *
968 * @param string $address The address the message is being sent to
969 * @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
970 * or DELAY. If you specify NEVER all other notifications are ignored.
971 *
972 * @return bool
973 */
974 public function recipient($address, $dsn = '')
975 {
976 if (empty($dsn)) {
977 $rcpt = 'RCPT TO:<'.$address.'>';
978 } else {
979 $dsn = strtoupper($dsn);
980 $notify = [];
981
982 if (strpos($dsn, 'NEVER') !== false) {
983 $notify[] = 'NEVER';
984 } else {
985 foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
986 if (strpos($dsn, $value) !== false) {
987 $notify[] = $value;
988 }
989 }
990 }
991
992 $rcpt = 'RCPT TO:<'.$address.'> NOTIFY='.implode(',', $notify);
993 }
994
995 return $this->sendCommand(
996 'RCPT TO',
997 $rcpt,
998 [250, 251]
999 );
1000 }
1001
1002 /**
1003 * Send SMTP XCLIENT command to server and check its return code.
1004 *
1005 * @return bool True on success
1006 */
1007 public function xclient(array $vars)
1008 {
1009 $xclient_options = "";
1010 foreach ($vars as $key => $value) {
1011 if (in_array($key, SMTP::$xclient_allowed_attributes)) {
1012 $xclient_options .= " {$key}={$value}";
1013 }
1014 }
1015 if (!$xclient_options) {
1016 return true;
1017 }
1018
1019 return $this->sendCommand('XCLIENT', 'XCLIENT'.$xclient_options, 250);
1020 }
1021
1022 /**
1023 * Send an SMTP RSET command.
1024 * Abort any transaction that is currently in progress.
1025 * Implements RFC 821: RSET <CRLF>.
1026 *
1027 * @return bool True on success
1028 */
1029 public function reset()
1030 {
1031 return $this->sendCommand('RSET', 'RSET', 250);
1032 }
1033
1034 /**
1035 * Send a command to an SMTP server and check its return code.
1036 *
1037 * @param string $command The command name - not sent to the server
1038 * @param string $commandstring The actual command to send
1039 * @param int|array $expect One or more expected integer success codes
1040 *
1041 * @return bool True on success
1042 */
1043 protected function sendCommand($command, $commandstring, $expect)
1044 {
1045 if (!$this->connected()) {
1046 $this->setError("Called $command without being connected");
1047
1048 return false;
1049 }
1050 //Reject line breaks in all commands
1051 if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
1052 $this->setError("Command '$command' contained line breaks");
1053
1054 return false;
1055 }
1056 $this->client_send($commandstring.static::LE, $command);
1057
1058 $this->last_reply = $this->get_lines();
1059 //Fetch SMTP code and possible error code explanation
1060 $matches = [];
1061 if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
1062 $code = (int)$matches[1];
1063 $code_ex = (count($matches) > 2 ? $matches[2] : null);
1064 //Cut off error code from each response line
1065 $detail = preg_replace(
1066 "/{$code}[ -]".
1067 ($code_ex ? str_replace('.', '\\.', $code_ex).' ' : '').'/m',
1068 '',
1069 $this->last_reply
1070 );
1071 } else {
1072 //Fall back to simple parsing if regex fails
1073 $code = (int)substr($this->last_reply, 0, 3);
1074 $code_ex = null;
1075 $detail = substr($this->last_reply, 4);
1076 }
1077
1078 $this->edebug('SERVER -> CLIENT: '.$this->last_reply, self::DEBUG_SERVER);
1079
1080 if (!in_array($code, (array)$expect, true)) {
1081 $this->setError(
1082 "$command command failed",
1083 $detail,
1084 $code,
1085 $code_ex
1086 );
1087 $this->edebug(
1088 'SMTP ERROR: '.$this->error['error'].': '.$this->last_reply,
1089 self::DEBUG_CLIENT
1090 );
1091
1092 return false;
1093 }
1094
1095 //Don't clear the error store when using keepalive
1096 if ($command !== 'RSET') {
1097 $this->setError('');
1098 }
1099
1100 return true;
1101 }
1102
1103 /**
1104 * Send an SMTP SAML command.
1105 * Starts a mail transaction from the email address specified in $from.
1106 * Returns true if successful or false otherwise. If True
1107 * the mail transaction is started and then one or more recipient
1108 * commands may be called followed by a data command. This command
1109 * will send the message to the users terminal if they are logged
1110 * in and send them an email.
1111 * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
1112 *
1113 * @param string $from The address the message is from
1114 *
1115 * @return bool
1116 */
1117 public function sendAndMail($from)
1118 {
1119 return $this->sendCommand('SAML', "SAML FROM:$from", 250);
1120 }
1121
1122 /**
1123 * Send an SMTP VRFY command.
1124 *
1125 * @param string $name The name to verify
1126 *
1127 * @return bool
1128 */
1129 public function verify($name)
1130 {
1131 return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
1132 }
1133
1134 /**
1135 * Send an SMTP NOOP command.
1136 * Used to keep keep-alives alive, doesn't actually do anything.
1137 *
1138 * @return bool
1139 */
1140 public function noop()
1141 {
1142 return $this->sendCommand('NOOP', 'NOOP', 250);
1143 }
1144
1145 /**
1146 * Send an SMTP TURN command.
1147 * This is an optional command for SMTP that this class does not support.
1148 * This method is here to make the RFC821 Definition complete for this class
1149 * and _may_ be implemented in future.
1150 * Implements from RFC 821: TURN <CRLF>.
1151 *
1152 * @return bool
1153 */
1154 public function turn()
1155 {
1156 $this->setError('The SMTP TURN command is not implemented');
1157 $this->edebug('SMTP NOTICE: '.$this->error['error'], self::DEBUG_CLIENT);
1158
1159 return false;
1160 }
1161
1162 /**
1163 * Send raw data to the server.
1164 *
1165 * @param string $data The data to send
1166 * @param string $command Optionally, the command this is part of, used only for controlling debug output
1167 *
1168 * @return int|bool The number of bytes sent to the server or false on error
1169 */
1170 public function client_send($data, $command = '')
1171 {
1172 //If SMTP transcripts are left enabled, or debug output is posted online
1173 //it can leak credentials, so hide credentials in all but lowest level
1174 if (
1175 self::DEBUG_LOWLEVEL > $this->do_debug &&
1176 in_array($command, ['User & Password', 'Username', 'Password'], true)
1177 ) {
1178 $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
1179 } else {
1180 $this->edebug('CLIENT -> SERVER: '.$data, self::DEBUG_CLIENT);
1181 }
1182 set_error_handler([$this, 'errorHandler']);
1183 $result = fwrite($this->smtp_conn, $data);
1184 restore_error_handler();
1185
1186 return $result;
1187 }
1188
1189 /**
1190 * Get the latest error.
1191 *
1192 * @return array
1193 */
1194 public function getError()
1195 {
1196 return $this->error;
1197 }
1198
1199 /**
1200 * Get SMTP extensions available on the server.
1201 *
1202 * @return array|null
1203 */
1204 public function getServerExtList()
1205 {
1206 return $this->server_caps;
1207 }
1208
1209 /**
1210 * Get metadata about the SMTP server from its HELO/EHLO response.
1211 * The method works in three ways, dependent on argument value and current state:
1212 * 1. HELO/EHLO has not been sent - returns null and populates $this->error.
1213 * 2. HELO has been sent -
1214 * $name == 'HELO': returns server name
1215 * $name == 'EHLO': returns boolean false
1216 * $name == any other string: returns null and populates $this->error
1217 * 3. EHLO has been sent -
1218 * $name == 'HELO'|'EHLO': returns the server name
1219 * $name == any other string: if extension $name exists, returns True
1220 * or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
1221 *
1222 * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
1223 *
1224 * @return string|bool|null
1225 */
1226 public function getServerExt($name)
1227 {
1228 if (!$this->server_caps) {
1229 $this->setError('No HELO/EHLO was sent');
1230
1231 return null;
1232 }
1233
1234 if (!array_key_exists($name, $this->server_caps)) {
1235 if ('HELO' === $name) {
1236 return $this->server_caps['EHLO'];
1237 }
1238 if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
1239 return false;
1240 }
1241 $this->setError('HELO handshake was used; No information about server extensions available');
1242
1243 return null;
1244 }
1245
1246 return $this->server_caps[$name];
1247 }
1248
1249 /**
1250 * Get the last reply from the server.
1251 *
1252 * @return string
1253 */
1254 public function getLastReply()
1255 {
1256 return $this->last_reply;
1257 }
1258
1259 /**
1260 * Read the SMTP server's response.
1261 * Either before eof or socket timeout occurs on the operation.
1262 * With SMTP we can tell if we have more lines to read if the
1263 * 4th character is '-' symbol. If it is a space then we don't
1264 * need to read anything else.
1265 *
1266 * @return string
1267 */
1268 protected function get_lines()
1269 {
1270 //If the connection is bad, give up straight away
1271 if (!is_resource($this->smtp_conn)) {
1272 return '';
1273 }
1274 $data = '';
1275 $endtime = 0;
1276 stream_set_timeout($this->smtp_conn, $this->Timeout);
1277 if ($this->Timelimit > 0) {
1278 $endtime = time() + $this->Timelimit;
1279 }
1280 $selR = [$this->smtp_conn];
1281 $selW = null;
1282 while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1283 //Must pass vars in here as params are by reference
1284 //solution for signals inspired by https://github.com/symfony/symfony/pull/6540
1285 set_error_handler([$this, 'errorHandler']);
1286 $n = stream_select($selR, $selW, $selW, $this->Timelimit);
1287 restore_error_handler();
1288
1289 if ($n === false) {
1290 $message = $this->getError()['detail'];
1291
1292 $this->edebug(
1293 'SMTP -> get_lines(): select failed ('.$message.')',
1294 self::DEBUG_LOWLEVEL
1295 );
1296
1297 //stream_select returns false when the `select` system call is interrupted
1298 //by an incoming signal, try the select again
1299 if (stripos($message, 'interrupted system call') !== false) {
1300 $this->edebug(
1301 'SMTP -> get_lines(): retrying stream_select',
1302 self::DEBUG_LOWLEVEL
1303 );
1304 $this->setError('');
1305 continue;
1306 }
1307
1308 break;
1309 }
1310
1311 if (!$n) {
1312 $this->edebug(
1313 'SMTP -> get_lines(): select timed-out in ('.$this->Timelimit.' sec)',
1314 self::DEBUG_LOWLEVEL
1315 );
1316 break;
1317 }
1318
1319 //Deliberate noise suppression - errors are handled afterwards
1320 $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
1321 $this->edebug('SMTP INBOUND: "'.trim($str).'"', self::DEBUG_LOWLEVEL);
1322 $data .= $str;
1323 //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
1324 //or 4th character is a space or a line break char, we are done reading, break the loop.
1325 //String array access is a significant micro-optimisation over strlen
1326 if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
1327 break;
1328 }
1329 //Timed-out? Log and break
1330 $info = stream_get_meta_data($this->smtp_conn);
1331 if ($info['timed_out']) {
1332 $this->edebug(
1333 'SMTP -> get_lines(): stream timed-out ('.$this->Timeout.' sec)',
1334 self::DEBUG_LOWLEVEL
1335 );
1336 break;
1337 }
1338 //Now check if reads took too long
1339 if ($endtime && time() > $endtime) {
1340 $this->edebug(
1341 'SMTP -> get_lines(): timelimit reached ('.
1342 $this->Timelimit.' sec)',
1343 self::DEBUG_LOWLEVEL
1344 );
1345 break;
1346 }
1347 }
1348
1349 return $data;
1350 }
1351
1352 /**
1353 * Enable or disable VERP address generation.
1354 *
1355 * @param bool $enabled
1356 */
1357 public function setVerp($enabled = false)
1358 {
1359 $this->do_verp = $enabled;
1360 }
1361
1362 /**
1363 * Get VERP address generation mode.
1364 *
1365 * @return bool
1366 */
1367 public function getVerp()
1368 {
1369 return $this->do_verp;
1370 }
1371
1372 /**
1373 * Set error messages and codes.
1374 *
1375 * @param string $message The error message
1376 * @param string $detail Further detail on the error
1377 * @param string $smtp_code An associated SMTP error code
1378 * @param string $smtp_code_ex Extended SMTP code
1379 */
1380 protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1381 {
1382 $this->error = [
1383 'error' => $message,
1384 'detail' => $detail,
1385 'smtp_code' => $smtp_code,
1386 'smtp_code_ex' => $smtp_code_ex,
1387 ];
1388 }
1389
1390 /**
1391 * Set debug output method.
1392 *
1393 * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
1394 */
1395 public function setDebugOutput($method = 'echo')
1396 {
1397 $this->Debugoutput = $method;
1398 }
1399
1400 /**
1401 * Get debug output method.
1402 *
1403 * @return string
1404 */
1405 public function getDebugOutput()
1406 {
1407 return $this->Debugoutput;
1408 }
1409
1410 /**
1411 * Set debug output level.
1412 *
1413 * @param int $level
1414 */
1415 public function setDebugLevel($level = 0)
1416 {
1417 $this->do_debug = $level;
1418 }
1419
1420 /**
1421 * Get debug output level.
1422 *
1423 * @return int
1424 */
1425 public function getDebugLevel()
1426 {
1427 return $this->do_debug;
1428 }
1429
1430 /**
1431 * Set SMTP timeout.
1432 *
1433 * @param int $timeout The timeout duration in seconds
1434 */
1435 public function setTimeout($timeout = 0)
1436 {
1437 $this->Timeout = $timeout;
1438 }
1439
1440 /**
1441 * Get SMTP timeout.
1442 *
1443 * @return int
1444 */
1445 public function getTimeout()
1446 {
1447 return $this->Timeout;
1448 }
1449
1450 /**
1451 * Reports an error number and string.
1452 *
1453 * @param int $errno The error number returned by PHP
1454 * @param string $errmsg The error message returned by PHP
1455 * @param string $errfile The file the error occurred in
1456 * @param int $errline The line number the error occurred on
1457 */
1458 protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1459 {
1460 $notice = 'Connection failed.';
1461 $this->setError(
1462 $notice,
1463 $errmsg,
1464 (string)$errno
1465 );
1466 $this->edebug(
1467 "$notice Error #$errno: $errmsg [$errfile line $errline]",
1468 self::DEBUG_CONNECTION
1469 );
1470 }
1471
1472 /**
1473 * Extract and return the ID of the last SMTP transaction based on
1474 * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1475 * Relies on the host providing the ID in response to a DATA command.
1476 * If no reply has been received yet, it will return null.
1477 * If no pattern was matched, it will return false.
1478 *
1479 * @return bool|string|null
1480 */
1481 protected function recordLastTransactionID()
1482 {
1483 $reply = $this->getLastReply();
1484
1485 if (empty($reply)) {
1486 $this->last_smtp_transaction_id = null;
1487 } else {
1488 $this->last_smtp_transaction_id = false;
1489 foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1490 $matches = [];
1491 if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1492 $this->last_smtp_transaction_id = trim($matches[1]);
1493 break;
1494 }
1495 }
1496 }
1497
1498 return $this->last_smtp_transaction_id;
1499 }
1500
1501 /**
1502 * Get the queue/transaction ID of the last SMTP transaction
1503 * If no reply has been received yet, it will return null.
1504 * If no pattern was matched, it will return false.
1505 *
1506 * @return bool|string|null
1507 *
1508 * @see recordLastTransactionID()
1509 */
1510 public function getLastTransactionID()
1511 {
1512 return $this->last_smtp_transaction_id;
1513 }
1514 }
1515