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-2014 The SquirrelMail Project Team
  9.  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  10.  * @version $Id: Deliver_SMTP.class.php 14437 2014-01-21 22:15:03Z 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.         /* Lets introduce ourselves */
  158.         fputs($stream"EHLO $helohost\r\n");
  159.         // Read ehlo response
  160.         $tmp $this->parse_ehlo_response($stream);
  161.         if ($this->errorCheck($tmp,$stream)) {
  162.             // fall back to HELO if EHLO is not supported (error 5xx)
  163.             if ($this->dlv_ret_nr{0== '5'{
  164.                 fputs($stream"HELO $helohost\r\n");
  165.                 $tmp fgets($stream,1024);
  166.                 if ($this->errorCheck($tmp,$stream)) {
  167.                     return(0);
  168.                 }
  169.             else {
  170.                 return(0);
  171.             }
  172.         }
  173.  
  174.         /**
  175.          * Implementing SMTP STARTTLS (rfc2487) in php 5.1.0+
  176.          * http://www.php.net/stream-socket-enable-crypto
  177.          */
  178.         if ($use_smtp_tls === 2{
  179.             if (function_exists('stream_socket_enable_crypto')) {
  180.                 // don't try starting tls, when client thinks that it is already active
  181.                 if ($this->tls_enabled{
  182.                     $this->dlv_msg = _("TLS session is already activated.");
  183.                     return 0;
  184.                 elseif (!array_key_exists('STARTTLS',$this->ehlo)) {
  185.                     // check for starttls in ehlo response
  186.                     $this->dlv_msg _("SMTP STARTTLS is enabled in SquirrelMail configuration, but used SMTP server does not support it");
  187.                     return 0;
  188.                 }
  189.  
  190.                 // issue starttls command
  191.                 fputs($stream"STARTTLS\r\n");
  192.                 // get response
  193.                 $tmp fgets($stream,1024);
  194.                 if ($this->errorCheck($tmp,$stream)) {
  195.                     return 0;
  196.                 }
  197.  
  198.                 // start crypto on connection. suppress function errors.
  199.                 if (@stream_socket_enable_crypto($stream,true,STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
  200.                     // starttls was successful (rfc2487 5.2 Result of the STARTTLS Command)
  201.                     // get new EHLO response
  202.                     fputs($stream"EHLO $helohost\r\n");
  203.                     // Read ehlo response
  204.                     $tmp $this->parse_ehlo_response($stream);
  205.                     if ($this->errorCheck($tmp,$stream)) {
  206.                         // don't revert to helo here. server must support ESMTP
  207.                         return 0;
  208.                     }
  209.                     // set information about started tls
  210.                     $this->tls_enabled true;
  211.                 else {
  212.                     /**
  213.                      * stream_socket_enable_crypto() call failed.
  214.                      */
  215.                     $this->dlv_msg _("Unable to start TLS.");
  216.                     return 0;
  217.                     // Bug: can't get error message. See comments in sqimap_create_stream().
  218.                 }
  219.             else {
  220.                 // php install does not support stream_socket_enable_crypto() function
  221.                 $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.");
  222.                 return 0;
  223.             }
  224.         }
  225.  
  226.         // FIXME: check ehlo response before using authentication
  227.  
  228.         // Try authentication by a plugin
  229.         //
  230.         // NOTE: there is another hook in functions/auth.php called "smtp_auth" 
  231.         // that allows a plugin to specify a different set of login credentials 
  232.         // (so is slightly mis-named, but is too old to change), so be careful 
  233.         // that you do not confuse your hook names.
  234.         //
  235.         $smtp_auth_args array(
  236.             'auth_mech' => $smtp_auth_mech,
  237.             'user' => $user,
  238.             'pass' => $pass,
  239.             'host' => $host,
  240.             'port' => $port,
  241.             'stream' => $stream,
  242.         );
  243.         if (boolean_hook_function('smtp_authenticate'$smtp_auth_args1)) {
  244.             // authentication succeeded
  245.         else if (( $smtp_auth_mech == 'cram-md5'or $smtp_auth_mech == 'digest-md5' )) {
  246.             // Doing some form of non-plain auth
  247.             if ($smtp_auth_mech == 'cram-md5'{
  248.                 fputs($stream"AUTH CRAM-MD5\r\n");
  249.             elseif ($smtp_auth_mech == 'digest-md5'{
  250.                 fputs($stream"AUTH DIGEST-MD5\r\n");
  251.             }
  252.  
  253.             $tmp fgets($stream,1024);
  254.  
  255.             if ($this->errorCheck($tmp,$stream)) {
  256.                 return(0);
  257.             }
  258.  
  259.             // At this point, $tmp should hold "334 <challenge string>"
  260.             $chall substr($tmp,4);
  261.             // Depending on mechanism, generate response string
  262.             if ($smtp_auth_mech == 'cram-md5'{
  263.                 $response cram_md5_response($user,$pass,$chall);
  264.             elseif ($smtp_auth_mech == 'digest-md5'{
  265.                 $response digest_md5_response($user,$pass,$chall,'smtp',$host);
  266.             }
  267.             fputs($stream$response);
  268.  
  269.             // Let's see what the server had to say about that
  270.             $tmp fgets($stream,1024);
  271.             if ($this->errorCheck($tmp,$stream)) {
  272.                 return(0);
  273.             }
  274.  
  275.             // CRAM-MD5 is done at this point.  If DIGEST-MD5, there's a bit more to go
  276.             if ($smtp_auth_mech == 'digest-md5'{
  277.                 // $tmp contains rspauth, but I don't store that yet. (No need yet)
  278.                 fputs($stream,"\r\n");
  279.                 $tmp fgets($stream,1024);
  280.  
  281.                 if ($this->errorCheck($tmp,$stream)) {
  282.                 return(0);
  283.                 }
  284.             }
  285.         // CRAM-MD5 and DIGEST-MD5 code ends here
  286.         elseif ($smtp_auth_mech == 'none'{
  287.         // No auth at all, just send helo and then send the mail
  288.         // We already said hi earlier, nothing more is needed.
  289.         elseif ($smtp_auth_mech == 'login'{
  290.             // The LOGIN method
  291.             fputs($stream"AUTH LOGIN\r\n");
  292.             $tmp fgets($stream1024);
  293.  
  294.             if ($this->errorCheck($tmp$stream)) {
  295.                 return(0);
  296.             }
  297.             fputs($streambase64_encode ($user"\r\n");
  298.             $tmp fgets($stream1024);
  299.             if ($this->errorCheck($tmp$stream)) {
  300.                 return(0);
  301.             }
  302.  
  303.             fputs($streambase64_encode($pass"\r\n");
  304.             $tmp fgets($stream1024);
  305.             if ($this->errorCheck($tmp$stream)) {
  306.                 return(0);
  307.             }
  308.         elseif ($smtp_auth_mech == "plain"{
  309.             /* SASL Plain */
  310.             $auth base64_encode("$user\0$user\0$pass");
  311.  
  312.             $query "AUTH PLAIN\r\n";
  313.             fputs($stream$query);
  314.             $read=fgets($stream1024);
  315.  
  316.             if (substr($read,0,3== '334'// OK so far..
  317.                 fputs($stream"$auth\r\n");
  318.                 $read fgets($stream1024);
  319.             }
  320.  
  321.             $results=explode(" ",$read,3);
  322.             $response=$results[1];
  323.             $message=$results[2];
  324.         else {
  325.             /* Right here, they've reached an unsupported auth mechanism.
  326.             This is the ugliest hack I've ever done, but it'll do till I can fix
  327.             things up better tomorrow.  So tired... */
  328.             if ($this->errorCheck("535 Unable to use this auth type",$stream)) {
  329.                 return(0);
  330.             }
  331.         }
  332.  
  333.         /* Ok, who is sending the message? */
  334.         $fromaddress (strlen($from->mailbox&& $from->host?
  335.             $from->mailbox.'@'.$from->host '';
  336.         fputs($stream'MAIL FROM:<'.$fromaddress.">\r\n");
  337.         $tmp fgets($stream1024);
  338.         if ($this->errorCheck($tmp$stream)) {
  339.             return(0);
  340.         }
  341.  
  342.         /* send who the recipients are */
  343.         for ($i 0$cnt count($to)$i $cnt$i++{
  344.             if (!$to[$i]->host$to[$i]->host $domain;
  345.             if (strlen($to[$i]->mailbox)) {
  346.                 fputs($stream'RCPT TO:<'.$to[$i]->mailbox.'@'.$to[$i]->host.">\r\n");
  347.                 $tmp fgets($stream1024);
  348.                 if ($this->errorCheck($tmp$stream)) {
  349.                     return(0);
  350.                 }
  351.             }
  352.         }
  353.  
  354.         for ($i 0$cnt count($cc)$i $cnt$i++{
  355.             if (!$cc[$i]->host$cc[$i]->host $domain;
  356.             if (strlen($cc[$i]->mailbox)) {
  357.                 fputs($stream'RCPT TO:<'.$cc[$i]->mailbox.'@'.$cc[$i]->host.">\r\n");
  358.                 $tmp fgets($stream1024);
  359.                 if ($this->errorCheck($tmp$stream)) {
  360.                     return(0);
  361.                 }
  362.             }
  363.         }
  364.  
  365.         for ($i 0$cnt count($bcc)$i $cnt$i++{
  366.             if (!$bcc[$i]->host$bcc[$i]->host $domain;
  367.             if (strlen($bcc[$i]->mailbox)) {
  368.                 fputs($stream'RCPT TO:<'.$bcc[$i]->mailbox.'@'.$bcc[$i]->host.">\r\n");
  369.                 $tmp fgets($stream1024);
  370.                 if ($this->errorCheck($tmp$stream)) {
  371.                     return(0);
  372.                 }
  373.             }
  374.         }
  375.         /* Lets start sending the actual message */
  376.         fputs($stream"DATA\r\n");
  377.         $tmp fgets($stream1024);
  378.         if ($this->errorCheck($tmp$stream)) {
  379.                 return(0);
  380.         }
  381.         return $stream;
  382.     }
  383.  
  384.     function finalizeStream($stream{
  385.         fputs($stream"\r\n.\r\n")/* end the DATA part */
  386.         $tmp fgets($stream1024);
  387.         $this->errorCheck($tmp$stream);
  388.         if ($this->dlv_ret_nr != 250{
  389.                 return(0);
  390.         }
  391.         fputs($stream"QUIT\r\n")/* log off */
  392.         fclose($stream);
  393.         return true;
  394.     }
  395.  
  396.     /* check if an SMTP reply is an error and set an error message) */
  397.     function errorCheck($line$smtpConnection{
  398.  
  399.         $err_num substr($line03);
  400.         $this->dlv_ret_nr $err_num;
  401.         $server_msg substr($line4);
  402.  
  403.         while(substr($line04== ($err_num.'-')) {
  404.             $line fgets($smtpConnection1024);
  405.             $server_msg .= substr($line4);
  406.         }
  407.  
  408.         if ( ((int) $err_num{0}4{
  409.             return false;
  410.         }
  411.  
  412.         switch ($err_num{
  413.         case '421'$message _("Service not available, closing channel");
  414.             break;
  415.         case '432'$message _("A password transition is needed");
  416.             break;
  417.         case '450'$message _("Requested mail action not taken: mailbox unavailable");
  418.             break;
  419.         case '451'$message _("Requested action aborted: error in processing");
  420.             break;
  421.         case '452'$message _("Requested action not taken: insufficient system storage");
  422.             break;
  423.         case '454'$message _("Temporary authentication failure");
  424.             break;
  425.         case '500'$message _("Syntax error; command not recognized");
  426.             break;
  427.         case '501'$message _("Syntax error in parameters or arguments");
  428.             break;
  429.         case '502'$message _("Command not implemented");
  430.             break;
  431.         case '503'$message _("Bad sequence of commands");
  432.             break;
  433.         case '504'$message _("Command parameter not implemented");
  434.             break;
  435.         case '530'$message _("Authentication required");
  436.             break;
  437.         case '534'$message _("Authentication mechanism is too weak");
  438.             break;
  439.         case '535'$message _("Authentication failed");
  440.             break;
  441.         case '538'$message _("Encryption required for requested authentication mechanism");
  442.             break;
  443.         case '550'$message _("Requested action not taken: mailbox unavailable");
  444.             break;
  445.         case '551'$message _("User not local; please try forwarding");
  446.             break;
  447.         case '552'$message _("Requested mail action aborted: exceeding storage allocation");
  448.             break;
  449.         case '553'$message _("Requested action not taken: mailbox name not allowed");
  450.             break;
  451.         case '554'$message _("Transaction failed");
  452.             break;
  453.         default:    $message _("Unknown response");
  454.             break;
  455.         }
  456.  
  457.         $this->dlv_msg $message;
  458.         $this->dlv_server_msg $server_msg;
  459.  
  460.         return true;
  461.     }
  462.  
  463.     function authPop($pop_server=''$pop_port=''$user$pass{
  464.         if (!$pop_port{
  465.             $pop_port 110;
  466.         }
  467.         if (!$pop_server{
  468.             $pop_server 'localhost';
  469.         }
  470.         $popConnection @fsockopen($pop_server$pop_port$err_no$err_str);
  471.         if (!$popConnection{
  472.             error_log("Error connecting to POP Server ($pop_server:$pop_port)"
  473.                 . " $err_no : $err_str");
  474.             return false;
  475.         else {
  476.             $tmp fgets($popConnection1024)/* banner */
  477.             if (substr($tmp03!= '+OK'{
  478.                 return false;
  479.             }
  480.             fputs($popConnection"USER $user\r\n");
  481.             $tmp fgets($popConnection1024);
  482.             if (substr($tmp03!= '+OK'{
  483.                 return false;
  484.             }
  485.             fputs($popConnection'PASS ' $pass "\r\n");
  486.             $tmp fgets($popConnection1024);
  487.             if (substr($tmp03!= '+OK'{
  488.                 return false;
  489.             }
  490.             fputs($popConnection"QUIT\r\n")/* log off */
  491.             fclose($popConnection);
  492.         }
  493.         return true;
  494.     }
  495.  
  496.     /**
  497.      * Parses ESMTP EHLO response (rfc1869)
  498.      *
  499.      * Reads SMTP response to EHLO command and fills class variables
  500.      * (ehlo array and domain string). Returns last line.
  501.      * @param stream $stream smtp connection stream.
  502.      * @return string last ehlo line
  503.      * @since 1.5.1
  504.      */
  505.     function parse_ehlo_response($stream{
  506.         // don't cache ehlo information
  507.         $this->ehlo=array();
  508.         $ret '';
  509.         $firstline true;
  510.         /**
  511.          * ehlo mailclient.example.org
  512.          * 250-mail.example.org
  513.          * 250-PIPELINING
  514.          * 250-SIZE 52428800
  515.          * 250-DATAZ
  516.          * 250-STARTTLS
  517.          * 250-AUTH LOGIN PLAIN
  518.          * 250 8BITMIME
  519.          */
  520.         while ($line=fgets($stream1024)){
  521.             // match[1] = first symbol after 250
  522.             // match[2] = domain or ehlo-keyword
  523.             // match[3] = greeting or ehlo-param
  524.             // match space after keyword in ehlo-keyword CR LF
  525.             if (preg_match("/^250(-|\s)(\S*)\s+(\S.*)\r\n/",$line,$match)||
  526.                 preg_match("/^250(-|\s)(\S*)\s*\r\n/",$line,$match)) {
  527.                 if ($firstline{
  528.                     // first ehlo line (250[-\ ]domain SP greeting)
  529.                     $this->domain $match[2];
  530.                     $firstline=false;
  531.                 elseif (!isset($match[3])) {
  532.                     // simple one word extension
  533.                     $this->ehlo[strtoupper($match[2])]='';
  534.                 elseif (!preg_match("/\s/",trim($match[3]))) {
  535.                     // extension with one option
  536.                     // yes, I know about ctype extension. no, i don't want to depend on it
  537.                     $this->ehlo[strtoupper($match[2])]=trim($match[3]);
  538.                 else {
  539.                     // ehlo-param with spaces
  540.                     $this->ehlo[strtoupper($match[2])]=explode(' ',trim($match[3]));
  541.                 }
  542.                 if ($match[1]==' '{
  543.                     // stop while cycle, if we reach last 250 line
  544.                     $ret $line;
  545.                     break;
  546.                 }
  547.             else {
  548.                 // this is not 250 response
  549.                 $ret $line;
  550.                 break;
  551.             }
  552.         }
  553.         return $ret;
  554.     }
  555. }

Documentation generated on Thu, 23 Oct 2014 04:18:23 +0200 by phpDocumentor 1.4.3