1<?php 2 3/** 4 * Deliver_SMTP.class.php 5 * 6 * SMTP delivery backend for the Deliver class. 7 * 8 * @copyright 1999-2021 The SquirrelMail Project Team 9 * @license http://opensource.org/licenses/gpl-license.php GNU Public License 10 * @version $Id: Deliver_SMTP.class.php 14914 2021-04-15 17:18:59Z pdontthink $ 11 * @package squirrelmail 12 */ 13 14/** This of course depends upon Deliver */ 15require_once(SM_PATH . 'class/deliver/Deliver.class.php'); 16 17/** 18 * Deliver messages using SMTP 19 * @package squirrelmail 20 */ 21class Deliver_SMTP extends Deliver { 22 23 /** 24 * Array keys are uppercased ehlo keywords 25 * array key values are ehlo params. If ehlo-param contains space, it is splitted into array. 26 * @var array ehlo 27 * @since 1.4.23 and 1.5.1 28 */ 29 var $ehlo = array(); 30 31 /** 32 * @var string domain 33 * @since 1.4.23 and 1.5.1 34 */ 35 var $domain = ''; 36 37 /** 38 * SMTP STARTTLS rfc: "Both the client and the server MUST know if there 39 * is a TLS session active." 40 * Variable should be set to true, when encryption is turned on. 41 * @var boolean 42 * @since 1.4.23 and 1.5.1 43 */ 44 var $tls_enabled = false; 45 46 function preWriteToStream(&$s) { 47 if ($s) { 48 if ($s[0] == '.') $s = '.' . $s; 49 $s = str_replace("\n.","\n..",$s); 50 } 51 } 52 53 function initStream($message, $domain, $length=0, $host='', $port='', $user='', $pass='', $authpop=false, $pop_host='', $stream_options=array()) { 54 global $use_smtp_tls, $smtp_auth_mech; 55 56 if ($authpop) { 57 $this->authPop($user, $pass, $pop_host, ''); 58 } 59 60 $rfc822_header = $message->rfc822_header; 61 62 $from = $rfc822_header->from[0]; 63 $to = $rfc822_header->to; 64 $cc = $rfc822_header->cc; 65 $bcc = $rfc822_header->bcc; 66 $content_type = $rfc822_header->content_type; 67 68 // MAIL FROM: <from address> MUST be empty in cae of MDN (RFC2298) 69 if ($content_type->type0 == 'multipart' && 70 $content_type->type1 == 'report' && 71 isset($content_type->properties['report-type']) && 72 $content_type->properties['report-type']=='disposition-notification') { 73 // reinitialize the from object because otherwise the from header somehow 74 // is affected. This $from var is used for smtp command MAIL FROM which 75 // is not the same as what we put in the rfc822 header. 76 $from = new AddressStructure(); 77 $from->host = ''; 78 $from->mailbox = ''; 79 } 80 81 // for backward compatibility: boolean $use_smtp_tls set 82 // to TRUE means to use plain TLS (as opposed to STARTTLS) 83 // 84 if ($use_smtp_tls === TRUE) 85 $use_smtp_tls = 1; 86 87 // NB: Using "ssl://" ensures the highest possible TLS version 88 // will be negotiated with the server (whereas "tls://" only 89 // uses TLS version 1.0) 90 // 91 if ($use_smtp_tls == 1) { 92 if ((check_php_version(4,3)) && (extension_loaded('openssl'))) { 93 if (function_exists('stream_socket_client')) { 94 $server_address = 'ssl://' . $host . ':' . $port; 95 $ssl_context = @stream_context_create($stream_options); 96 $connect_timeout = ini_get('default_socket_timeout'); 97 // null timeout is broken 98 if ($connect_timeout == 0) 99 $connect_timeout = 30; 100 $stream = @stream_socket_client($server_address, $errorNumber, $errorString, $connect_timeout, STREAM_CLIENT_CONNECT, $ssl_context); 101 } else { 102 $stream = @fsockopen('ssl://' . $host, $port, $errorNumber, $errorString); 103 } 104 $this->tls_enabled = true; 105 } else { 106 /** 107 * don't connect to server when user asks for smtps and 108 * PHP does not support it. 109 */ 110 $errorNumber = ''; 111 $errorString = _("Secure SMTP (TLS) is enabled in SquirrelMail configuration, but used PHP version does not support it."); 112 } 113 } else { 114 $stream = @fsockopen($host, $port, $errorNumber, $errorString); 115 } 116 117 if (!$stream) { 118 // reset tls state var to default value, if connection fails 119 $this->tls_enabled = false; 120 // set error messages 121 $this->dlv_msg = $errorString; 122 $this->dlv_ret_nr = $errorNumber; 123 $this->dlv_server_msg = _("Can't open SMTP stream."); 124 return(0); 125 } 126 // get server greeting 127 $tmp = fgets($stream, 1024); 128 if ($this->errorCheck($tmp, $stream)) { 129 return(0); 130 } 131 132 /* 133 * If $_SERVER['HTTP_HOST'] is set, use that in our HELO to the SMTP 134 * server. This should fix the DNS issues some people have had 135 */ 136 if (sqgetGlobalVar('HTTP_HOST', $HTTP_HOST, SQ_SERVER)) { // HTTP_HOST is set 137 // optionally trim off port number 138 if($p = strrpos($HTTP_HOST, ':')) { 139 $HTTP_HOST = substr($HTTP_HOST, 0, $p); 140 } 141 $helohost = $HTTP_HOST; 142 } else { // For some reason, HTTP_HOST is not set - revert to old behavior 143 $helohost = $domain; 144 } 145 146 // if the host is an IPv4 address, enclose it in brackets 147 // 148 if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $helohost)) 149 $helohost = '[' . $helohost . ']'; 150 151 $hook_result = do_hook_function('smtp_helo_override', $helohost); 152 if (!empty($hook_result)) $helohost = $hook_result; 153 154 /* Lets introduce ourselves */ 155 fputs($stream, "EHLO $helohost\r\n"); 156 // Read ehlo response 157 $tmp = $this->parse_ehlo_response($stream); 158 if ($this->errorCheck($tmp,$stream)) { 159 // fall back to HELO if EHLO is not supported (error 5xx) 160 if ($this->dlv_ret_nr[0] == '5') { 161 fputs($stream, "HELO $helohost\r\n"); 162 $tmp = fgets($stream,1024); 163 if ($this->errorCheck($tmp,$stream)) { 164 return(0); 165 } 166 } else { 167 return(0); 168 } 169 } 170 171 /** 172 * Implementing SMTP STARTTLS (rfc2487) in php 5.1.0+ 173 * http://www.php.net/stream-socket-enable-crypto 174 */ 175 if ($use_smtp_tls === 2) { 176 if (function_exists('stream_socket_enable_crypto')) { 177 // don't try starting tls, when client thinks that it is already active 178 if ($this->tls_enabled) { 179 $this->dlv_msg = _("TLS session is already activated."); 180 return 0; 181 } elseif (!array_key_exists('STARTTLS',$this->ehlo)) { 182 // check for starttls in ehlo response 183 $this->dlv_msg = _("SMTP STARTTLS is enabled in SquirrelMail configuration, but used SMTP server does not support it"); 184 return 0; 185 } 186 187 // issue starttls command 188 fputs($stream, "STARTTLS\r\n"); 189 // get response 190 $tmp = fgets($stream,1024); 191 if ($this->errorCheck($tmp,$stream)) { 192 return 0; 193 } 194 195 // start crypto on connection. suppress function errors. 196 if (@stream_socket_enable_crypto($stream,true,STREAM_CRYPTO_METHOD_TLS_CLIENT)) { 197 // starttls was successful (rfc2487 5.2 Result of the STARTTLS Command) 198 // get new EHLO response 199 fputs($stream, "EHLO $helohost\r\n"); 200 // Read ehlo response 201 $tmp = $this->parse_ehlo_response($stream); 202 if ($this->errorCheck($tmp,$stream)) { 203 // don't revert to helo here. server must support ESMTP 204 return 0; 205 } 206 // set information about started tls 207 $this->tls_enabled = true; 208 } else { 209 /** 210 * stream_socket_enable_crypto() call failed. 211 */ 212 $this->dlv_msg = _("Unable to start TLS."); 213 return 0; 214 // Bug: can't get error message. See comments in sqimap_create_stream(). 215 } 216 } else { 217 // php install does not support stream_socket_enable_crypto() function 218 $this->dlv_msg = _("SMTP STARTTLS is enabled in SquirrelMail configuration, but used PHP version does not support functions that allow to enable encryption on open socket."); 219 return 0; 220 } 221 } 222 223 // FIXME: check ehlo response before using authentication 224 225 // Try authentication by a plugin 226 // 227 // NOTE: there is another hook in functions/auth.php called "smtp_auth" 228 // that allows a plugin to specify a different set of login credentials 229 // (so is slightly mis-named, but is too old to change), so be careful 230 // that you do not confuse your hook names. 231 // 232 $smtp_auth_args = array( 233 'auth_mech' => $smtp_auth_mech, 234 'user' => $user, 235 'pass' => $pass, 236 'host' => $host, 237 'port' => $port, 238 'stream' => $stream, 239 ); 240 if (boolean_hook_function('smtp_authenticate', $smtp_auth_args, 1)) { 241 // authentication succeeded 242 } else if (( $smtp_auth_mech == 'cram-md5') or ( $smtp_auth_mech == 'digest-md5' )) { 243 // Doing some form of non-plain auth 244 if ($smtp_auth_mech == 'cram-md5') { 245 fputs($stream, "AUTH CRAM-MD5\r\n"); 246 } elseif ($smtp_auth_mech == 'digest-md5') { 247 fputs($stream, "AUTH DIGEST-MD5\r\n"); 248 } 249 250 $tmp = fgets($stream,1024); 251 252 if ($this->errorCheck($tmp,$stream)) { 253 return(0); 254 } 255 256 // At this point, $tmp should hold "334 <challenge string>" 257 $chall = substr($tmp,4); 258 // Depending on mechanism, generate response string 259 if ($smtp_auth_mech == 'cram-md5') { 260 $response = cram_md5_response($user,$pass,$chall); 261 } elseif ($smtp_auth_mech == 'digest-md5') { 262 $response = digest_md5_response($user,$pass,$chall,'smtp',$host); 263 } 264 fputs($stream, $response); 265 266 // Let's see what the server had to say about that 267 $tmp = fgets($stream,1024); 268 if ($this->errorCheck($tmp,$stream)) { 269 return(0); 270 } 271 272 // CRAM-MD5 is done at this point. If DIGEST-MD5, there's a bit more to go 273 if ($smtp_auth_mech == 'digest-md5') { 274 // $tmp contains rspauth, but I don't store that yet. (No need yet) 275 fputs($stream,"\r\n"); 276 $tmp = fgets($stream,1024); 277 278 if ($this->errorCheck($tmp,$stream)) { 279 return(0); 280 } 281 } 282 // CRAM-MD5 and DIGEST-MD5 code ends here 283 } elseif ($smtp_auth_mech == 'none') { 284 // No auth at all, just send helo and then send the mail 285 // We already said hi earlier, nothing more is needed. 286 } elseif ($smtp_auth_mech == 'login') { 287 // The LOGIN method 288 fputs($stream, "AUTH LOGIN\r\n"); 289 $tmp = fgets($stream, 1024); 290 291 if ($this->errorCheck($tmp, $stream)) { 292 return(0); 293 } 294 fputs($stream, base64_encode ($user) . "\r\n"); 295 $tmp = fgets($stream, 1024); 296 if ($this->errorCheck($tmp, $stream)) { 297 return(0); 298 } 299 300 fputs($stream, base64_encode($pass) . "\r\n"); 301 $tmp = fgets($stream, 1024); 302 if ($this->errorCheck($tmp, $stream)) { 303 return(0); 304 } 305 } elseif ($smtp_auth_mech == "plain") { 306 /* SASL Plain */ 307 $auth = base64_encode("$user\0$user\0$pass"); 308 309 $query = "AUTH PLAIN\r\n"; 310 fputs($stream, $query); 311 $read=fgets($stream, 1024); 312 313 if (substr($read,0,3) == '334') { // OK so far.. 314 fputs($stream, "$auth\r\n"); 315 $read = fgets($stream, 1024); 316 } 317 318 $results=explode(" ",$read,3); 319 $response=$results[1]; 320 $message=$results[2]; 321 } else { 322 /* Right here, they've reached an unsupported auth mechanism. 323 This is the ugliest hack I've ever done, but it'll do till I can fix 324 things up better tomorrow. So tired... */ 325 if ($this->errorCheck("535 Unable to use this auth type",$stream)) { 326 return(0); 327 } 328 } 329 330 /* Ok, who is sending the message? */ 331 $fromaddress = (strlen($from->mailbox) && $from->host) ? 332 $from->mailbox.'@'.$from->host : ''; 333 fputs($stream, 'MAIL FROM:<'.$fromaddress.">\r\n"); 334 $tmp = fgets($stream, 1024); 335 if ($this->errorCheck($tmp, $stream)) { 336 return(0); 337 } 338 339 /* send who the recipients are */ 340 for ($i = 0, $cnt = count($to); $i < $cnt; $i++) { 341 if (!$to[$i]->host) $to[$i]->host = $domain; 342 if (strlen($to[$i]->mailbox)) { 343 fputs($stream, 'RCPT TO:<'.$to[$i]->mailbox.'@'.$to[$i]->host.">\r\n"); 344 $tmp = fgets($stream, 1024); 345 if ($this->errorCheck($tmp, $stream)) { 346 return(0); 347 } 348 } 349 } 350 351 for ($i = 0, $cnt = count($cc); $i < $cnt; $i++) { 352 if (!$cc[$i]->host) $cc[$i]->host = $domain; 353 if (strlen($cc[$i]->mailbox)) { 354 fputs($stream, 'RCPT TO:<'.$cc[$i]->mailbox.'@'.$cc[$i]->host.">\r\n"); 355 $tmp = fgets($stream, 1024); 356 if ($this->errorCheck($tmp, $stream)) { 357 return(0); 358 } 359 } 360 } 361 362 for ($i = 0, $cnt = count($bcc); $i < $cnt; $i++) { 363 if (!$bcc[$i]->host) $bcc[$i]->host = $domain; 364 if (strlen($bcc[$i]->mailbox)) { 365 fputs($stream, 'RCPT TO:<'.$bcc[$i]->mailbox.'@'.$bcc[$i]->host.">\r\n"); 366 $tmp = fgets($stream, 1024); 367 if ($this->errorCheck($tmp, $stream)) { 368 return(0); 369 } 370 } 371 } 372 /* Lets start sending the actual message */ 373 fputs($stream, "DATA\r\n"); 374 $tmp = fgets($stream, 1024); 375 if ($this->errorCheck($tmp, $stream)) { 376 return(0); 377 } 378 return $stream; 379 } 380 381 function finalizeStream($stream) { 382 fputs($stream, "\r\n.\r\n"); /* end the DATA part */ 383 $tmp = fgets($stream, 1024); 384 $this->errorCheck($tmp, $stream); 385 if ($this->dlv_ret_nr != 250) { 386 return(0); 387 } 388 fputs($stream, "QUIT\r\n"); /* log off */ 389 fclose($stream); 390 return true; 391 } 392 393 /* check if an SMTP reply is an error and set an error message) */ 394 function errorCheck($line, $smtpConnection) { 395 396 $err_num = substr($line, 0, 3); 397 $this->dlv_ret_nr = $err_num; 398 $server_msg = substr($line, 4); 399 400 while(substr($line, 0, 4) == ($err_num.'-')) { 401 $line = fgets($smtpConnection, 1024); 402 $server_msg .= substr($line, 4); 403 } 404 405 if ( ((int) $err_num[0]) < 4) { 406 return false; 407 } 408 409 switch ($err_num) { 410 case '421': $message = _("Service not available, closing channel"); 411 break; 412 case '432': $message = _("A password transition is needed"); 413 break; 414 case '450': $message = _("Requested mail action not taken: mailbox unavailable"); 415 break; 416 case '451': $message = _("Requested action aborted: error in processing"); 417 break; 418 case '452': $message = _("Requested action not taken: insufficient system storage"); 419 break; 420 case '454': $message = _("Temporary authentication failure"); 421 break; 422 case '500': $message = _("Syntax error; command not recognized"); 423 break; 424 case '501': $message = _("Syntax error in parameters or arguments"); 425 break; 426 case '502': $message = _("Command not implemented"); 427 break; 428 case '503': $message = _("Bad sequence of commands"); 429 break; 430 case '504': $message = _("Command parameter not implemented"); 431 break; 432 case '530': $message = _("Authentication required"); 433 break; 434 case '534': $message = _("Authentication mechanism is too weak"); 435 break; 436 case '535': $message = _("Authentication failed"); 437 break; 438 case '538': $message = _("Encryption required for requested authentication mechanism"); 439 break; 440 case '550': $message = _("Requested action not taken: mailbox unavailable"); 441 break; 442 case '551': $message = _("User not local; please try forwarding"); 443 break; 444 case '552': $message = _("Requested mail action aborted: exceeding storage allocation"); 445 break; 446 case '553': $message = _("Requested action not taken: mailbox name not allowed"); 447 break; 448 case '554': $message = _("Transaction failed"); 449 break; 450 default: $message = _("Unknown response"); 451 break; 452 } 453 454 $this->dlv_msg = $message; 455 $this->dlv_server_msg = nl2br(sm_encode_html_special_chars($server_msg)); 456 457 return true; 458 } 459 460 function authPop($user, $pass, $pop_server='', $pop_port='') { 461 if (!$pop_port) { 462 $pop_port = 110; 463 } 464 if (!$pop_server) { 465 $pop_server = 'localhost'; 466 } 467 $popConnection = @fsockopen($pop_server, $pop_port, $err_no, $err_str); 468 if (!$popConnection) { 469 error_log("Error connecting to POP Server ($pop_server:$pop_port)" 470 . " $err_no : $err_str"); 471 } else { 472 $tmp = fgets($popConnection, 1024); /* banner */ 473 if (substr($tmp, 0, 3) != '+OK') { 474 return(0); 475 } 476 fputs($popConnection, "USER $user\r\n"); 477 $tmp = fgets($popConnection, 1024); 478 if (substr($tmp, 0, 3) != '+OK') { 479 return(0); 480 } 481 fputs($popConnection, 'PASS ' . $pass . "\r\n"); 482 $tmp = fgets($popConnection, 1024); 483 if (substr($tmp, 0, 3) != '+OK') { 484 return(0); 485 } 486 fputs($popConnection, "QUIT\r\n"); /* log off */ 487 fclose($popConnection); 488 } 489 } 490 491 /** 492 * Parses ESMTP EHLO response (rfc1869) 493 * 494 * Reads SMTP response to EHLO command and fills class variables 495 * (ehlo array and domain string). Returns last line. 496 * @param stream $stream smtp connection stream. 497 * @return string last ehlo line 498 * @since 1.4.23 and 1.5.1 499 */ 500 function parse_ehlo_response($stream) { 501 // don't cache ehlo information 502 $this->ehlo=array(); 503 $ret = ''; 504 $firstline = true; 505 /** 506 * ehlo mailclient.example.org 507 * 250-mail.example.org 508 * 250-PIPELINING 509 * 250-SIZE 52428800 510 * 250-DATAZ 511 * 250-STARTTLS 512 * 250-AUTH LOGIN PLAIN 513 * 250 8BITMIME 514 */ 515 while ($line=fgets($stream, 1024)){ 516 // match[1] = first symbol after 250 517 // match[2] = domain or ehlo-keyword 518 // match[3] = greeting or ehlo-param 519 // match space after keyword in ehlo-keyword CR LF 520 if (preg_match("/^250(-|\s)(\S*)\s+(\S.*)\r\n/",$line,$match)|| 521 preg_match("/^250(-|\s)(\S*)\s*\r\n/",$line,$match)) { 522 if ($firstline) { 523 // first ehlo line (250[-\ ]domain SP greeting) 524 $this->domain = $match[2]; 525 $firstline=false; 526 } elseif (!isset($match[3])) { 527 // simple one word extension 528 $this->ehlo[strtoupper($match[2])]=''; 529 } elseif (!preg_match("/\s/",trim($match[3]))) { 530 // extension with one option 531 // yes, I know about ctype extension. no, i don't want to depend on it 532 $this->ehlo[strtoupper($match[2])]=trim($match[3]); 533 } else { 534 // ehlo-param with spaces 535 $this->ehlo[strtoupper($match[2])]=explode(' ',trim($match[3])); 536 } 537 if ($match[1]==' ') { 538 // stop while cycle, if we reach last 250 line 539 $ret = $line; 540 break; 541 } 542 } else { 543 // this is not 250 response 544 $ret = $line; 545 break; 546 } 547 } 548 return $ret; 549 } 550 551} 552 553