Passed
Push — 0.4 ( 16a786...92a6f9 )
by Zaahid
06:10
created

MessageParser::newMimePartForMessage()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 1
nop 2
crap 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
namespace ZBateson\MailMimeParser\Message;
8
9
use ZBateson\MailMimeParser\Message;
10
use ZBateson\MailMimeParser\Stream\PartStreamRegistry;
11
12
/**
13
 * Parses a mail mime message into its component parts.  To invoke, call
14
 * MailMimeParser::parse.
15
 *
16
 * @author Zaahid Bateson
17
 */
18
class MessageParser
19
{
20
    /**
21
     * @var \ZBateson\MailMimeParser\Message the Message object that the read
22
     * mail mime message will be parsed into
23
     */
24
    protected $message;
25
    
26
    /**
27
     * @var \ZBateson\MailMimeParser\Message\MimePartFactory the MimePartFactory object
28
     * used to create parts.
29
     */
30
    protected $partFactory;
31
    
32
    /**
33
     * @var \ZBateson\MailMimeParser\Stream\PartStreamRegistry the
34
     *      PartStreamRegistry 
35
     * object used to register stream parts.
36
     */
37
    protected $partStreamRegistry;
38
    
39
    /**
40
     * Sets up the parser with its dependencies.
41
     * 
42
     * @param \ZBateson\MailMimeParser\Message $m
43
     * @param \ZBateson\MailMimeParser\Message\MimePartFactory $pf
44
     * @param \ZBateson\MailMimeParser\Stream\PartStreamRegistry $psr
45
     */
46 6
    public function __construct(Message $m, MimePartFactory $pf, PartStreamRegistry $psr)
47
    {
48 6
        $this->message = $m;
49 6
        $this->partFactory = $pf;
50 6
        $this->partStreamRegistry = $psr;
51 6
    }
52
    
53
    /**
54
     * Parses the passed stream handle into the ZBateson\MailMimeParser\Message
55
     * object and returns it.
56
     * 
57
     * @param resource $fhandle the resource handle to the input stream of the
58
     *        mime message
59
     * @return \ZBateson\MailMimeParser\Message
60
     */
61 6
    public function parse($fhandle)
62
    {
63 6
        $this->partStreamRegistry->register($this->message->getObjectId(), $fhandle);
64 6
        $this->read($fhandle, $this->message);
65 6
        return $this->message;
66
    }
67
    
68
    /**
69
     * Ensures the header isn't empty, and contains a colon character, then
70
     * splits it and assigns it to $part
71
     * 
72
     * @param string $header
73
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
74
     */
75 6
    private function addRawHeaderToPart($header, MimePart $part)
76
    {
77 6
        if ($header !== '' && strpos($header, ':') !== false) {
78 6
            $a = explode(':', $header, 2);
79 6
            if ($part->getHeader($a[0]) !== null) {
80
                return;
81
            }
82 6
            $part->setRawHeader($a[0], trim($a[1]));
83
        }
84 6
    }
85
86
    /**
87
     * Reads a line of up to the passed number of characters.  If the line is
88
     * larger than that, the remaining characters in the line are read and
89
     * discarded, and only the first part is returned.
90
     *
91
     * @param resource $handle
92
     * @param int $size
93
     * @return string
94
     */
95 6
    private function readLine($handle, $size = 4096)
96
    {
97 6
        $ret = $line = fgets($handle, $size);
98 6
        while (strlen($line) === $size - 1 && substr($line, -1) !== "\n") {
99 1
            $line = fgets($handle, $size);
100
        }
101 6
        return $ret;
102
    }
103
104
    /**
105
     * Reads header lines up to an empty line, adding them to the passed $part.
106
     * 
107
     * @param resource $handle the resource handle to read from
108
     * @param \ZBateson\MailMimeParser\Message\MimePart $part the current part to add
109
     *        headers to
110
     */
111 6
    protected function readHeaders($handle, MimePart $part)
112
    {
113 6
        $header = '';
114
        do {
115 6
            $line = $this->readLine($handle);
116 6
            if ($line[0] !== "\t" && $line[0] !== ' ') {
117 6
                $this->addRawHeaderToPart($header, $part);
118 6
                $header = '';
119
            } else {
120 2
                $line = "\r\n" . $line;
121
            }
122 6
            $header .= rtrim($line, "\r\n");
123 6
        } while ($header !== '');
124 6
    }
125
    
126
    /**
127
     * Finds the end of the Mime part at the current read position in $handle
128
     * and sets $boundaryLength to the number of bytes in the part, and
129
     * $endBoundaryFound to true if it's an 'end' boundary, meaning there are no
130
     * further parts for the current mime part (ends with --).
131
     * 
132
     * @param resource $handle
133
     * @param string $boundary
134
     * @param int $boundaryLength
135
     * @param boolean $endBoundaryFound
136
     */
137 2
    private function findPartBoundaries($handle, $boundary, &$boundaryLength, &$endBoundaryFound)
138
    {
139
        do {
140 2
            $line = fgets($handle);
141 2
            $boundaryLength = strlen($line);
142 2
            $test = rtrim($line);
143 2
            if ($test === "--$boundary") {
144 2
                break;
145 2
            } elseif ($test === "--$boundary--") {
146 2
                $endBoundaryFound = true;
147 2
                break;
148
            }
149 2
        } while (!feof($handle));
150 2
    }
151
    
152
    /**
153
     * Adds the part to its parent.
154
     * 
155
     * @param MimePart $part
156
     */
157 4
    private function addToParent(MimePart $part)
158
    {
159 4
        if ($part->getParent() !== null) {
160 2
            $part->getParent()->addPart($part);
161
        }
162 4
    }
163
    
164
    /**
165
     * 
166
     * 
167
     * @param type $handle
168
     * @param MimePart $part
169
     * @param Message $message
170
     * @param type $contentStartPos
0 ignored issues
show
Bug introduced by
The type ZBateson\MailMimeParser\Message\type was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
171
     * @param type $boundaryLength
172
     */
173 4
    protected function attachStreamHandles($handle, MimePart $part, Message $message, $contentStartPos, $boundaryLength)
174
    {
175 4
        $end = ftell($handle) - $boundaryLength;
0 ignored issues
show
Bug introduced by
$handle of type ZBateson\MailMimeParser\Message\type is incompatible with the type resource expected by parameter $handle of ftell(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

175
        $end = ftell(/** @scrutinizer ignore-type */ $handle) - $boundaryLength;
Loading history...
176 4
        $this->partStreamRegistry->attachContentPartStreamHandle($part, $message, $contentStartPos, $end);
0 ignored issues
show
Bug introduced by
$contentStartPos of type ZBateson\MailMimeParser\Message\type is incompatible with the type integer expected by parameter $start of ZBateson\MailMimeParser\...ntentPartStreamHandle(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
        $this->partStreamRegistry->attachContentPartStreamHandle($part, $message, /** @scrutinizer ignore-type */ $contentStartPos, $end);
Loading history...
177 4
        $this->partStreamRegistry->attachOriginalPartStreamHandle($part, $message, $part->startHandlePosition, $end);
0 ignored issues
show
Bug introduced by
The property startHandlePosition does not seem to exist on ZBateson\MailMimeParser\Message\MimePart.
Loading history...
178
        
179 4
        if ($part->getParent() !== null) {
180
            do {
181 2
                $end = ftell($handle);
182 2
            } while (!feof($handle) && rtrim(fgets($handle)) === '');
183 2
            fseek($handle, $end, SEEK_SET);
0 ignored issues
show
Bug introduced by
$handle of type ZBateson\MailMimeParser\Message\type is incompatible with the type resource expected by parameter $handle of fseek(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

183
            fseek(/** @scrutinizer ignore-type */ $handle, $end, SEEK_SET);
Loading history...
184 2
            $this->partStreamRegistry->attachOriginalPartStreamHandle(
185 2
                $part->getParent(),
186 2
                $message,
187 2
                $part->getParent()->startHandlePosition,
188 2
                $end
189
            );
190
        }
191 4
    }
192
    
193
    /**
194
     * Reads the content of a mime part up to a boundary, or the entire message
195
     * if no boundary is specified.
196
     * 
197
     * readPartContent may be called to skip to the first boundary to read its
198
     * headers, in which case $skipPart should be true.
199
     * 
200
     * If the end boundary is found, the method returns true.
201
     * 
202
     * @param resource $handle the input stream resource
203
     * @param \ZBateson\MailMimeParser\Message $message the current Message
204
     *        object
205
     * @param \ZBateson\MailMimeParser\Message\MimePart $part the current MimePart
206
     *        object to load the content into.
207
     * @param string $boundary the MIME boundary
208
     * @param boolean $skipPart pass true if the intention is to read up to the
209
     *        beginning MIME boundary's headers
210
     * @return boolean if the end boundary is found
211
     */
212 4
    protected function readPartContent($handle, Message $message, MimePart $part, $boundary, $skipPart)
213
    {
214 4
        $start = ftell($handle);
215 4
        $boundaryLength = 0;
216 4
        $endBoundaryFound = false;
217 4
        if ($boundary !== null) {
0 ignored issues
show
introduced by
The condition $boundary !== null is always true.
Loading history...
218 2
            $this->findPartBoundaries($handle, $boundary, $boundaryLength, $endBoundaryFound);
219
        } else {
220 2
            fseek($handle, 0, SEEK_END);
221
        }
222 4
        $type = $part->getHeaderValue('Content-Type', 'text/plain');
223 4
        if (!$skipPart || preg_match('~multipart/\w+~i', $type)) {
224 4
            $this->attachStreamHandles($handle, $part, $message, $start, $boundaryLength);
0 ignored issues
show
Bug introduced by
$start of type integer is incompatible with the type ZBateson\MailMimeParser\Message\type expected by parameter $contentStartPos of ZBateson\MailMimeParser\...::attachStreamHandles(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

224
            $this->attachStreamHandles($handle, $part, $message, /** @scrutinizer ignore-type */ $start, $boundaryLength);
Loading history...
Bug introduced by
$handle of type resource is incompatible with the type ZBateson\MailMimeParser\Message\type expected by parameter $handle of ZBateson\MailMimeParser\...::attachStreamHandles(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

224
            $this->attachStreamHandles(/** @scrutinizer ignore-type */ $handle, $part, $message, $start, $boundaryLength);
Loading history...
225 4
            $this->addToParent($part);
226
        }
227 4
        return $endBoundaryFound;
228
    }
229
    
230
    /**
231
     * Returns the boundary from the parent MimePart, or the current boundary if
232
     * $parent is null
233
     * 
234
     * @param string $curBoundary
235
     * @param \ZBateson\MailMimeParser\Message\MimePart $parent
236
     * @return string
237
     */
238 2
    private function getParentBoundary($curBoundary, MimePart $parent = null)
239
    {
240 2
        return $parent !== null ?
241 1
            $parent->getHeaderParameter('Content-Type', 'boundary') :
242 2
            $curBoundary;
243
    }
244
    
245
    /**
246
     * Instantiates and returns a new MimePart setting the part's parent to
247
     * either the passed $parent, or $message if $parent is null.
248
     * 
249
     * @param \ZBateson\MailMimeParser\Message $message
250
     * @param \ZBateson\MailMimeParser\Message\MimePart $parent
251
     * @return \ZBateson\MailMimeParser\Message\MimePart
252
     */
253 4
    private function newMimePartForMessage(Message $message, MimePart $parent = null)
254
    {
255 4
        $nextPart = $this->partFactory->newMimePart();
256 4
        $nextPart->setParent($parent === null ? $message : $parent);
257 4
        return $nextPart;
258
    }
259
    
260
    /**
261
     * Keeps reading if an end boundary is found, to find the parent's boundary
262
     * and the part's content.
263
     * 
264
     * @param resource $handle
265
     * @param \ZBateson\MailMimeParser\Message $message
266
     * @param \ZBateson\MailMimeParser\Message\MimePart $parent
267
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
268
     * @param string $boundary
269
     * @param bool $skipFirst
270
     * @return \ZBateson\MailMimeParser\Message\MimePart
271
     */
272 4
    private function readMimeMessageBoundaryParts(
273
        $handle,
274
        Message $message,
275
        MimePart $parent,
276
        MimePart $part,
277
        $boundary,
278
        $skipFirst
279
    ) {
280 4
        $skipPart = $skipFirst;
281 4
        while ($this->readPartContent($handle, $message, $part, $boundary, $skipPart) && $parent !== null) {
282 2
            $parent = $parent->getParent();
283
            // $boundary used by next call to readPartContent
284 2
            $boundary = $this->getParentBoundary($boundary, $parent);
285 2
            $skipPart = true;
286
        }
287 4
        return $this->newMimePartForMessage($message, $parent);
288
    }
289
    
290
    /**
291
     * Finds the boundaries for the current MimePart, reads its content and
292
     * creates and returns the next part, setting its parent part accordingly.
293
     * 
294
     * @param resource $handle The handle to read from
295
     * @param \ZBateson\MailMimeParser\Message $message The current Message
296
     * @param \ZBateson\MailMimeParser\Message\MimePart $part 
297
     * @return MimePart
298
     */
299 4
    protected function readMimeMessagePart($handle, Message $message, MimePart $part)
300
    {
301 4
        $boundary = $part->getHeaderParameter('Content-Type', 'boundary');
302 4
        $skipFirst = true;
303 4
        $parent = $part;
304
305 4
        if ($boundary === null || !$part->isMultiPart()) {
306
            // either there is no boundary (possibly no parent boundary either) and message is read
307
            // till the end, or we're in a boundary already and content should be read till the parent
308
            // boundary is reached
309 4
            if ($part->getParent() !== null) {
310 2
                $parent = $part->getParent();
311 2
                $boundary = $parent->getHeaderParameter('Content-Type', 'boundary');
312
            }
313 4
            $skipFirst = false;
314
        }
315 4
        return $this->readMimeMessageBoundaryParts($handle, $message, $parent, $part, $boundary, $skipFirst);
316
    }
317
    
318
    /**
319
     * Extracts the filename and end position of a UUEncoded part.
320
     * 
321
     * The filename is set to the passed $nextFilename parameter.  The end
322
     * position is returned.
323
     * 
324
     * @param resource $handle the current file handle
325
     * @param int &$nextMode is assigned the value of the next file mode or null
326
     *        if not found
327
     * @param string &$nextFilename is assigned the value of the next filename
328
     *        or null if not found
329
     * @param int &$end assigned the offset position within the passed resource
330
     *        $handle of the end of the uuencoded part
331
     */
332 2
    private function findNextUUEncodedPartPosition($handle)
333
    {
334 2
        $end = ftell($handle);
335
        do {
336 2
            $line = trim(fgets($handle));
337 2
            $matches = null;
338 2
            if (preg_match('/^begin [0-7]{3} .*$/', $line, $matches)) {
339 1
                fseek($handle, $end);
340 1
                break;
341
            }
342 2
            $end = ftell($handle);
343 2
        } while (!feof($handle));
344 2
        return $end;
345
    }
346
    
347
    /**
348
     * Reads one part of a UUEncoded message and adds it to the passed Message
349
     * as a MimePart.
350
     * 
351
     * The method reads up to the first 'begin' part of the message, or to the
352
     * end of the message if no 'begin' exists.
353
     * 
354
     * @param resource $handle
355
     * @param \ZBateson\MailMimeParser\Message $message
356
     * @return string
357
     */
358 2
    protected function readUUEncodedOrPlainTextPart($handle, Message $message)
359
    {
360 2
        $start = ftell($handle);
361 2
        $line = trim(fgets($handle));
362 2
        $end = $this->findNextUUEncodedPartPosition($handle);
363 2
        $part = $message;
364 2
        if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) {
365 1
            $mode = $matches[1];
366 1
            $filename = $matches[2];
367 1
            $part = $this->partFactory->newUUEncodedPart($mode, $filename);
368 1
            $message->addPart($part);
369
        }
370 2
        $this->partStreamRegistry->attachContentPartStreamHandle($part, $message, $start, $end);
371 2
    }
372
    
373
    /**
374
     * Reads the message from the input stream $handle into $message.
375
     * 
376
     * The method will loop to read headers and find and parse multipart-mime
377
     * message parts and uuencoded attachments (as mime-parts), adding them to
378
     * the passed Message object.
379
     * 
380
     * @param resource $handle
381
     * @param \ZBateson\MailMimeParser\Message $message
382
     */
383 6
    protected function read($handle, Message $message)
384
    {
385 6
        $part = $message;
386 6
        $part->startHandlePosition = 0;
0 ignored issues
show
Bug introduced by
The property startHandlePosition does not seem to exist on ZBateson\MailMimeParser\Message.
Loading history...
387 6
        $this->readHeaders($handle, $message);
388
        do {
389 6
            if (!$message->isMime()) {
390 2
                $this->readUUEncodedOrPlainTextPart($handle, $message);
391
            } else {
392 4
                $part = $this->readMimeMessagePart($handle, $message, $part);
393 4
                $part->startHandlePosition = ftell($handle);
0 ignored issues
show
Bug introduced by
The property startHandlePosition does not seem to exist on ZBateson\MailMimeParser\Message\MimePart.
Loading history...
394 4
                $this->readHeaders($handle, $part);
395
            }
396 6
        } while (!feof($handle));
397 6
    }
398
}
399