Completed
Branch master (689ff6)
by Asmir
01:29
created

MimeParser::createMessage()   C

Complexity

Conditions 7
Paths 13

Size

Total Lines 40
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 7.0753

Importance

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