Some input desired
Moderator: General Moderators
- John Cartwright
- Site Admin
- Posts: 11470
- Joined: Tue Dec 23, 2003 2:10 am
- Location: Toronto
- Contact:
- Chris Corbyn
- Breakbeat Nuttzer
- Posts: 13098
- Joined: Wed Mar 24, 2004 7:57 am
- Location: Melbourne, Australia
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.
- Chris Corbyn
- Breakbeat Nuttzer
- Posts: 13098
- Joined: Wed Mar 24, 2004 7:57 am
- Location: Melbourne, Australia
OK I answered my own question... turns out it really is that simpled11wtq wrote: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
- Christopher
- Site Administrator
- Posts: 13596
- Joined: Wed Aug 25, 2004 7:54 pm
- Location: New York, NY, US
- Chris Corbyn
- Breakbeat Nuttzer
- Posts: 13098
- Joined: Wed Mar 24, 2004 7:57 am
- Location: Melbourne, Australia
Nothing that I can think of... I just personally dislike not using the appropriate keywords when they're available... I'll run two concurrent versions for downloading.arborint wrote:Looks great. Is there any reason why the class could not be made to work in PHP4 other than public/private and __construct?
- Chris Corbyn
- Breakbeat Nuttzer
- Posts: 13098
- Joined: Wed Mar 24, 2004 7:57 am
- Location: Melbourne, Australia
OK now I'm thinking about optional extensions and/or plugins to include. Some have been mentioned:
A templating engine
A logger (to DB, XML, Text file etc)
What else? There's something I did think of:
A SPAM checker -- Not for blocking SPAM of course, but something that runs spamassassin over it and reports if the mail will likely be caught as junk.
If this had plugin support I'm sure there most be loads of things people could think of to write.
A templating engine
A logger (to DB, XML, Text file etc)
What else? There's something I did think of:
A SPAM checker -- Not for blocking SPAM of course, but something that runs spamassassin over it and reports if the mail will likely be caught as junk.
If this had plugin support I'm sure there most be loads of things people could think of to write.
- Christopher
- Site Administrator
- Posts: 13596
- Joined: Wed Aug 25, 2004 7:54 pm
- Location: New York, NY, US
I think the auth code could be made pluggable in a similar way to the connections.
For things like templating, perhaps a simple Filter Chain that allowed you to post process the email would be the way to go. But would it have access to an array of parts or the whole finished email? I think the former and the mailer would then assemble everything.
And I think if you added command() method to the connections you would get all low level I/O out of the mailer class.
For things like templating, perhaps a simple Filter Chain that allowed you to post process the email would be the way to go. But would it have access to an array of parts or the whole finished email? I think the former and the mailer would then assemble everything.
And I think if you added command() method to the connections you would get all low level I/O out of the mailer class.
(#10850)
- Chris Corbyn
- Breakbeat Nuttzer
- Posts: 13098
- Joined: Wed Mar 24, 2004 7:57 am
- Location: Melbourne, Australia
I guess for the authentication stuff you could do something like:
But I'm concious of increasing the complexity of the interface too much, so that less-savvy developers will avoid using this class. Something like authentication is almost certainly required/expected if it's going to be used on SMTP servers... I could have the class automatically load in the default authenticators (PLAIN, LOGIN, CRAM-MD5) but then provide something like I've shown above to provide a way to load in some others (KERBEROS, DIGEST-MD5...). The idea is that the mailer looks to see what methods the SMTP server supports and goes through the one we have implemented to see if they are compatible.
Perhaps like having the connection classes with a couple of methods:
The mailer class could then simply provide a means to access it's own command() method as soon as the connection class gets loaded.... I assume the reason this would be useful is in situations where a certain connection needs some additional set up by speaking with the SMTP server.
I haven't really thought too deeply into how a template system would work but it would probably be a good idea to make everything pluggable in any case... with some sort of well-handled plugin interface the flexibility would be massive.
Code: Select all
$mailer->loadAuthenticator(new loginAuthenticator, 'LOGIN');
$mailer->loadAuthenticator(new cramMD5Authenticator, 'CRAM-MD5');
$mailer->loadAuthenticator(new plainAuthenticator, 'PLAIN');
$mailer->authenticate('username', 'password');Perhaps like having the connection classes with a couple of methods:
Code: Select all
public function setCommandCallback(&$obj, $function)
{
$this->baseObject = $obj;
$this->commandFunction = $function;
}
//Yuck!
public function command($comm)
{
return $this->baseObject->{$this->commandFunction}($comm);
}I haven't really thought too deeply into how a template system would work but it would probably be a good idea to make everything pluggable in any case... with some sort of well-handled plugin interface the flexibility would be massive.
- Chris Corbyn
- Breakbeat Nuttzer
- Posts: 13098
- Joined: Wed Mar 24, 2004 7:57 am
- Location: Melbourne, Australia
I've updated the code to make the authenticators pluggable:
The loader looks like this:
With an authenticator class looking like this:
The authenticator class MUST provide a public property "serverString" which identifies what the server would call this method, it must contain a run() method to execute the authentication and also a loadBaseObject() method to provide access to the sockets and the command() method.
I'm wondering... because this basically induces this sort of a pattern:
There's actually recursion there where each class refers back to itself via the other class in a never-ending fashion... is this bad?? It works though.
Code: Select all
$mailer->loadAuthenticator(new LOGIN_authenticator);
$mailer->authenticate('username', 'pasword');Code: Select all
public function authenticate($username, $password=null)
{
if (!$this->esmtp || empty($this->authTypes)) return false;
foreach ($this->authenticators as $name => $object)
{
if (in_array($name, $this->authTypes))
{
return $this->authenticators[$name]->run($username, $password);
}
}
//If we get this far, no authenticators were used
$this->logError('The server doesn\'t like any of dMail\'s implemented authentication mechanisms', 0);
$this->failed = true;
return false;
}
public function loadAuthenticator(&$object)
{
$this->authenticators[$object->serverString] =& $object;
$this->authenticators[$object->serverString]->loadBaseObject(&$this);
}Code: Select all
class LOGIN_authenticator
{
public $serverString = 'LOGIN';
private $baseObject;
public function __construct()
{
//
}
public function loadBaseObject(&$object)
{
$this->baseObject =& $object;
}
public function run($username, $password)
{
return $this->authLOGIN($username, $password);
}
private function authLOGIN($username, $password)
{
$response = $this->baseObject->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->baseObject->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->baseObject->command(base64_encode($password));
if ($this->baseObject->responseCode == 235) return true;
}
}
}
}
//If the logic got down here then the authentication failed
$this->baseObject->logError('Authentication failed using LOGIN', $this->responseCode);
$this->baseObject->failed = true;
return false;
}
}
?>I'm wondering... because this basically induces this sort of a pattern:
Code: Select all
class baseClass
{
private $innerObj;
function loadObject(&$object)
{
$this->innerObj =& $object;
$this->innerObj->loadObject(&$this);
}
}
class innerClass
{
private $baseObj;
function loadObject(&$object)
{
$this->baseObj =& $obj;
}
}
$object = new baseClass;
$object->loadObject(new innerClass);- Christopher
- Site Administrator
- Posts: 13596
- Joined: Wed Aug 25, 2004 7:54 pm
- Location: New York, NY, US
I am wondering if this might be better?
Code: Select all
$mailer= new Mailer(new SmtpConnection());
$authenticator = new LOGIN_authenticator($mailer);
$authenticator->authenticate('username', 'pasword');(#10850)
- Chris Corbyn
- Breakbeat Nuttzer
- Posts: 13098
- Joined: Wed Mar 24, 2004 7:57 am
- Location: Melbourne, Australia
Yes it looks better logically but then you're expecting your users to know what authentication mechanism to use... the other way the mailer class does it for you. i.e. Would you know whether smtp.ntlworld.com needed LOGIN, PLAIN or CRAM-MD5 ?arborint wrote:I am wondering if this might be better?Code: Select all
$mailer= new Mailer(new SmtpConnection()); $authenticator = new LOGIN_authenticator($mailer); $authenticator->authenticate('username', 'pasword');
Perhaps something along those lines where the authenticator contains a container like I did with the mailer... so we can then load in as many as we need.
Thanks for the input
- Christopher
- Site Administrator
- Posts: 13596
- Joined: Wed Aug 25, 2004 7:54 pm
- Location: New York, NY, US
It seems like if you didn't know you would try them until one worked. For searching for the right type, maybe just create a helper function like:
Code: Select all
$mailer= new Mailer(new SmtpConnection());
$authenticator = authenticator_type_search($mailer, array('LOGIN', 'PLAIN', 'CRAM-MD5'));
$authenticator->authenticate('username', 'pasword');(#10850)
- Chris Corbyn
- Breakbeat Nuttzer
- Posts: 13098
- Joined: Wed Mar 24, 2004 7:57 am
- Location: Melbourne, Australia
Sorry I didn't really like that idea of having yet another function for the user to learn how to usearborint wrote:It seems like if you didn't know you would try them until one worked. For searching for the right type, maybe just create a helper function like:Code: Select all
$mailer= new Mailer(new SmtpConnection()); $authenticator = authenticator_type_search($mailer, array('LOGIN', 'PLAIN', 'CRAM-MD5')); $authenticator->authenticate('username', 'pasword');
Moving on however, a plugin system has been put in place but I'm wondering if I've perhaps gone about it in a roundabout way.
What I've done, is implemented the plugin loader much like how the authenticators are loaded (they get passed an a reference to $this). I've then moved a few things out into public properties (such as the current mail that being sent) so that they can be interacted with. (Now I think this is quite nice and clever
My main question is about the event handlers... does it sound like a good idea or just me getting over-excited?
Code: Select all
//This is called at relevant steps in the mailer i.e. in send(), $this->runPluginMethods('onSend');
private function runPluginMethods($func)
{
foreach ($this->plugins as $name => $object)
{
if (method_exists($this->plugins[$name], $func))
{
$this->plugins[$name]->$func();
}
}
}- Chris Corbyn
- Breakbeat Nuttzer
- Posts: 13098
- Joined: Wed Mar 24, 2004 7:57 am
- Location: Melbourne, Australia
Regarding my post above, here's the full class (notice the calls to various event handlers throught?)
NOTE: I had to change the name of the class since dMail is actually an MTA which was confusing.
NOTE: I had to change the name of the class since dMail is actually an MTA which was confusing.
Code: Select all
<?php
/*
Swift Mailer: A Flexible PHP Mailer Class.
Version: 0.0.1
Author: Chris Corbyn
Date: 10th May 2006
(C) Copyright 2006 Chris Corbyn - All Rights Reserved.
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
* Pluggable SMTP Authentication (LOGIN, PLAIN, MD5-CRAM)
* Secure Socket Layer connections (SSL)
* Loadable plugin support with event handling features
-----------------------------------------------------------------------
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 Swift
{
private
//Standard properties
$connection,
$authenticators = array(),
$plugins = array(),
$esmtp = false,
$authTypes = array(),
$domain = 'SwiftUser',
//MIME support
$mimeBoundary,
$mimeWarning,
$parts = array(),
$attachments = array(),
//Used internally to check the status
$expectedCodes = array(
'ehlo' => 250,
'helo' => 250,
'auth' => 334,
'mail' => 250,
'rcpt' => 250,
'data' => 354
);
public
$failed = false,
$responseCode,
//For plugins (& used internally)
$currentMail = 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: Swift by Chris Corbyn\r\n",
$currentCommand = '',
//Just logging stuff
$errors = array(),
$transactions = array(),
$lastTransaction,
$lastError,
$lastResponse;
public function __construct($object, $domain)
{
$this->domain = $domain;
$this->connection =& $object;
if (!$this->connection->start())
{
$this->failed = true;
$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;
}
public function addExpectedCode($command, $code)
{
$this->expectedCodes[$command] = (int) $code;
}
//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;
}
}
public function loadPlugin(&$object)
{
$this->plugins[$object->pluginName] =& $object;
$this->plugins[$object->pluginName]->loadBaseObject(&$this);
if (method_exists($this->plugins[$object->pluginName], 'onLoad'))
{
$this->plugins[$object->pluginName]->onLoad();
}
}
public function &getPlugin($name)
{
if (isset($this->plugins[$name]))
{
return $this->plugins[$name];
}
}
private function runPluginMethods($func)
{
foreach ($this->plugins as $name => $object)
{
if (method_exists($this->plugins[$name], $func))
{
$this->plugins[$name]->$func();
}
}
}
//AUTHENTICATION MECHANISMS BELOW (See RFC 2554)
private function loadDefaultAuthenticators()
{
$dir = dirname(__FILE__).'/Swift';
if (file_exists($dir) && is_dir($dir))
{
$handle = opendir($dir);
while ($file = readdir($handle))
{
if (preg_match('@^(Swift_\w*?_Authenticator)\.php$@', $file, $matches))
{
require_once($dir.'/'.$file);
$class = $matches[1];
$this->loadAuthenticator(new $class);
}
}
closedir($handle);
}
}
public function authenticate($username, $password=null)
{
if (empty($this->authenticators)) $this->loadDefaultAuthenticators();
if (!$this->esmtp || empty($this->authTypes)) return false;
foreach ($this->authenticators as $name => $object)
{
if (in_array($name, $this->authTypes))
{
if ($this->authenticators[$name]->run($username, $password))
{
$this->runPluginMethods('onAuthenticate');
return true;
}
else return false;
}
}
//If we get this far, no authenticators were used
$this->logError('The server doesn\'t like any of Swift\'s implemented authentication mechanisms', 0);
$this->failed = true;
return false;
}
public function loadAuthenticator(&$object)
{
$this->authenticators[$object->serverString] =& $object;
$this->authenticators[$object->serverString]->loadBaseObject(&$this);
}
//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 = 'swift-'.strtoupper(md5($string.microtime()));
} 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->runPluginMethods('onFlush');
$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: Swift 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)
{
$this->currentMail = $this->buildMail($address, $from, $subject, $body, $type, $encoding, $get_body);
if (!$get_body && !empty($cached_body))
{
$this->currentMail[] = $this->makeRecipientHeaders($address).$cached_body; //Since one wasn't returned
}
$this->runPluginMethods('onBeforeSend');
foreach ($this->currentMail 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->runPluginMethods('onSend');
}
$this->flush(true);
return true;
}
//Connect errors, timeouts, anything that went wrong but
// wasn't brought up by SMTP itself
public 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->lastTransaction = array(
'command' => $command,
'time' => microtime(),
'response' => $this->getResponse()
);
$this->runPluginMethods('onLog');
$this->transactions[] = $this->lastTransaction;
}
//Read back the data from our socket
private function getResponse()
{
if (!$this->connection->readHook) return false;
$ret = "";
while (true)
{
$tmp = fgets($this->connection->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;
$this->runPluginMethods('onResponse');
return $this->lastResponse;
}
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 '';
}
//Issue a command the the connection
public function command($comm)
{
$this->currentCommand = $comm;
$this->runPluginMethods('onBeforeCommand');
if (!$this->connection->writeHook || $this->failed) return false;
$command_keyword = $this->getCommandKeyword($this->currentCommand);
//SMTP commands must end with CRLF
if (substr($this->currentCommand, -2) != "\r\n") $this->currentCommand .= "\r\n";
if (@fwrite($this->connection->writeHook, $this->currentCommand))
{
$this->logTransaction($this->currentCommand);
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;
}
}
$this->runPluginMethods('onCommand');
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->connection->writeHook)
{
$this->command("quit\r\n");
$this->connection->stop();
}
$this->runPluginMethods('onClose');
}
public function hasFailed()
{
return $this->failed;
}
public function fail()
{
$this->runPluginMethods('onFail');
$this->failed = true;
}
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;
}
}
?>