Completed
Push — master ( 689ff6...b3cc36 )
by Asmir
01:57
created

MimeParser::parseParts()   C

Complexity

Conditions 10
Paths 65

Size

Total Lines 60
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 10.0686

Importance

Changes 0
Metric Value
dl 0
loc 60
c 0
b 0
f 0
ccs 31
cts 34
cp 0.9118
rs 6.5333
cc 10
eloc 41
nc 65
nop 2
crap 10.0686

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Goetas\Mail\ToSwiftMailParser;
4
5
use Goetas\Mail\ToSwiftMailParser\Exception\InvalidMessageFormatException;
6
use Goetas\Mail\ToSwiftMailParser\Mime\ContentDecoder;
7
use Goetas\Mail\ToSwiftMailParser\Mime\HeaderDecoder;
8
9
class MimeParser
10
{
11
    private const SWIFT_CONTAINER_CACHE_KEY = 'cache';
12
    private const SWIFT_CONTAINER_ID_GENERATOR_KEY = 'mime.idgenerator';
13
14
    protected $removeHeaders = array("Received", "From", "X-Original-To", "MIME-Version", "Received-SPF", "Delivered-To");
15
    protected $allowedHeaders = array("return-path", "subject");
16
    /**
17
     * @var ContentDecoder
18
     */
19
    private $contentDecoder;
20
21
    /**
22
     * @var HeaderDecoder
23
     */
24
    private $headerDecoder;
25
26
    /**
27
     * @var \Swift_DependencyContainer
28
     */
29
    private $swiftContainer;
30
31 6
    public function __construct(array $allowedHeaders = array(), array $removeHeaders = array())
32
    {
33 6
        $this->contentDecoder = new ContentDecoder ();
34 6
        $this->headerDecoder = new HeaderDecoder ();
35
36 6
        $this->allowedHeaders = array_merge($this->allowedHeaders, $allowedHeaders);
37 6
        $this->removeHeaders = array_merge($this->removeHeaders, $removeHeaders);
38 6
    }
39
40
    public function setSwiftDependencyContainer(\Swift_DependencyContainer $swiftContainer)
41
    {
42
        $this->swiftContainer = $swiftContainer;
43
    }
44
45 3
    private function getSwiftDependencyContainer(): \Swift_DependencyContainer
46
    {
47 3
        if ($this->swiftContainer === null) {
48 3
            $this->swiftContainer = \Swift_DependencyContainer::getInstance();
49
        }
50 3
        return $this->swiftContainer;
51
    }
52
53 3
    private function getIdGenertor(): \Swift_IdGenerator
54
    {
55 3
        return $this->getSwiftDependencyContainer()->lookup(self::SWIFT_CONTAINER_ID_GENERATOR_KEY);
56
    }
57
58 3
    private function getCache(): \Swift_KeyCache
59
    {
60 3
        return $this->getSwiftDependencyContainer()->lookup(self::SWIFT_CONTAINER_CACHE_KEY);
61
    }
62
63 1
    public function parseFile(string $path, bool $fillHeaders = false, \Swift_Mime_SimpleMimeEntity $message = null): \Swift_Mime_SimpleMimeEntity
64
    {
65 1
        $fp = fopen($path, "rb");
66 1
        $message = $this->parseStream($fp, $fillHeaders, $message);
67 1
        fclose($fp);
68 1
        return $message;
69
    }
70
71 1
    public function parseString(string $string, bool $fillHeaders = false, \Swift_Mime_SimpleMimeEntity $message = null): \Swift_Mime_SimpleMimeEntity
72
    {
73 1
        $fp = fopen("php://memory", "wb");
74 1
        fwrite($fp, $string);
75 1
        rewind($fp);
76 1
        $message = $this->parseStream($fp, $fillHeaders, $message);
77 1
        fclose($fp);
78 1
        return $message;
79
    }
80
81
    /**
82
     * @param resource $stream
83
     */
84 6
    public function parseStream($stream, bool $fillHeaders = false, \Swift_Mime_SimpleMimeEntity $message = null): \Swift_Mime_SimpleMimeEntity
85
    {
86 6
        $partHeaders = $this->extractHeaders($stream);
87
88 6
        $filteredHeaders = $this->filterHeaders($partHeaders);
89
90 6
        $parts = $this->parseParts($stream, $partHeaders);
91
92 5
        if (!$message) {
93 5
            $message = new \Swift_Message ();
94
        }
95
96 5
        $headers = $this->createHeadersSet($filteredHeaders);
97
98 5
        foreach ($headers->getAll() as $name => $header) {
99 5
            if ($fillHeaders || in_array(strtolower($header->getFieldName()), $this->allowedHeaders)) {
100 5
                $message->getHeaders()->set($header);
101
            }
102
        }
103 5
        $this->createMessage($parts, $message);
104
105 5
        return $message;
106
    }
107
108 6
    protected function extractHeaders($stream): array
109
    {
110 6
        $headers = array();
111 6
        $hName = null;
112 6
        while (!feof($stream)) {
113 6
            $row = fgets($stream);
114 6
            if ($row == "\r\n" || $row == "\n" || $row == "\r") {
115 6
                break;
116
            }
117 6
            if (preg_match('/^([a-z0-9\-]+)\s*:(.*)/i', $row, $mch)) {
118 6
                $hName = strtolower($mch [1]);
119 6
                if (!in_array($hName, array("content-type", "content-transfer-encoding"))) {
120 6
                    $hName = $mch [1];
121
                }
122 6
                $row = $mch [2];
123
            }
124 6
            if (empty($hName)) {
125
                continue;
126
            }
127 6
            $headers [$hName] [] = trim($row);
128
        }
129 6
        foreach ($headers as $header => $values) {
130 6
            $headers [$header] = $this->headerDecoder->decode(trim(implode(" ", $values)));
131
        }
132 6
        return $headers;
133
    }
134
135 6
    private function filterHeaders(array $headers): array
136
    {
137 6
        foreach ($headers as $header => $values) {
138 6
            if (in_array(strtolower($header), $this->removeHeaders) && !in_array(strtolower($header), $this->allowedHeaders)) {
139 6
                unset ($headers [$header]);
140
            }
141
        }
142 6
        return $headers;
143
    }
144
145 6
    protected function parseParts($stream, array $partHeaders): array
146
    {
147 6
        $parts = array();
148 6
        $contentType = $this->extractValueHeader($this->getContentType($partHeaders));
149
150 6
        $boundary = null;
151 6
        if (stripos($contentType, 'multipart/') !== false) {
152 4
            $headerParts = $this->extractHeaderParts($this->getContentType($partHeaders));
153 4
            if (empty($headerParts["boundary"])) {
154 1
                throw new InvalidMessageFormatException("The Content-Type header is not well formed, boundary is missing");
155
            }
156 3
            $boundary = $headerParts["boundary"];
157
        }
158
159
        try {
160
            // body
161 5
            $this->extractPart($stream, $boundary, $this->getTransferEncoding($partHeaders));
162 5
        } catch (Exception\EndOfPartReachedException $e) {
163
            $parts = array(
164 5
                "type" => $contentType,
165 5
                "headers" => $partHeaders,
166 5
                "body" => $e->getData(),
167 5
                "boundary" => $boundary,
168
                "parts" => array()
169
            );
170
        }
171
172 5
        if ($boundary) {
173 3
            $childContentType = null;
174 3
            while (!feof($stream)) {
175
                try {
176 3
                    $partHeaders = $this->extractHeaders($stream);
177 3
                    $childContentType = $this->extractValueHeader($this->getContentType($partHeaders));
178
179 3
                    if (stripos($childContentType, 'multipart/') !== false) {
180
                        $parts["parts"][] = $this->parseParts($stream, $partHeaders);
181
                        try {
182
                            $this->extractPart($stream, $boundary, $this->getTransferEncoding($partHeaders));
183
                        } catch (Exception\EndOfPartReachedException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
184
                        }
185
                    } else {
186 3
                        $this->extractPart($stream, $boundary, $this->getTransferEncoding($partHeaders));
187
                    }
188 3
                } catch (Exception\EndOfPartReachedException $e) {
189 3
                    $parts["parts"][] = array(
190 3
                        "type" => $childContentType,
191 3
                        "parent-type" => $contentType,
192 3
                        "headers" => $partHeaders,
193 3
                        "body" => $e->getData(),
194
                        "parts" => array()
195
                    );
196
197 3
                    if ($e instanceof Exception\EndOfMultiPartReachedException) {
198 3
                        break;
199
                    }
200
                }
201
            }
202
        }
203 5
        return $parts;
204
    }
205
206 6
    private function extractValueHeader($header): string
207
    {
208 6
        $pos = stripos($header, ';');
209 6
        if ($pos !== false) {
210 4
            return substr($header, 0, $pos);
211
        } else {
212 5
            return $header;
213
        }
214
    }
215
216 6
    private function getContentType(array $partHeaders): string
217
    {
218 6
        if (array_key_exists('content-type', $partHeaders)) {
219 6
            return $partHeaders['content-type'];
220
        }
221
222
        return '';
223
    }
224
225 6
    private function extractHeaderParts(string $header): array
226
    {
227 6
        if (stripos($header, ';') !== false) {
228
229 4
            $parts = explode(";", $header);
230 4
            array_shift($parts);
231 4
            $p = array();
232 4
            foreach ($parts as $pv) {
233 4
                if (!trim($pv)) {
234 4
                    continue;
235
                }
236 3
                list ($k, $v) = explode("=", trim($pv), 2);
237 3
                $p [$k] = trim($v, '"');
238
            }
239 4
            return $p;
240
        } else {
241 5
            return array();
242
        }
243
    }
244
245
    /**
246
     * @throws Exception\EndOfMultiPartReachedException
247
     * @throws Exception\EndOfPartReachedException
248
     */
249 5
    protected function extractPart($stream, ?string $boundary, string $encoding): void
250
    {
251 5
        $rows = array();
252 5
        while (!feof($stream)) {
253 5
            $row = fgets($stream);
254
255 5
            if ($boundary !== null) {
256 3
                if (strpos($row, "--$boundary--") === 0) {
257 3
                    throw new Exception\EndOfMultiPartReachedException($this->contentDecoder->decode(implode("", $rows), $encoding));
258
                }
259 3
                if (strpos($row, "--$boundary") === 0) {
260 3
                    throw new Exception\EndOfPartReachedException($this->contentDecoder->decode(implode("", $rows), $encoding));
261
                }
262
            }
263 5
            $rows [] = $row;
264
        }
265 2
        throw new Exception\EndOfMultiPartReachedException($this->contentDecoder->decode(implode("", $rows), $encoding));
266
    }
267
268 5
    private function getTransferEncoding(array $partHeaders): string
269
    {
270 5
        if (array_key_exists('content-transfer-encoding', $partHeaders)) {
271 1
            return $partHeaders ['content-transfer-encoding'];
272
        }
273
274 4
        return '';
275
    }
276
277 5
    protected function createHeadersSet(array $headersRaw): \Swift_Mime_SimpleHeaderSet
278
    {
279 5
        $headers = \Swift_DependencyContainer::getInstance()->lookup('mime.headerset');
280
281 5
        foreach ($headersRaw as $name => $value) {
282 5
            switch (strtolower($name)) {
283 5
                case "content-type":
284 5
                    $parts = $this->extractHeaderParts($value);
285 5
                    unset ($parts ["boundary"]);
286 5
                    $headers->addParameterizedHeader($name, $this->extractValueHeader($value), $parts);
287 5
                    break;
288 5
                case "return-path":
289 1
                    if (preg_match_all('/([a-z][a-z0-9_\-\.]*@[a-z0-9\.\-]*\.[a-z]{2,5})/i', $value, $mch)) {
290 1
                        foreach ($mch [0] as $k => $mails) {
291 1
                            $headers->addPathHeader($name, $mch [1] [$k]);
292
                        }
293
                    }
294 1
                    break;
295 5
                case "date":
296 1
                    $headers->addDateHeader($name, new \DateTime($value));
297 1
                    break;
298 5
                case "to":
299 5
                case "from":
300 5
                case "bcc":
301 5
                case "reply-to":
302 5
                case "cc":
303 5
                    $adresses = array();
304 5
                    if (preg_match_all('/(.*?)<([a-z][a-z0-9_\-\.]*@[a-z0-9\.\-]*\.[a-z]{2,5})>\s*[;,]*/i', $value, $mch)) {
305 5
                        foreach ($mch [0] as $k => $mail) {
306 5
                            if (!$mch [1] [$k]) {
307
                                $adresses [$mch [2] [$k]] = trim($mch [2] [$k]);
308
                            } else {
309 5
                                $adresses [$mch [2] [$k]] = trim($mch [1] [$k]);
310
                            }
311
                        }
312
                    } elseif (preg_match_all('/([a-z][a-z0-9_\-\.]*@[a-z0-9\.\-]*\.[a-z]{2,5})/i', $value, $mch)) {
313
                        foreach ($mch [0] as $k => $mails) {
314
                            $adresses [$mch [1] [$k]] = trim($mch [1] [$k]);
315
                        }
316
                    }
317 5
                    $headers->addMailboxHeader($name, $adresses);
318 5
                    break;
319
                default:
320 5
                    $headers->addTextHeader($name, $value);
321 5
                    break;
322
            }
323
        }
324 5
        return $headers;
325
    }
326
327 5
    protected function createMessage(array $message, \Swift_Mime_SimpleMimeEntity $entity): void
328
    {
329 5
        if (stripos($message["type"], 'multipart/') !== false) {
330
331 3
            if (strpos($message["type"], '/alternative')) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
332 3
            } elseif (strpos($message["type"], '/related')) {
333
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_RELATED;
334 3
            } elseif (strpos($message["type"], '/mixed')) {
335 3
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_MIXED;
336
            } else {
337
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_TOP;
338
            }
339
340 3
            $childs = array();
341 3
            foreach ($message ["parts"] as $part) {
342
343 3
                $headers = $this->createHeadersSet($part["headers"]);
344 3
                $encoder = $this->getEncoder($this->getTransferEncoding($part["headers"]));
345
346 3
                if (stripos($part["type"], 'multipart/') !== false) {
347
                    $newEntity = new \Swift_Mime_MimePart ($headers, $encoder, $this->getCache(), $this->getIdGenertor());
348
                } else {
349 3
                    $newEntity = new \Swift_Mime_SimpleMimeEntity ($headers, $encoder, $this->getCache(), $this->getIdGenertor());
350
                }
351
352 3
                $this->createMessage($part, $newEntity);
353
354 3
                $ref = new \ReflectionObject ($newEntity);
355 3
                $m = $ref->getMethod('setNestingLevel');
356 3
                $m->setAccessible(true);
357 3
                $m->invoke($newEntity, $nestingLevel);
0 ignored issues
show
Bug introduced by
The variable $nestingLevel does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
358
359 3
                $childs [] = $newEntity;
360
            }
361
362 3
            $entity->setContentType($part["type"]);
0 ignored issues
show
Bug introduced by
The variable $part seems to be defined by a foreach iteration on line 341. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
363 3
            $entity->setChildren($childs);
364
        } else {
365 5
            $entity->setBody($message ["body"], $message["type"]);
366
        }
367 5
    }
368
369 3
    protected function getEncoder(string $type): \Swift_Mime_ContentEncoder
370
    {
371
        switch ($type) {
372 3
            case "base64":
373
                return \Swift_DependencyContainer::getInstance()->lookup('mime.base64contentencoder');
374 3
            case "8bit":
375
                return \Swift_DependencyContainer::getInstance()->lookup('mime.8bitcontentencoder');
376 3
            case "7bit":
377
                return \Swift_DependencyContainer::getInstance()->lookup('mime.7bitcontentencoder');
378
            default:
379 3
                return \Swift_DependencyContainer::getInstance()->lookup('mime.qpcontentencoder');
380
        }
381
    }
382
}
383