181 'exim' =>
'/[\d]{3} OK id=(.*)/',
182 'sendmail' =>
'/[\d]{3} 2.0.0 (.*) Message/',
183 'postfix' =>
'/[\d]{3} 2.0.0 Ok: queued as (.*)/',
184 'Microsoft_ESMTP' =>
'/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/',
185 'Amazon_SES' =>
'/[\d]{3} Ok (.*)/',
186 'SendGrid' =>
'/[\d]{3} Ok: queued as (.*)/',
187 'CampaignMonitor' =>
'/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
214 'smtp_code_ex' =>
'',
253 protected function edebug($str, $level = 0)
255 if ($level > $this->do_debug) {
259 if ($this->Debugoutput instanceof \
Psr\Log\LoggerInterface) {
260 $this->Debugoutput->debug($str);
265 if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, [
'error_log',
'html',
'echo'])) {
266 call_user_func($this->Debugoutput, $str, $level);
270 switch ($this->Debugoutput) {
277 echo gmdate(
'Y-m-d H:i:s'),
' ', htmlentities(
278 preg_replace(
'/[\r\n]+/',
'', $str),
286 $str = preg_replace(
'/\r\n|\r/m',
"\n", $str);
287 echo gmdate(
'Y-m-d H:i:s'),
317 if (null === $streamok) {
318 $streamok = function_exists(
'stream_socket_client');
325 $this->
setError(
'Already connected to a server');
330 $port = self::DEFAULT_PORT;
334 "Connection: opening to $host:$port, timeout=$timeout, options=" .
336 self::DEBUG_CONNECTION
341 $socket_context = stream_context_create(
$options);
342 set_error_handler([$this,
'errorHandler']);
343 $this->smtp_conn = stream_socket_client(
348 STREAM_CLIENT_CONNECT,
351 restore_error_handler();
355 'Connection: stream_socket_client not available, falling back to fsockopen',
356 self::DEBUG_CONNECTION
358 set_error_handler([$this,
'errorHandler']);
359 $this->smtp_conn = fsockopen(
366 restore_error_handler();
369 if (!is_resource($this->smtp_conn)) {
371 'Failed to connect to server',
377 'SMTP ERROR: ' . $this->error[
'error']
378 .
": $errstr ($errno)",
384 $this->
edebug(
'Connection: opened', self::DEBUG_CONNECTION);
387 if (strpos(PHP_OS,
'WIN') !== 0) {
388 $max = (int) ini_get(
'max_execution_time');
390 if (0 !== $max && $timeout > $max) {
391 @set_time_limit($timeout);
393 stream_set_timeout($this->smtp_conn, $timeout, 0);
397 $this->
edebug(
'SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
409 if (!$this->
sendCommand(
'STARTTLS',
'STARTTLS', 220)) {
414 $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
418 if (defined(
'STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
419 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
420 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
424 set_error_handler([$this,
'errorHandler']);
425 $crypto_ok = stream_socket_enable_crypto(
430 restore_error_handler();
432 return (
bool) $crypto_ok;
454 if (!$this->server_caps) {
455 $this->
setError(
'Authentication is not allowed before HELO/EHLO');
460 if (array_key_exists(
'EHLO', $this->server_caps)) {
462 if (!array_key_exists(
'AUTH', $this->server_caps)) {
463 $this->
setError(
'Authentication is not allowed at this stage');
470 $this->
edebug(
'Auth method requested: ' . ($authtype ?:
'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
472 'Auth methods available on the server: ' . implode(
',', $this->server_caps[
'AUTH']),
477 if (null !== $authtype && !in_array($authtype, $this->server_caps[
'AUTH'],
true)) {
478 $this->
edebug(
'Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
482 if (empty($authtype)) {
485 foreach ([
'CRAM-MD5',
'LOGIN',
'PLAIN',
'XOAUTH2'] as $method) {
486 if (in_array($method, $this->server_caps[
'AUTH'],
true)) {
491 if (empty($authtype)) {
492 $this->
setError(
'No supported authentication methods found');
496 $this->
edebug(
'Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
499 if (!in_array($authtype, $this->server_caps[
'AUTH'],
true)) {
500 $this->
setError(
"The requested authentication method \"$authtype\" is not supported by the server");
504 } elseif (empty($authtype)) {
510 if (!$this->
sendCommand(
'AUTH',
'AUTH PLAIN', 334)) {
516 base64_encode(
"\0" . $username .
"\0" .
$password),
525 if (!$this->
sendCommand(
'AUTH',
'AUTH LOGIN', 334)) {
528 if (!$this->
sendCommand(
'Username', base64_encode($username), 334)) {
531 if (!$this->
sendCommand(
'Password', base64_encode($password), 235)) {
537 if (!$this->
sendCommand(
'AUTH CRAM-MD5',
'AUTH CRAM-MD5', 334)) {
541 $challenge = base64_decode(substr($this->last_reply, 4));
544 $response = $username .
' ' . $this->
hmac($challenge, $password);
550 if (null === $OAuth) {
553 $oauth = $OAuth->getOauth64();
556 if (!$this->
sendCommand(
'AUTH',
'AUTH XOAUTH2 ' . $oauth, 235)) {
561 $this->
setError(
"Authentication method \"$authtype\" is not supported");
581 if (function_exists(
'hash_hmac')) {
594 if (strlen(
$key) > $bytelen) {
597 $key = str_pad(
$key, $bytelen, chr(0x00));
598 $ipad = str_pad(
'', $bytelen, chr(0x36));
599 $opad = str_pad(
'', $bytelen, chr(0x5c));
600 $k_ipad =
$key ^ $ipad;
601 $k_opad =
$key ^ $opad;
603 return md5($k_opad . pack(
'H*', md5($k_ipad .
$data)));
613 if (is_resource($this->smtp_conn)) {
614 $sock_status = stream_get_meta_data($this->smtp_conn);
615 if ($sock_status[
'eof']) {
618 'SMTP NOTICE: EOF caught while checking if connected',
641 $this->server_caps = null;
642 $this->helo_rply = null;
643 if (is_resource($this->smtp_conn)) {
645 fclose($this->smtp_conn);
646 $this->smtp_conn = null;
647 $this->
edebug(
'Connection: closed', self::DEBUG_CONNECTION);
664 public function data($msg_data)
680 $lines = explode(
"\n", str_replace([
"\r\n",
"\r"],
"\n", $msg_data));
687 $field = substr($lines[0], 0, strpos($lines[0],
':'));
689 if (!empty($field) && strpos($field,
' ') ===
false) {
693 foreach ($lines as $line) {
695 if ($in_headers && $line ===
'') {
700 while (isset($line[self::MAX_LINE_LENGTH])) {
703 $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH),
' ');
707 $pos = self::MAX_LINE_LENGTH - 1;
708 $lines_out[] = substr($line, 0, $pos);
709 $line = substr($line, $pos);
712 $lines_out[] = substr($line, 0, $pos);
714 $line = substr($line, $pos + 1);
718 $line =
"\t" . $line;
721 $lines_out[] = $line;
724 foreach ($lines_out as $line_out) {
726 if (!empty($line_out) && $line_out[0] ===
'.') {
727 $line_out =
'.' . $line_out;
729 $this->
client_send($line_out . static::LE,
'DATA');
736 $this->Timelimit *= 2;
740 $this->Timelimit = $savetimelimit;
775 $noerror = $this->
sendCommand($hello, $hello .
' ' . $host, 250);
780 $this->server_caps = null;
794 $this->server_caps = [];
795 $lines = explode(
"\n", $this->helo_rply);
797 foreach ($lines as
$n =>
$s) {
799 $s = trim(substr(
$s, 4));
803 $fields = explode(
' ',
$s);
804 if (!empty($fields)) {
807 $fields = $fields[0];
809 $name = array_shift($fields);
812 $fields = ($fields ? $fields[0] : 0);
815 if (!is_array($fields)) {
823 $this->server_caps[
$name] = $fields;
842 $useVerp = ($this->do_verp ?
' XVERP' :
'');
846 'MAIL FROM:<' .
$from .
'>' . $useVerp,
860 public function quit($close_on_error =
true)
862 $noerror = $this->
sendCommand(
'QUIT',
'QUIT', 221);
864 if ($noerror || $close_on_error) {
887 $rcpt =
'RCPT TO:<' . $address .
'>';
892 if (strpos(
$dsn,
'NEVER') !==
false) {
895 foreach ([
'SUCCESS',
'FAILURE',
'DELAY'] as $value) {
896 if (strpos(
$dsn, $value) !==
false) {
902 $rcpt =
'RCPT TO:<' . $address .
'> NOTIFY=' . implode(
',', $notify);
936 $this->
setError(
"Called $command without being connected");
941 if ((strpos($commandstring,
"\n") !==
false) || (strpos($commandstring,
"\r") !==
false)) {
942 $this->
setError(
"Command '$command' contained line breaks");
946 $this->
client_send($commandstring . static::LE, $command);
951 if (preg_match(
'/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
952 $code = (int) $matches[1];
953 $code_ex = (count($matches) > 2 ? $matches[2] : null);
955 $detail = preg_replace(
957 ($code_ex ? str_replace(
'.',
'\\.', $code_ex) .
' ' :
'') .
'/m',
963 $code = (int) substr($this->last_reply, 0, 3);
965 $detail = substr($this->last_reply, 4);
968 $this->
edebug(
'SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
970 if (!in_array(
$code, (array) $expect,
true)) {
972 "$command command failed",
978 'SMTP ERROR: ' . $this->error[
'error'] .
': ' . $this->last_reply,
1006 return $this->
sendCommand(
'SAML',
"SAML FROM:$from", 250);
1018 return $this->
sendCommand(
'VRFY',
"VRFY $name", [250, 251]);
1043 $this->
setError(
'The SMTP TURN command is not implemented');
1044 $this->
edebug(
'SMTP NOTICE: ' . $this->error[
'error'], self::DEBUG_CLIENT);
1061 if (self::DEBUG_LOWLEVEL > $this->do_debug &&
1062 in_array($command, [
'User & Password',
'Username',
'Password'],
true)) {
1063 $this->
edebug(
'CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
1065 $this->
edebug(
'CLIENT -> SERVER: ' .
$data, self::DEBUG_CLIENT);
1067 set_error_handler([$this,
'errorHandler']);
1069 restore_error_handler();
1113 if (!$this->server_caps) {
1114 $this->
setError(
'No HELO/EHLO was sent');
1119 if (!array_key_exists(
$name, $this->server_caps)) {
1120 if (
'HELO' ===
$name) {
1121 return $this->server_caps[
'EHLO'];
1123 if (
'EHLO' ===
$name || array_key_exists(
'EHLO', $this->server_caps)) {
1126 $this->
setError(
'HELO handshake was used; No information about server extensions available');
1131 return $this->server_caps[
$name];
1156 if (!is_resource($this->smtp_conn)) {
1161 stream_set_timeout($this->smtp_conn, $this->Timeout);
1162 if ($this->Timelimit > 0) {
1167 while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1169 if (!stream_select($selR, $selW, $selW, $this->Timelimit)) {
1171 'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit .
' sec)',
1172 self::DEBUG_LOWLEVEL
1177 $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
1178 $this->
edebug(
'SMTP INBOUND: "' . trim($str) .
'"', self::DEBUG_LOWLEVEL);
1183 if (!isset($str[3]) || $str[3] ===
' ' || $str[3] ===
"\r" || $str[3] ===
"\n") {
1187 $info = stream_get_meta_data($this->smtp_conn);
1188 if (
$info[
'timed_out']) {
1190 'SMTP -> get_lines(): stream timed-out (' . $this->Timeout .
' sec)',
1191 self::DEBUG_LOWLEVEL
1196 if ($endtime && time() > $endtime) {
1198 'SMTP -> get_lines(): timelimit reached (' .
1199 $this->Timelimit .
' sec)',
1200 self::DEBUG_LOWLEVEL
1216 $this->do_verp = $enabled;
1241 'detail' => $detail,
1242 'smtp_code' => $smtp_code,
1243 'smtp_code_ex' => $smtp_code_ex,
1254 $this->Debugoutput = $method;
1274 $this->do_debug = $level;
1294 $this->Timeout = $timeout;
1315 protected function errorHandler($errno, $errmsg, $errfile =
'', $errline = 0)
1317 $notice =
'Connection failed.';
1324 "$notice Error #$errno: $errmsg [$errfile line $errline]",
1325 self::DEBUG_CONNECTION
1342 if (empty($reply)) {
1343 $this->last_smtp_transaction_id = null;
1345 $this->last_smtp_transaction_id =
false;
1346 foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1348 if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1349 $this->last_smtp_transaction_id = trim($matches[1]);
data($msg_data)
Send an SMTP DATA command.
hmac($data, $key)
Calculate an MD5 HMAC hash.
setTimeout($timeout=0)
Set SMTP timeout.
parseHelloFields($type)
Parse a reply to HELO/EHLO command to discover server extensions.
getLastReply()
Get the last reply from the server.
quit($close_on_error=true)
Send an SMTP QUIT command.
verify($name)
Send an SMTP VRFY command.
recordLastTransactionID()
Extract and return the ID of the last SMTP transaction based on a list of patterns provided in SMTP::...
getDebugLevel()
Get debug output level.
getError()
Get the latest error.
sendHello($hello, $host)
Send an SMTP HELO or EHLO command.
get_lines()
Read the SMTP server's response.
foreach($paths as $path) $dsn
setError($message, $detail='', $smtp_code='', $smtp_code_ex='')
Set error messages and codes.
$last_smtp_transaction_id
getDebugOutput()
Get debug output method.
edebug($str, $level=0)
Output debugging info via a user-selected method.
sendAndMail($from)
Send an SMTP SAML command.
catch(Exception $e) $message
recipient($address, $dsn='')
Send an SMTP RCPT command.
close()
Close the socket and clean up the state of the class.
getLastTransactionID()
Get the queue/transaction ID of the last SMTP transaction If no reply has been received yet...
getVerp()
Get VERP address generation mode.
connect($host, $port=null, $timeout=30, $options=[])
Connect to an SMTP server.
getServerExt($name)
Get metadata about the SMTP server from its HELO/EHLO response.
sendCommand($command, $commandstring, $expect)
Send a command to an SMTP server and check its return code.
hello($host='')
Send an SMTP HELO or EHLO command.
mail($from)
Send an SMTP MAIL command.
setVerp($enabled=false)
Enable or disable VERP address generation.
$smtp_transaction_id_patterns
client_send($data, $command='')
Send raw data to the server.
startTLS()
Initiate a TLS (encrypted) session.
getServerExtList()
Get SMTP extensions available on the server.
turn()
Send an SMTP TURN command.
getTimeout()
Get SMTP timeout.
errorHandler($errno, $errmsg, $errfile='', $errline=0)
Reports an error number and string.
noop()
Send an SMTP NOOP command.
reset()
Send an SMTP RSET command.
setDebugOutput($method='echo')
Set debug output method.
authenticate( $username, $password, $authtype=null, $OAuth=null)
Perform SMTP authentication.
PHPMailer RFC821 SMTP email transport class.
connected()
Check connection state.
setDebugLevel($level=0)
Set debug output level.
Get an OAuth2 token from an OAuth2 provider.