158 'exim' =>
'/[0-9]{3} OK id=(.*)/',
159 'sendmail' =>
'/[0-9]{3} 2.0.0 (.*) Message/',
160 'postfix' =>
'/[0-9]{3} 2.0.0 Ok: queued as (.*)/'
218 protected function edebug($str, $level = 0)
220 if ($level > $this->do_debug) {
224 if (!in_array($this->Debugoutput, array(
'error_log',
'html',
'echo'))
and is_callable($this->Debugoutput)) {
225 call_user_func($this->Debugoutput, $str, $level);
228 switch ($this->Debugoutput) {
235 echo gmdate(
'Y-m-d H:i:s') .
' ' . htmlentities(
236 preg_replace(
'/[\r\n]+/',
'', $str),
244 $str = preg_replace(
'/(\r\n|\r|\n)/ms',
"\n", $str);
245 echo gmdate(
'Y-m-d H:i:s') .
"\t" . str_replace(
262 public function connect($host, $port =
null, $timeout = 30, $options = array())
267 if (is_null($streamok)) {
268 $streamok = function_exists(
'stream_socket_client');
275 $this->
setError(
'Already connected to a server');
283 "Connection: opening to $host:$port, timeout=$timeout, options=" .
284 var_export($options,
true),
285 self::DEBUG_CONNECTION
290 $socket_context = stream_context_create($options);
291 set_error_handler(array($this,
'errorHandler'));
292 $this->smtp_conn = stream_socket_client(
297 STREAM_CLIENT_CONNECT,
300 restore_error_handler();
304 "Connection: stream_socket_client not available, falling back to fsockopen",
305 self::DEBUG_CONNECTION
307 set_error_handler(array($this,
'errorHandler'));
308 $this->smtp_conn = fsockopen(
315 restore_error_handler();
318 if (!is_resource($this->smtp_conn)) {
320 'Failed to connect to server',
325 'SMTP ERROR: ' . $this->error[
'error']
326 .
": $errstr ($errno)",
331 $this->
edebug(
'Connection: opened', self::DEBUG_CONNECTION);
334 if (substr(PHP_OS, 0, 3) !=
'WIN') {
335 $max = ini_get(
'max_execution_time');
337 if ($max != 0 && $timeout > $max) {
338 @set_time_limit($timeout);
340 stream_set_timeout($this->smtp_conn, $timeout, 0);
344 $this->
edebug(
'SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER);
355 if (!$this->
sendCommand(
'STARTTLS',
'STARTTLS', 220)) {
360 $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
364 if (defined(
'STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
365 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
366 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
370 set_error_handler(array($this,
'errorHandler'));
371 $crypto_ok = stream_socket_enable_crypto(
376 restore_error_handler();
400 if (!$this->server_caps) {
401 $this->
setError(
'Authentication is not allowed before HELO/EHLO');
405 if (array_key_exists(
'EHLO', $this->server_caps)) {
407 if (!array_key_exists(
'AUTH', $this->server_caps)) {
408 $this->
setError(
'Authentication is not allowed at this stage');
414 self::edebug(
'Auth method requested: ' . ($authtype ? $authtype :
'UNKNOWN'), self::DEBUG_LOWLEVEL);
416 'Auth methods available on the server: ' . implode(
',', $this->server_caps[
'AUTH']),
420 if (
empty($authtype)) {
421 foreach (array(
'CRAM-MD5',
'LOGIN',
'PLAIN',
'NTLM',
'XOAUTH2')
as $method) {
422 if (in_array($method, $this->server_caps[
'AUTH'])) {
427 if (
empty($authtype)) {
428 $this->
setError(
'No supported authentication methods found');
431 self::edebug(
'Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
434 if (!in_array($authtype, $this->server_caps[
'AUTH'])) {
435 $this->
setError(
"The requested authentication method \"$authtype\" is not supported by the server");
438 } elseif (
empty($authtype)) {
444 if (!$this->
sendCommand(
'AUTH',
'AUTH PLAIN', 334)) {
450 base64_encode(
"\0" . $username .
"\0" . $password),
459 if (!$this->
sendCommand(
'AUTH',
'AUTH LOGIN', 334)) {
462 if (!$this->
sendCommand(
"Username", base64_encode($username), 334)) {
465 if (!$this->
sendCommand(
"Password", base64_encode($password), 235)) {
472 if (is_null($OAuth)) {
475 $oauth = $OAuth->getOauth64();
478 if (!$this->
sendCommand(
'AUTH',
'AUTH XOAUTH2 ' . $oauth, 235)) {
491 require_once
'extras/ntlm_sasl_client.php';
492 $temp =
new stdClass;
493 $ntlm_client =
new ntlm_sasl_client_class;
495 if (!$ntlm_client->initialize($temp)) {
498 'You need to enable some modules in your php.ini file: '
499 . $this->error[
'error'],
505 $msg1 = $ntlm_client->typeMsg1($realm, $workstation);
509 'AUTH NTLM ' . base64_encode($msg1),
517 $challenge = substr($this->last_reply, 3);
518 $challenge = base64_decode($challenge);
519 $ntlm_res = $ntlm_client->NTLMResponse(
520 substr($challenge, 24, 8),
524 $msg3 = $ntlm_client->typeMsg3(
531 return $this->
sendCommand(
'Username', base64_encode($msg3), 235);
534 if (!$this->
sendCommand(
'AUTH CRAM-MD5',
'AUTH CRAM-MD5', 334)) {
538 $challenge = base64_decode(substr($this->last_reply, 4));
541 $response = $username .
' ' . $this->
hmac($challenge, $password);
544 return $this->
sendCommand(
'Username', base64_encode($response), 235);
546 $this->
setError(
"Authentication method \"$authtype\" is not supported");
561 protected function hmac($data, $key)
563 if (function_exists(
'hash_hmac')) {
564 return hash_hmac(
'md5', $data, $key);
576 if (strlen($key) > $bytelen) {
577 $key = pack(
'H*', md5($key));
579 $key = str_pad($key, $bytelen, chr(0x00));
580 $ipad = str_pad(
'', $bytelen, chr(0x36));
581 $opad = str_pad(
'', $bytelen, chr(0x5c));
582 $k_ipad = $key ^ $ipad;
583 $k_opad = $key ^ $opad;
585 return md5($k_opad . pack(
'H*', md5($k_ipad . $data)));
595 if (is_resource($this->smtp_conn)) {
596 $sock_status = stream_get_meta_data($this->smtp_conn);
597 if ($sock_status[
'eof']) {
600 'SMTP NOTICE: EOF caught while checking if connected',
621 $this->server_caps =
null;
622 $this->helo_rply =
null;
623 if (is_resource($this->smtp_conn)) {
625 fclose($this->smtp_conn);
626 $this->smtp_conn =
null;
627 $this->
edebug(
'Connection: closed', self::DEBUG_CONNECTION);
643 public function data($msg_data)
659 $lines = explode(
"\n", str_replace(array(
"\r\n",
"\r"),
"\n", $msg_data));
666 $field = substr($lines[0], 0, strpos($lines[0],
':'));
668 if (!
empty($field) && strpos($field,
' ') ===
false) {
672 foreach ($lines
as $line) {
673 $lines_out = array();
674 if ($in_headers
and $line ==
'') {
679 while (isset($line[self::MAX_LINE_LENGTH])) {
682 $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH),
' ');
686 $pos = self::MAX_LINE_LENGTH - 1;
687 $lines_out[] = substr($line, 0, $pos);
688 $line = substr($line, $pos);
691 $lines_out[] = substr($line, 0, $pos);
693 $line = substr($line, $pos + 1);
697 $line =
"\t" . $line;
700 $lines_out[] = $line;
703 foreach ($lines_out
as $line_out) {
705 if (!
empty($line_out)
and $line_out[0] ==
'.') {
706 $line_out =
'.' . $line_out;
715 $this->Timelimit = $this->Timelimit * 2;
716 $result = $this->
sendCommand(
'DATA END',
'.', 250);
719 $this->Timelimit = $savetimelimit;
750 $noerror = $this->
sendCommand($hello, $hello .
' ' . $host, 250);
755 $this->server_caps =
null;
768 $this->server_caps = array();
769 $lines = explode(
"\n", $this->helo_rply);
771 foreach ($lines
as $n =>
$s) {
773 $s = trim(substr(
$s, 4));
777 $fields = explode(
' ',
$s);
778 if (!
empty($fields)) {
781 $fields = $fields[0];
783 $name = array_shift($fields);
786 $fields = ($fields ? $fields[0] : 0);
789 if (!is_array($fields)) {
797 $this->server_caps[$name] = $fields;
815 $useVerp = ($this->do_verp ?
' XVERP' :
'');
818 'MAIL FROM:<' . $from .
'>' . $useVerp,
831 public function quit($close_on_error =
true)
833 $noerror = $this->
sendCommand(
'QUIT',
'QUIT', 221);
835 if ($noerror
or $close_on_error) {
855 'RCPT TO:<' . $address .
'>',
883 $this->
setError(
"Called $command without being connected");
887 if (strpos($commandstring,
"\n") !==
false or strpos($commandstring,
"\r") !==
false) {
888 $this->
setError(
"Command '$command' contained line breaks");
896 if (preg_match(
"/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches)) {
898 $code_ex = (count($matches) > 2 ? $matches[2] :
null);
900 $detail = preg_replace(
902 ($code_ex ? str_replace(
'.',
'\\.', $code_ex) .
' ' :
'') .
"/m",
908 $code = substr($this->last_reply, 0, 3);
910 $detail = substr($this->last_reply, 4);
913 $this->
edebug(
'SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
915 if (!in_array($code, (array)$expect)) {
917 "$command command failed",
923 'SMTP ERROR: ' . $this->error[
'error'] .
': ' . $this->last_reply,
948 return $this->
sendCommand(
'SAML',
"SAML FROM:$from", 250);
959 return $this->
sendCommand(
'VRFY',
"VRFY $name", array(250, 251));
984 $this->
setError(
'The SMTP TURN command is not implemented');
985 $this->
edebug(
'SMTP NOTICE: ' . $this->error[
'error'], self::DEBUG_CLIENT);
997 $this->
edebug(
"CLIENT -> SERVER: $data", self::DEBUG_CLIENT);
998 set_error_handler(array($this,
'errorHandler'));
999 $result = fwrite($this->smtp_conn, $data);
1000 restore_error_handler();
1045 if (!$this->server_caps) {
1046 $this->
setError(
'No HELO/EHLO was sent');
1051 if (!array_key_exists($name, $this->server_caps)) {
1052 if ($name ==
'HELO') {
1053 return $this->server_caps[
'EHLO'];
1055 if ($name ==
'EHLO' || array_key_exists(
'EHLO', $this->server_caps)) {
1058 $this->
setError(
'HELO handshake was used. Client knows nothing about server extensions');
1062 return $this->server_caps[$name];
1087 if (!is_resource($this->smtp_conn)) {
1092 stream_set_timeout($this->smtp_conn, $this->Timeout);
1093 if ($this->Timelimit > 0) {
1096 while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1097 $str = @fgets($this->smtp_conn, 515);
1098 $this->
edebug(
"SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL);
1099 $this->
edebug(
"SMTP -> get_lines(): \$str is \"$str\"", self::DEBUG_LOWLEVEL);
1104 if (!isset($str[3])
or (isset($str[3])
and $str[3] ==
' ')) {
1108 $info = stream_get_meta_data($this->smtp_conn);
1109 if ($info[
'timed_out']) {
1111 'SMTP -> get_lines(): timed-out (' . $this->Timeout .
' sec)',
1112 self::DEBUG_LOWLEVEL
1117 if ($endtime
and time() > $endtime) {
1119 'SMTP -> get_lines(): timelimit reached (' .
1120 $this->Timelimit .
' sec)',
1121 self::DEBUG_LOWLEVEL
1135 $this->do_verp = $enabled;
1154 protected function setError($message, $detail =
'', $smtp_code =
'', $smtp_code_ex =
'')
1156 $this->error = array(
1157 'error' => $message,
1158 'detail' => $detail,
1159 'smtp_code' => $smtp_code,
1160 'smtp_code_ex' => $smtp_code_ex
1170 $this->Debugoutput = $method;
1188 $this->do_debug = $level;
1206 $this->Timeout = $timeout;
1225 protected function errorHandler($errno, $errmsg, $errfile =
'', $errline = 0)
1227 $notice =
'Connection failed.';
1234 $notice .
' Error #' . $errno .
': ' . $errmsg .
" [$errfile line $errline]",
1235 self::DEBUG_CONNECTION
1251 if (
empty($reply)) {
1252 $this->last_smtp_transaction_id =
null;
1254 $this->last_smtp_transaction_id =
false;
1255 foreach ($this->smtp_transaction_id_patterns
as $smtp_transaction_id_pattern) {
1256 if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1257 $this->last_smtp_transaction_id = $matches[1];