├── .gitignore ├── src ├── Message.php ├── Sender.php ├── Server.php ├── Request.php ├── RequestInterface.php ├── MessageTrait.php ├── MessageInterface.php └── Connection.php ├── phive.xml ├── composer.json ├── tests ├── ServerTest.php └── ConnectionTest.php ├── .scrutinizer.yml ├── phpunit.xml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | /nbproject/private/ -------------------------------------------------------------------------------- /src/Message.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sam-it/react-smtp", 3 | "require": { 4 | "psr/http-message": "^1", 5 | "sam-it/proxy": "^0.4", 6 | "guzzlehttp/psr7": "^1.3" 7 | }, 8 | "description" : "SMTP Server based on ReactPHP", 9 | "license" : "MIT", 10 | "authors": [ 11 | { 12 | "name": "Sam", 13 | "email": "sam@mousa.nl" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4" : { 18 | "SamIT\\React\\Smtp\\" : ["src"] 19 | 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/ServerTest.php: -------------------------------------------------------------------------------- 1 | createLoopMock(); 15 | $server = new Server($loop); 16 | 17 | $this->assertInstanceOf(Server::class, $server); 18 | } 19 | 20 | private function createLoopMock() 21 | { 22 | return $this->createMock(\React\EventLoop\LoopInterface::class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | createLoopMock(); 16 | $server = new Server($loop); 17 | 18 | $server->listen(0); 19 | $conn = new Connection($server->master, $loop); 20 | 21 | $this->assertInstanceOf(Connection::class, $conn); 22 | } 23 | 24 | private function createLoopMock() 25 | { 26 | return $this->createMock(\React\EventLoop\LoopInterface::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - 'tests/*' 4 | checks: 5 | php: true 6 | coding_style: 7 | php: 8 | spaces: 9 | around_operators: 10 | concatenation: true 11 | build: 12 | tests: 13 | override: 14 | - 15 | command: 'php phpunit.phar --coverage-clover=coverage-file' 16 | coverage: 17 | file: 'coverage-file' 18 | format: 'php-clover' 19 | 20 | dependencies: 21 | before: 22 | - 'wget https://phar.phpunit.de/phpunit.phar' 23 | environment: 24 | php: 25 | version: 7.1.0 26 | tools: 27 | php_sim: false 28 | php_cpd: false -------------------------------------------------------------------------------- /src/Sender.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 22 | } 23 | 24 | 25 | /** 26 | * @param ConnectionInterface $connection 27 | * @param MessageInterface $message 28 | * @param Request $request 29 | */ 30 | public function send( 31 | ConnectionInterface $connection, 32 | MessageInterface $message, 33 | Request $request 34 | ) { 35 | 36 | // $connection->on 37 | 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | tests 13 | 14 | 15 | 16 | 17 | src 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 SAM-IT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 22 | parent::__construct($loop); 23 | } 24 | 25 | public function createConnection($socket) 26 | { 27 | $conn = new Connection($socket, $this->loop); 28 | $conn->recipientLimit = $this->recipientLimit; 29 | $conn->bannerDelay = $this->bannerDelay; 30 | // We let messages "bubble up" from the connection to the server. 31 | $conn->on('message', function() { 32 | $this->emit('message', func_get_args()); 33 | }); 34 | return $conn; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | validateEmail($email); 22 | $new = clone $this; 23 | $new->recipients[$email] = $name; 24 | return $new; 25 | } 26 | 27 | public function withAddedRecipients(array $recipients) 28 | { 29 | $new = clone $this; 30 | foreach($recipients as $email => $name) { 31 | $this->validateEmail($email); 32 | $new->recipients[$email] = $name; 33 | } 34 | return $new; 35 | } 36 | 37 | public function withRecipient($email, $name = null) 38 | { 39 | $this->validateEmail($email); 40 | $new = clone $this; 41 | $new->recipients = []; 42 | $new->recipients[$email] = $name; 43 | return $new; 44 | } 45 | 46 | public function withRecipients(array $recipients) { 47 | $new = clone $this; 48 | $new->recipients = []; 49 | foreach($recipients as $email => $name) { 50 | $this->validateEmail($email); 51 | $new->recipients[$email] = $name; 52 | } 53 | return $new; 54 | } 55 | 56 | /** 57 | * @throws \InvalidArgumentException 58 | */ 59 | protected function validateEmail($email) 60 | { 61 | if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { 62 | throw new \InvalidArgumentException("$email is not a valid email address"); 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-smtp 2 | SMTP Server based on ReactPHP 3 | 4 | It supports many concurrent STMP connections. 5 | 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/SAM-IT/react-smtp/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/SAM-IT/react-smtp/?branch=master) 7 | [![Code Coverage](https://scrutinizer-ci.com/g/SAM-IT/react-smtp/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/SAM-IT/react-smtp/?branch=master) 8 | [![Build Status](https://scrutinizer-ci.com/g/SAM-IT/react-smtp/badges/build.png?b=master)](https://scrutinizer-ci.com/g/SAM-IT/react-smtp/build-status/master) 9 | 10 | 11 | # Usage example 12 | 13 | ## Server: 14 | 15 | ```` 16 | include 'vendor/autoload.php'; 17 | 18 | $loop = React\EventLoop\Factory::create(); 19 | $server = new \SamIT\React\Smtp\Server($loop); 20 | $server->listen(8025); 21 | $server->on('message', function($from, array $recipients, \SamIT\React\Smtp\Message $message, \SamIT\React\Smtp\Connection $connection) { 22 | var_dump($message->getHeaders()); 23 | var_dump($message->getBody()->getSize()); 24 | }); 25 | $loop->run(); 26 | 27 | ```` 28 | 29 | ### Example client using PHPMailer: 30 | ```` 31 | 32 | include 'vendor/autoload.php'; 33 | 34 | $mail = new PHPMailer(); 35 | 36 | $mail->isSMTP(); 37 | $mail->Host = 'localhost'; 38 | $mail->Port = 8025; 39 | $mail->SMTPDebug = true; 40 | 41 | $mail->setFrom('from@example.com', 'Mailer'); 42 | $mail->addAddress('joe@example.net', 'Joe User'); // Add a recipient 43 | $mail->addAddress('ellen@example.com'); // Name is optional 44 | $mail->addReplyTo('info@example.com', 'Information'); 45 | $mail->addCC('cc@example.com'); 46 | $mail->addBCC('bcc@example.com'); 47 | 48 | $mail->Subject = 'Here is the subject'; 49 | $mail->Body = 'This is the HTML message body in bold!'; 50 | $mail->AltBody = 'This is the body in plain text for non-HTML mail clients'; 51 | 52 | if(!$mail->send()) { 53 | echo 'Message could not be sent.'; 54 | echo 'Mailer Error: ' . $mail->ErrorInfo; 55 | } else { 56 | echo 'Message has been sent'; 57 | } 58 | ```` -------------------------------------------------------------------------------- /src/RequestInterface.php: -------------------------------------------------------------------------------- 1 | name pairs. 29 | * @throws \InvalidArgumentException for invalid email. 30 | */ 31 | public function withRecipients(array $recipients); 32 | 33 | /** 34 | * Return an instance with the provided recipients added to the original recipient(s). 35 | * 36 | * This method MUST be implemented in such a way as to retain the 37 | * immutability of the message, and MUST return an instance that has the 38 | * new and/or updated header and value. 39 | * 40 | * @return self 41 | * @param array $recipients Array of email => name pairs. 42 | * @throws \InvalidArgumentException for invalid email. 43 | */ 44 | public function withAddedRecipient($email, $name = null); 45 | 46 | /** 47 | * Return an instance with the provided recipients added to the original recipient(s). 48 | * 49 | * This method MUST be implemented in such a way as to retain the 50 | * immutability of the message, and MUST return an instance that has the 51 | * new and/or updated header and value. 52 | * 53 | * @return self 54 | * @param array $recipients Array of email => name pairs. 55 | * @throws \InvalidArgumentException for invalid email. 56 | */ 57 | public function withAddedRecipients(array $recipients); 58 | 59 | } -------------------------------------------------------------------------------- /src/MessageTrait.php: -------------------------------------------------------------------------------- 1 | headers[] = [$name, $value]; 18 | return $new; 19 | 20 | } 21 | 22 | public function getHeaderLine($name) { 23 | throw new \Exception('SMTP Message does not support merging headers with the same name.'); 24 | } 25 | 26 | public function getHeader($header) 27 | { 28 | $result = []; 29 | $header = strtolower($header); 30 | foreach($this->headers as list($name, $value)) { 31 | if ($header == strtolower($name)) { 32 | $result[] = [$name, $value]; 33 | } 34 | } 35 | return $result; 36 | } 37 | 38 | public function getHeaders() { 39 | return $this->headers; 40 | } 41 | 42 | public function hasHeader($header) 43 | { 44 | return !empty($this->getHeader($header)); 45 | } 46 | 47 | public function withoutHeader($header) 48 | { 49 | throw new \Exception('SMTP Message does not support removing headers.'); 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function withRecipient($email, $name = null) 56 | { 57 | return $this->withRecipients([$email => $name]); 58 | } 59 | 60 | public function withRecipients(array $recipients) 61 | { 62 | $new = clone $this; 63 | $new->recipients = $recipients; 64 | return $new; 65 | } 66 | 67 | public function withAddedRecipient($email, $name = null) 68 | { 69 | $new = clone $this; 70 | $new->recipients[$email] = $name; 71 | return $new; 72 | } 73 | 74 | /** 75 | * Return an instance with the provided recipients added to the original recipient(s). 76 | * 77 | * This method MUST be implemented in such a way as to retain the 78 | * immutability of the message, and MUST return an instance that has the 79 | * new and/or updated header and value. 80 | * 81 | * @return self 82 | * @param array $recipients Array of email => name pairs. 83 | * @throws \InvalidArgumentException for invalid email. 84 | */ 85 | public function withAddedRecipients(array $recipients) 86 | { 87 | $new = clone $this; 88 | $new->recipients = array_merge($this->recipients, $recipients); 89 | return $new; 90 | } 91 | 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/MessageInterface.php: -------------------------------------------------------------------------------- 1 | getHeaders() as $index => [$name, $value]) { 13 | * echo $name . ": " . $value; 14 | * } 15 | * 16 | * While header names are not case-sensitive, getHeaders() must preserve the 17 | * exact case in which headers were originally specified. It must also preserve the order of headers, appending 18 | * new ones to the end. This is especially important when signing schemes like DKIM are in use. 19 | * 20 | * @return array Returns an array of the message's headers. Each 21 | * key a header index, and each value MUST be an array of name, value pairs for that header. 22 | */ 23 | public function getHeaders(); 24 | 25 | /** 26 | * This function will throw an exception since replacing headers is will mangle SMTP messages. 27 | * @throws \Exception 28 | */ 29 | public function withHeader($name, $value); 30 | 31 | /** 32 | * This function will throw an exception since removing headers is will mangle SMTP messages. 33 | * @throws \Exception 34 | */ 35 | public function withoutHeader($name); 36 | 37 | /** 38 | * This function will throw an exception since removing headers is will mangle SMTP messages. 39 | * @throws \Exception 40 | */ 41 | public function getHeaderLine($name); 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public function withAddedHeader($name, $value); 47 | 48 | /** 49 | * Retrieves a message header value by the given case-insensitive name. 50 | * 51 | * This method returns an array of all the header values of the given 52 | * case-insensitive header name. 53 | * 54 | * If the header does not appear in the message, this method MUST return an 55 | * empty array. 56 | * 57 | * @param string $name Case-insensitive header field name. 58 | * @return string[] An array of string values as provided for the given 59 | * header. If the header does not appear in the message, this method MUST 60 | * return an empty array. The keys are the names of the header. 61 | */ 62 | public function getHeader($name); 63 | 64 | /** 65 | * Return an instance with the provided value replacing the original recipient(s). 66 | * 67 | * This method MUST be implemented in such a way as to retain the 68 | * immutability of the message, and MUST return an instance that has the 69 | * new and/or updated header and value. 70 | * 71 | * @return self 72 | * @param string $email 73 | * @param string $name 74 | * @throws \InvalidArgumentException for invalid email. 75 | */ 76 | public function withRecipient($email, $name = null); 77 | 78 | /** 79 | * Return an instance with the provided values replacing the original recipient(s). 80 | * 81 | * This method MUST be implemented in such a way as to retain the 82 | * immutability of the message, and MUST return an instance that has the 83 | * new and/or updated header and value. 84 | * 85 | * @return self 86 | * @param array $recipients Array of email => name pairs. 87 | * @throws \InvalidArgumentException for invalid email. 88 | */ 89 | public function withRecipients(array $recipients); 90 | 91 | /** 92 | * Return an instance with the provided recipients added to the original recipient(s). 93 | * 94 | * This method MUST be implemented in such a way as to retain the 95 | * immutability of the message, and MUST return an instance that has the 96 | * new and/or updated header and value. 97 | * 98 | * @return self 99 | * @param array $recipients Array of email => name pairs. 100 | * @throws \InvalidArgumentException for invalid email. 101 | */ 102 | public function withAddedRecipient($email, $name = null); 103 | 104 | /** 105 | * Return an instance with the provided recipients added to the original recipient(s). 106 | * 107 | * This method MUST be implemented in such a way as to retain the 108 | * immutability of the message, and MUST return an instance that has the 109 | * new and/or updated header and value. 110 | * 111 | * @return self 112 | * @param array $recipients Array of email => name pairs. 113 | * @throws \InvalidArgumentException for invalid email. 114 | */ 115 | public function withAddedRecipients(array $recipients); 116 | } -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | '/^QUIT$/', 29 | 'Helo' => '/^HELO (.*)$/', 30 | 'Ehlo' => '/^EHLO (.*)$/', 31 | 'MailFrom' => '/^MAIL FROM:\s*(.*)$/', 32 | 'Reset' => '/^RSET$/', 33 | 'RcptTo' => '/^RCPT TO:\s*(.*)$/', 34 | 'StartData' => '/^DATA$/', 35 | 'StartHeader' => '/^(\w+):\s*(.*)$/', 36 | 'StartBody' => '/^$/', 37 | 'Unfold' => '/^ (.*)$/', 38 | 'EndData' => '/^\.$/', 39 | 'BodyLine' => '/^(.*)$/', 40 | 'EndBody' => '/^\.$/' 41 | ]; 42 | 43 | protected $states = [ 44 | self::STATUS_NEW => [ 45 | 'Quit', 'Helo', 'Ehlo' 46 | ], 47 | self::STATUS_INIT => [ 48 | 'MailFrom', 49 | 'Quit' 50 | 51 | ], 52 | self::STATUS_FROM => [ 53 | 'RcptTo', 54 | 'Quit', 55 | 'Reset', 56 | ], 57 | self::STATUS_TO => [ 58 | 'Quit', 59 | 'StartData', 60 | 'Reset', 61 | 'RcptTo', 62 | 63 | ], 64 | self::STATUS_HEADERS => [ 65 | 'EndBody', 66 | 'StartHeader', 67 | 'StartBody', 68 | ], 69 | self::STATUS_UNFOLDING => [ 70 | 'StartBody', 71 | 'EndBody', 72 | 'Unfold', 73 | 'StartHeader', 74 | ], 75 | self::STATUS_BODY => [ 76 | 'EndBody', 77 | 'BodyLine' 78 | ], 79 | self::STATUS_PROCESSING => [ 80 | 81 | ] 82 | 83 | 84 | 85 | ]; 86 | 87 | protected $state = self::STATUS_NEW; 88 | 89 | protected $banner = 'Welcome to ReactPHP SMTP Server'; 90 | /** 91 | * @var bool Accept messages by default 92 | */ 93 | protected $acceptByDefault = true; 94 | /** 95 | * If there are event listeners, how long will they get to accept or reject a message? 96 | * @var int 97 | */ 98 | protected $defaultActionTimeout = 5; 99 | /** 100 | * The timer for the default action, canceled in [accept] and [reject] 101 | * @var TimerInterface 102 | */ 103 | protected $defaultActionTimer; 104 | /** 105 | * The current line buffer used by handleData. 106 | * @var string 107 | */ 108 | protected $lineBuffer = ''; 109 | 110 | /** 111 | * @var string Name of the header in the foldBuffer. 112 | */ 113 | protected $foldHeader = ''; 114 | /** 115 | * Buffer used for unfolding multiline headers.. 116 | * @var string 117 | */ 118 | protected $foldBuffer = ''; 119 | protected $from; 120 | protected $recipients = []; 121 | /** 122 | * @var Message 123 | */ 124 | protected $message; 125 | 126 | public $bannerDelay = 0; 127 | 128 | 129 | public $recipientLimit = 100; 130 | 131 | public function __construct($stream, LoopInterface $loop) 132 | { 133 | parent::__construct($stream, $loop); 134 | stream_get_meta_data($stream); 135 | // We sleep for 3 seconds, if client does not wait for our banner we disconnect. 136 | $disconnect = function($data, ConnectionInterface $conn) { 137 | $conn->end("I can break rules too, bye.\n"); 138 | }; 139 | $this->on('data', $disconnect); 140 | $this->reset(self::STATUS_NEW); 141 | $this->on('line', [$this, 'handleCommand']); 142 | if ($this->bannerDelay > 0) { 143 | $loop->addTimer($this->bannerDelay, function () use ($disconnect) { 144 | $this->sendReply(220, $this->banner); 145 | $this->removeListener('data', $disconnect); 146 | }); 147 | } else { 148 | $this->sendReply(220, $this->banner); 149 | } 150 | } 151 | 152 | /** 153 | * We read until we find an and of line sequence for SMTP. 154 | * http://www.jebriggs.com/blog/2010/07/smtp-maximum-line-lengths/ 155 | * @param $stream 156 | */ 157 | public function handleData($stream) 158 | { 159 | // Socket is raw, not using fread as it's interceptable by filters 160 | // See issues #192, #209, and #240 161 | $data = stream_socket_recvfrom($stream, $this->bufferSize);; 162 | 163 | $limit = $this->state == self::STATUS_BODY ? 1000 : 512; 164 | if ('' !== $data && false !== $data) { 165 | $this->lineBuffer .= $data; 166 | if (strlen($this->lineBuffer) > $limit) { 167 | $this->sendReply(500, "Line length limit exceeded."); 168 | $this->lineBuffer = ''; 169 | } 170 | 171 | $delimiter = "\r\n"; 172 | while(false !== $pos = strpos($this->lineBuffer, $delimiter)) { 173 | $line = substr($this->lineBuffer, 0, $pos); 174 | $this->lineBuffer = substr($this->lineBuffer, $pos + strlen($delimiter)); 175 | $this->emit('line', [$line, $this]); 176 | } 177 | } 178 | 179 | if ('' === $data || false === $data || !is_resource($stream) || feof($stream)) { 180 | $this->end(); 181 | } 182 | } 183 | 184 | /** 185 | * Parses the command from the beginning of the line. 186 | * 187 | * @param string $line 188 | * @return string[] An array containing the command and all arguments. 189 | */ 190 | protected function parseCommand($line) 191 | { 192 | 193 | foreach ($this->states[$this->state] as $key) { 194 | if (preg_match(self::REGEXES[$key], $line, $matches) === 1) { 195 | $matches[0] = $key; 196 | $this->emit('debug', ["$line match for $key (" . self::REGEXES[$key] . ")"]); 197 | return $matches; 198 | } else { 199 | $this->emit('debug', ["$line does not match for $key (" . self::REGEXES[$key] . ")"]); 200 | } 201 | } 202 | return [null]; 203 | } 204 | 205 | protected function handleCommand($line) 206 | { 207 | $arguments = $this->parseCommand($line); 208 | $command = array_shift($arguments); 209 | if ($command === null) { 210 | $this->sendReply(500, array_merge( 211 | $this->states[$this->state], 212 | ["Unexpected or unknown command."] 213 | )); 214 | } else { 215 | call_user_func_array([$this, "handle{$command}Command"], $arguments); 216 | } 217 | } 218 | 219 | protected function sendReply($code, $message, $close = false) 220 | { 221 | $out = ''; 222 | if (is_array($message)) { 223 | $last = array_pop($message); 224 | foreach($message as $line) { 225 | $out .= "$code-$line\r\n"; 226 | } 227 | $this->write($out); 228 | $message = $last; 229 | } 230 | if ($close) { 231 | $this->end("$code $message\r\n"); 232 | } else { 233 | $this->write("$code $message\r\n"); 234 | } 235 | 236 | } 237 | 238 | protected function handleResetCommand() 239 | { 240 | $this->reset(); 241 | $this->sendReply(250, "Reset OK"); 242 | } 243 | protected function handleHeloCommand($domain) 244 | { 245 | $this->state = self::STATUS_INIT; 246 | $this->sendReply(250, "Hello {$domain} @ {$this->getRemoteAddress()}"); 247 | } 248 | 249 | protected function handleEhloCommand($domain) 250 | { 251 | $this->state = self::STATUS_INIT; 252 | $this->sendReply(250, "Hello {$domain} @ {$this->getRemoteAddress()}"); 253 | } 254 | 255 | protected function handleMailFromCommand($arguments) 256 | { 257 | 258 | // Parse the email. 259 | if (preg_match('/\<(?.*)\>( .*)?/', $arguments, $matches) == 1) { 260 | $this->state = self::STATUS_FROM; 261 | $this->from = $matches['email']; 262 | $this->sendReply(250, "MAIL OK"); 263 | } else { 264 | $this->sendReply(500, "Invalid mail argument"); 265 | } 266 | 267 | } 268 | 269 | protected function handleQuitCommand() 270 | { 271 | $this->sendReply(221, "Goodbye.", true); 272 | 273 | } 274 | 275 | protected function handleRcptToCommand($arguments) { 276 | // Parse the recipient. 277 | if (preg_match('/^(?.*?)\s*?\<(?.*)\>\s*$/', $arguments, $matches) == 1) { 278 | // Always set to 3, since this command might occur multiple times. 279 | $this->state = self::STATUS_TO; 280 | $this->recipients[$matches['email']] = $matches['name']; 281 | $this->sendReply(250, "Accepted"); 282 | } else { 283 | $this->sendReply(500, "Invalid RCPT TO argument."); 284 | } 285 | } 286 | 287 | protected function handleStartDataCommand() 288 | { 289 | $this->state = self::STATUS_HEADERS; 290 | $this->sendReply(354, "Enter message, end with CRLF . CRLF"); 291 | } 292 | 293 | protected function handleUnfoldCommand($content) 294 | { 295 | $this->foldBuffer .= $content; 296 | } 297 | 298 | protected function handleStartHeaderCommand($name, $content) 299 | { 300 | // Check if status is unfolding. 301 | if ($this->state === self::STATUS_UNFOLDING) { 302 | $this->message = $this->message->withAddedHeader($this->foldHeader, $this->foldBuffer); 303 | } 304 | 305 | $this->foldBuffer = $content; 306 | $this->foldHeader = $name; 307 | $this->state = self::STATUS_UNFOLDING; 308 | } 309 | 310 | protected function handleStartBodyCommand() 311 | { 312 | // Check if status is unfolding. 313 | if ($this->state === self::STATUS_UNFOLDING) { 314 | $this->message = $this->message->withAddedHeader($this->foldHeader, $this->foldBuffer); 315 | } 316 | $this->state = self::STATUS_BODY; 317 | 318 | } 319 | 320 | protected function handleEndBodyCommand() 321 | { 322 | // Check if status is unfolding. 323 | if ($this->state === self::STATUS_UNFOLDING) { 324 | $this->message = $this->message->withAddedHeader($this->foldHeader, $this->foldBuffer); 325 | } 326 | 327 | $this->state = self::STATUS_PROCESSING; 328 | /** 329 | * Default action, using timer so that callbacks above can be called asynchronously. 330 | */ 331 | $this->defaultActionTimer = $this->loop->addTimer($this->defaultActionTimeout, function() { 332 | if ($this->acceptByDefault) { 333 | $this->accept(); 334 | } else { 335 | $this->reject(); 336 | } 337 | }); 338 | 339 | 340 | 341 | $this->emit('message', [ 342 | 'from' => $this->from, 343 | 'recipients' => $this->recipients, 344 | 'message' => $this->message, 345 | 'connection' => $this, 346 | ]); 347 | } 348 | protected function handleBodyLineCommand($line) 349 | { 350 | $this->message->getBody()->write($line); 351 | } 352 | 353 | /** 354 | * Reset the SMTP session. 355 | * By default goes to the initialized state (ie no new EHLO or HELO is required / possible.) 356 | * 357 | * @param int $state The state to go to. 358 | */ 359 | protected function reset($state = self::STATUS_INIT) { 360 | $this->state = $state; 361 | $this->from = null; 362 | $this->recipients = []; 363 | $this->message = new Message(); 364 | } 365 | 366 | public function accept($message = "OK") { 367 | if ($this->state != self::STATUS_PROCESSING) { 368 | throw new \DomainException("SMTP Connection not in a valid state to accept a message."); 369 | } 370 | $this->loop->cancelTimer($this->defaultActionTimer); 371 | unset($this->defaultActionTimer); 372 | $this->sendReply(250, $message); 373 | $this->reset(); 374 | } 375 | 376 | public function reject($code = 550, $message = "Message not accepted") { 377 | if ($this->state != self::STATUS_PROCESSING) { 378 | throw new \DomainException("SMTP Connection not in a valid state to reject message."); 379 | } 380 | $this->defaultActionTimer->cancel(); 381 | unset($this->defaultActionTimer); 382 | $this->sendReply($code, $message); 383 | $this->reset(); 384 | } 385 | 386 | /** 387 | * Delay the default action by $seconds. 388 | * @param int $seconds 389 | */ 390 | public function delay($seconds) { 391 | if (isset($this->defaultActionTimer)) { 392 | $this->defaultActionTimer->cancel(); 393 | $this->defaultActionTimer = $this->loop->addTimer($seconds, $this->defaultActionTimer->getCallback()); 394 | } 395 | } 396 | 397 | } 398 | --------------------------------------------------------------------------------