Passed
Push — master ( 46ed75...ca2387 )
by Zaahid
03:33
created

MimeParserService   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 144
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 15
eloc 43
c 0
b 0
f 0
dl 0
loc 144
ccs 48
cts 48
cp 1
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A canParse() 0 3 1
A readBoundaryLine() 0 12 4
A findContentBoundary() 0 17 4
A parseContent() 0 5 1
A parseNextChild() 0 8 2
A createPart() 0 14 2
1
<?php
2
/**
3
 * This file is part of the ZBateson\MailMimeParser project.
4
 *
5
 * @license http://opensource.org/licenses/bsd-license.php BSD
6
 */
7
8
namespace ZBateson\MailMimeParser\Parser;
9
10
use ZBateson\MailMimeParser\Message\Factory\PartHeaderContainerFactory;
11
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
12
use ZBateson\MailMimeParser\Parser\Proxy\ParserMessageProxyFactory;
13
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
14
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxyFactory;
15
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;
16
17
/**
18
 * Parses content and children of MIME parts.
19
 *
20
 * @author Zaahid Bateson
21
 */
22
class MimeParserService extends AbstractParserService
23
{
24
    /**
25
     * @var PartHeaderContainerFactory Factory service for creating
26
     *      PartHeaderContainers for headers.
27
     */
28
    protected PartHeaderContainerFactory $partHeaderContainerFactory;
29
30
    /**
31
     * @var HeaderParserService The HeaderParser service.
32
     */
33
    protected HeaderParserService $headerParser;
34
35 13
    public function __construct(
36
        ParserMessageProxyFactory $parserMessageProxyFactory,
37
        ParserMimePartProxyFactory $parserMimePartProxyFactory,
38
        PartBuilderFactory $partBuilderFactory,
39
        PartHeaderContainerFactory $partHeaderContainerFactory,
40
        HeaderParserService $headerParser
41
    ) {
42 13
        parent::__construct($parserMessageProxyFactory, $parserMimePartProxyFactory, $partBuilderFactory);
43 13
        $this->partHeaderContainerFactory = $partHeaderContainerFactory;
44 13
        $this->headerParser = $headerParser;
45
    }
46
47
    /**
48
     * Returns true if the passed PartBuilder::isMime() method returns true.
49
     *
50
     */
51 106
    public function canParse(PartBuilder $part) : bool
52
    {
53 106
        return $part->isMime();
54
    }
55
56
    /**
57
     * Reads up to 2048 bytes of input from the passed resource handle,
58
     * discarding portions of a line that are longer than that, and returning
59
     * the read portions of the line.
60
     *
61
     * The method also calls $proxy->setLastLineEndingLength which is used in
62
     * findContentBoundary() to set the exact end byte of a part.
63
     *
64
     * @param resource $handle
65
     */
66 110
    private function readBoundaryLine($handle, ParserMimePartProxy $proxy) : string
67
    {
68 110
        $size = 2048;
69 110
        $isCut = false;
70 110
        $line = \fgets($handle, $size);
71 110
        while (\strlen($line) === $size - 1 && \substr($line, -1) !== "\n") {
72 1
            $line = \fgets($handle, $size);
73 1
            $isCut = true;
74
        }
75 110
        $ret = \rtrim($line, "\r\n");
76 110
        $proxy->setLastLineEndingLength(\strlen($line) - \strlen($ret));
77 110
        return ($isCut) ? '' : $ret;
78
    }
79
80
    /**
81
     * Reads 2048-byte lines from the passed $handle, calling
82
     * $partBuilder->setEndBoundaryFound with the passed line until it returns
83
     * true or the stream is at EOF.
84
     *
85
     * setEndBoundaryFound returns true if the passed line matches a boundary
86
     * for the $partBuilder itself or any of its parents.
87
     *
88
     * Lines longer than 2048 bytes are returned as single lines of 2048 bytes,
89
     * the longer line is not returned separately but is simply discarded.
90
     *
91
     * Once a boundary is found, setStreamPartAndContentEndPos is called with
92
     * the passed $handle's read pos before the boundary and its line separator
93
     * were read.
94
     */
95 110
    private function findContentBoundary(ParserMimePartProxy $proxy) : static
96
    {
97 110
        $handle = $proxy->getMessageResourceHandle();
98
        // last separator before a boundary belongs to the boundary, and is not
99
        // part of the current part, if a part is immediately followed by a
100
        // boundary, this could result in a '-1' or '-2' content length
101 110
        while (!\feof($handle)) {
102 110
            $endPos = \ftell($handle) - $proxy->getLastLineEndingLength();
103 110
            $line = $this->readBoundaryLine($handle, $proxy);
104 110
            if (\substr($line, 0, 2) === '--' && $proxy->setEndBoundaryFound($line)) {
105 76
                $proxy->setStreamPartAndContentEndPos($endPos);
106 76
                return $this;
107
            }
108
        }
109 101
        $proxy->setStreamPartAndContentEndPos(\ftell($handle));
110 101
        $proxy->setEof();
111 101
        return $this;
112
    }
113
114 110
    public function parseContent(ParserPartProxy $proxy) : static
115
    {
116 110
        $proxy->setStreamContentStartPos($proxy->getMessageResourceHandlePos());
117 110
        $this->findContentBoundary($proxy);
118 110
        return $this;
119
    }
120
121
    /**
122
     * Calls the header parser to fill the passed $headerContainer, then calls
123
     * $this->parserManager->createParserProxyFor($child);
124
     *
125
     * The method first checks though if the 'part' represents hidden content
126
     * past a MIME end boundary, which some messages like to include, for
127
     * instance:
128
     *
129
     * ```
130
     * --outer-boundary--
131
     * --boundary
132
     * content
133
     * --boundary--
134
     * some hidden information
135
     * --outer-boundary--
136
     * ```
137
     *
138
     * In this case, $this->parserPartProxyFactory is called directly to create
139
     * a part, $this->parseContent is called immediately to parse it and discard
140
     * it, and null is returned.
141
     */
142 75
    private function createPart(ParserMimePartProxy $parent, PartHeaderContainer $headerContainer, PartBuilder $child) : ?ParserPartProxy
143
    {
144 75
        if (!$parent->isEndBoundaryFound()) {
145 74
            $this->headerParser->parse(
146 74
                $child->getMessageResourceHandle(),
147 74
                $headerContainer
148 74
            );
149 74
            $parserProxy = $this->parserManager->createParserProxyFor($child);
150 74
            return $parserProxy;
151
        }
152
        // reads content past an end boundary if there is any
153 71
        $parserProxy = $this->parserPartProxyFactory->newInstance($child, $this);
154 71
        $this->parseContent($parserProxy);
155 71
        return null;
156
    }
157
158 103
    public function parseNextChild(ParserMimePartProxy $proxy) : ?ParserPartProxy
159
    {
160 103
        if ($proxy->isParentBoundaryFound()) {
161 101
            return null;
162
        }
163 75
        $headerContainer = $this->partHeaderContainerFactory->newInstance();
164 75
        $child = $this->partBuilderFactory->newChildPartBuilder($headerContainer, $proxy);
165 75
        return $this->createPart($proxy, $headerContainer, $child);
166
    }
167
}
168