Completed
Pull Request — master (#15)
by
unknown
02:13
created

MimeParser::filterHeaders()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.9666
c 0
b 0
f 0
cc 4
nc 3
nop 1
crap 4
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
                    } else {
182 5
                        $this->extractPart($stream, $boundary, $this->getTransferEncoding($partHeaders));
183
                    }
184 5
                } catch (Exception\EndOfPartReachedException $e) {
185 5
                    $parts["parts"][] = array(
186 5
                        "type" => $childContentType,
187 5
                        "parent-type" => $contentType,
188 5
                        "headers" => $partHeaders,
189 5
                        "body" => $e->getData(),
190
                        "parts" => array()
191
                    );
192
193 5
                    if ($e instanceof Exception\EndOfMultiPartReachedException) {
194 5
                        break;
195
                    }
196
                }
197
            }
198
        }
199 9
        return $parts;
200
    }
201
202 10
    private function extractValueHeader($header): string
203
    {
204 10
        $pos = stripos($header, ';');
205 10
        if ($pos !== false) {
206 7
            return substr($header, 0, $pos);
207
        } else {
208 8
            return $header;
209
        }
210
    }
211
212 10
    private function getContentType(array $partHeaders): string
213
    {
214 10
        if (array_key_exists('content-type', $partHeaders)) {
215 10
            return $partHeaders['content-type'];
216
        }
217
218
        return '';
219
    }
220
221 10
    private function extractHeaderParts(string $header): array
222
    {
223 10
        if (stripos($header, ';') !== false) {
224
225 7
            $parts = explode(";", $header);
226 7
            array_shift($parts);
227 7
            $p = array();
228 7
            foreach ($parts as $pv) {
229 7
                if (strpos($pv, '=') === false) {
230 6
                    continue;
231
                }
232 6
                list ($k, $v) = explode("=", trim($pv), 2);
233 6
                $p[$k] = trim($v, '"');
234
            }
235 7
            return $p;
236
        } else {
237 8
            return array();
238
        }
239
    }
240
241
    /**
242
     * @throws Exception\EndOfMultiPartReachedException
243
     * @throws Exception\EndOfPartReachedException
244
     */
245 9
    protected function extractPart($stream, ?string $boundary, string $encoding): void
246
    {
247 9
        $rows = array();
248 9
        while (!feof($stream)) {
249 9
            $row = fgets($stream);
250
251 9
            if ($boundary !== null) {
252 6
                if (strpos($row, "--$boundary--") === 0) {
253 5
                    throw new Exception\EndOfMultiPartReachedException($this->contentDecoder->decode(implode("", $rows), $encoding));
254
                }
255 6
                if (strpos($row, "--$boundary") === 0) {
256 5
                    throw new Exception\EndOfPartReachedException($this->contentDecoder->decode(implode("", $rows), $encoding));
257
                }
258
            }
259 9
            $rows[] = $row;
260
        }
261 4
        throw new Exception\EndOfMultiPartReachedException($this->contentDecoder->decode(implode("", $rows), $encoding));
262
    }
263
264 9
    private function getTransferEncoding(array $partHeaders): string
265
    {
266 9
        if (array_key_exists('content-transfer-encoding', $partHeaders)) {
267 2
            return $partHeaders['content-transfer-encoding'];
268
        }
269
270 7
        return '';
271
    }
272
273 9
    protected function createHeadersSet(array $headersRaw): \Swift_Mime_SimpleHeaderSet
274
    {
275 9
        $headers = \Swift_DependencyContainer::getInstance()->lookup('mime.headerset');
276
277 9
        foreach ($headersRaw as $name => $value) {
278 9
            switch (strtolower($name)) {
279 9
                case "content-type":
280 9
                    $parts = $this->extractHeaderParts($value);
281 9
                    unset ($parts["boundary"]);
282 9
                    $headers->addParameterizedHeader($name, $this->extractValueHeader($value), $parts);
283 9
                    break;
284 9
                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 9
                case "date":
292 1
                    $headers->addDateHeader($name, new \DateTime($value));
293 1
                    break;
294 9
                case "to":
295 9
                case "from":
296 9
                case "bcc":
297 9
                case "reply-to":
298 9
                case "cc":
299 9
                    $adresses = array();
300 9
                    if (preg_match_all('/(.*?)<([a-z][a-z0-9_\-\.]*@[a-z0-9\.\-]*\.[a-z]{2,5})>\s*[;,]*/i', $value, $mch)) {
301 9
                        foreach ($mch[0] as $k => $mail) {
302 9
                            if (!$mch[1][$k]) {
303
                                $adresses[$mch[2][$k]] = trim($mch[2][$k]);
304
                            } else {
305 9
                                $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 9
                    $headers->addMailboxHeader($name, $adresses);
314 9
                    break;
315
                default:
316 9
                    $headers->addTextHeader($name, $value);
317 9
                    break;
318
            }
319
        }
320 9
        return $headers;
321
    }
322
323 9
    protected function createMessage(array $message, \Swift_Mime_SimpleMimeEntity $entity): void
324
    {
325 9
        if (stripos($message["type"], 'multipart/') !== false && !empty($message["parts"])) {
326
327 5
            if (strpos($message["type"], '/alternative')) {
328
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_ALTERNATIVE;
329 5
            } elseif (strpos($message["type"], '/related')) {
330
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_RELATED;
331 5
            } elseif (strpos($message["type"], '/mixed')) {
332 5
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_MIXED;
333
            } else {
334
                $nestingLevel = \Swift_Mime_SimpleMimeEntity::LEVEL_TOP;
335
            }
336
337 5
            $childs = array();
338 5
            foreach ($message["parts"] as $part) {
339
340 5
                $headers = $this->createHeadersSet($part["headers"]);
341 5
                $encoder = $this->getEncoder($this->getTransferEncoding($part["headers"]));
342
343 5
                if (stripos($part["type"], 'multipart/') !== false) {
344
                    $newEntity = new \Swift_Mime_MimePart ($headers, $encoder, $this->getCache(), $this->getIdGenertor());
345
                } else {
346 5
                    $newEntity = new \Swift_Mime_SimpleMimeEntity ($headers, $encoder, $this->getCache(), $this->getIdGenertor());
347
                }
348
349 5
                $this->createMessage($part, $newEntity);
350
351 5
                $ref = new \ReflectionObject ($newEntity);
352 5
                $m = $ref->getMethod('setNestingLevel');
353 5
                $m->setAccessible(true);
354 5
                $m->invoke($newEntity, $nestingLevel);
355
356 5
                $childs[] = $newEntity;
357
            }
358
359 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 338. 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...
360 5
            $entity->setChildren($childs);
361
        } else {
362 9
            $entity->setBody($message["body"], $message["type"]);
363
        }
364 9
    }
365
366 5
    protected function getEncoder(string $type): \Swift_Mime_ContentEncoder
367
    {
368
        switch ($type) {
369 5
            case "base64":
370
                return \Swift_DependencyContainer::getInstance()->lookup('mime.base64contentencoder');
371 5
            case "8bit":
372
                return \Swift_DependencyContainer::getInstance()->lookup('mime.8bitcontentencoder');
373 5
            case "7bit":
374
                return \Swift_DependencyContainer::getInstance()->lookup('mime.7bitcontentencoder');
375
            default:
376 5
                return \Swift_DependencyContainer::getInstance()->lookup('mime.qpcontentencoder');
377
        }
378
    }
379
}