Completed
Pull Request — master (#17)
by
unknown
02:09
created

MimeParser::getCurrentLine()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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