Completed
Push — master ( 069dd8...974ac9 )
by Asmir
02:01
created

MimeParser::extractHeaderParts()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 8.1426

Importance

Changes 0
Metric Value
dl 0
loc 32
ccs 15
cts 21
cp 0.7143
rs 8.4746
c 0
b 0
f 0
cc 7
nc 8
nop 1
crap 8.1426
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')) {
345
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_ALTERNATIVE;
346 5
            } elseif (strpos($message["type"], '/related')) {
347
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_RELATED;
348 5
            } elseif (strpos($message["type"], '/mixed')) {
349 5
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_MIXED;
350
            } else {
351
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_TOP;
352
            }
353
354 5
            $childs = array();
355 5
            foreach ($message["parts"] as $part) {
356
357 5
                $headers = $this->createHeadersSet($part["headers"]);
358 5
                $encoder = $this->getEncoder($this->getTransferEncoding($part["headers"]));
359
360 5
                if (stripos($part["type"], 'multipart/') !== false) {
361
                    $newEntity = new \Swift_Mime_MimePart ($headers, $encoder, $this->getCache(), $this->getIdGenertor());
362
                } else {
363 5
                    $newEntity = new \Swift_Mime_SimpleMimeEntity ($headers, $encoder, $this->getCache(), $this->getIdGenertor());
364
                }
365
366 5
                $this->createMessage($part, $newEntity);
367
368 5
                $ref = new \ReflectionObject ($newEntity);
369 5
                $m = $ref->getMethod('setNestingLevel');
370 5
                $m->setAccessible(true);
371 5
                $m->invoke($newEntity, $nestingLevel);
372
373 5
                $childs[] = $newEntity;
374
            }
375
376 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 355. 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...
377 5
            $entity->setChildren($childs);
378
        } else {
379 9
            $entity->setBody($message["body"], $message["type"]);
380
        }
381 9
    }
382
383 5
    protected function getEncoder(string $type): \Swift_Mime_ContentEncoder
384
    {
385
        switch ($type) {
386 5
            case "base64":
387
                return \Swift_DependencyContainer::getInstance()->lookup('mime.base64contentencoder');
388 5
            case "8bit":
389
                return \Swift_DependencyContainer::getInstance()->lookup('mime.8bitcontentencoder');
390 5
            case "7bit":
391
                return \Swift_DependencyContainer::getInstance()->lookup('mime.7bitcontentencoder');
392
            default:
393 5
                return \Swift_DependencyContainer::getInstance()->lookup('mime.qpcontentencoder');
394
        }
395
    }
396
}
397