├── .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 | [](https://scrutinizer-ci.com/g/SAM-IT/react-smtp/?branch=master)
7 | [](https://scrutinizer-ci.com/g/SAM-IT/react-smtp/?branch=master)
8 | [](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 |
--------------------------------------------------------------------------------