Connection   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 387
Duplicated Lines 13.18 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 15.07%

Importance

Changes 0
Metric Value
wmc 46
lcom 1
cbo 5
dl 51
loc 387
ccs 22
cts 146
cp 0.1507
rs 8.3999
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 2
D handleData() 0 26 10
A parseCommand() 0 14 3
A handleCommand() 0 13 2
A sendReply() 0 18 4
A handleHeloCommand() 0 5 1
A handleEhloCommand() 0 5 1
A handleMailFromCommand() 13 13 2
A handleRcptToCommand() 11 11 2
A handleStartDataCommand() 0 5 1
A handleUnfoldCommand() 0 4 1
A handleStartHeaderCommand() 3 11 2
A handleStartBodyCommand() 3 9 2
B handleEndBodyCommand() 3 28 3
A handleBodyLineCommand() 0 4 1
A reset() 0 6 1
A accept() 9 9 2
A reject() 9 9 2
A delay() 0 6 2
A handleResetCommand() 0 5 1
A handleQuitCommand() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Connection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Connection, and based on these observations, apply Extract Interface, too.

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)
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()
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 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") {
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