Posted: Fri May 05, 2006 3:26 am
I'm releasing this soon, but I have a quick question... apologies, this is more of a PHP Code questionn than a theory one. If I want to use an SSL connection to the server is it really just as easy as:
fsockopen('ssl://smtp.server.com', $port)
in place of
fsockopen('smtp.server.com', $port)
?
That seems too easy
Here's the code at present of anybody wants a gander.
Supports:
The connection classes haven't changed since the last post... the mailer class now also caches the email in a local variable when sending to many addresses, thus saving time on encoding.
I was looking over the docs at phpMailer and it seems that almost everything that does, mine does with less lines of code. They do have a few more encoding types though but those can be easily added.
fsockopen('ssl://smtp.server.com', $port)
in place of
fsockopen('smtp.server.com', $port)
?
That seems too easy
Here's the code at present of anybody wants a gander.
Supports:
- Batch Emailing via a single connection
- Multipart/MIME Version 1.0 type mail
- Attachments passed as string data so can link with GD/LibPDF etc.
- Custom Headers
- SMTP Authentication (PLAIN, LOGIN, CRAM-MD5)
- Custom SMTP commands
- Sendmail support
- Ability to write your own custom connection classes for unusual scenarios
- No need to use the mail() function
Code: Select all
<?php
/*
dMail: A Flexible PHP Mailer Class.
Version: 0.0.0
Author: Chris Corbyn
Date: 1st May 2006
Current functionality:
* Send uses one single connection to the SMTP server
* Doesn't rely on mail()
* Custom Headers
* Sends Multipart messages, handles encoding
* Sends Plain-text single-part emails
* Batch emailing
* Support for multiple attachments
* Sendmail (or other binary) support
* SMTP Authentication (LOGIN, PLAIN, MD5-CRAM)
Things to add:
* SSL (Not sure?)
-----------------------------------------------------------------------
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
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
"Chris Corbyn" <chris@w3style.co.uk>
*/
class mailer
{
private
//Standard properties
$mailObject,
$esmtp = false,
$authTypes = array(),
// * Hey this library is FREE so it's not much to ask But if you really do want to
// remove this header then go ahead of course... what's GPL for?
$headers = "X-Mailer: dMail by Chris Corbyn\r\n",
$domain = 'dMailUser',
//MIME support
$mimeBoundary,
$mimeWarning,
$parts = array(),
$attachments = array(),
//Used internally to check the status
$responseCode,
$expectedCodes = array(
'ehlo' => 250,
'helo' => 250,
'auth' => 334,
'mail' => 250,
'rcpt' => 250,
'data' => 354
),
$failed = false;
public
//Just logging stuff
$errors = array(),
$transactions = array(),
$lastResponse;
public function __construct($object, $domain)
{
$this->domain = $domain;
$this->mailObject =& $object;
if (!$this->mailObject->start())
{
$this->logError('Unable to open a connection in the mail object', 0);
}
else
{
//What did the server greet us with on connect?
$this->logTransaction();
if ($this->supportsESMTP($this->lastResponse))
{
//Just being polite
$list = $this->command("EHLO {$this->domain}\r\n");
$this->getAuthenticationMethods($list);
$this->esmtp = true;
}
else $this->command("HELO {$this->domain}\r\n");
}
$this->mimeWarning = "This part of the E-mail should never be seen. If\r\n".
"you are reading this, consider upgrading your e-mail\r\n".
"client to a MIME-compatible client.";
}
private function supportsESMTP($greeting)
{
//Not mentiioned in RFC 2821 but this how it's done
if (preg_match('/\bESMTP\b/', $greeting)) return true;
else return false;
}
//Looks for a line that says the server supports AUTH and
// then checks the types it allows
private function getAuthenticationMethods($list)
{
preg_match("/^250[\-\ ]AUTH\ (.*)\r\n/m", $list, $matches);
if (!empty($matches[1]))
{
$types = explode(' ', $matches[1]);
$this->authTypes = $types;
}
}
//AUTHENTICATION MECHANISMS BELOW (See RFC 2554)
public function authenticate($username, $password=null)
{
if (!$this->esmtp || empty($this->authTypes)) return false;
if (in_array('LOGIN', $this->authTypes)) return $this->authLOGIN($username, $password);
elseif (in_array('CRAM-MD5', $this->authTypes)) return $this->authCRAM_MD5($username, $password);
elseif (in_array("PLAIN", $this->authTypes)) return $this->authPLAIN($username, $password);
else
{
$this->logError('The server doesn\'t like any of dMail\'s implemented authentication mechanisms', 0);
$this->failed = true;
return false;
}
}
//Authenticate using LOGIN (base64 encoded speaking here)
private function authLOGIN($username, $password)
{
$response = $this->command("AUTH LOGIN\r\n");
//This should be the server OK go ahead and give me a username
preg_match('/^334\ (.*)$/', $response, $matches);
if (!empty($matches[1]))
{
$decoded_response = base64_decode($matches[1]);
if (strtolower($decoded_response) == 'username:')
{
$response = $this->command(base64_encode($username));
//This should be the server saying now give me a password
preg_match('/^334\b\ (.*)$/', $response, $matches);
if (!empty($matches[1]))
{
$decoded_response = base64_decode($matches[1]);
if (strtolower($decoded_response) == 'password:')
{
//235 is a good authentication response!
$this->command(base64_encode($password));
if ($this->responseCode == 235) return true;
}
}
}
}
//If the logic got down here then the authentication failed
$this->logError('Authentication failed using LOGIN', $this->responseCode);
$this->failed = true;
return false;
}
private function authPLAIN($username, $password)
{
//The authorization string uses ascii null as a separator (See RFC 2554)
$auth_string = base64_encode("$username\0$username\0$password");
$this->command("AUTH PLAIN\r\n");
if ($this->responseCode == 334)
{
$this->command("$auth_string\r\n");
//This should be the server saying OK
if ($this->responseCode == 235)
{
return true;
}
}
$this->logError('Authentication failed using PLAIN', $this->responseCode);
$this->failed = true;
return false;
}
private function authCRAM_MD5($username, $password)
{
$response = $this->command("AUTH CRAM-MD5\r\n");
preg_match('/^334\ (.*)$/', $response, $matches);
if (!empty($matches[1]))
{
//This response is a base64 encoded challenge "<123456.123456789@domain.tld>"
$decoded_response = base64_decode($matches[1]);
//We need to generate a digest using this challenge
$digest = $username.' '.$this->_authGenerateCRAM_MD5_Response($password, $decoded_response);
//We then send the username and digest as a base64 encoded string
$auth_string = base64_encode($digest);
$this->command("$auth_string\r\n");
if ($this->responseCode == 235) //235 means OK
{
return true;
}
}
$this->logError('Authentication failed using CRAM-MD5', $this->responseCode);
$this->failed = true;
return false;
}
//This has been lifted from a PEAR implementation at
// http://pear.php.net/package/Auth_SASL/
private function _authGenerateCRAM_MD5_Response($password, $challenge)
{
if (strlen($password) > 64)
$password = pack('H32', md5($password));
if (strlen($password) < 64)
$password = str_pad($password, 64, chr(0));
$k_ipad = substr($password, 0, 64) ^ str_repeat(chr(0x36), 64);
$k_opad = substr($password, 0, 64) ^ str_repeat(chr(0x5C), 64);
$inner = pack('H32', md5($k_ipad.$challenge));
$digest = md5($k_opad.$inner);
return $digest;
}
//END AUTHENTICATION
//Each multipart section of an email needs a unique boundary
// This should get one for us
private function getMimeBoundary($string=false)
{
$force = true;
if (!$string)
{
$force = false;
$string = implode('', $this->parts);
$string .= implode('', $this->attachments);
}
if ($this->mimeBoundary && !$force) return $this->mimeBoundary;
else
{ //Make sure we don't (as if it would ever happen!) -
// produce a hash that's actually in the email already
do
{
$this->mimeBoundary = 'dMail-'.strtoupper(md5($string));
} while(strpos($string, $this->mimeBoundary));
}
return $this->mimeBoundary;
}
public function addHeaders($string)
{
$this->headers .= $string;
if (substr($this->headers, -2) != "\r\n")
$this->headers .= "\r\n";
}
public function setMimeBoundary($string)
{
$this->mimeBoundary = $string;
}
public function setMimeWarning($warning)
{
$this->mimeWarning = $warning;
}
//When a multipart message has been sent, and the user wants to
// send a different message through the same connection flush() needs
// to be called to clear the mime parts
public function flush($clear_headers=false)
{
$this->parts = array();
$this->attachments = array();
$this->mimeBoundary = null;
//See comment above the headers property above the constructor before editing this line! *
if ($clear_headers) $this->headers = "X-Mailer: dMail by Chris Corbyn\r\n";
}
//Dish out the appropriate commands to send an email through the server
public function send($to, $from, $subject, $body=false, $type='text/plain', $encoding='7bit')
{
if (!is_array($to)) $to = array($to);
$get_body = true;
$cached_body = '';
foreach ($to as $address)
{
//** I need to work on a cache here **
$data = $this->buildMail($address, $from, $subject, $body, $type, $encoding, $loop);
echo count($data).'**';
if (!$get_body && !empty($cached_body))
{
$data[] = $this->makeRecipientHeaders($address).$cached_body; //Since one wasn't returned
}
foreach ($data as $command)
{
if ($get_body && $this->responseCode == 354) //This means we're about to send the DATA part
{
$cached_body = $command;
$command = $this->makeRecipientHeaders($address).$command;
}
if (!$this->command($command)) return false;
}
$get_body = false;
}
$this->flush(true);
return true;
}
//Connect errors, timeouts, anything that went wrong but
// wasn't brought up by SMTP itself
private function logError($errstr, $errno=0)
{
$this->errors[] = array(
'num' => $errno,
'time' => microtime(),
'message' => $errstr
);
}
//Keep track of everything that gets said
public function logTransaction($command='')
{
$this->transactions[] = array(
'command' => $command,
'time' => microtime(),
'response' => $this->getResponse()
);
}
//Read back the data from our socket
private function getResponse()
{
if (!$this->mailObject->readHook) return false;
$ret = "";
while (true)
{
$tmp = fgets($this->mailObject->readHook);
//The last line of SMTP replies have a space after the status number
// They do NOT have an EOF so while(!feof($socket)) will hang!
if (!preg_match('/^\d+\ /', $tmp)) $ret .= $tmp;
else
{
$ret .= $tmp;
break;
}
}
$this->responseCode = $this->getResponseCode($ret);
$this->lastResponse = $ret;
return $ret;
}
private function getResponseCode($string)
{
if (preg_match('/^\d+/', $string, $matches))
{
return (int) $matches[0];
}
else return 0;
}
private function getCommandKeyword($comm)
{
if (preg_match('/^\s*([\S]+)/', $comm, $matches))
{
if (!empty($matches[1])) return strtolower($matches[1]);
else return '';
}
else return '';
}
public function command($comm)
{
if (!$this->mailObject->writeHook || $this->failed) return false;
$command_keyword = $this->getCommandKeyword($comm);
//SMTP commands must end with CRLF
if (substr($comm, -2) != "\r\n") $comm .= "\r\n";
if (@fwrite($this->mailObject->writeHook, $comm))
{
$this->logTransaction($comm);
if (array_key_exists($command_keyword, $this->expectedCodes))
{
if ($this->expectedCodes[$command_keyword] != $this->responseCode)
{
$this->failed = true;
$this->logError($this->lastResponse, $this->responseCode);
return false;
}
}
return $this->lastResponse;
}
else return false;
}
//Add a part to a multipart message
public function addPart($string, $type='text/plain', $encoding='7bit')
{
$ret = "Content-Type: $type\r\n".
"Content-Transfer-Encoding: $encoding\r\n\r\n".
$this->encode($string, $encoding);
$this->parts[] = $ret;
}
//Attachments are added as base64 encoded data
public function addAttachment($data, $filename, $type)
{
$ret = "Content-Type: $type; ".
"name=\"$filename\";\r\n".
"Content-Transfer-Encoding: base64\r\n".
"Content-Disposition: attachment;\r\n\r\n".
chunk_split($this->encode($data, 'base64'));
$this->attachments[] = $ret;
}
public function close()
{
if ($this->mailObject->writeHook)
{
$this->command("quit\r\n");
$this->mailObject->stop();
}
}
public function hasFailed()
{
return $this->failed;
}
private function encode($string, $type)
{
$type = strtolower($type);
switch ($type)
{
case 'base64':
$string = base64_encode($string);
break;
//
case 'quoted-printable':
$string = $this->quotedPrintableEncode($string);
//
case '7bit':
default:
break;
}
return $string;
}
//From php.net by user bendi at interia dot pl
private function quotedPrintableEncode($string)
{
$string = preg_replace('/[^\x21-\x3C\x3E-\x7E\x09\x20]/e', 'sprintf( "=%02x", ord ( "$0" ) ) ;', $string);
preg_match_all('/.{1,73}([^=]{0,3})?/', $string, $matches);
return implode("=\r\n", $matches[0]);
}
//Because SMTP data transmissions end with CRLF <dot> CRLF
// we need to remove any of these from the string.
private function escapeDot($string)
{
$stack = array();
$lines = explode("\r\n", $string);
foreach ($lines as $l)
{
if ($l == '.') $l = ' .'; //Quite simple... just add some whitespace padding!
$stack[] = $l;
}
return implode("\r\n", $stack);
}
private function makeRecipientHeaders($address)
{
return "To: $address\r\n";
}
//Builds the list of commands to send the email
private function buildMail($to, $from, $subject, $body, $type='text/plain', $encoding='7bit', $return_data_part=true)
{
$date = date('r'); //RFC 2822 date
if ($return_data_part)
{
if ($body) //Ignore multipart messaging
{
$data = "From: $from\r\n".
"Reply-To: $from\r\n".
"Subject: $subject\r\n".
"Date: $date\r\n".
"{$this->headers}".
"Content-Type: $type\r\n".
"Content-Transfer-Encoding: $encoding\r\n\r\n".
$this->escapeDot($this->encode($body, $encoding));
}
else //Build a multipart message
{
$boundary = $this->getMimeBoundary(); //Overall MIME boundary
$message_body = implode("\r\n\r\n--$boundary\r\n", $this->parts);
if (!empty($this->attachments)) //Make a sub-message that contains attachment data
{
$attachment_boundary = $this->getMimeBoundary(implode('', $this->attachments));
$attachments = implode("\r\n\r\n--$attachment_boundary\r\n", $this->attachments);
$attachments = "\r\n\r\n--$boundary\r\n".
"Content-Type: multipart/alternative;\r\n".
" boundary=\"$attachment_boundary\"\r\n".
"Content-Transfer-Encoding: 7bit\r\n\r\n".
"\r\n\r\n--$attachment_boundary\r\n".
$attachments.
"\r\n--$attachment_boundary--\r\n";
}
$data = "From: $from\r\n".
"Reply-To: $from\r\n".
"Subject: $subject\r\n".
"Date: $date\r\n".
"{$this->headers}".
"MIME-Version: 1.0\r\n".
"Content-Type: multipart/mixed;\r\n".
" boundary=\"{$boundary}\"\r\n".
"Content-Transfer-Encoding: 7bit\r\n\r\n";
$data .= $this->mimeWarning;
if (isset($attachments)) $message_body .= $attachments;
$data .= "\r\n\r\n--$boundary\r\n".
"$message_body\r\n".
"\r\n--$boundary--";
}
$data = $this->escapeDot($data);
$data .= "\r\n.\r\n";
}
$ret = array(
//Can be spoofed but that's not my problem
"mail from: $from\r\n",
//Recipient
"rcpt to: $to\r\n",
//Inform that we're about the give our message
"data\r\n"
);
if ($return_data_part) $ret[] = $data;
return $ret;
}
}
?>I was looking over the docs at phpMailer and it seems that almost everything that does, mine does with less lines of code. They do have a few more encoding types though but those can be easily added.