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

Documentation generated on Thu, 20 Jun 2013 04:19:17 +0200 by phpDocumentor 1.4.3