Sending emails with PHP’s sockets and SMTP

As a continuation on releasing more of OneLobby's code, here is a useful set of classes that I made for sending emails. Normally one would use PHP's mail function, but everytime you call that function it opens and closes a socket. If you want to send out large amounts of email, this process can be very inefficient. That's where these classes come in handy...

	<?php

	/*
	OneLobby SendMail class and related supporting classes
	Copyright (C) 2007 Peter Goodman

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
	*/

	//--------------------------------------------
	// A class to deal with creating and sending mail.
	// Works with the normal mail method and also SMTP.
	//--------------------------------------------

	class SendMail
	{	
		function &getFinder($type = 'PHP')
		{
			$class = $type .'_MailMessage';
			$ret = NULL;

			if(class_exists($class))
			{
				$ret = &new $class();
			}

			return $ret;
		}
	}

	class MailMessage 
	{
		var $_headers = array();

		var $_message = "";
		var $_subject = "";

		var $_as_html = FALSE;

		var $_bcc = array();
		var $_to;
		var $_from;

		//--------------------------------------------
		// Constructor, set some headers.
		//--------------------------------------------

		function MailMessage()
		{
			$this->_headers[] = "X-Priority: 3";
			$this->_headers[] = "X-Mailer: OneLobby Mailer";
			$this->_headers[] = "Content-Transfer-Encoding: 8bit";
			$this->_headers[] = "Date: ". date("r", time());
			$this->_headers[] = "Content-Transfer-Encoding: quoted-printable";
		}

		//--------------------------------------------
		// Set this message type to be plain text.
		//--------------------------------------------

		function asPlainText()
		{
			$this->_headers[] = "Content-Type: text/plain; charset=UTF-8";
		}

		//--------------------------------------------
		// Set this message type to be HTML
		//--------------------------------------------

		function asHTML()
		{
			$this->_headers[] = "MIME-Version: 1.0";
			$this->_headers[] = "Content-Type: text/html; charset=UTF-8";
			$this->_as_html = TRUE;
		}

		//--------------------------------------------
		// Set who this message is from.
		//--------------------------------------------

		function setFrom($email, $name = FALSE)
		{
			if(!$name)
			{
				$name = "OneLobby Mailer";
			}

			$email = $this->_cleanString($email);
			$name = $this->_cleanString($name);

			$this->_from = $email;

			$this->_headers[] = "Return-Path: <{$email}>";
			$this->_headers[] = "From: \"{$name}\" <{$email}>";
			$this->_headers[] = "Message-Id: <". md5(uniqid(rand())) .".". preg_replace("~[^a-z0-9]~i", "", $name) ."@". $this->_smpt_server .">";
		}

		//--------------------------------------------
		// Set who this message is to. This can either be
		// an array of people or a single email address.
		//--------------------------------------------

		function setTo($email, $name = FALSE)
		{	
			$email = $this->_cleanString($email);

			if($name)
			{
				$name = $this->_cleanString($name);
				$to = "To: \"{$name}\" <{$email}>";
			}
			else
			{
				$to = "To: {$email}";
			}

			$this->_to = $email;
			$this->_headers[] = $to;
		}

		//--------------------------------------------
		// Set the Bcc's (Blind Carbon Copy) for this email
		//--------------------------------------------

		function setBCC($emails)
		{
			$this->_bcc = $emails;
		}

		//--------------------------------------------
		// Set the subject of the mail.
		//--------------------------------------------

		function setSubject($subject)
		{
			$subject = $this->_cleanString($subject);

			$this->_subject = $subject;

			$this->_headers[] = "Subject: {$subject}";
		}

		//--------------------------------------------
		// Set the message of the mail.
		//--------------------------------------------

		function setMessage($message)
		{	
			$this->_message = $message;
		}

		//--------------------------------------------
		// Build the headers.
		//--------------------------------------------

		function &buildHeaders()
		{
			$ret = "";

			foreach($this->_headers as $header)
			{
				$ret .= $header ."\n";
			}

			return $ret;
		}

		//--------------------------------------------
		// Build the email.
		//--------------------------------------------

		function &buildMessage()
		{
			$length = !$this->_as_html ? 68 : 62;
			$break = !$this->_as_html ? "\n" : "<br />\n";

			$message = $this->_cleanMessage($this->_message);
			$message = $this->_wordWrap($message, $length, $break);

			//--------------------------------------------
			// If we're sending from windows, this is some
			// sort of known bug..
			//--------------------------------------------

			if(isset($_SERVER['SERVER_NAME']))
			{
				if(preg_match("~Win32~i", $_SERVER['SERVER_NAME']))
				{
					$message = str_replace("\n.", "\n..", $message);
				}
			}

			return $message ."\n";
		}

		//--------------------------------------------
		// Clean a string.
		//--------------------------------------------

		function _cleanString($str)
		{
			$str = preg_replace("~[ \r\n\t]~", " ", $str);
			$str = preg_replace("~,,~", ",", $str);
			$str = preg_replace("~\#\[\]'\"\(\):;/\$!£%\^&\*\{\}~", "", $str);
			return $str;
		}

		//--------------------------------------------
		// Clean a message.
		//--------------------------------------------

		function _cleanMessage($msg)
		{
			$msg = preg_replace("~(\r\n|\r|\n)~", "\n", $msg);
			$msg = preg_replace("~,,~", ",", $msg);

			if(!$this->_as_html)
			{
				$msg = strip_tags($msg);
				$msg = html_entity_decode($msg, ENT_QUOTES);
			}

			$msg = trim($msg);

			return $msg;
		}

		//--------------------------------------------
		// Word wrap function, care of the PHP Manual comments,
		// thank's Paolo Stefan.
		//--------------------------------------------

		function _wordWrap($text, $size, $separator)
		{
			$new_text = '';
			$text_1 = explode('>',$text);
			$sizeof = sizeof($text_1);
			$remain = $size;

			for ($i = 0; $i < $sizeof; ++$i) 
			{
				$text_2 = explode('<',$text_1[$i]);

				if (!empty($text_2[0])) 
				{
					$perl = '/([^\\n\\r .]{'. $remain .',})/i';
					$possibly_splitted= preg_replace( $perl, '$1'.$separator, $text_2[0] );

					$splitted = explode($separator,$possibly_splitted);
					$remain -= strlen_utf($splitted[0]);
					if($remain<=0) 
					{
						$remain = $size;
					}

					$new_text .= $possibly_splitted;
				}

				if (!empty($text_2[1])) 
				{
					$new_text .= '<' . $text_2[1] . '>';
				}
			}
			return $new_text;
		}

		function sendMessage()
		{
			assert(FALSE);
		}
	}

	class PHP_MailMessage extends MailMessage
	{
		function sendMessage()
		{
			$headers = &$this->buildHeaders();
			$message = &$this->buildMessage();
			$from = &$this->_from;
			$to = &$this->_to;
			$subject = &$this->_subject;

			//--------------------------------------------
			// Deal with bcc's
			//--------------------------------------------
			$bcc = "";
			foreach($this->_bcc as $email)
			{
				if(preg_match("~[^ ]+\@[^ ]+~", $email))
				{
					$bcc .= " <$email>";
					$sep = ",";
				}
			}

			if($bcc != "")
			{
				$this->_headers[] = "Bcc: ". $bcc;
			}

			//--------------------------------------------
			// If we're using PHP's built in mail function.
			//--------------------------------------------
			if (!@mail( $to, $subject, $message, $headers))
			{
				trigger_error("Could not send the mail with mail().");
			}
		}
	}

	class SMTP_MailMessage extends MailMessage
	{
		function sendMessage()
		{
			$headers = &$this->buildHeaders();
			$message = &$this->buildMessage();
			$from = &$this->_from;
			$to = &$this->_to;
			$subject = &$this->_subject;

			//--------------------------------------------
			// If we're sending with SMTP..
			//--------------------------------------------

			//--------------------------------------------
			// Set who this mail is from.
			//--------------------------------------------

			if($this->socketCommand("MAIL FROM: <". $from .">") != 250)
			{
				trigger_error("Could not set who this mail is from.", E_USER_ERROR);
			}

			//--------------------------------------------
			// Get the array of who this message is to.
			//--------------------------------------------

			$bcc = $this->_message->getBcc();
			$bcc[] = $to;

			//--------------------------------------------
			// Loop over who this message is to and set them as
			// a receiver of the message.
			//--------------------------------------------

			foreach($this->_bcc as $email)
			{
				if(preg_match("~[^ ]+\@[^ ]+~", $email))
				{
					if($this->socketCommand("RCPT TO: <". $email .">") != 250)
					{
						trigger_error("Could not send email to: $email", E_USER_ERROR);
						return;
					}
				}
			}

			//--------------------------------------------
			// Let's start to put the data of the message in.
			//--------------------------------------------

			if($this->socketCommand("DATA") != 354)
			{
				trigger_error("Could not set data.", E_USER_ERROR);
			}

			//--------------------------------------------
			// Add the message data in.
			//--------------------------------------------

			$data = $headers ."\n\n". $message;

			//--------------------------------------------
			// First replace all \n's with CR/LF then replace
			// \n.\r\n with \n. \r\n because the next socket
			// command after this is <CRLF>.<CRLF>

			//--------------------------------------------

			$data = preg_replace("~(?<!\r)\n~is", "\r\n", $data);
			$data = str_replace("\n.\r\n", "\n. \r\n", $data );

			fputs($this->_smtp_fp, $data);

			//--------------------------------------------
			// We've put all the message input in. We're done.
			//--------------------------------------------

			if($this->socketCommand("\r\n.") != 250)
			{
				trigger_error("Could not finish data input.", E_USER_ERROR);
			}

			//--------------------------------------------
			// Close the socket.
			//--------------------------------------------

			$this->smtpClose();
		}

		//--------------------------------------------
		// Connect to a SMTP server
		//--------------------------------------------

		function smtpConnect($host = "localhost", $port = 25, $user = FALSE, $pass = FALSE)
		{
			$this->_smtp_fp = fsockopen($host, $port, $error_no, $error_str, 30);

			if(!$this->_smtp_fp)
			{
				break;
			}

			$this->_smtp = TRUE;

			//--------------------------------------------
			// If we have a user and pass, try to log in to
			// the SMTP server.
			//--------------------------------------------

			if($user && $pass)
			{
				//--------------------------------------------
				// Try to identify us as the sender.
				//--------------------------------------------

				if($this->socketCommand("EHLO ". $host) != 250)
				{
					trigger_error("Could not identify as the sender.");
				}

				if($this->socketCommand("AUTH LOGIN") == 334)
				{
					//--------------------------------------------
					// Make sure the username is good.
					//--------------------------------------------

					if($this->socketCommand(base64_encode($user)) != 334)
					{
						trigger_error("Invalid SMTP username.");
					}

					//--------------------------------------------
					// Make sure the password is good.
					//--------------------------------------------

					if($this->socketCommand(base64_encode($pass)) != 334)
					{
						trigger_error("Invalid SMTP password.");
					}
				}
			}
			else
			{
				//--------------------------------------------
				// Try to identify us as the sender.
				//--------------------------------------------

				if($this->socketCommand("HELO ". $host) != 250)
				{
					trigger_error("Could not identify as the sender.");
				}
			}
		}

		//--------------------------------------------
		// Disconnect from the SMTP server.
		//--------------------------------------------

		function smtpDisconnect()
		{
			if($this->_smtp)
			{
				$this->socketCommand("QUIT");

				fclose($this->_smtp_fp);
			}
		}

		//--------------------------------------------
		// Send a command to the socket and return the
		// SMTP code.
		//--------------------------------------------

		function socketCommand($command)
		{
			fputs($this->_smtp_fp, $command ."\r\n");

			$this->socketGetReturnInfo();

			return $this->_smtp_code;
		}

		//--------------------------------------------
		// Parse the info of the last command, get the code
		// and the message.
		//--------------------------------------------

		function socketGetReturnInfo()
		{
			$this->_smtp_code = FALSE;
			$this->_smtp_msg = "";

			while($line = fgets($this->_smtp_fp, 515))
			{
				$this->_smtp_msg .= $line;

				if($line{4} == " ")
				{
					break;
				}
			}

			$this->_smtp_code = substr_utf($this->_smtp_msg, 0, 3);
		}
	}

	?>
	

Hope they come in handy!

Comments

if($this->socketCommand("EHLO ". $host) != 250)

something looks odd there :D shouln't that be HELO or HELLO

You'd think that but the way I did it follows the protocols in RFC 2821, http://www.ietf.org/rfc/rfc2821.txt

Add a Comment