* * 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/Socket.php'); class qcEvents_Socket_Server_SMTP_Command { /* The SMTP-Server we are running on */ private $Server = null; /* The actual Command we are handling */ private $Command = ''; /* Parameter for this command */ private $Parameter = null; /* Our response-code */ private $Code = null; /* Message for the response */ private $Message = null; /* Callback when the command was finished */ private $Callback = null; /* Private data to pass to the callback */ private $Private = null; // {{{ __construct /** * Create a new Command-Object * * @access friendly * @return void **/ function __construct (qcEvents_Socket_Server_SMTP $Server, $Command, $Parameter = null) { $this->Server = $Server; $this->Command = $Command; $this->Parameter = $Parameter; } // }}} // {{{ __toString /** * Cast this object into a string * * @access friendly * @return string **/ function __toString () { return $this->Command; } // }}} // {{{ hasParameter /** * Check if this command has parameter assigned * * @access public * @return bool **/ public function hasParameter () { return (($this->Parameter !== null) && (strlen ($this->Parameter) > 0)); } // }}} // {{{ getParameter /** * Retrive the parameter for this command * * @access public * @return string **/ public function getParameter () { return $this->Parameter; } // }}} // {{{ setResponse /** * Store the response for this command * * @param int $Code The response-code * @param mixed $Message (optional) Message for the response, may be multi-line * @param callable $Callback (optional) A callback to be called once the response was send * @param mixed $Private (optional) Some private data to be passed to the callback * * @access public * @return void **/ public function setResponse ($Code, $Message = null, callable $Callback = null, $Private = null) { $this->Code = $Code; $this->Message = $Message; $this->Callback = $Callback; $this->Private = $Private; $this->Server->smtpCommandReady ($this); } // }}} public function getCode () { return $this->Code; } public function getMessage () { return $this->Message; } public function getCallback () { return $this->Callback; } public function getCallbackPrivate () { return $this->Private; } } /** * SMTP-Server * ----------- * Simple SMTP-Server-Implementation (RFC 5321) * * @class qcEvents_Socket_Server_SMTP * @extends qcEvents_Socket * @package qcEvents * @revision 01 **/ class qcEvents_Socket_Server_SMTP extends qcEvents_Socket { /* Protocol-States */ 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; /* Our current protocol-state */ private $smtpState = qcEvents_Socket_Server_SMTP::SMTP_STATE_DISCONNECTED; /* Are we ready to accept messages */ private $smtpReady = true; /* Do we allow pipelining */ private $smtpPipelining = true; /* Internal buffer for incoming SMTP-Data */ private $smtpBuffer = ''; /* Current SMTP-Command being executed */ private $smtpCommand = null; /* Registered SMTP-Commands */ private $smtpCommands = array (); /* Remote Name from HELO/EHLO */ private $smtpRemoteName = null; /* Originator of mail */ private $mailOriginator = null; /* Receivers for mail */ private $mailReceivers = array (); /* Body of current mail */ private $mailData = array (); // {{{ __construct /** * Create a new SMTP-Server * * @access friendly * @return void **/ function __construct () { // Inherit to our parent call_user_func_array ('parent::__construct', func_get_args ()); // Register SMTP-Commands $this->smtpAddCommand ('QUIT', array ($this, 'smtpQuit'), self::SMTP_STATE_CONNECTING); $this->smtpAddCommand ('HELO', array ($this, 'smtpHelo'), self::SMTP_STATE_CONNECTING); $this->smtpAddCommand ('EHLO', array ($this, 'smtpHelo'), self::SMTP_STATE_CONNECTING); $this->smtpAddCommand ('MAIL', array ($this, 'smtpMail'), self::SMTP_STATE_CONNECTED); $this->smtpAddCommand ('RCPT', array ($this, 'smtpRcpt'), self::SMTP_STATE_TRANSACTION); $this->smtpAddCommand ('DATA', array ($this, 'smtpData'), self::SMTP_STATE_TRANSACTION); $this->smtpAddCommand ('RSET', array ($this, 'smtpReset'), self::SMTP_STATE_CONNECTED); $this->smtpAddCommand ('HELP', array ($this, 'smtpUnimplemented'), self::SMTP_STATE_CONNECTED); $this->smtpAddCommand ('EXPN', array ($this, 'smtpUnimplemented'), self::SMTP_STATE_CONNECTED); $this->smtpAddCommand ('VRFY', array ($this, 'smtpUnimplemented'), self::SMTP_STATE_CONNECTED); $this->smtpAddCommand ('NOOP', array ($this, 'smtpNoop'), self::SMTP_STATE_CONNECTING); // Register hooks $this->addHook ('socketConnected', array ($this, 'smtpConnected')); } // }}} // {{{ smtpGreetingLines /** * Retrive all lines for the greeting * * @access protected * @return mixed **/ protected function smtpGreetingLines () { return array ('ESMTP qcEvents-Mail/0.1'); } // }}} // {{{ smtpDomainname /** * Retrive the domainname of this smtp-server * * @access protected * @return string **/ protected function smtpDomainname () { return gethostname (); } // }}} // {{{ smtpAuthenticated /** * Check if the remote party was authenticated * * @access public * @return bool **/ public function smtpAuthenticated () { return false; } // }}} // {{{ getSMTPRemoteName /** * Retrive the name that the remote party identified with * * @access public * @return string **/ public function getSMTPRemoteName () { return $this->smtpRemoteName; } // }}} // {{{ smtpOriginator /** * Retrive the originator of the current mail-transaction * * @access public * @return string **/ public function smtpOriginator () { return $this->mailOriginator; } // }}} // {{{ smtpConnected /** * Internal Callback: The underlying socket was connected * * @access protected * @return void **/ protected final function smtpConnected () { // Set new status $this->smtpSetState (self::SMTP_STATE_CONNECTING); // Check if the smtp-service is ready if (!$this->smtpReady) return $this->smtpSendResponse (554, ''); // Retrive all lines for the greeting $Lines = $this->smtpGreetingLines (); // Prepend our domainname to the response if (count ($Lines) > 0) $Lines [0] = $this->smtpDomainname () . ' ' . $Lines [0]; else $Lines [] = $this->smtpDomainname (); // Write out the response return $this->smtpSendResponse (220, $Lines); } // }}} // {{{ socketReceive /** * Internal Callback: Data was received over the wire * * @param string $Data * * @access protected * @return void **/ protected function socketReceive ($Data) { // Append the received data to our internal buffer if (strlen ($Data) > 0) { $this->smtpBuffer .= $Data; unset ($Data); } if ($this->smtpCommand) { // Check for pipelining if (!$this->smtpPipelining) { $this->smtpSendResponse (520); $this->disconnect (); $this->smtpCommand = null; } // Check if the command waits for additional data $Code = $this->smtpCommand->getCode (); if (!($dataWait = (($Code > 299) && ($Code < 400)))) return; } // Check for commands that are ready while (($p = strpos ($this->smtpBuffer, "\n")) !== false) { // Retrive the command from the line $Command = rtrim (substr ($this->smtpBuffer, 0, $p)); $this->smtpBuffer = substr ($this->smtpBuffer, $p + 1); // Check for an active command if ($this->smtpCommand !== null) { // Check if the command waits for additional data $Code = $this->smtpCommand->getCode (); if (($Code > 299) && ($Code < 400)) { call_user_func ($this->smtpCommand->getCallback (), $this->smtpCommand, $Command, $this->smtpCommand->getCallbackPrivate ()); continue; } // Check for pipelining if (!$this->smtpPipelining) { $this->smtpSendResponse (520); $this->disconnect (); $this->smtpCommand = null; continue; } } // Check if there are parameters if (($p = strpos ($Command, ' ')) !== false) { $Parameter = ltrim (substr ($Command, $p + 1)); $Command = strtoupper (substr ($Command, 0, $p)); } else { $Parameter = null; $Command = strtoupper ($Command); } // Register the command $this->smtpCommand = $Handle = new qcEvents_Socket_Server_SMTP_Command ($this, $Command, $Parameter); // Check if we are accepting commands (always allow QUIT-Command to be executed) if (!$this->smtpReady && ($Command != 'QUIT')) { $Handle->setResponse (503); continue; } // Check if the command is known if (!isset ($this->smtpCommands [$Command])) { $Handle->setResponse (500); continue; } // Check our state if ($this->smtpState < $this->smtpCommands [$Command][1]) { $Handle->setResponse (503); continue; } // Run the command call_user_func ($this->smtpCommands [$Command][0], $Handle); break; } } // }}} // {{{ smtpAddCommand /** * Register a command-handler for SMTP * * @param string $Command The Command-Verb * @param callable $Callback The callback to run for the command * @param enum $minState (optional) The minimal state we have to be in for this command * * @access protected * @return void **/ protected function smtpAddCommand ($Command, callable $Callback, $minState = self::SMTP_STATE_DISCONNECTED) { $this->smtpCommands [$Command] = array ($Callback, $minState); } // }}} // {{{ smtpSetState /** * Set our protocol-state * * @param enum $State The protocol-state to set * * @access protected * @return void **/ protected function smtpSetState ($State) { $this->smtpState = $State; } // }}} // {{{ smtpExplodeMailParams /** * Split up mail-adress and parameters * * @param string $Data * * @access private * @return array **/ private function smtpExplodeMailParams ($Data) { // Check where to start $haveBrackets = ($Data [0] == '<'); $p = ($haveBrackets ? 1 : 0); $l = strlen ($Data); // Retrive the localpart if ($Data [$p] == '"') { for ($i = $p + 1; $i < $l; $i++) if ($Data [$i] == '\\') { $c = ord ($Data [++$i]); if (($c < 32) || ($c > 126)) break; } elseif ($Data [$i] != '"') { $c = ord ($Data [$i]); if (($c < 32) || ($c == 34) || ($c == 92) || ($c > 126)) break; } else break; $Localpart = substr ($Data, $p, $i - $p + 2); $p = $i + 2; } else { for ($i = $p; $i < $l; $i++) { $C = ord ($Data [$i]); if (($C < 33) || ($C == 34) || (($C > 39) && ($C < 42)) || ($C == 44) || (($C > 57) && ($C < 61)) || ($C == 62) || ($C == 64) || (($C > 90) && ($C < 94)) || ($C > 126)) break; } $Localpart = substr ($Data, $p, $i - $p); $p = $i; } if ($Data [$p++] != '@') return false; // Retrive the domain if (($e = strpos ($Data, ($haveBrackets ? '>' : ' '), $p)) === false) return false; $Domain = substr ($Data, $p, $e - $p); $p = $e + 1; $Mail = $Localpart . '@' . $Domain; // Check for additional parameter $Parameter = ltrim (substr ($Data, $p)); $Parameters = array (); if (strlen ($Parameter) > 0) foreach (explode (' ', $Parameter) as $Value) if (($p = strpos ($Value, '=')) !== false) $Parameters [substr ($Value, 0, $p)] = substr ($Value, $p + 1); else $Parameters [$Value] = true; return array ($Mail, $Parameters); } // }}} // {{{ smtpHelo /** * Internal Callback: EHLO/HELO-Command was received * * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access protected * @return void **/ protected function smtpHelo (qcEvents_Socket_Server_SMTP_Command $Command) { // Check if there is a parameter given if (!$Command->hasParameter ()) return $Command->setResponse (501); // Store the remote name $this->smtpRemoteName = $Command->getParameter (); // Change our current state $this->smtpSetState (self::SMTP_STATE_CONNECTED); // Write out features $Features = array ($this->smtpDomainname ()); // Check for extended HELO if ($Command == 'EHLO') { $Features [] = '8BITMIME'; if ($this->smtpPipelining) $Features [] = 'PIPELINING'; } $Command->setResponse (250, $Features); } // }}} // {{{ smtpNoop /** * Internal callback: Do nothing but a 250-Response * * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access protected * @return void **/ protected function smtpNoop (qcEvents_Socket_Server_SMTP_Command $Command) { $Command->setResponse (250); } // }}} // {{{ smtpUnimplemented /** * Internal callback: Do nothing but return a not-implemented error * * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access protected * @return void **/ protected function smtpUnimplemented (qcEvents_Socket_Server_SMTP_Command $Command) { $Command->setResponse (502); } // }}} // {{{ smtpQuit /** * Internal Callback: QUIT-Command was issued * * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access protected * @return void **/ protected function smtpQuit (qcEvents_Socket_Server_SMTP_Command $Command) { $Command->setResponse (221, $this->smtpDomainname () . ' Service closing transmission channel', array ($this, 'disconnect')); } // }}} // {{{ smtpMail /** * Internal Callback: MAIL-Command was received * * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access protected * @return void **/ protected function smtpMail (qcEvents_Socket_Server_SMTP_Command $Command) { // Check if we are in the right protocol-state if ($this->smtpState !== self::SMTP_STATE_CONNECTED) return $Command->setResponse (503); // Check if there is a parameter given if (!$Command->hasParameter ()) return $Command->setResponse (501); // Retrive the Originator $Originator = $Command->getParameter (); // Check if this is realy a MAIL FROM: if (strtoupper (substr ($Originator, 0, 5)) != 'FROM:') return $Command->setResponse (501); $Originator = ltrim (substr ($Originator, 5)); // Parse the parameter if (!is_array ($Parameters = $this->smtpExplodeMailParams ($Originator))) return $Command->setResponse (501); // Fire the callback to validate and set $this->___callback ('smtpSetOriginator', $Parameters [0], $Parameters [1], array ($this, 'smtpMailResult'), $Command); } // }}} // {{{ smtpMailResult /** * Callback: The originator of a mail was accepted or rejected * * @param bool $Result * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access public * @return void **/ public function smtpMailResult ($Result, qcEvents_Socket_Server_SMTP_Command $Command) { // Convert a boolean-Result into an smtp-code if ($Result === false) $Result = 550; elseif ($Result === true) $Result = 250; // Check if the command was successull (and switch into transaction-state) if ($Result < 300) $this->smtpSetState (self::SMTP_STATE_TRANSACTION); // Finish the command $Command->setResponse ($Result); } // }}} // {{{ smtpRcpt /** * Internal Callback: Receive a receiver for the current transaction * * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access protected * @return void **/ protected function smtpRcpt (qcEvents_Socket_Server_SMTP_Command $Command) { // Check if there is a parameter given if (!$Command->hasParameter ()) return $Command->setResponse (501); // Retrive the Receiver $Receiver = $Command->getParameter (); // Check if this is realy a RCPT TO: if (strtoupper (substr ($Receiver, 0, 3)) != 'TO:') return $Command->setResponse (501); $Receiver = ltrim (substr ($Receiver, 3)); // Parse the parameter if (!is_array ($Parameters = $this->smtpExplodeMailParams ($Receiver))) return $Command->setResponse (501); // Fire the callback to validate and set $this->___callback ('smtpAddReceiver', $Parameters [0], $Parameters [1], array ($this, 'smtpRcptResult'), $Command); } // }}} // {{{ smtpRcptResult /** * Callback: A receiver for the current transaction was accepted or rejected * * @param bool $Result * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access public * @return void **/ public function smtpRcptResult ($Result, qcEvents_Socket_Server_SMTP_Command $Command) { // Convert a boolean-Result into an smtp-code if ($Result === false) $Result = 550; elseif ($Result === true) $Result = 250; // Finish the command $Command->setResponse ($Result); } // }}} // {{{ smtpData /** * Internal Callback: DATA-Command was received * * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access protected * @return void **/ protected function smtpData (qcEvents_Socket_Server_SMTP_Command $Command) { // Check if there is a parameter given if ($Command->hasParameter ()) return $Command->setResponse (504); // Accept incoming mail-data $Command->setResponse (354, null, array ($this, 'smtpDataIncoming')); } // }}} // {{{ smtpDataIncoming /** * Internal Callback: Handle incoming message data * * @param qcEvents_Socket_Server_SMTP_Command $Command * @param string $Data * * @access public * @return void **/ public function smtpDataIncoming (qcEvents_Socket_Server_SMTP_Command $Command, $Data) { // Check for end of incoming data if ($Data == '.') return $this->___callback ('smtpMessageReceived', implode ("\r\n", $this->mailData), array ($this, 'smtpDataResult'), $Command); // Check for a transparent '.' if ($Data == '..') $Data = '.'; // Append to internal buffer $this->mailData [] = $Data; } // }}} // {{{ smtpDataResult /** * Internal callback: Write out status for DATA-Command * * @param bool $Result * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access public * @return void **/ public function smtpDataResult ($Result, qcEvents_Socket_Server_SMTP_Command $Command) { // Convert a boolean-result into a numeric one if ($Result === true) $Result = 250; elseif ($Result === false) $Result = 554; $Command->setResponse ($Result); } // }}} // {{{ smtpReset /** * Internal Callback: Reset an ongoing transaction * * @param qcEvents_Socket_Server_SMTP_Command $Command * * @access protected * @return void **/ protected function smtpReset (qcEvents_Socket_Server_SMTP_Command $Command) { // Remove any data transmitted for an transaction $this->mailOriginator = null; $this->mailReceivers = array (); // Set our internal state $this->smtpSetState (self::SMTP_STATE_CONNECTED); // Complete the command $Command->setResponse (250); } // }}} // {{{ smtpCommandReady /** * A command was finished * * @param qcEvents_Socket_Server_SMTP_Command $Command The received command * * @access public * @return void **/ public function smtpCommandReady (qcEvents_Socket_Server_SMTP_Command $Command) { // Check if this command is the first running command if ($Command !== $this->smtpCommand) return; // Write out its response $this->smtpSendResponse ($Code = $this->smtpCommand->getCode (), $this->smtpCommand->getMessage ()); // Check if this is an intermediate response if (($Code > 299) && ($Code < 400)) return; // Fire a callback if (($Callback = $this->smtpCommand->getCallback ()) !== null) call_user_func ($Callback, $this->smtpCommand->getCallbackPrivate ()); // Release the command $this->smtpCommand = null; // Proceed to next command if (strlen ($this->smtpBuffer) > 0) $this->socketReceive (''); } // }}} // {{{ smtpSendResponse /** * Write out an SMTP-Response * * @access private * @return void **/ private function smtpSendResponse ($Code, $Message = null) { static $Codes = array ( 221 => 'Service closing transmission channel', 250 => 'Ok', 354 => 'Start mail input; end with .', 451 => 'Requested action aborted: error in processing', 500 => 'Syntax error, command unrecognized', 501 => 'Syntax error in parameters or arguments', 502 => 'Command not implemented', 503 => 'bad sequence of commands', 504 => 'Command parameter not implemented', 520 => 'Pipelining not allowed', # This is not on the RFC 550 => 'Requested action not taken: mailbox unavailable', 554 => 'Transaction failed', ); // Check if to return a default response-message if (($Message === null) && (isset ($Codes [$Code]))) $Message = $Codes [$Code]; // Make sure the message is an array if (!is_array ($Message)) $Message = array ($Message); // Write out all message-lines while (($c = count ($Message)) > 0) { $Text = array_shift ($Message); $this->mwrite ($Code, ($c > 1 ? '-' : ' '), $Text, "\r\n"); } } // }}} // {{{ smtpSetOriginator /** * Callback: Try to store the originator of a mail-transaction * * @param string $Originator * @param array $Parameters * @param callable $Callback * @param mixed $Private (optional) * * The given callback is expected to be fired in the form of * * function (bool $Status, mixed $Private) { } * * * @access protected * @return void **/ protected function smtpSetOriginator ($Originator, $Parameters, callable $Callback, $Private = null) { // Simply store the originator $this->mailOriginator = $Originator; // Fire the callback call_user_func ($Callback, true, $Private); } // }}} // {{{ smtpAddReceiver /** * Callback: Try to add a recevier to the current mail-transaction * * @param string $Receiver * @param array $Parameters * @param callable $Callback * @param mixed $Private (optional) * * The given callback is expected to be fired in the form of * * function (bool $Status, mixed $Private) { } * * * @access protected * @return void **/ protected function smtpAddReceiver ($Receiver, $Parameters, callable $Callback, $Private = null) { // Append to receivers $this->mailReceivers [] = $Receiver; // Fire the callback call_user_func ($Callback, true, $Private); } // }}} // {{{ smtpMessageReceived /** * Callback: Message-Data was completely received * * @param string $Body The actual message-data * @param callable $Callback A callback to run after this one (with a status) * @param mixed $Private (optional) Some private data to pass to the callback * * The given callback is expected to be fired in the form of * * function (bool $Status, mixed $Private) { } * * * @access protected * @return void **/ protected function smtpMessageReceived ($Body, callable $Callback, $Private = null) { // Fire the callback call_user_func ($Callback, true, $Private); } // }}} } ?>