Completed
Pull Request — master (#11)
by
unknown
01:39
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 10
    public function __construct(array $allowedHeaders = array(), array $removeHeaders = array())
32
    {
33 10
        $this->contentDecoder = new ContentDecoder ();
34 10
        $this->headerDecoder = new HeaderDecoder ();
35
36 10
        $this->allowedHeaders = array_merge($this->allowedHeaders, $allowedHeaders);
37 10
        $this->removeHeaders = array_merge($this->removeHeaders, $removeHeaders);
38 10
    }
39
40
    public function setSwiftDependencyContainer(\Swift_DependencyContainer $swiftContainer)
41
    {
42
        $this->swiftContainer = $swiftContainer;
43
    }
44
45 5
    private function getSwiftDependencyContainer(): \Swift_DependencyContainer
46
    {
47 5
        if ($this->swiftContainer === null) {
48 5
            $this->swiftContainer = \Swift_DependencyContainer::getInstance();
49
        }
50 5
        return $this->swiftContainer;
51
    }
52
53 5
    private function getIdGenertor(): \Swift_IdGenerator
54
    {
55 5
        return $this->getSwiftDependencyContainer()->lookup(self::SWIFT_CONTAINER_ID_GENERATOR_KEY);
56
    }
57
58 5
    private function getCache(): \Swift_KeyCache
59
    {
60 5
        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 10
    public function parseStream($stream, bool $fillHeaders = false, \Swift_Mime_SimpleMimeEntity $message = null): \Swift_Mime_SimpleMimeEntity
85
    {
86 10
        $partHeaders = $this->extractHeaders($stream);
87
88 10
        $filteredHeaders = $this->filterHeaders($partHeaders);
89
90 10
        $parts = $this->parseParts($stream, $partHeaders);
91
92 9
        if (!$message) {
93 9
            $message = new \Swift_Message ();
94
        }
95
96 9
        $headers = $this->createHeadersSet($filteredHeaders);
97
98 9
        foreach ($headers->getAll() as $name => $header) {
99 9
            if ($fillHeaders || in_array(strtolower($header->getFieldName()), $this->allowedHeaders)) {
100 9
                $message->getHeaders()->set($header);
101
            }
102
        }
103 9
        $this->createMessage($parts, $message);
104
105 9
        return $message;
106
    }
107
108 10
    protected function extractHeaders($stream): array
109
    {
110 10
        $headers = array();
111 10
        $hName = null;
112 10
        while (!feof($stream)) {
113 10
            $row = fgets($stream);
114 10
            if ($row == "\r\n" || $row == "\n" || $row == "\r") {
115 10
                break;
116
            }
117 10
            if (preg_match('/^([a-z0-9\-]+)\s*:(.*)/i', $row, $mch)) {
118 10
                $hName = strtolower($mch[1]);
119 10
                if (!in_array($hName, array("content-type", "content-transfer-encoding"))) {
120 10
                    $hName = $mch[1];
121
                }
122 10
                $row = $mch[2];
123
            }
124 10
            if (empty($hName)) {
125
                continue;
126
            }
127 10
            $headers[$hName][] = trim($row);
128
        }
129 10
        foreach ($headers as $header => $values) {
130 10
            $headers[$header] = $this->headerDecoder->decode(trim(implode(" ", $values)));
131
        }
132 10
        return $headers;
133
    }
134
135 10
    private function filterHeaders(array $headers): array
136
    {
137 10
        foreach ($headers as $header => $values) {
138 10
            if (in_array(strtolower($header), $this->removeHeaders) && !in_array(strtolower($header), $this->allowedHeaders)) {
139 10
                unset ($headers[$header]);
140
            }
141
        }
142 10
        return $headers;
143
    }
144
145 10
    protected function parseParts($stream, array $partHeaders): array
146
    {
147 10
        $parts = array();
148 10
        $contentType = $this->extractValueHeader($this->getContentType($partHeaders));
149
150 10
        $boundary = null;
151 10
        if (stripos($contentType, 'multipart/') !== false) {
152 7
            $headerParts = $this->extractHeaderParts($this->getContentType($partHeaders));
153 7
            if (empty($headerParts["boundary"])) {
154 1
                throw new InvalidMessageFormatException("The Content-Type header is not well formed, boundary is missing");
155
            }
156 6
            $boundary = $headerParts["boundary"];
157
        }
158
159
        try {
160
            // body
161 9
            $this->extractPart($stream, $boundary, $this->getTransferEncoding($partHeaders));
162 9
        } catch (Exception\EndOfPartReachedException $e) {
163
            $parts = array(
164 9
                "type" => $contentType,
165 9
                "headers" => $partHeaders,
166 9
                "body" => $e->getData(),
167 9
                "boundary" => $boundary,
168
                "parts" => array()
169
            );
170
        }
171
172 9
        if ($boundary) {
173 6
            $childContentType = null;
174 6
            while (!feof($stream)) {
175
                try {
176 5
                    $partHeaders = $this->extractHeaders($stream);
177 5
                    $childContentType = $this->extractValueHeader($this->getContentType($partHeaders));
178
179 5
                    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 5
                        $this->extractPart($stream, $boundary, $this->getTransferEncoding($partHeaders));
187
                    }
188 5
                } catch (Exception\EndOfPartReachedException $e) {
189 5
                    $parts["parts"][] = array(
190 5
                        "type" => $childContentType,
191 5
                        "parent-type" => $contentType,
192 5
                        "headers" => $partHeaders,
193 5
                        "body" => $e->getData(),
194
                        "parts" => array()
195
                    );
196
197 5
                    if ($e instanceof Exception\EndOfMultiPartReachedException) {
198 5
                        break;
199
                    }
200
                }
201
            }
202
        }
203 9
        return $parts;
204
    }
205
206 10
    private function extractValueHeader($header): string
207
    {
208 10
        $pos = stripos($header, ';');
209 10
        if ($pos !== false) {
210 7
            return substr($header, 0, $pos);
211
        } else {
212 8
            return $header;
213
        }
214
    }
215
216 10
    private function getContentType(array $partHeaders): string
217
    {
218 10
        if (array_key_exists('content-type', $partHeaders)) {
219 10
            return $partHeaders['content-type'];
220
        }
221
222
        return '';
223
    }
224
225 10
    private function extractHeaderParts(string $header): array
226
    {
227 10
        if (stripos($header, ';') !== false) {
228
229 7
            $parts = explode(";", $header);
230 7
            array_shift($parts);
231 7
            $p = array();
232 7
            $part = '';
233 7
            foreach ($parts as $pv) {
234 7
                if (preg_match('/="[^"]+$/', $pv)) {
235
                    $part = $pv;
236
                    continue;
237
                }
238 7
                if ($part !== '') {
239
                    $part .= ';' . $pv;
240
                    if (preg_match('/="[^"]+$/', $part)) {
241
                        continue;
242
                    } else {
243
                        $pv = $part;
244
                    }
245
                }
246 7
                if (strpos($pv, '=') === false) {
247 6
                    continue;
248
                }
249 6
                list ($k, $v) = explode("=", trim($pv), 2);
250 6
                $p[$k] = trim($v, '"');
251
            }
252 7
            return $p;
253
        } else {
254 8
            return array();
255
        }
256
    }
257
258
    /**
259
     * @throws Exception\EndOfMultiPartReachedException
260
     * @throws Exception\EndOfPartReachedException
261
     */
262 9
    protected function extractPart($stream, ?string $boundary, string $encoding): void
263
    {
264 9
        $rows = array();
265 9
        while (!feof($stream)) {
266 9
            $row = fgets($stream);
267
268 9
            if ($boundary !== null) {
269 6
                if (strpos($row, "--$boundary--") === 0) {
270 5
                    throw new Exception\EndOfMultiPartReachedException($this->contentDecoder->decode(implode("", $rows), $encoding));
271
                }
272 6
                if (strpos($row, "--$boundary") === 0) {
273 5
                    throw new Exception\EndOfPartReachedException($this->contentDecoder->decode(implode("", $rows), $encoding));
274
                }
275
            }
276 9
            $rows[] = $row;
277
        }
278 4
        throw new Exception\EndOfMultiPartReachedException($this->contentDecoder->decode(implode("", $rows), $encoding));
279
    }
280
281 9
    private function getTransferEncoding(array $partHeaders): string
282
    {
283 9
        if (array_key_exists('content-transfer-encoding', $partHeaders)) {
284 2
            return $partHeaders['content-transfer-encoding'];
285
        }
286
287 7
        return '';
288
    }
289
290 9
    protected function createHeadersSet(array $headersRaw): \Swift_Mime_SimpleHeaderSet
291
    {
292 9
        $headers = \Swift_DependencyContainer::getInstance()->lookup('mime.headerset');
293
294 9
        foreach ($headersRaw as $name => $value) {
295 9
            switch (strtolower($name)) {
296 9
                case "content-type":
297 9
                    $parts = $this->extractHeaderParts($value);
298 9
                    unset ($parts["boundary"]);
299 9
                    $headers->addParameterizedHeader($name, $this->extractValueHeader($value), $parts);
300 9
                    break;
301 9
                case "return-path":
302 1
                    if (preg_match_all('/([a-z][a-z0-9_\-\.]*@[a-z0-9\.\-]*\.[a-z]{2,5})/i', $value, $mch)) {
303 1
                        foreach ($mch[0] as $k => $mails) {
304 1
                            $headers->addPathHeader($name, $mch[1][$k]);
305
                        }
306
                    }
307 1
                    break;
308 9
                case "date":
309 1
                    $headers->addDateHeader($name, new \DateTime($value));
310 1
                    break;
311 9
                case "to":
312 9
                case "from":
313 9
                case "bcc":
314 9
                case "reply-to":
315 9
                case "cc":
316 9
                    $adresses = array();
317 9
                    if (preg_match_all('/(.*?)<([a-z][a-z0-9_\-\.]*@[a-z0-9\.\-]*\.[a-z]{2,5})>\s*[;,]*/i', $value, $mch)) {
318 9
                        foreach ($mch[0] as $k => $mail) {
319 9
                            if (!$mch[1][$k]) {
320
                                $adresses[$mch[2][$k]] = trim($mch[2][$k]);
321
                            } else {
322 9
                                $adresses[$mch[2][$k]] = trim($mch[1][$k]);
323
                            }
324
                        }
325
                    } elseif (preg_match_all('/([a-z][a-z0-9_\-\.]*@[a-z0-9\.\-]*\.[a-z]{2,5})/i', $value, $mch)) {
326
                        foreach ($mch[0] as $k => $mails) {
327
                            $adresses[$mch[1][$k]] = trim($mch[1][$k]);
328
                        }
329
                    }
330 9
                    $headers->addMailboxHeader($name, $adresses);
331 9
                    break;
332
                default:
333 9
                    $headers->addTextHeader($name, $value);
334 9
                    break;
335
            }
336
        }
337 9
        return $headers;
338
    }
339
340 9
    protected function createMessage(array $message, \Swift_Mime_SimpleMimeEntity $entity): void
341
    {
342 9
        if (stripos($message["type"], 'multipart/') !== false && !empty($message["parts"])) {
343
344 5
            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...
345 5
            } elseif (strpos($message["type"], '/related')) {
346
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_RELATED;
347 5
            } elseif (strpos($message["type"], '/mixed')) {
348 5
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_MIXED;
349
            } else {
350
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_TOP;
351
            }
352
353 5
            $childs = array();
354 5
            foreach ($message["parts"] as $part) {
355
356 5
                $headers = $this->createHeadersSet($part["headers"]);
357 5
                $encoder = $this->getEncoder($this->getTransferEncoding($part["headers"]));
358
359 5
                if (stripos($part["type"], 'multipart/') !== false) {
360
                    $newEntity = new \Swift_Mime_MimePart ($headers, $encoder, $this->getCache(), $this->getIdGenertor());
361
                } else {
362 5
                    $newEntity = new \Swift_Mime_SimpleMimeEntity ($headers, $encoder, $this->getCache(), $this->getIdGenertor());
363
                }
364
365 5
                $this->createMessage($part, $newEntity);
366
367 5
                $ref = new \ReflectionObject ($newEntity);
368 5
                $m = $ref->getMethod('setNestingLevel');
369 5
                $m->setAccessible(true);
370 5
                $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...
371
372 5
                $childs[] = $newEntity;
373
            }
374
375 5
            $entity->setContentType($part["type"]);
0 ignored issues
show
Bug introduced by
The variable $part seems to be defined by a foreach iteration on line 354. 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...
376 5
            $entity->setChildren($childs);
377
        } else {
378 9
            $entity->setBody($message["body"], $message["type"]);
379
        }
380 9
    }
381
382 5
    protected function getEncoder(string $type): \Swift_Mime_ContentEncoder
383
    {
384
        switch ($type) {
385 5
            case "base64":
386
                return \Swift_DependencyContainer::getInstance()->lookup('mime.base64contentencoder');
387 5
            case "8bit":
388
                return \Swift_DependencyContainer::getInstance()->lookup('mime.8bitcontentencoder');
389 5
            case "7bit":
390
                return \Swift_DependencyContainer::getInstance()->lookup('mime.7bitcontentencoder');
391
            default:
392 5
                return \Swift_DependencyContainer::getInstance()->lookup('mime.qpcontentencoder');
393
        }
394
    }
395
}
396