Completed
Pull Request — master (#12)
by
unknown
13:17
created

MimeParser::getCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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