Source for file Deliver_SMTP.class.php

Documentation is available at Deliver_SMTP.class.php

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

Documentation generated on Mon, 13 Jan 2020 04:22:23 +0100 by phpDocumentor 1.4.3