includes/framework/QEmailServer.class.php
<?php
/**
* This EmailServer (and its dependent EmailMessage class) allows the application to send
* messages via any accessible SMTP server.
*
* The QEmailServer class, specifically, is an abstract class and is NOT meant to be instantiated.
* It has one public static method, Send, which takes in a QEmailMessage object.
*/
abstract class QEmailServer extends QBaseClass {
/**
* Server Hostname or IP Address of the server running the SMTP service.
* Using an IP address is slightly faster, but using a Hostname is easier to manage.
* Defaults to "localhost".
*
* @var string SmtpServer
*/
public static $SmtpServer = 'localhost';
/**
* Port of the SMTP Service on the SmtpServer, usually 25
*
* @var integer SmtpPort
*/
public static $SmtpPort = 25;
/**
* IP Address of the Originating Server (e.g. the IP address of this server)
* used for the EHLO command in the SMTP protocol. Defaults to the
* QApplication::$ServerAddress variable, which uses the PHP $_SERVER
* constants to determine the correct IP address.
*
* @var string OriginatingServerIp
*/
public static $OriginatingServerIp;
/**
* Whether or not we are running in Test Mode. Test Mode allows you
* to develop e-mail-based applications without actually having access to
* an SMTP server or the Internet. Instead of messages being sent out,
* the messages and corresponding SMTP communication will be saved to disk.
*
* @var boolean $TestMode
*/
public static $TestMode = false;
/**
* The directory where TestMode e-mail files will be saved to. The process
* running the webserver *must* have write access to this directory. Default
* is "/tmp", which makes sense in unix/linux/mac environments. Windows users
* will likely need to set up their own temp directories.
*
* @var string $TestModeDirectory
*/
public static $TestModeDirectory = __TMP__;
/**
* Boolean flag signifying whether SMTP's AUTH PLAIN should be used
*
* @var bool $AuthPlain
*/
public static $AuthPlain = false;
/**
* Boolean flag signifying whether SMTP's AUTH LOGIN should be used
*
* @var bool $AuthLogin
*/
public static $AuthLogin = false;
/**
* SMTP Username to use for AUTH PLAIN or LOGIN
*
* @var string $SmtpUsername
*/
public static $SmtpUsername = '';
/**
* SMTP Password to use for AUTH PLAIN or LOGIN
*
* @var string $SmtpPassword
*/
public static $SmtpPassword = '';
/**
* Encoding Type (if null, will default to the QApplication::$EncodingType)
*
* @var string $EncodingType
*/
public static $EncodingType = null;
/**
* Uses regular expression matching to return an array of valid e-mail addresses
*
* @param string $strAddresses Single string containing e-mail addresses and anything else
* @return string[] An array of e-mail addresses only, or NULL if none
*/
public static function GetEmailAddresses($strAddresses) {
$strAddressArray = null;
// Address Lines cannot have any linebreaks
if ((strpos($strAddresses, "\r") !== false) ||
(strpos($strAddresses, "\n") !== false))
return null;
preg_match_all ("/[a-zA-Z0-9_.'%+-]+[@][\-a-zA-Z0-9_.]+/", $strAddresses, $strAddressArray);
if ((is_array($strAddressArray)) &&
(array_key_exists(0, $strAddressArray)) &&
(is_array($strAddressArray[0])) &&
(array_key_exists(0, $strAddressArray[0]))) {
return $strAddressArray[0];
}
// If we're here, then no addresses were found in $strAddress
// so return null
return null;
}
/**
* This will check to see if an email address is considered "Valid" according to RFC 2822.
* It utilizes the GetEmailAddresses static method, which does the actual logic work of checking.
* @param string $strEmailAddress
* @return boolean
*/
public static function IsEmailValid($strEmailAddress) {
$strEmailAddressArray = QEmailServer::GetEmailAddresses($strEmailAddress);
return ((count($strEmailAddressArray) == 1) && ($strEmailAddressArray[0] == $strEmailAddress));
}
/**
* Encodes given 8 bit string to a quoted-printable string,
* @param string $strString
* @param boolean $blnSubject
* @return encoded string
*/
private static function QuotedPrintableEncode($strString, $blnSubject = false) {
if ( function_exists('quoted_printable_encode') )
$strText = quoted_printable_encode($strString);
else {
$strText = preg_replace( '/[^\x21-\x3C\x3E-\x7E\x09\x20]/e', 'sprintf( "=%02X", ord ( "$0" ) ) ;', $strString );
preg_match_all( '/.{1,73}([^=]{0,2})?/', $strText, $arrMatch );
$strText = implode( '=' . "\r\n", $arrMatch[0] );
}
if ($blnSubject) {
// Replace spaces with undercores, see RFC 1342
$strText = str_replace("_", "=5F", $strText);
$strText = str_replace(" ", "_", $strText);
// Remove newlines
$strText = str_replace("=\r\n", "", $strText);
} else {
// Escape leading dots with another dot
$strText = str_replace("\n.", "\n..", $strText);
}
return $strText;
}
/**
* Sends a message out via SMTP according to the server, ip, etc. preferences
* as set up on the class. Takes in a QEmailMessage object.
* Will throw a QEmailException exception on any error.
*
* @param QEmailMessage $objMessage Message to Send
*
* @throws QEmailException
* @return void
*/
public static function Send(QEmailMessage $objMessage) {
$objResource = null;
if (QEmailServer::$TestMode) {
// Open up a File Resource to the TestModeDirectory
$strArray = explode(' ', microtime());
$strFileName = sprintf('%s/email_%s%s.txt', QEmailServer::$TestModeDirectory, $strArray[1], substr($strArray[0], 1));
$objResource = fopen($strFileName, 'w');
if (!$objResource)
throw new QEmailException(sprintf('Unable to open Test SMTP connection to: %s', $strFileName));
// Clear the Read Buffer
if (!feof($objResource))
fgets($objResource, 4096);
// Write the Connection Command
fwrite($objResource, sprintf("telnet %s %s\r\n", QEmailServer::$SmtpServer, QEmailServer::$SmtpPort));
} else {
$objResource = fsockopen(QEmailServer::$SmtpServer, QEmailServer::$SmtpPort);
if (!$objResource)
throw new QEmailException(sprintf('Unable to open SMTP connection to: %s %s', QEmailServer::$SmtpServer, QEmailServer::$SmtpPort));
}
// Connect
$strResponse = null;
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
// Iterate through all "220-" responses (stop at "220 ")
while ((substr($strResponse, 0, 3) == "220") && (substr($strResponse, 0, 4) != "220 "))
if (!feof($objResource))
$strResponse = fgets($objResource, 4096);
// Check for a "220" response
if (!QEmailServer::$TestMode)
if ((strpos($strResponse, "220") === false) || (strpos($strResponse, "220") != 0))
throw new QEmailException(sprintf('Error Response on Connect: %s', $strResponse));
}
// Send: EHLO
fwrite($objResource, sprintf("EHLO %s\r\n", QEmailServer::$OriginatingServerIp));
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
// Iterate through all "250-" responses (stop at "250 ")
while ((substr($strResponse, 0, 3) == "250") && (substr($strResponse, 0, 4) != "250 "))
if (!feof($objResource))
$strResponse = fgets($objResource, 4096);
// Check for a "250" response
if (!QEmailServer::$TestMode)
if ((strpos($strResponse, "250") === false) || (strpos($strResponse, "250") != 0))
throw new QEmailException(sprintf('Error Response on EHLO: %s', $strResponse));
}
// Send Authentication
if (QEmailServer::$AuthPlain) {
fwrite($objResource, "AUTH PLAIN " . base64_encode(QEmailServer::$SmtpUsername . "\0" . QEmailServer::$SmtpUsername . "\0" . QEmailServer::$SmtpPassword) . "\r\n");
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
if ((strpos($strResponse, "235") === false) || (strpos($strResponse, "235") != 0))
throw new QEmailException(sprintf('Error in response from AUTH PLAIN: %s', $strResponse));
}
}
if (QEmailServer::$AuthLogin) {
fwrite($objResource,"AUTH LOGIN\r\n");
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
if (!QEmailServer::$TestMode)
if ((strpos($strResponse, "334") === false) || (strpos($strResponse, "334") != 0))
throw new QEmailException(sprintf('Error in response from AUTH LOGIN: %s', $strResponse));
}
fwrite($objResource, base64_encode(QEmailServer::$SmtpUsername) . "\r\n");
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
if (!QEmailServer::$TestMode)
if ((strpos($strResponse, "334") === false) || (strpos($strResponse, "334") != 0))
throw new QEmailException(sprintf('Error in response from AUTH LOGIN: %s', $strResponse));
}
fwrite($objResource, base64_encode(QEmailServer::$SmtpPassword) . "\r\n");
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
if (!QEmailServer::$TestMode)
if ((strpos($strResponse, "235") === false) || (strpos($strResponse, "235") != 0))
throw new QEmailException(sprintf('Error in response from AUTH LOGIN: %s', $strResponse));
}
}
// Setup MAIL FROM line
$strAddressArray = QEmailServer::GetEmailAddresses($objMessage->From);
if (count($strAddressArray) != 1)
throw new QEmailException(sprintf('Not a valid From address: %s', $objMessage->From));
// Send: MAIL FROM line
fwrite($objResource, sprintf("MAIL FROM: <%s>\r\n", $strAddressArray[0]));
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
// Check for a "250" response
if (!QEmailServer::$TestMode)
if ((strpos($strResponse, "250") === false) || (strpos($strResponse, "250") != 0))
throw new QEmailException(sprintf('Error Response on MAIL FROM: %s', $strResponse));
}
// Setup RCPT TO line(s)
$strAddressToArray = QEmailServer::GetEmailAddresses($objMessage->To);
if (!$strAddressToArray)
throw new QEmailException(sprintf('Not a valid To address: %s', $objMessage->To));
$strAddressCcArray = QEmailServer::GetEmailAddresses($objMessage->Cc);
if (!$strAddressCcArray)
$strAddressCcArray = array();
$strAddressBccArray = QEmailServer::GetEmailAddresses($objMessage->Bcc);
if (!$strAddressBccArray)
$strAddressBccArray = array();
$strAddressCcBccArray = array_merge($strAddressCcArray, $strAddressBccArray);
$strAddressArray = array_merge($strAddressToArray, $strAddressCcBccArray);
// Send: RCPT TO line(s)
foreach ($strAddressArray as $strAddress) {
fwrite($objResource, sprintf("RCPT TO: <%s>\r\n", $strAddress));
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
// Check for a "250" response
if (!QEmailServer::$TestMode)
if ((strpos($strResponse, "250") === false) || (strpos($strResponse, "250") != 0))
throw new QEmailException(sprintf('Error Response on RCPT TO: %s', $strResponse));
}
}
// Send: DATA
fwrite($objResource, "DATA\r\n");
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
// Check for a "354" response
if (!QEmailServer::$TestMode)
if ((strpos($strResponse, "354") === false) || (strpos($strResponse, "354") != 0))
throw new QEmailException(sprintf('Error Response on DATA: %s', $strResponse));
}
// Send: Required Headers
fwrite($objResource, sprintf("Date: %s\r\n", QDateTime::NowToString(QDateTime::FormatRfc5322)));
fwrite($objResource, sprintf("To: %s\r\n", $objMessage->To));
fwrite($objResource, sprintf("From: %s\r\n", $objMessage->From));
if($objMessage->ReplyTo) {
fwrite($objResource, sprintf("Reply-To: %s\r\n", $objMessage->ReplyTo));
}
if($objMessage->Sender) {
fwrite($objResource, sprintf("Sender: %s\r\n", $objMessage->Sender));
}
// Setup Encoding Type (use QEmailServer if specified, otherwise default to QApplication's)
if (!($strEncodingType = QEmailServer::$EncodingType))
$strEncodingType = QApplication::$EncodingType;
// Send: Optional Headers
if ($objMessage->Subject) {
if ($objMessage->EncodeSubject) {
fwrite($objResource, sprintf("Subject: =?%s?Q?%s?=\r\n", $strEncodingType, self::QuotedPrintableEncode($objMessage->Subject, true)));
} else {
fwrite($objResource, sprintf("Subject: %s\r\n", $objMessage->Subject));
}
}
if ($objMessage->Cc)
fwrite($objResource, sprintf("Cc: %s\r\n", $objMessage->Cc));
// Send: Content-Type Header (if applicable)
// First, setup boundaries (may be needed if multipart)
$strBoundary = sprintf('qcubed_mixed_boundary_%s', md5(microtime()));
$strAltBoundary = sprintf('qcubed_alt_boundary_%s', md5(microtime()));
// Send: Other Headers (if any)
foreach ($objArray = $objMessage->HeaderArray as $strKey => $strValue)
fwrite($objResource, sprintf("%s: %s\r\n", $strKey, $strValue));
// if we are adding an html or files to the message we need these headers.
if ($objMessage->HasFiles || $objMessage->HtmlBody) {
fwrite($objResource, "MIME-Version: 1.0\r\n");
fwrite($objResource, sprintf("Content-Type: multipart/mixed;\r\n boundary=\"%s\"\r\n", $strBoundary));
fwrite($objResource, sprintf("This is a multipart message in MIME format.\r\n\r\n", $strBoundary));
fwrite($objResource, sprintf("--%s\r\n", $strBoundary));
}
// Send: Body
if ($objMessage->HtmlBody) {
fwrite($objResource, sprintf("Content-Type: multipart/alternative;\r\n boundary=\"%s\"\r\n\r\n", $strAltBoundary));
fwrite($objResource, sprintf("--%s\r\n", $strAltBoundary));
fwrite($objResource, sprintf("Content-Type: text/plain; charset=\"%s\"\r\n", $strEncodingType));
fwrite($objResource, sprintf("Content-Transfer-Encoding: quoted-printable\r\n\r\n"));
fwrite($objResource, self::QuotedPrintableEncode($objMessage->Body));
fwrite($objResource, "\r\n\r\n");
fwrite($objResource, sprintf("--%s\r\n", $strAltBoundary));
fwrite($objResource, sprintf("Content-Type: text/html; charset=\"%s\"\r\n", $strEncodingType));
fwrite($objResource, sprintf("Content-Transfer-Encoding: quoted-printable\r\n\r\n"));
fwrite($objResource, self::QuotedPrintableEncode($objMessage->HtmlBody));
fwrite($objResource, "\r\n\r\n");
fwrite($objResource, sprintf("--%s--\r\n", $strAltBoundary));
} else if($objMessage->HasFiles) {
fwrite($objResource, sprintf("Content-Type: multipart/alternative;\r\n boundary=\"%s\"\r\n\r\n", $strAltBoundary));
fwrite($objResource, sprintf("--%s\r\n", $strAltBoundary));
fwrite($objResource, sprintf("Content-Type: text/plain; charset=\"%s\"\r\n", $strEncodingType));
fwrite($objResource, sprintf("Content-Transfer-Encoding: quoted-printable\r\n\r\n"));
fwrite($objResource, self::QuotedPrintableEncode($objMessage->Body));
fwrite($objResource, "\r\n\r\n");
fwrite($objResource, sprintf("--%s--\r\n", $strAltBoundary));
} else {
fwrite($objResource, sprintf("Content-Type: text/plain; charset=\"%s\"\r\n", $strEncodingType));
fwrite($objResource, sprintf("Content-Transfer-Encoding: quoted-printable\r\n\r\n"));
fwrite($objResource, "\r\n" . self::QuotedPrintableEncode($objMessage->Body));
}
// Send: File Attachments
if($objMessage->HasFiles) {
foreach ($objArray = $objMessage->FileArray as $objFile) {
fwrite($objResource, sprintf("--%s\r\n", $strBoundary));
fwrite($objResource, sprintf("Content-Type: %s;\r\n", $objFile->MimeType ));
fwrite($objResource, sprintf(" name=\"%s\"\r\n", $objFile->FileName ));
fwrite($objResource, "Content-Transfer-Encoding: base64\r\n");
fwrite($objResource, sprintf("Content-Length: %s\r\n", strlen($objFile->EncodedFileData)));
fwrite($objResource, "Content-Disposition: attachment;\r\n");
fwrite($objResource, sprintf(" filename=\"%s\"\r\n\r\n", $objFile->FileName));
fwrite($objResource, $objFile->EncodedFileData);
// foreach (explode("\r\n", $objFile->EncodedFileData) as $strLine) {
// $strLine = trim($strLine);
// fwrite($objResource, $strLine . "\r\n");
// }
}
}
// close a message with these boundaries if the message had files or had html
if($objMessage->HasFiles || $objMessage->HtmlBody)
fwrite($objResource, sprintf("\r\n\r\n--%s--\r\n", $strBoundary)); // send end of file attachments...
// Send: Message End
fwrite($objResource, "\r\n.\r\n");
if (!feof($objResource)) {
$strResponse = fgets($objResource, 4096);
// Check for a "250" response
if (!QEmailServer::$TestMode)
if ((strpos($strResponse, "250") === false) || (strpos($strResponse, "250") != 0))
throw new QEmailException(sprintf('Error Response on DATA finish: %s', $strResponse));
}
// Send: QUIT
fwrite($objResource, "QUIT\r\n");
if (!feof($objResource))
$strResponse = fgets($objResource, 4096);
// Close the Resource
fclose($objResource);
if (QEmailServer::$TestMode)
chmod($strFileName, 0777);
}
}
// PHP does not allow Static Class Variables to be set to non-constants.
// So we set QEmailServer's OriginatingServerIp to QApplication's ServerAddress here.
QEmailServer::$OriginatingServerIp = QApplication::$ServerAddress;
class QEmailException extends QCallerException {}
class QEmailAttachment extends QBaseClass {
protected $strFilePath;
protected $strMimeType;
protected $strFileName;
protected $strEncodedFileData;
public function __construct($strFilePath, $strSpecifiedMimeType = null, $strSpecifiedFileName = null) {
// Set File Path
if (!is_file(realpath($strFilePath)))
throw new QCallerException('File Not Found: ' . $strFilePath);
$this->strFilePath = realpath($strFilePath);
// Set the File MIME Type -- if Explicitly Set, use it
if ($strSpecifiedMimeType)
$this->strMimeType = $strSpecifiedMimeType;
// otherwise, use QMimeType to determine
else
$this->strMimeType = QMimeType::GetMimeTypeForFile($this->strFilePath);
// Set the File Name -- if explicitly set, use it
if ($strSpecifiedFileName)
$this->strFileName = $strSpecifiedFileName;
// Otherwise, use basename() to determine
else
$this->strFileName = basename($this->strFilePath);
// Read file into a Base64 Encoded Data Stream
$strFileContents = file_get_contents($this->strFilePath, false);
$this->strEncodedFileData = chunk_split(base64_encode($strFileContents));
}
public function __get($strName) {
switch ($strName) {
case 'FilePath': return $this->strFilePath;
case 'MimeType': return $this->strMimeType;
case 'FileName': return $this->strFileName;
case 'EncodedFileData': return $this->strEncodedFileData;
default:
try {
return parent::__get($strName);
} catch (QCallerException $objExc) {
$objExc->IncrementOffset();
throw $objExc;
}
}
}
}
class QEmailStringAttachment extends QEmailAttachment {
public function __construct($strContent, $strSpecifiedMimeType, $strSpecifiedFileName) {
// Set the File MIME Type -- if Explicitly Set, use it
if ($strSpecifiedMimeType) {
$this->strMimeType = $strSpecifiedMimeType;
}
// Set the File Name -- if explicitly set, use it
if ($strSpecifiedFileName) {
$this->strFileName = $strSpecifiedFileName;
}
// Read file into a Base64 Encoded Data Stream
$this->strEncodedFileData = chunk_split(base64_encode($strContent));
}
}
/**
* An email message that you can send with QEmailServer
*
* @property string $From
* @property string $ReplyTo
* @property string $Sender
* @property string $To
* @property string $Cc
* @property string $Bcc
* @property string $Subject
* @property string $Body
* @property string $HtmlBody
* @property boolean $EncodeSubject Whether to encode the subject of the email using UTF-8. Default is true. You might want to turn this off if sending to text message portals (i.e. 123-4567@sprint.message.com)
*/
class QEmailMessage extends QBaseClass {
protected $strFrom;
protected $strReplyTo;
protected $strSender;
protected $strTo;
protected $strSubject;
protected $strBody;
protected $strHtmlBody;
protected $strCc;
protected $strBcc;
protected $strHeaderArray = array();
protected $objFileArray = array();
protected $blnEncodeSubject = true; // useful for sending to text message hubs, they don't like charset declaractions
public function AddAttachment(QEmailAttachment $objFile) {
$this->objFileArray[$objFile->FileName] = $objFile;
}
public function Attach($strFilePath, $strSpecifiedMimeType = null, $strSpecifiedFileName = null) {
$this->AddAttachment(new QEmailAttachment($strFilePath, $strSpecifiedMimeType, $strSpecifiedFileName));
}
public function RemoveAttachment($strName) {
if (array_key_exists($strName, $this->objFileArray))
unset($this->objFileArray[$strName]);
}
public function SetHeader($strName, $strValue) {
$this->strHeaderArray[$strName] = $strValue;
}
public function GetHeader($strName) {
if (array_key_exists($strName, $this->strHeaderArray))
return $this->strHeaderArray[$strName];
return null;
}
public function RemoveHeader($strName) {
if (array_key_exists($strName, $this->strHeaderArray))
unset($this->strHeaderArray[$strName]);
}
public function __construct($strFrom = null, $strTo = null, $strSubject = null, $strBody = null) {
$this->strFrom = $strFrom;
$this->strTo = $strTo;
// We must cleanup the Subject and Body -- use the Property to set
$this->Subject = $strSubject;
$this->Body = $strBody;
}
public function __get($strName) {
switch ($strName) {
case 'From' : return $this->strFrom;
case 'ReplyTo' : return $this->strReplyTo;
case 'Sender' : return $this->strSender;
case 'To' : return $this->strTo;
case 'Subject': return $this->strSubject;
case 'Body': return $this->strBody;
case 'HtmlBody': return $this->strHtmlBody;
case 'Cc': return $this->strCc;
case 'Bcc': return $this->strBcc;
case 'HeaderArray': return $this->strHeaderArray;
case 'FileArray': return $this->objFileArray;
case 'HasFiles': return (count($this->objFileArray) > 0) ? true : false;
case 'EncodeSubject': return $this->blnEncodeSubject;
default:
try {
return parent::__get($strName);
} catch (QCallerException $objExc) {
$objExc->IncrementOffset();
throw $objExc;
}
}
}
public function __set($strName, $mixValue) {
try {
switch ($strName) {
case 'From' : return ($this->strFrom = QType::Cast($mixValue, QType::String));
case 'ReplyTo' : return ($this->strReplyTo = QType::Cast($mixValue, QType::String));
case 'Sender' : return ($this->strSender = QType::Cast($mixValue, QType::String));
case 'To': return ($this->strTo = QType::Cast($mixValue, QType::String));
case 'Subject':
$strSubject = trim(QType::Cast($mixValue, QType::String));
$strSubject = str_replace("\r", "", $strSubject);
$strSubject = str_replace("\n", " ", $strSubject);
return ($this->strSubject = $strSubject);
case 'Body':
$strBody = QType::Cast($mixValue, QType::String);
$strBody = str_replace("\r", "", $strBody);
$strBody = str_replace("\n", "\r\n", $strBody);
$strBody = str_replace("\n.", "\n..", $strBody);
return ($this->strBody = $strBody);
case 'HtmlBody':
$strHtmlBody = QType::Cast($mixValue, QType::String);
$strHtmlBody = str_replace("\r", "", $strHtmlBody);
$strHtmlBody = str_replace("\n", "\r\n", $strHtmlBody);
$strHtmlBody = str_replace("\n.", "\n..", $strHtmlBody);
return ($this->strHtmlBody = $strHtmlBody);
case 'Cc': return ($this->strCc = QType::Cast($mixValue, QType::String));
case 'Bcc': return ($this->strBcc = QType::Cast($mixValue, QType::String));
case 'EncodeSubject': return ($this->blnEncodeSubject = QType::Cast($mixValue, QType::Boolean));
default: return (parent::__set($strName, $mixValue));
}
} catch (QInvalidCastException $objExc) {
$objExc->IncrementOffset();
throw $objExc;
}
}
}