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 14840 2020-01-07 07:42:38Z pdontthink $
  11.  * @package squirrelmail
  12.  */
  13.  
  14. /** This of course depends upon Deliver */
  15. require_once(SM_PATH 'class/deliver/Deliver.class.php');
  16.  
  17. /**
  18.  * Deliver messages using SMTP
  19.  * @package squirrelmail
  20.  */
  21. class 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($pop_host''$user$pass);
  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_timeoutSTREAM_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($stream1024);
  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_HOSTSQ_SERVER)) // HTTP_HOST is set
  137.             // optionally trim off port number
  138.             if($p strrpos($HTTP_HOST':')) {
  139.                 $HTTP_HOST substr($HTTP_HOST0$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_args1)) {
  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($stream1024);
  290.  
  291.             if ($this->errorCheck($tmp$stream)) {
  292.                 return(0);
  293.             }
  294.             fputs($streambase64_encode ($user"\r\n");
  295.             $tmp fgets($stream1024);
  296.             if ($this->errorCheck($tmp$stream)) {
  297.                 return(0);
  298.             }
  299.  
  300.             fputs($streambase64_encode($pass"\r\n");
  301.             $tmp fgets($stream1024);
  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($stream1024);
  312.  
  313.             if (substr($read,0,3== '334'// OK so far..
  314.                 fputs($stream"$auth\r\n");
  315.                 $read fgets($stream1024);
  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($stream1024);
  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($stream1024);
  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($stream1024);
  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($stream1024);
  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($stream1024);
  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($stream1024);
  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($line03);
  397.         $this->dlv_ret_nr $err_num;
  398.         $server_msg substr($line4);
  399.  
  400.         while(substr($line04== ($err_num.'-')) {
  401.             $line fgets($smtpConnection1024);
  402.             $server_msg .= substr($line4);
  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($pop_server=''$pop_port=''$user$pass{
  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($popConnection1024)/* banner */
  473.             if (substr($tmp03!= '+OK'{
  474.                 return(0);
  475.             }
  476.             fputs($popConnection"USER $user\r\n");
  477.             $tmp fgets($popConnection1024);
  478.             if (substr($tmp03!= '+OK'{
  479.                 return(0);
  480.             }
  481.             fputs($popConnection'PASS ' $pass "\r\n");
  482.             $tmp fgets($popConnection1024);
  483.             if (substr($tmp03!= '+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($stream1024)){
  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. }

Documentation generated on Mon, 13 Jan 2020 04:24:32 +0100 by phpDocumentor 1.4.3