1
|
|
|
<?php |
2
|
|
|
namespace SamIT\React\Smtp; |
3
|
|
|
|
4
|
|
|
|
5
|
|
|
use React\Dns\Query\TimeoutException; |
6
|
|
|
use React\EventLoop\LoopInterface; |
7
|
|
|
use React\EventLoop\Timer\TimerInterface; |
8
|
|
|
use React\Socket\ConnectionInterface; |
9
|
|
|
use React\Stream\WritableStream; |
10
|
|
|
|
11
|
|
|
class Connection extends \React\Socket\Connection{ |
12
|
|
|
const STATUS_NEW = 0; |
13
|
|
|
const STATUS_INIT = 1; |
14
|
|
|
const STATUS_FROM = 2; |
15
|
|
|
const STATUS_TO = 3; |
16
|
|
|
const STATUS_HEADERS = 4; |
17
|
|
|
const STATUS_UNFOLDING = 5; |
18
|
|
|
const STATUS_BODY = 6; |
19
|
|
|
|
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* This status is used when all mail data has been received and the system is deciding whether to accept or reject. |
23
|
|
|
*/ |
24
|
|
|
const STATUS_PROCESSING = 7; |
25
|
|
|
|
26
|
|
|
|
27
|
|
|
const REGEXES = [ |
28
|
|
|
'Quit' => '/^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
|
1 |
|
public function __construct($stream, LoopInterface $loop) |
132
|
|
|
{ |
133
|
1 |
|
parent::__construct($stream, $loop); |
134
|
1 |
|
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
|
1 |
|
}; |
139
|
1 |
|
$this->on('data', $disconnect); |
140
|
1 |
|
$this->reset(self::STATUS_NEW); |
141
|
1 |
|
$this->on('line', [$this, 'handleCommand']); |
142
|
1 |
|
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
|
1 |
|
$this->sendReply(220, $this->banner); |
149
|
|
|
} |
150
|
1 |
|
} |
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
|
1 |
|
protected function sendReply($code, $message, $close = false) |
220
|
|
|
{ |
221
|
1 |
|
$out = ''; |
222
|
1 |
|
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
|
1 |
|
if ($close) { |
231
|
|
|
$this->end("$code $message\r\n"); |
232
|
|
|
} else { |
233
|
1 |
|
$this->write("$code $message\r\n"); |
234
|
|
|
} |
235
|
|
|
|
236
|
1 |
|
} |
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
|
|
View Code Duplication |
protected function handleMailFromCommand($arguments) |
|
|
|
|
256
|
|
|
{ |
257
|
|
|
|
258
|
|
|
// Parse the email. |
259
|
|
|
if (preg_match('/\<(?<email>.*)\>( .*)?/', $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
|
|
View Code Duplication |
protected function handleRcptToCommand($arguments) { |
|
|
|
|
276
|
|
|
// Parse the recipient. |
277
|
|
|
if (preg_match('/^(?<name>.*?)\s*?\<(?<email>.*)\>\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
|
|
View Code Duplication |
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
|
|
View Code Duplication |
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
|
|
View Code Duplication |
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
|
1 |
|
protected function reset($state = self::STATUS_INIT) { |
360
|
1 |
|
$this->state = $state; |
361
|
1 |
|
$this->from = null; |
362
|
1 |
|
$this->recipients = []; |
363
|
1 |
|
$this->message = new Message(); |
364
|
1 |
|
} |
365
|
|
|
|
366
|
|
View Code Duplication |
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
|
|
View Code Duplication |
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
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.