Login   Register  
PHP Classes
elePHPant
Icontem

File: smtp.class.inc

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Steffen Stollfu▀  >  smtp.class.inc  >  smtp.class.inc  >  Download  
File: smtp.class.inc
Role: Class source
Content type: text/plain
Description: PHP5 Class File
Class: smtp.class.inc
provide access on the smtp protocol
Author: By
Last change: a complete rewrite from scratch for PHP5

version 0.7.1-dev:
- support for socket extension and stream functions
- IPv6 support (But this I had never tested)
- New Trac/SVN page for better support on future request and bugs
http://trac.assembla.com/smtp-class-inc
- using of php5 exception handling mechanism
- multiple message sending support over one connection (The old class doesn't support this)
Date: 6 years ago
Size: 19,520 bytes
 

Contents

Class file image Download
<?php
/**
 * The SMTP Class provide access to a smtp server.
 * Multiple message sending is possible, too.
 *  
 *
 * @package: smtp.class.inc
 * @author: Steffen 'j0inty' Stollfu├č
 * @created 12.06.2008
 * @copyright: Steffen 'j0inty' Stollfu├č
 * @version: 0.7.1-dev
 */
/**
 * Class SMTP_Exception
 *
 * @author j0inty
 * @version 1.1.0-final
 */
class SMTP_Exception extends Exception
{
	/**
	 * SMTP Exception constructor
	 * 
	 * @param integer $intErrorCode
	 * @param string $strErrorMessage
	 */
	function __construct($intErrorCode, $strErrorMessage = null)
	{
		switch($intErrorCode)
		{
			case SMTP::ERR_SOCKETS:
				$strErrorMessage = "Sockets Error: (". socket_last_error() .") -- ". socket_strerror(socket_last_error());
			break;
			
			case SMTP::ERR_NOT_IMPLEMENTED:
				$strErrorMessage = "Not implemented now.";
			break;
		}
		parent::__construct($strErrorMessage, $intErrorCode);
	}
	/**
	 * Store the Exception string to a given file
	 * 
	 * @param string $strLogFile  logfile name with path
	 */
	public function saveToFile($strLogFile)
	{
		if( !$resFp = @fopen($strLogFile,"a+") )
		{
			return false;
		}
		$strMsg = date("Y-m-d H:i:s -- ") . $this;
		if( !@fputs($resFp, $strMsg, strlen($strMsg)) )
		{
			return false;
		}
		@fclose($resFp);
	}
	/**
	 * toString method
	 */
	public function __toString()
	{
		return __CLASS__ ."[". $this->getCode() ."] -- ". $this->getMessage() ." in file ". $this->getFile() ." at line ". $this->getLine().PHP_EOL."Trace: ". $this->getTraceAsString() .PHP_EOL;
	}
}

/**
 * SMTP class
 * 
 * @author j0inty
 */
class SMTP
{
	/**
	 * E-Mail Priotities
	 * 
	 * @var const integer
	 */
	const MESSAGE_PRIO_LOW = 5;
	const MESSAGE_PRIO_MEDIUM = 3;
	const MESSAGE_PRIO_HIGH = 1;
	/**
	 * @var const string XMAILER
	 */
	const XMAILER = "smtp.class.php 0.7.1-final -- Steffen 'j0inty' Stollfuss";
	/**
	 * @var const integer No errors occured
	 * @access public
	 */
	const ERR_NONE = 0;
	/**
	 * @var const integer parameter error
	 */
	const ERR_PARAMETER = 1;
	/**
	 * @var const integer logging error
	 */
	const ERR_LOG = 2;
	/**
	 * @var const integer sockets extension error
	 */
	const ERR_SOCKETS = 3;
	/**
	 * @var const integer sockets extension error
	 */
	const ERR_STREAM = 4;
	/**
	 * @var const integer invalid response code came from the server
	 */
	const ERR_INVALID_RESPONSE = 5;
	/**
	 * @var const integer comes when a function isn't implemented yet
	 */
	const ERR_NOT_IMPLEMENTED = 20;
	/**
	 * Authorization method constant
	 */
	const AUTH_PLAIN = 100;
	/**
	 * Regular Expression for a valid email adress
	 * I know that this regex isn't the best
	 * 
	 */
	const REGEX_EMAIL = "((<)|([A-Za-z0-9._-](\+[A-Za-z0-9])*)+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}|(>))";
	/**
	 * default buffer size for socket reads
	 * 
	 * @var const integer
	 */
	const DEFAULT_BUFFER_SIZE = 4096;
	/**
	 * @var string SMTP Server Hostname
	 * @access private
	 */
	private $strHostname = null;
	/**
	 * @var string $strIPAdress
	 * @access private
	 */
	private $strIPAdress = null;
	/**
	 * @var integer SMTP Server Port
	 * @access private
	 */
	private $intPort = 25;
	/**
	 * @var resource Socket
	 * @access private
	 */
	private $resSocket = false;
	/**
	 * @var boolean $bSocketConnected
	 */
	private $bSocketConnected = false;
	/**
	 * @var array Connection Timeout
	 * @access private
	 */
	private $arrConnectionTimeout = array("sec" => "10", "msec" => "0");
	/**
	 * @var boolean Using sockets extension
	 * @access private
	 */
	private $bUseSockets = true;
	/**
	 * @var boolean  Hide the username at the log file (sha256)
	 * @access private
	 */
	private $bHideUsernameAtLog = true;
	/**
	 * @var string log path to file
	 * @access private
	 */
	private $strLogFile = null;
	/**
	 * @var boolean log filedescriptor opened
	 * @access private
	 */
	private $bLogOpened = false;
	/**
	 * @var resource log file descriptor
	 * @access private 
	 */
	private $resLogFp = false;
	/**
	 * @var string  Use it as prefix for every log line
	 * @access private
	 */
	private $strLogDatetimeFormat = "Y-m-d H:i:s";
	/**
	 * constructor
	 * 
	 * @param string $strLogFile filename where the class will log
	 * @param boolean $bHideUsernameAtLog should the username hide in the logfile
	 * @param boolean $bUseSockets should the class use the sockets extension ?
	 * 
	 * @return SMTP SMTP class object
	 * @throw SMTP_Exception
	 * @access public
	 */
	function __construct( $strLogFile = null, $bHideUsernameAtLog = true, $bUseSockets = true)
	{
		// Check input
		if( !is_bool($bHideUsernameAtLog) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER,"Invalid hide username at logfile parameter given");
		}
		if( !is_bool($bUseSockets) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER,"Invalid UseSockets parameter given");
		}
		elseif( $bUseSockets && !extension_loaded("sockets") )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER,"You choose the socket extension support, but this isn't available");
		}
		if( is_string($strLogFile) )
		{
			$this->strLogFile = $strLogFile;
			$this->openLog();
		}
		$this->bHideUsernameAtLog = $bHideUsernameAtLog;
		$this->bUseSockets = $bUseSockets;
	}
	
	/**
	 * destructor
	 * 
	 * @access public
	 * @throw SMTP_Exception
	 */
	function __destruct()
	{
		$this->disconnect();
		$this->closeLog();
	}
	/**
	 * connect to the smtp server
	 * 
	 * @param string $strHostname  IP or hostname of the smtp server
	 * @param integer $intPort  Port where the smtp server is listen to
	 * @param array $arrConnectionTimeout
	 * @param boolean $bIPv6
	 * 
	 * @access public
	 * @throw SMTP_Exception
	 */
	public function connect( $strHostname, $intPort = 25, $arrConnectionTimeout = null, $bIPv6 = false)
	{
		if( !is_string($strHostname) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER,"Invalid hostname string given");
		}
		if( !is_int($intPort) && $intPort > 0 && $intPort < 65535 )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER,"Invalid port given");
		}
		if( !is_bool($bIPv6) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER,"Invalid ipv6 given");
		}
		
		if( $this->bUseSockets )
		{
			if( !$this->resSocket = @socket_create( (($bIPv6) ? AF_INET6 : AF_INET), SOL_SOCKET, SOL_TCP ) )
			{
				throw new SMTP_Exception(self::ERR_SOCKETS);
			}
			
			$this->log((($bIPv6) ? "AF_INET6" : "AF_INET") ."-TCP-Socket created.");
			
			if(!is_null($arrConnectionTimeout) )
			{
				$this->setSocketTimeout($arrConnectionTimeout);
			}
			
			if( !@socket_connect($this->resSocket,$strHostname,$intPort)
				|| !@socket_getpeername($this->resSocket,$this->strIPAdress) )
			{
				throw new SMTP_Exception(self::ERR_SOCKETS);
			}
		}
		else
		{
			$dTimeout = (double) implode(".",$arrConnectionTimeout);
			if( !@fsockopen("tcp://". $strHostname .":". $intPort, &$intErrno, &$strError, $dTimeout) )
			{
				throw new SMTP_Exception(self::ERR_STREAM,"fopen: [". $intErrno ."] -- ". $strError);
			}
			if(!is_null($arrConnectionTimeout) ) $this->setSocketTimeout($arrConnectionTimeout);
			$this->strIPAdress = @gethostbyname($strHostname);
		}
		$this->strHostname = $strHostname;
		$this->intPort = $intPort;
		$this->bSocketConnected = true;
		$this->log("socket: Connected to ". $this->strIPAdress .":". $intPort ." [". $strHostname ."]");
		// Welcome message from the server
		$this->parseResponse("220");
		// EHLO Stuff
		$this->sendCommand("EHLO ". $strHostname,250);
		
	}
	/**
	 * disconnect from the smtp server
	 * 
	 * @access public
	 * @throw SMTP_Exception
	 */
	public function disconnect()
	{
		if( $this->bSocketConnected )
		{
			$this->sendCommand("QUIT", 221);
			if( $this->bUseSockets )
			{
				if( @socket_close($this->resSocket) === false)
				{
					throw new SMTP_Exception(self::ERR_SOCKETS);
				}
			}
			else
			{
				if( !@fclose($this->resSocket) )
				{
					throw new SMTP_Exception(self::ERR_STREAM,"fclose: Faild to close the socket" );
				}
			}
			$this->bSocketConnected = false;
			$this->log("socket: Disconnected from ". $this->strIPAdress .":". $this->intPort ." [". $this->strHostname ."]");
		}
		
	}
	/**
	 * login into the server
	 * 
	 * !!! Cauition !!!
	 * This is only need for smtp server with authorization, else you don't need to call this function
	 * 
	 * @param string $strUsername Username of your account on the smtp server
	 * @param string $strPassword The password for your account
	 * 
	 * @throw SMTP_Exception
	 * @access public
	 */
	public function login( $strUsername = "", $strPassword = "" , $intAuthMethod = self::AUTH_PLAIN )
	{
		if( empty($strUsername) || empty($strPassword) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER,"Invalid username or password given. If is no login needed don't use this function here.");
		}
		if( $intAuthMethod == self::AUTH_PLAIN )
		{
			$this->sendCommand("AUTH LOGIN", 334);
			$this->sendCommand(base64_encode($strUsername),334,"Username: ". $strUsername);
			$this->sendCommand(base64_encode($strPassword),235,"Password: ". sha1($strPassword));
		}
		else
		{
			throw new Exception(self::ERR_NOT_IMPLEMENTED);
		}
	}
	/**
	 * Send an email
	 * 
	 * @param string $strFrom  From/Sender Adress
	 * @param 
	 */
	public function sendMessage($strFrom, $mixedTo, $strSubject, $strMessage, $mixedOptionalHeader = null, $mixedCC = null, $mixedBCC = null, $intPriority = self::MESSAGE_PRIO_MEDIUM)
	{
		$arrMailHeader = array();
		if( !is_string($strFrom) || !preg_match(self::REGEX_EMAIL,$strFrom) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER, "Invalid \"from\" parameter given or invalid adress [". $strFrom ."]");
		}
		if( !is_array($mixedTo) )
		{
			if( !is_string($mixedTo) ) 
				throw new SMTP_Exception(self::ERR_PARAMETER, "Invalid \"to\" parameter given");
		}
		if( !is_string($strSubject) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER, "Invalid subject parameter given");
		}
		if( !is_string($strMessage) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER, "Invalid message parameter given");
		}
		if( !is_null($mixedOptionalHeader) && !is_array($mixedOptionalHeader) )
		{
			if( !is_string($mixedOptionalHeader) )
				throw new SMTP_Exception(self::ERR_PARAMETER, "Invalid optional header parameter given");
		}
		if( !is_null($mixedCC) && !is_array($mixedCC) )
		{
			if(!is_string($mixedCC))
				throw new SMTP_Exception(self::ERR_PARAMETER, "Invalid \"cc\" parameter given");
		}
		if( !is_null($mixedBCC) && !is_array($mixedBCC) )
		{
			if( !is_string($mixedBCC) )
				throw new SMTP_Exception(self::ERR_PARAMETER, "Invalid \"bcc\" parameter given");
		}
		if( !is_int($intPriority) || ($intPriority < 1 || $intPriority > 5) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER, "Invalid priority parameter given. Allowed is a value between [1-5]");
		}
		// Prepare the Header
		
		
		// From
		$this->sendCommand("MAIL FROM: <". $strFrom .">",250);
		$arrMailHeader["from"] = $strFrom;
		
		// TO
		if( !is_array($mixedTo) )
		{
			$this->rcptTo($mixedTo);
			$arrMailHeader["to"] = "<". $mixedTo .">";
		}
		else
		{
			$bFirst = true;
			$arrMailHeader["to"] = "";
			foreach( $mixedTo AS $email )
			{
				$this->rcptTo($email);
				if( $bFirst )
				{
					$arrMailHeader["to"] .= "<". $email .">";
					$bFirst = false;
				}
				else
				{
					$arrMailHeader["to"] .= ", <". $email .">";
				}
			}
		}
		
		// CC
		if( !is_null($mixedCC) )
		{
			if( !is_array($mixedCC) )
			{
				$this->rcptTo($mixedCC);
				$arrMailHeader["cc"] = "<". $mixedCC .">";
			}
			else
			{
				$bFirst = true;
				$arrMailHeader["cc"] = "";
				foreach( $mixedCC AS $email )
				{
					$this->rcptTo($email);
					if( $bFirst )
					{
						$arrMailHeader["cc"] .= "<". $email .">";
						$bFirst = false;
					}
					else
					{
						$arrMailHeader["cc"] .= ", <". $email .">";
					}
				}
			}
		}
		
		// BCC
		if( !is_null($mixedBCC) )
		{
			if( !is_array($mixedBCC) )
			{
				$this->rcptTo($mixedBCC);
				$arrMailHeader["bcc"] = "<". $mixedBCC .">";
			}
			else
			{
				$bFirst = true;
				$arrMailHeader["bcc"] = "";
				foreach( $mixedBCC AS $email )
				{
					$this->rcptTo($email);
					if( $bFirst )
					{
						$arrMailHeader["bcc"] .= "<". $email .">";
						$bFirst = false;
					}
					else
					{
						$arrMailHeader["bcc"] .= ", <". $email .">";
					}
				}
			}
		}
		
		/*
		 *  Send the message
		 */
		$this->sendCommand("DATA", 354);
		// Header first
		while( ($key = key($arrMailHeader)) != null )
		{
			$this->send(ucfirst($key) .": ". $arrMailHeader[$key]);
			next($arrMailHeader);
		}
		$this->send("Subject: ". $strSubject);
		$this->send("Date: ". date("r"));
		$this->send("X-Mailer: ". self::XMAILER);
		
		// default priority for a mail is 3 so we don't need to add a line for that
		if( $intPriority != 3 )
		{
			$this->send("X-Priority: ". $intPriority);
			if( $intPriority == 1 || $intPriority == 2 )
			{
				$this->send("X-MSMail-Priority: High");
			}
			else
			{
				$this->send("X-MSMail-Priority: Low");
			}
		}
		// Optional header
		if( !is_null($mixedOptionalHeader) )
		{
			if( !is_array($mixedOptionalHeader) )
			{
				$this->send($mixedOptionalHeader);
			}
			else
			{
				foreach( $mixedOptionalHeader as &$strHeader )
				{
					$this->send($strHeader);
				}
			}
		}
		// Close the Header part
		$this->send("");
		// Message body
		$this->send($strMessage);
		// Close the message
		$this->sendCommand(".",250);
	}
	/**
	 * Set the socket send and recv timeouts
	 * 
	 * @param array $arrTimeout
	 * 
	 * @throw SMTP_Exception
	 * @return void
	 * @access private
	 */
	private function setSocketTimeout( $arrTimeout )
	{
		if( !is_array($arrTimeout) || !is_int($arrTimeout["sec"]) || !is_int($arrTimeout["usec"]) )
		{
			throw new SMTP_Exception(self::ERR_PARAMETER,"Invalid connection timeout given");
		}
		if( $this->bUseSockets )
		{
			if( !@socket_set_option($this->resSocket,SOL_SOCKET,SO_RCVTIMEO,$arrTimeout)
				|| !@socket_set_option($this->resSocket,SOL_SOCKET,SO_SNDTIMEO,$arrTimeout) )
			{
				throw new SMTP_Exception(self::ERR_SOCKETS);
			}
		}
		else
		{
			if( !@stream_set_timeout($this->resSocket,$arrTimeout["sec"],$arrTimeout["usec"]) )
			{
				throw new SMTP_Exception(self::ERR_STREAM,"stream_set_timeout: Failed to set stream connection timeout");
			}
		}
		$this->log("socket timeout: ". implode(".",$arrTimeout) ." seconds");
	}
	/**
	 * Send a string to the server
	 * 
	 * @param string $str
	 * @param integer $intFlags  socket_send() flags (only need for socket_extension)
	 * 
	 * @throw SMTP_Exception
	 * @access private
	 */
	private function send( $str, $intFlags = 0 )
	{
		$str = $str ."\r\n";
		if( $this->bUseSockets )
		{
			if( !@socket_send($this->resSocket,$str,strlen($str),$intFlags) )
			{
				throw new SMTP_Exception(self::ERR_SOCKETS);
			}
		}
		else
		{
			if( !@fwrite($this->resSocket,$str,strlen($str)) )
			{
				throw new SMTP_Exception(self::ERR_STREAM,"fwrite: Failed to write to socket");
			}
		}
	}
	/**
	 * Recieve a string from the server
	 * 
	 * @param integer $intBufferSize
	 * 
	 * @throw SMTP_Exception
	 * @access private
	 */
	private function recvString( $intBufferSize = self::DEFAULT_BUFFER_SIZE )
	{
		$strBuffer = "";
		if( $this->bUseSockets )
		{
			if( ($strBuffer = @socket_read($this->resSocket, $intBufferSize, PHP_NORMAL_READ)) === false )
			{
				throw new SMTP_Exception(self::ERR_SOCKETS);
			}
			/*
			 * Workaround: PHP_NORMAL_READ stops at "\r" but the network string is terminated by "\r\n",
			 * 			   so we need to call socket_read again for this 1 char "\n"
			 */
			if( ($strBuffer2 = @socket_read($this->resSocket, 1, PHP_NORMAL_READ)) === false )
			{
				throw new SMTP_Exception(self::ERR_SOCKETS);
			}
			$strBuffer .= $strBuffer2;
		}
		else
		{
			if( !$strBuffer = @fgets($this->resSocket,$intBufferSize) )
			{
				throw new SMTP_Exception(self::ERR_STREAM,"fgets: Couldn't read string from socket");
			}
		}
		return $strBuffer;
	}
	/**
	 * Recieve a string from the socket and check was the need response code given.
	 * Else not it will throw an exception with the message from the server
	 * 
	 * @param integer $intNeededCode needed Responsecode from the server
	 * @param integer $intBufferSize  @see recvString()
	 * 
	 * @throw SMTP_Exception
	 * @access private
	 */
	private function parseResponse( $intNeededCode, $intBufferSize = self::DEFAULT_BUFFER_SIZE )
	{
		while(true)
		{
			$strBuffer = $this->recvString($intBufferSize);
			$this->log($strBuffer);
			if(preg_match("/^[0-9]{3}( )/", $strBuffer))
			{
				break;
			}
		}
		if( !preg_match("/^(". $intNeededCode .")/",$strBuffer) )
		{
			throw new SMTP_Exception(self::ERR_INVALID_RESPONSE,$strBuffer);
		}
	}
	/**
	 * Send the command to the server and check for the needed response code
	 * 
	 * @param string $cmd  command for the server
	 * @param integer $neededCode @see parseResponse()
	 * @param string $strLog  String that should log, else we use the command string
	 * @param integer $intFlags  @see send()
	 * 
	 * @throw SMTP_Exception
	 * @access private
	 */
	private function sendCommand( $strCommand, $intNeededCode, $strLog = null, $intFlags = 0 )
	{
		( !is_null($strLog) ) ? $this->log($strLog) : $this->log($strCommand);
		$this->send($strCommand,$intFlags);
		$this->parseResponse($intNeededCode);
	}
	/**
	 * reciept to
	 * 
	 * @param string $email  E-Mail-Adress
	 */
	private function rcptTo($email)
	{
		if( !is_null($email) )
		{
			if( !preg_match(self::REGEX_EMAIL,$email) )
			{
				throw new SMTP_Exception(self::ERR_PARAMETER,"Invalid email address given [". $email ."]");
			}
			$this->sendCommand("RCPT TO: ". $email, 250);
		}
	}
	/**
	 * open the log filedescriptor
	 * 
	 * @throw SMTP_Exception
	 * @access private
	 */
	private function openLog()
	{
		// Note: Constructor checks is it a string or not so we don't need to check for null again
		// Think about, test it.
		if( !$this->bLogOpened /*&& !is_null($this->strLogFile)*/ )
		{
			if( !$this->resLogFp = @fopen($this->strLogFile,"a+") )
			{
				throw new SMTP_Exception(self::ERR_LOG,"fopen: Couldn't open log file: ". $this->strLogFile);
			}
			$this->bLogOpened = true;
		}
	}
	/**
	 * close the log filedescriptor
	 * 
	 * @access private
	 */
	private function closeLog()
	{
		if( $this->bLogOpened )
		{
			@fclose($this->resLogFp);
			$this->bLogOpened = false;
		}
	}
	/**
	 * open the log filedescriptor
	 * 
	 * @param string $str  String to log
	 * 
	 * @throw SMTP_Exception
	 * @access private
	 */
	private function log( $str )
	{
		if( $this->bLogOpened )
		{
			$str = date($this->strLogDatetimeFormat). ": ". trim($str) .PHP_EOL;
			if( !@fwrite($this->resLogFp, $str, strlen($str)) )
			{
				throw new SMTP_Exception(self::ERR_LOG,"fwrite: Failed to wrote to logfile. (". trim($str) .")");
			}
		}
	}
}
?>