Test Failed
Push — master ( 49edac...2a6d5b )
by Sam
05:41
created

Connection::accept()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 9
Ratio 100 %

Importance

Changes 0
Metric Value
dl 9
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
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
    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] . ")");
0 ignored issues
show
Documentation introduced by
"{$line} match for {$key...lf::REGEXES[$key] . ')' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
197
                return $matches;
198
            } else {
199
                $this->emit('debug', "$line does not match for $key (" . self::REGEXES[$key] . ")");
0 ignored issues
show
Documentation introduced by
"{$line} does not match ...lf::REGEXES[$key] . ')' is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $command of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
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($domain)
0 ignored issues
show
Unused Code introduced by
The parameter $domain is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
239
    {
240
        $this->reset();
241
        $this->sendReply(250, "Reset OK");
242
    }
243
    protected function handleHeloCommand($domain)
0 ignored issues
show
Unused Code introduced by
The parameter $domain is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
244
    {
245
        $this->state = self::STATUS_INIT;
246
        $this->sendReply(250, "Hello {$this->getRemoteAddress()}");
247
    }
248
249
    protected function handleEhloCommand($domain)
0 ignored issues
show
Unused Code introduced by
The parameter $domain is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
250
    {
251
        $this->state = self::STATUS_INIT;
252
        $this->sendReply(250, "Hello {$this->getRemoteAddress()}");
253
    }
254
255 View Code Duplication
    protected function handleMailFromCommand($arguments)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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($arguments)
0 ignored issues
show
Unused Code introduced by
The parameter $arguments is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
270
    {
271
        $this->sendReply(221, "Goodbye.", true);
272
273
    }
274
275 View Code Duplication
    protected function handleRcptToCommand($arguments) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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 View Code Duplication
    public function accept($message = "OK") {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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") {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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