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

Documentation generated on Sat, 07 Oct 2006 16:10:40 +0300 by phpDocumentor 1.3.0RC6