<?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) .")");
}
}
}
}
?>
|