Passed
Push — master ( 679523...5e666c )
by Zaahid
03:22
created

MessageParser   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 236
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 70
dl 0
loc 236
ccs 80
cts 80
cp 1
rs 10
c 0
b 0
f 0
wmc 27

10 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 4 1
A addRawHeaderToPart() 0 5 3
A __construct() 0 6 1
A findContentBoundary() 0 16 3
A readPart() 0 12 4
A read() 0 10 1
A readUUEncodedOrPlainTextMessage() 0 21 3
A readPartContent() 0 11 3
A readHeaders() 0 12 5
A readLine() 0 7 3
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
namespace ZBateson\MailMimeParser\Message;
8
9
use Psr\Http\Message\StreamInterface;
10
use ZBateson\MailMimeParser\Message\Part\PartBuilder;
11
use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory;
12
use ZBateson\MailMimeParser\Message\Part\Factory\PartFactoryService;
13
use GuzzleHttp\Psr7\StreamWrapper;
14
15
/**
16
 * Parses a mail mime message into its component parts.  To invoke, call
17
 * MailMimeParser::parse.
18
 *
19
 * @author Zaahid Bateson
20
 */
21
class MessageParser
22
{
23
    /**
24
     * @var PartFactoryService service instance used to create MimePartFactory
25
     *      objects.
26
     */
27
    protected $partFactoryService;
28
    
29
    /**
30
     * @var PartBuilderFactory used to create PartBuilders
31
     */
32
    protected $partBuilderFactory;
33
    
34
    /**
35
     * @var int maintains the character length of the last line separator,
36
     *      typically 2 for CRLF, to keep track of the correct 'end' position
37
     *      for a part because the CRLF before a boundary is considered part of
38
     *      the boundary.
39
     */
40
    private $lastLineSeparatorLength = 0;
41
    
42
    /**
43
     * Sets up the parser with its dependencies.
44
     * 
45
     * @param PartFactoryService $pfs
46
     * @param PartBuilderFactory $pbf
47
     */
48 7
    public function __construct(
49
        PartFactoryService $pfs,
50
        PartBuilderFactory $pbf
51
    ) {
52 7
        $this->partFactoryService = $pfs;
53 7
        $this->partBuilderFactory = $pbf;
54 7
    }
55
    
56
    /**
57
     * Parses the passed stream into a ZBateson\MailMimeParser\Message object
58
     * and returns it.
59
     * 
60
     * @param StreamInterface $stream the stream to parse the message from
61
     * @return \ZBateson\MailMimeParser\Message
62
     */
63 7
    public function parse(StreamInterface $stream)
64
    {
65 7
        $partBuilder = $this->read($stream);
66 7
        return $partBuilder->createMessagePart($stream);
67
    }
68
    
69
    /**
70
     * Ensures the header isn't empty and contains a colon separator character,
71
     * then splits it and calls $partBuilder->addHeader.
72
     * 
73
     * @param string $header
74
     * @param PartBuilder $partBuilder
75
     */
76 6
    private function addRawHeaderToPart($header, PartBuilder $partBuilder)
77
    {
78 6
        if ($header !== '' && strpos($header, ':') !== false) {
79 6
            $a = explode(':', $header, 2);
80 6
            $partBuilder->addHeader($a[0], trim($a[1]));
81
        }
82 6
    }
83
84
    /**
85
     * Reads a line of up to the passed number of characters.  If the line is
86
     * larger than that, the remaining characters in the line are read and
87
     * discarded, and only the first part is returned.
88
     * 
89
     * @param resource $handle
90
     * @param int $size
91
     * @return string
92
     */
93 6
    private function readLine($handle, $size = 4096)
94
    {
95 6
        $ret = $line = fgets($handle, $size);
96 6
        while (strlen($line) === $size - 1 && substr($line, -1) !== "\n") {
97 1
            $line = fgets($handle, $size);
98
        }
99 6
        return $ret;
100
    }
101
102
    /**
103
     * Reads header lines up to an empty line, adding them to the passed
104
     * $partBuilder.
105
     * 
106
     * @param resource $handle the resource handle to read from
107
     * @param PartBuilder $partBuilder the current part to add headers to
108
     */
109 6
    protected function readHeaders($handle, PartBuilder $partBuilder)
110
    {
111 6
        $header = '';
112
        do {
113 6
            $line = $this->readLine($handle);
114 6
            if (empty($line) || $line[0] !== "\t" && $line[0] !== ' ') {
115 6
                $this->addRawHeaderToPart($header, $partBuilder);
116 6
                $header = '';
117
            } else {
118 2
                $line = "\r\n" . $line;
119
            }
120 6
            $header .= rtrim($line, "\r\n");
121 6
        } while ($header !== '');
122 6
    }
123
124
    /**
125
     * Reads lines from the passed $handle, calling
126
     * $partBuilder->setEndBoundaryFound with the passed line until it returns
127
     * true or the stream is at EOF.
128
     * 
129
     * setEndBoundaryFound returns true if the passed line matches a boundary
130
     * for the $partBuilder itself or any of its parents.
131
     * 
132
     * Once a boundary is found, setStreamPartAndContentEndPos is called with
133
     * the passed $handle's read pos before the boundary and its line separator
134
     * were read.
135
     * 
136
     * @param resource $handle
137
     * @param PartBuilder $partBuilder
138
     */
139 4
    private function findContentBoundary($handle, PartBuilder $partBuilder)
140
    {
141
        // last separator before a boundary belongs to the boundary, and is not
142
        // part of the current part
143 4
        while (!feof($handle)) {
144 4
            $endPos = ftell($handle) - $this->lastLineSeparatorLength;
145 4
            $line = fgets($handle);
146 4
            $test = rtrim($line, "\r\n");
147 4
            $this->lastLineSeparatorLength = strlen($line) - strlen($test);
148 4
            if ($partBuilder->setEndBoundaryFound($test)) {
149 2
                $partBuilder->setStreamPartAndContentEndPos($endPos);
150 2
                return;
151
            }
152
        }
153 4
        $partBuilder->setStreamPartAndContentEndPos(ftell($handle));
154 4
        $partBuilder->setEof();
155 4
    }
156
    
157
    /**
158
     * Reads content for a non-mime message.  If there are uuencoded attachment
159
     * parts in the message (denoted by 'begin' lines), those parts are read and
160
     * added to the passed $partBuilder as children.
161
     * 
162
     * @param resource $handle
163
     * @param PartBuilder $partBuilder
164
     * @return string
165
     */
166 3
    protected function readUUEncodedOrPlainTextMessage($handle, PartBuilder $partBuilder)
167
    {
168 3
        $partBuilder->setStreamContentStartPos(ftell($handle));
169 3
        $part = $partBuilder;
170 3
        while (!feof($handle)) {
171 2
            $start = ftell($handle);
172 2
            $line = trim(fgets($handle));
173 2
            if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) {
174 1
                $part = $this->partBuilderFactory->newPartBuilder(
175 1
                    $this->partFactoryService->getUUEncodedPartFactory()
176
                );
177 1
                $part->setStreamPartStartPos($start);
178
                // 'begin' line is part of the content
179 1
                $part->setStreamContentStartPos($start);
180 1
                $part->setProperty('mode', $matches[1]);
181 1
                $part->setProperty('filename', $matches[2]);
182 1
                $partBuilder->addChild($part);
183
            }
184 2
            $part->setStreamPartAndContentEndPos(ftell($handle));
185
        }
186 3
        $partBuilder->setStreamPartEndPos(ftell($handle));
187 3
    }
188
    
189
    /**
190
     * Reads content for a single part of a MIME message.
191
     * 
192
     * If the part being read is in turn a multipart part, readPart is called on
193
     * it recursively to read its headers and content.
194
     * 
195
     * The start/end positions of the part's content are set on the passed
196
     * $partBuilder, which in turn sets the end position of the part and its
197
     * parents.
198
     * 
199
     * @param resource $handle
200
     * @param PartBuilder $partBuilder
201
     */
202 4
    private function readPartContent($handle, PartBuilder $partBuilder)
203
    {
204 4
        $partBuilder->setStreamContentStartPos(ftell($handle));
205 4
        $this->findContentBoundary($handle, $partBuilder);
206 4
        if ($partBuilder->isMultiPart()) {
207 2
            while (!$partBuilder->isParentBoundaryFound()) {
208 2
                $child = $this->partBuilderFactory->newPartBuilder(
209 2
                    $this->partFactoryService->getMimePartFactory()
210
                );
211 2
                $partBuilder->addChild($child);
212 2
                $this->readPart($handle, $child);
213
            }
214
        }
215 4
    }
216
    
217
    /**
218
     * Reads a part and any of its children, into the passed $partBuilder,
219
     * either by calling readUUEncodedOrPlainTextMessage or readPartContent
220
     * after reading headers.
221
     * 
222
     * @param resource $handle
223
     * @param PartBuilder $partBuilder
224
     */
225 7
    protected function readPart($handle, PartBuilder $partBuilder)
226
    {
227 7
        $partBuilder->setStreamPartStartPos(ftell($handle));
228
        
229 7
        if ($partBuilder->canHaveHeaders()) {
230 6
            $this->readHeaders($handle, $partBuilder);
231 6
            $this->lastLineSeparatorLength = 0;
232
        }
233 7
        if ($partBuilder->getParent() === null && !$partBuilder->isMime()) {
234 3
            $this->readUUEncodedOrPlainTextMessage($handle, $partBuilder);
235
        } else {
236 4
            $this->readPartContent($handle, $partBuilder);
237
        }
238 7
    }
239
    
240
    /**
241
     * Reads the message from the passed stream and returns a PartBuilder
242
     * representing it.
243
     * 
244
     * @param StreamInterface $stream
245
     * @return PartBuilder
246
     */
247 7
    protected function read(StreamInterface $stream)
248
    {
249 7
        $partBuilder = $this->partBuilderFactory->newPartBuilder(
250 7
            $this->partFactoryService->getMessageFactory()
251
        );
252
        // the remaining parts use a resource handle for better performance...
253
        // it seems fgets does much better than Psr7\readline (not specifically
254
        // measured, but difference in running tests is big)
255 7
        $this->readPart(StreamWrapper::getResource($stream), $partBuilder);
256 7
        return $partBuilder;
257
    }
258
}
259