MimeParser::parseFile()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

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