* * 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 3 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, see . **/ require_once ('qcEvents/Interface/Stream/Consumer.php'); require_once ('qcEvents/Hookable.php'); /** * SMTP-Client * ----------- * Simple SMTP-Client-Implementation (RFC 5321) * * @class qcEvents_Stream_SMTP_Client * @extends qcEvents_Hookable * @package qcEvents * @revision 03 * @author Bernd Holzmueller * * @changelog 20130703 Added Support for RFC 3207 StartTLS * 20130703 Added basic Support for RFC 2034 Enhanced Status Codes * 20130703 Added Support for RFC 1870 SMTP Size Declaration * 20130704 Added Support for RFC 1985 ETRN Command (remote queue startup) * 20130705 Added Support for RFC 4954 SMTP Authentication **/ class qcEvents_Stream_SMTP_Client extends qcEvents_Hookable implements qcEvents_Interface_Stream_Consumer { /* The attached stream */ private $Stream = null; /* Protocol state */ const SMTP_STATE_DISCONNECTED = 0; const SMTP_STATE_CONNECTING = 1; const SMTP_STATE_CONNECTED = 2; const SMTP_STATE_TRANSACTION = 3; const SMTP_STATE_DISCONNECTING = 4; const SMTP_HANDSHAKE_START = 0; const SMTP_HANDSHAKE_EHLO = 1; const SMTP_HANDSHAKE_FALLBACK = 2; private $State = qcEvents_Stream_SMTP_Client::SMTP_STATE_DISCONNECTED; /* Internal read-buffer */ private $Buffer = ''; /* Domainname of this client */ private $clientName = null; /* Is this connection authenticated */ private $authenticated = false; /* State for handshake */ private $connectingState = qcEvents_Stream_SMTP_Client::SMTP_HANDSHAKE_START; /* Command-Buffer */ private $Command = null; private $Commands = array (); /* Response-Buffer */ private $responseCode = null; private $responseLines = array (); /* Last response from server */ private $lastCode = null; private $lastLines = null; /* Queued mails */ private $mailCurrent = null; private $mailQueue = array (); /* Domain of server */ private $serverDomain = null; /* Features supported by the server */ private $serverFeatures = null; private $initCallback = null; // {{{ getClientName /** * Retrive the name of this client * * @access public * @return string **/ public function getClientName () { if ($this->clientName !== null) return $this->clientName; elseif (function_exists ('gethostname')) return gethostname (); return 'smtpc.quarxconnect.org'; } // }}} // {{{ setClientName /** * Store the DNS-Name of this client * * @param string $Name * * @access public * @return bool **/ public function setClientName ($Name) { $this->clientName = $Name; return true; } // }}} // {{{ getLastCode /** * Retrive the last result-code * * @access public * @return int **/ public function getLastCode () { return $this->lastCode; } // }}} // {{{ hasFeature /** * Check if our peer supports a given feature * * @param string $Feature * * @access public * @return bool **/ public function hasFeature ($Feature) { return (is_array ($this->serverFeatures) && isset ($this->serverFeatures [$Feature])); } // }}} // {{{ startTLS /** * Try to enable encryption on this connection * * @param callable $Callback (optional) * @param mixed $Private (optional) * * The callback will be raised in the form of * * function (qcEvents_Stream_SMTP_Client $Self, bool $Status, mixed $Private = null) { } * * @access public * @return bool **/ public function startTLS (callable $Callback = null, $Private = null) { // Check if the server supports StartTLS if (!is_array ($this->serverFeatures) || !isset ($this->serverFeatures ['STARTTLS'])) { $this->___raiseCallback ($Callback, false, $Private); return false; } // Check if TLS is already active if ($this->Stream->tlsEnable ()) { $this->___raiseCallback ($Callback, true, $Private); return true; } // Issue the command return $this->issueSMTPCommand ( 'STARTTLS', null, self::SMTP_STATE_CONNECTED, self::SMTP_STATE_CONNECTING, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Callback, $Private) { // Check if the server does not want us to enable TLS if ($Code >= 300) { $this->___callback ('tlsFailed'); return $this->___raiseCallback ($Callback, false, $Private); } // Lock the command-pipeline $this->Command = true; // Try to start TLS-negotiation return $this->Stream->tlsEnable (true, function (qcEvents_Socket $Socket, $Status) use ($Callback, $Private) { // Unlock the command-pipeline $this->Command = null; // Check if TLS-negotiation failed if (!$Status) return $this->___raiseCallback ($Callback, false, $Private); // Restart the connection $this->connectingState = self::SMTP_HANDSHAKE_EHLO; $this->serverFeatures = null; $this->issueSMTPCommand ( 'EHLO', array ($this->getClientName ()), null, null, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Callback, $Private) { return $this->___raiseCallback ($Callback, true, $Private); } ); }); } ); } // }}} // {{{ authenticate /** * Try to authenticate this connection * * @param string $Username * @param string $Password * @param callable $Callback * @param mixed $Private (optional) * * The callback will be raised in the form * * function (qcEvents_Stream_SMTP_Client $Self, string $Username, bool $Status, mixed $Private = null) { } * * @access public * @return bool **/ public function authenticate ($Username, $Password, callable $Callback, $Private = null) { // Check if the server supports Authentication if (!is_array ($this->serverFeatures) || !isset ($this->serverFeatures ['AUTH'])) { $this->___raiseCallback ($Callback, $Username, false, $Private); return false; } // Don't authenticate twice if ($this->authenticated) { $this->___raiseCallback ($Callback, $Username, false, $Private); return false; } // Create an authenticator require_once ('qcAuth/SASL/Client.php'); $Client = new qcAuth_SASL_Client; $Client->setUsername ($Username); $Client->setPassword ($Password); $Mechanisms = $this->serverFeatures ['AUTH']; // Try to pick the first mechanism if (count ($Mechanisms) == 0) { $this->___raiseCallback ($Callback, $Username, false, $Private); return false; } $Mechanism = array_shift ($Mechanisms); while (!$Client->setMechanism ($Mechanism)) { if (count ($Mechanisms) == 0) { $this->___raiseCallback ($Callback, $Username, false, $Private); return false; } $Mechanism = array_shift ($Mechanisms); } // Setup SASL-Callback-Handler $saslFunc = null; $saslFunc = function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Username, $Password, $Client, &$Mechanisms, &$Mechanism, &$saslFunc, $Callback, $Private) { // Check if the authentication was successfull if (($Code >= 200) && ($Code < 300)) { // Mark ourself as authenticated $this->authenticated = true; // Issue a EHLO-Command $this->connectingState = self::SMTP_HANDSHAKE_EHLO; $this->serverFeatures = null; return $this->issueSMTPCommand ( 'EHLO', array ($this->getClientName ()), null, null, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Username, $Callback, $Private) { return $this->___raiseCallback ($Callback, $Username, true, $Private); } ); // Check wheter to send the next chunk } elseif (($Code >= 300) && ($Code < 400)) return base64_encode ($Client->getResponse ()); // Check if authentication failed at all elseif ($Code == 535) return $this->___raiseCallback ($Callback, $Username, false, $Private); // Check if there are mechanisms left if (count ($Mechanisms) == 0) return $this->___raiseCallback ($Callback, $Username, false, $Private); // Try to pick the next mechanism $Mechanism = array_shift ($Mechanisms); while (!$Client->setMechanism ($Mechanism)) { if (count ($Mechanisms) == 0) return $this->___raiseCallback ($Callback, $Username, false, $Private); $Mechanism = array_shift ($Mechanisms); } // Ask the server for that mechanism return $this->issueSMTPCommand ( 'AUTH', array ($Mechanism, base64_encode ($Client->getInitialResponse ())), self::SMTP_STATE_CONNECTED, self::SMTP_STATE_CONNECTING, $saslFunc, null, $saslFunc ); }; // Issue the first AUTH-Command return $this->issueSMTPCommand ( 'AUTH', array ($Mechanism, base64_encode ($Client->getInitialResponse ())), self::SMTP_STATE_CONNECTED, self::SMTP_STATE_CONNECTING, $saslFunc, null, $saslFunc ); } // }}} // {{{ startMail /** * Start the submission of an e-mail * * @param string $Originator Originator of the mail * @param array $Params (optional) Additional parameters for this command (for extensions) * @param callable $Callback (optional) A callback to fire once the command was processed * @param mixed $Private (optional) Any private data to pass to the callback * * The callback will be raised in the form of * * function (qcEvents_Stream_SMTP_Client $Self, string $Originator, array $Params, bool $Status, mixed $Private = null) { } * * @access public * @return bool **/ public function startMail ($Originator, $Params = null, callable $Callback, $Private = null) { // Make sure the originator is valid if ($Originator [0] != '<') $Originator = '<' . $Originator . '>'; // Handle params if ($Params !== null) { $iParams = $Params; $Params = array (); if (is_array ($iParams)) foreach ($iParams as $k=>$v) $Params [] = $k . '=' . $v; } // Issue the command return $this->issueSMTPCommand ( 'MAIL FROM:' . $Originator, $Params, self::SMTP_STATE_CONNECTED, self::SMTP_STATE_TRANSACTION, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Originator, $Params, $Callback, $Private) { $this->___raiseCallback ($Callback, $Originator, (is_array ($Params) ? $Params : array ()), ($Code < 400), $Private); } ); } // }}} // {{{ addReceiver /** * Append a receiver for an ongoing transaction * * @param string $Receiver * @param array $Params (optional) * @param callable $Callback * @param mixed $Private (optional) * * The callback will be raised in the form of * * function (qcEvents_Stream_SMTP_Client $Self, string $Receiver, array $Params, bool $Status, mixed $Private = null) { } * * @access public * @return bool **/ public function addReceiver ($Receiver, $Params = null, callable $Callback, $Private = null) { // Make sure the originator is valid if ($Receiver [0] != '<') $Receiver = '<' . $Receiver . '>'; // Issue the command return $this->issueSMTPCommand ( 'RCPT TO:' . $Receiver, $Params, self::SMTP_STATE_TRANSACTION, null, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Receiver, $Params, $Callback, $Private) { $this->___raiseCallback ($Callback, $Receiver, (is_array ($Params) ? $Params : array ()), ($Code < 400), $Private); } ); } // }}} // {{{ sendData /** * Submit Mail-Data * * @param string $Mail * @param callable $Callback (optional) * @param mixed $Private (optional) * * The callback will be raised in the form of * * function (qcEvents_Stream_SMTP_Client $Self, bool $Status, mixed $Private) { } * * @access public * @return bool **/ public function sendData ($Mail, callable $Callback, $Private = null) { // Issue the command return $this->issueSMTPCommand ( 'DATA', null, self::SMTP_STATE_TRANSACTION, self::SMTP_STATE_CONNECTED, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Callback, $Private) { $this->___raiseCallback ($Callback, ($Code < 400), $Private); }, null, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Mail) { $p = 0; while (($p = strpos ($Mail, "\r\n.\r\n", $p)) !== false) $Mail = substr ($Mail, 0, $p + 2) . '.' . substr ($Mail, $p + 2); return $Mail . (substr ($Mail, -2, 2) == "\r\n" ? '' : "\r\n") . ".\r\n"; } ); } // }}} // {{{ reset /** * Abort any ongoing mail-transaction * * @param callable $Callback (optional) * @param mixed $Private (optional) * * The callback will be raised in the form of * * function (qcEvents_Stream_SMTP_Client $Self, bool $Status, mixed $Private) { } * * @access public * @return bool **/ public function reset (callable $Callback = null, $Private = null) { // Issue the command return $this->issueSMTPCommand ( 'RSET', null, null, self::SMTP_STATE_CONNECTED, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Callback, $Private) { $this->___raiseCallback ($Callback, ($Code < 400), $Private); } ); } // }}} // {{{ verify /** * Verfiy a username or mailbox * * @param string $Mailbox * @param callable $Callback * @param mixed $Private (optional) * * The callback will be raised in the form of * * function (qcEvents_Stream_SMTP_Client $Self, string $Mailbox, mixed $Result, string $Fullname = null, bool $Status, mixed $Private = null) { } * * @access public * @return bool **/ public function verify ($Mailbox, callable $Callback, $Private = null) { // Issue the command return $this->issueSMTPCommand ( 'VRFY', array ($Mailbox), null, null, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Mailbox, $Callback, $Private) { $Fullname = null; // Handle a successfull response if ($Status = (($Code >=200) && ($Code < 300))) { $Result = array_shift ($Lines); if (($p = strpos ($Result, '<')) !== false) { $Fullname = rtrim (substr ($Result, 0, $p)); $Result = substr ($Result, $p + 1, strrpos ($Result, '>') - $p - 1); } // Handle failure } else { $Result = array (); foreach ($Lines as $Line) if (($p = strpos ($Line, '<')) !== false) $Result [] = substr ($Line, $p + 1, strrpos ($Line, '>') - $p - 1); } // Raise the callback return $this->___raiseCallback ($Callback, $Mailbox, $Result, $Fullname, $Status, $Private); } ); } // }}} // {{{ noOp /** * Do nothing, but let the server know * * @param callable $Callback (optional) * @param mixed $Private (optional) * * The callback will be raised in the form of * * function qcEvents_Stream_SMTP_Client $Self, bool $Status, mixed $Private = null) { } * * @access public * @return bool **/ public function noOp (callable $Callback = null, $Private = null) { // Issue the command return $this->issueSMTPCommand ( 'NOOP', null, null, null, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Callback, $Private) { return $this->___raiseCallback ($Callback, ($Code < 400), $Private); } ); } // }}} // {{{ startQueue /** * Start/Flush the remote queue for a domain at the servers site * * @param string $Domaim * @param callable $Callback (optional) * @param mixed $Private (optional) * * The callback will be raised in the form of * * function qcEvents_Stream_SMTP_Client $Self, string $Domain, bool $Status, mixed $Private = null) { } * * @access public * @return bool **/ public function startQueue ($Domain, callable $Callback = null, $Private = null) { // Check if the server supports ETRN if (!is_array ($this->serverFeatures) || !isset ($this->serverFeatures ['ETRN'])) { $this->___raiseCallback ($Callback, $Domain, false, $Private); return false; } // Issue the command return $this->issueSMTPCommand ( 'ETRN', array ($Domain), null, null, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Domain, $Callback, $Private) { return $this->___raiseCallback ($Callback, $Domain, ($Code < 400), $Private); } ); } // }}} // {{{ close /** * Ask the server to close this session * * @param callable $Callback (optional) * @param mixed $Private (optional) * * The callback will be raised in the form of * * function (qcEvents_Stream_SMTP_Client $Self, bool $Status, mixed $Private = null) { } * * @access public * @return bool **/ public function close (callable $Callback = null, $Private = null) { // Check if our stream is already closed if (!is_object ($this->Stream)) { // Fire the callback directly $this->___raiseCallback ($Callback, true, $Private); // Check if we are in disconnected state if ($this->State == self::SMTP_STATE_DISCONNECTED) return; // Set disconnected state $this->smtpSetState (self::SMTP_STATE_DISCONNECTED); $this->___callback ('smtpDisconnected'); $this->___callback ('eventClosed'); } // Issue the command return $this->issueSMTPCommand ( 'QUIT', null, null, self::SMTP_STATE_DISCONNECTING, function (qcEvents_Stream_SMTP_Client $Self, $Code, $Lines) use ($Callback, $Private) { return $this->___raiseCallback ($Callback, (($Code >= 200) && ($Code < 300)), $Private); } ); } // }}} // {{{ sendMail /** * Submit an entire mail * * @param string $Originator * @param array $Receivers * @param string $Mail * @param callable $Callback (optional) * @param mixed $Private (optional) * * The callback will be raised in the form of * * function (qcEvents_Stream_SMTP_Client $Self, string $Originator, array $Receivers, string $Mail, bool $Status, mixed $Private = null) { } * * @access public * @return bool **/ public function sendMail ($Originator, $Receivers, $Mail, callable $Callback = null, $Private = null) { // Check the size if (is_array ($this->serverFeatures) && isset ($this->serverFeatures ['SIZE']) && (count ($this->serverFeatures ['SIZE']) > 0) && (strlen ($Mail) > $this->serverFeatures ['SIZE'][0]) && ($this->serverFeatures ['SIZE'][0] > 0)) { $this->___raiseCallback ($Callback, $Originator, $Receivers, $Mail, false, $Private); return false; } // Enqueue the mail $this->mailQueue [] = array ($Originator, $Receivers, $Mail, $Callback, $Private, $Receivers, array ()); // Try to start the submission $this->runMailQueue (); } // }}} // {{{ runMailQueue /** * Check wheter to enqueue the next mail * * @access private * @return void **/ private function runMailQueue () { // Check if there is a mail being transmitted if ($this->mailCurrent !== null) return; // Check if the queue is empty if (count ($this->mailQueue) == 0) return; // Enqueue the next mail $this->mailCurrent = array_shift ($this->mailQueue); // Generate parameters if (is_array ($this->serverFeatures) && isset ($this->serverFeatures ['SIZE'])) $Params = array ('SIZE' => strlen ($this->mailCurrent [2])); else $Params = null; // Start the submission $this->startMail ($this->mailCurrent [0], $Params, function (qcEvents_Stream_SMTP_Client $Self, $Originator, $Params, $Status) { // Check if we are in transaction-mode right now if (!$Status) { // Remember the current mail $mc = $this->mailCurrent; // Reset the mail-queue $this->mailCurrent = null; $this->reset (function () { $this->runMailQueue (); }); // Raise the callback $this->___raiseCallback ($mc [3], $mc [0], $mc [1], $mc [2], false, $mc [4]); } // Submit receivers $receiverFunc = null; $receiverFunc = function (qcEvents_Stream_SMTP_Client $Self, $Receiver, $Params, $Status) use (&$receiverFunc) { // Check wheter to append to successfull receivers if ($Status) $this->mailCurrent [6][] = $Receiver; else $this->mailCurrent [7] = $this->lastCode; // Check wheter to submit the next receiver if (count ($this->mailCurrent [5]) > 0) return $this->addReceiver (array_shift ($this->mailCurrent [5]), null, $receiverFunc); // Check if the server accepted any receiver if (count ($this->mailCurrent [6]) == 0) { // Restore the last error-code $this->lastCode = $this->mailCurrent [7]; // Remember the current mail $mc = $this->mailCurrent; // Reset the mail-queue $this->mailCurrent = null; $this->reset (function () { $this->runMailQueue (); }); // Raise the callback return $this->___raiseCallback ($mc [3], $mc [0], $mc [1], $mc [2], false, $mc [4]); } // Submit the mail $this->sendData ($this->mailCurrent [2], function (qcEvents_Stream_SMTP_Client $Self, $Status) { // Remember the current mail $mc = $this->mailCurrent; // Reset the mail-queue $this->mailCurrent = null; // Raise the callback $this->___raiseCallback ($mc [3], $mc [0], $mc [6], $mc [2], true, $mc [4]); // Move forward to next queue-item $this->runMailQueue (); }); }; // Try to add the first receiver $this->addReceiver (array_shift ($this->mailCurrent [5]), null, $receiverFunc); }); } // }}} // {{{ smtpSetState /** * Change our protocol-state * * @param enum $State * * @access private * @return void **/ private function smtpSetState ($State) { // Check if anything was changed if ($this->State == $State) return; // Set the state $oState = $this->State; $this->State = $State; // Fire a callback $this->___callback ('smtpStateChanged', $State, $oState); } // }}} // {{{ smtpCheckState /** * Check our internal state how it will be when all commands are executed * * @access private * @return enum **/ private function smtpCheckState () { // Start with our current state $State = $this->State; // Check the current command if (($this->Command !== null) && isset ($this->Command [6]) && ($this->Command [6] !== null)) $State = $this->Command [6]; // Check all commands on the pipe foreach ($this->Commands as $Command) if (isset ($Command [6]) && ($Command [6] !== null)) $State = $Command [6]; return $State; } // }}} // {{{ issueSMTPCommand /** * Issue an SMTP-Command * * @param string $Verb * @param array $Args (optional) * @param enum $requiredState (optional) * @param enum $setState (optional) * @param callable $Callback (optional) * @param mixed $Private (optional) * @param callable $ContinuationCallback (optional) * @param mixed $ContinuationPrivate (optional) * * @access private * @return void **/ private function issueSMTPCommand ($Verb, $Args = null, $requiredState = null, $setState = null, callable $Callback = null, $Private = null, callable $ContinuationCallback = null, $ContinuationPrivate = null) { // Just push the command to the queue $this->Commands [] = array ($Verb, $Args, $ContinuationCallback, $ContinuationPrivate, $Callback, $Private, $setState, $requiredState); // Try to issue the next command $this->smtpExecuteCommand (); return true; } // }}} // {{{ smtpExecuteCommand /** * Try to execute the next pending command * * @access private * @return void **/ private function smtpExecuteCommand () { // Check if there is a command active if ($this->Command !== null) return; // Check if there are pending commands if (count ($this->Commands) == 0) return; // Retrive the next command while (($c = count ($this->Commands)) > 0) { $this->Command = array_shift ($this->Commands); // Check the required state if (($this->Command [7] === null) || ($this->State == $this->Command [7])) break; // Fire a failed callback $this->___raiseCallback ($this->Command [4], 503, array (), $this->Command [5]); if ($c > 1) continue; return ($this->Command = null); } // Write the command to the queue $Command = $this->Command [0]; if (is_array ($this->Command [1]) && (count ($this->Command [1]) > 0)) $Command .= ' ' . implode (' ', $this->Command [1]); $this->Stream->write ($Command . "\r\n"); // Raise a callback for this $this->___callback ('smtpCommand', $this->Command [0], $this->Command [1], $Command); } // }}} // {{{ initStreamConsumer /** * Setup ourself to consume data from a stream * * @param qcEvents_Interface_Source $Source * @param callable $Callback (optional) Callback to raise once the pipe is ready * @param mixed $Private (optional) Any private data to pass to the callback * * The callback will be raised in the form of * * function (qcEvents_Interface_Stream_Consumer $Self, bool $Status, mixed $Private = null) { } * * @access public * @return callable **/ public function initStreamConsumer (qcEvents_Interface_Stream $Source, callable $Callback = null, $Private = null) { // Check if this is really a new stream if ($this->Stream === $Source) { $this->___raiseCallback ($Callback, true, $Private); return; } // Check if we have a stream assigned if (is_object ($this->Stream)) $this->Stream->unpipe ($this); // Reset our state $this->Stream = $Source; $this->Buffer = ''; $this->authenticated = false; $this->connectingState = self::SMTP_HANDSHAKE_START; $this->Command = null; $this->Commands = array (); $this->responseCode = null; $this->responseLines = array (); $this->lastCode = null; $this->lastLines = null; $this->mailCurrent = null; $this->mailQueue = array (); $this->serverDomain = null; $this->serverFeatures = null; $this->smtpSetState (self::SMTP_STATE_CONNECTING); // Raise callbacks $this->___callback ('eventPipedStream', $Source); $this->___callback ('smtpConnecting'); if ($Callback) $this->initCallback = array ($Callback, $Private); else $this->initCallback = null; } // }}} // {{{ deinitConsumer /** * Callback: A source was removed from this consumer * * @param qcEvents_Interface_Source $Source * @param callable $Callback (optional) * @þaram mixed $Private (optional) * * The callback will be raised in the form of * * function (qcEvents_Interface_Consumer $Self, bool $Status, mixed $Private = null) { } * * @access public * @return void **/ public function deinitConsumer (qcEvents_Interface_Source $Source, callable $Callback = null, $Private = null) { // Check if the source is authentic if ($this->Stream !== $Source) { $this->___raiseCallback ($Callback, false, $Private); return; } // Remove the stream $this->Stream = null; // Reset our state $this->close (); // Raise the final callback $this->___raiseCallback ($Callback, true, $Private); } // }}} // {{{ consume /** * Consume a set of data * * @param mixed $Data * @param qcEvents_Interface_Source $Source * * @access public * @return void **/ public function consume ($Data, qcEvents_Interface_Source $Source) { // Append data to internal buffer $this->Buffer .= $Data; unset ($Data); // Read lines from buffer $s = 0; while (($p = strpos ($this->Buffer, "\n", $s)) !== false) { // Strip the line from the buffer $Line = substr ($this->Buffer, $s, $p - $s); $s = $p + 1; // Retrive the code from the line $Code = intval (substr ($Line, 0, 3)); // Check if this is a multiline-message if ($Multiline = (($l = strlen ($Line)) > 3)) $Multiline = ($Line [3] == '-'); // Handle enhanced response-codes if (is_array ($this->serverFeatures) && isset ($this->serverFeatures ['ENHANCEDSTATUSCODES']) && (($p = strpos ($Line, ' ', 9)) !== false)) $eCode = substr ($Line, 4, $p - 4); else $p = 3; // Push the response to local buffer if ($this->responseCode === null) $this->responseCode = $Code; elseif ($this->responseCode != $Code) { trigger_error ('SMTP-Protocol-Error: Invalid Response-Code on multi-line reply found. Quitting.'); $this->Command = null; $this->Commands = array (); $this->close (); break; } $this->responseLines [] = substr ($Line, $p + 1); // Wait for further responses on multiline-responses if ($Multiline) continue; // Retrive buffered lines $this->lastCode = $Code; $this->lastLines = $Lines = $this->responseLines; unset ($Line); // Clear local buffer $this->responseCode = null; $this->responseLines = array (); // Raise a callback for this $this->___callback ('smtpResponse', $Code, $Lines); // Check for continuation if (($Code >= 300) && ($Code < 400)) { if (is_callable ($this->Command [2])) { $this->Stream->write ($this->___raiseCallback ($this->Command [2], $Code, $Lines, $this->Command [3])); continue; } trigger_error ('Server wants continuation, but we dont have a callback for this', E_USER_ERROR); } // Check if we are connecting if ($this->State == self::SMTP_STATE_CONNECTING) { // Peek the current command if ($this->Command) $Command = $this->Command; else $Command = array (null, null, null, null, null, null); // Handle the server's greeting if ($this->connectingState == self::SMTP_HANDSHAKE_START) { // Check if the server does not want us to connect // The RFC says only 554 here, we check them all though if ($Code >= 500) { $this->Buffer = ''; return $this->close (function () { $this->___callback ('smtpConnectionFailed'); if ($this->initCallback) { $this->___raiseCallback ($this->initCallback [0], false, $this->initCallback [1]); $this->initCallback = null; } }); } // Do the client-initiation $this->connectingState = self::SMTP_HANDSHAKE_EHLO; $this->Command = null; $this->issueSMTPCommand ('EHLO', array ($this->getClientName ()), null, null, $Command [4], $Command [5], $Command [2], $Command [3]); continue; // Handle the response to our own Greeting } elseif ($Code >= 500) { // Handle strange errors, were both EHLO and HELO failed if ($this->connectingState > self::SMTP_HANDSHAKE_EHLO) { $this->Buffer = ''; return $this->close (function () { $this->___callback ('smtpConnectionFailed'); if ($this->initCallback) { $this->___raiseCallback ($this->initCallback [0], false, $this->initCallback [1]); $this->initCallback = null; } }); } // Try HELO-Fallback $this->connectingState = self::SMTP_HANDSHAKE_FALLBACK; $this->Command = null; $this->issueSMTPCommand ('HELO', array ($this->getClientName ()), null, null, $Command [4], $Command [5], $Command [2], $Command [3]); continue; } // Retrive domainname of server $this->serverDomain = array_shift ($Lines); if (($p = strpos ($this->serverDomain, ' ')) !== false) $this->serverDomain = substr ($this->serverDomain, 0, $p); // Handle an EHLO-Response if ($this->connectingState == self::SMTP_HANDSHAKE_EHLO) { $this->serverFeatures = array (); foreach ($Lines as $Line) { $Info = explode (' ', trim ($Line)); $Keyword = strtoupper (array_shift ($Info)); $this->serverFeatures [$Keyword] = $Info; } // Server does not support EHLO } else $this->serverFeatures = false; // Change our protocol-state $this->smtpSetState (self::SMTP_STATE_CONNECTED); // Fire the callback (only if not TLS was enabled) if (($this->Command === null) || ($this->Command [4] === null)) { $this->___callback ('smtpConnected'); if ($this->initCallback) { $this->___raiseCallback ($this->initCallback [0], true, $this->initCallback [1]); $this->initCallback = null; } } } // Handle normal replies if (($this->Command [6] !== null) && ($Code >= 200) && ($Code < 300)) $this->smtpSetState ($this->Command [6]); $this->___raiseCallback ($this->Command [4], $Code, $Lines, $this->Command [5]); // Remove the current command $this->Command = null; // Try to issue any pending commands $this->smtpExecuteCommand (); } // Truncate the buffer $this->Buffer = substr ($this->Buffer, $s); } // }}} // {{{ smtpStateChanged /** * Callback: SMTP-Protocol-State was changed * * @param enum $newState * @param enum $oldState * * @access protected * @return void **/ protected function smtpStateChanged ($newState, $oldState) { } // }}} // {{{ smtpConnecting /** * Callback: SMTP-Connection is being established * * @access protected * @return void **/ protected function smtpConnecting () { } // }}} // {{{ smtpConnected /** * Callback: SMTP-Connection is ready for action * * @access protected * @return void **/ protected function smtpConnected () { } // }}} // {{{ smtpConnectionFailed /** * Callback: SMTP-Connection could not be established * * @access protected * @return void **/ protected function smtpConnectionFailed () { } // }}} // {{{ smtpDisconnected /** * Callback: SMTP-Connection was closed * * @access protected * @return void **/ protected function smtpDisconnected () { } // }}} // {{{ smtpCommand /** * Callback: A SMTP-Command was issued to the server * * @param string $Command * * @access protected * @return void **/ protected function smtpCommand ($Command) { } // }}} // {{{ smtpResponse /** * Callback: A Response was received from the server * * @param int $Code * @param array $Lines * * @access protected * @return void **/ protected function smtpResponse ($Code, $Lines) { } // }}} // {{{ eventReadable /** * Callback: Never used. * * @access protected * @return void **/ protected function eventReadable () { } // }}} // {{{ eventClosed /** * Callback: The client-connection was closed * * @access protected * @return void **/ protected function eventClosed () { } // }}} } ?>