Posted: Tue May 02, 2006 12:02 am
That's enough debating on naming conventions.
A community of PHP developers offering assistance, advice, discussion, and friendship.
http://forums.devnetwork.net/
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;
}
}
?>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
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?
Code: Select all
$mailer->loadAuthenticator(new loginAuthenticator, 'LOGIN');
$mailer->loadAuthenticator(new cramMD5Authenticator, 'CRAM-MD5');
$mailer->loadAuthenticator(new plainAuthenticator, 'PLAIN');
$mailer->authenticate('username', 'password');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);
}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;
}
}
?>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);Code: Select all
$mailer= new Mailer(new SmtpConnection());
$authenticator = new LOGIN_authenticator($mailer);
$authenticator->authenticate('username', 'pasword');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');
Code: Select all
$mailer= new Mailer(new SmtpConnection());
$authenticator = authenticator_type_search($mailer, array('LOGIN', 'PLAIN', 'CRAM-MD5'));
$authenticator->authenticate('username', 'pasword');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');
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();
}
}
}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;
}
}
?>