Passed
Push — 0.4 ( 3becaf...016b60 )
by Zaahid
03:14
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 5
    public function __construct(Message $m, MimePartFactory $pf, PartStreamRegistry $psr)
47
    {
48 5
        $this->message = $m;
49 5
        $this->partFactory = $pf;
50 5
        $this->partStreamRegistry = $psr;
51 5
    }
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 5
    public function parse($fhandle)
62
    {
63 5
        $this->partStreamRegistry->register($this->message->getObjectId(), $fhandle);
64 5
        $this->read($fhandle, $this->message);
65 5
        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 5
    private function addRawHeaderToPart($header, MimePart $part)
76
    {
77 5
        if ($header !== '' && strpos($header, ':') !== false) {
78 5
            $a = explode(':', $header, 2);
79 5
            if ($part->getHeader($a[0]) !== null) {
80
                return;
81
            }
82 5
            $part->setRawHeader($a[0], trim($a[1]));
83
        }
84 5
    }
85
    
86
    /**
87
     * Reads header lines up to an empty line, adding them to the passed $part.
88
     * 
89
     * @param resource $handle the resource handle to read from
90
     * @param \ZBateson\MailMimeParser\Message\MimePart $part the current part to add
91
     *        headers to
92
     */
93 5
    protected function readHeaders($handle, MimePart $part)
94
    {
95 5
        $header = '';
96
        do {
97 5
            $line = fgets($handle, 1000);
98 5
            if ($line[0] !== "\t" && $line[0] !== ' ') {
99 5
                $this->addRawHeaderToPart($header, $part);
100 5
                $header = '';
101
            } else {
102 1
                $line = "\r\n" . $line;
103
            }
104 5
            $header .= rtrim($line, "\r\n");
105 5
        } while ($header !== '');
106 5
    }
107
    
108
    /**
109
     * Finds the end of the Mime part at the current read position in $handle
110
     * and sets $boundaryLength to the number of bytes in the part, and
111
     * $endBoundaryFound to true if it's an 'end' boundary, meaning there are no
112
     * further parts for the current mime part (ends with --).
113
     * 
114
     * @param resource $handle
115
     * @param string $boundary
116
     * @param int $boundaryLength
117
     * @param boolean $endBoundaryFound
118
     */
119 2
    private function findPartBoundaries($handle, $boundary, &$boundaryLength, &$endBoundaryFound)
120
    {
121
        do {
122 2
            $line = fgets($handle);
123 2
            $boundaryLength = strlen($line);
124 2
            $test = rtrim($line);
125 2
            if ($test === "--$boundary") {
126 2
                break;
127 2
            } elseif ($test === "--$boundary--") {
128 2
                $endBoundaryFound = true;
129 2
                break;
130
            }
131 2
        } while (!feof($handle));
132 2
    }
133
    
134
    /**
135
     * Adds the part to its parent.
136
     * 
137
     * @param MimePart $part
138
     */
139 3
    private function addToParent(MimePart $part)
140
    {
141 3
        if ($part->getParent() !== null) {
142 2
            $part->getParent()->addPart($part);
143
        }
144 3
    }
145
    
146
    /**
147
     * 
148
     * 
149
     * @param type $handle
150
     * @param MimePart $part
151
     * @param Message $message
152
     * @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...
153
     * @param type $boundaryLength
154
     */
155 3
    protected function attachStreamHandles($handle, MimePart $part, Message $message, $contentStartPos, $boundaryLength)
156
    {
157 3
        $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

157
        $end = ftell(/** @scrutinizer ignore-type */ $handle) - $boundaryLength;
Loading history...
158 3
        $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

158
        $this->partStreamRegistry->attachContentPartStreamHandle($part, $message, /** @scrutinizer ignore-type */ $contentStartPos, $end);
Loading history...
159 3
        $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...
160
        
161 3
        if ($part->getParent() !== null) {
162
            do {
163 2
                $end = ftell($handle);
164 2
            } while (!feof($handle) && rtrim(fgets($handle)) === '');
165 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

165
            fseek(/** @scrutinizer ignore-type */ $handle, $end, SEEK_SET);
Loading history...
166 2
            $this->partStreamRegistry->attachOriginalPartStreamHandle(
167 2
                $part->getParent(),
168 2
                $message,
169 2
                $part->getParent()->startHandlePosition,
170 2
                $end
171
            );
172
        }
173 3
    }
174
    
175
    /**
176
     * Reads the content of a mime part up to a boundary, or the entire message
177
     * if no boundary is specified.
178
     * 
179
     * readPartContent may be called to skip to the first boundary to read its
180
     * headers, in which case $skipPart should be true.
181
     * 
182
     * If the end boundary is found, the method returns true.
183
     * 
184
     * @param resource $handle the input stream resource
185
     * @param \ZBateson\MailMimeParser\Message $message the current Message
186
     *        object
187
     * @param \ZBateson\MailMimeParser\Message\MimePart $part the current MimePart
188
     *        object to load the content into.
189
     * @param string $boundary the MIME boundary
190
     * @param boolean $skipPart pass true if the intention is to read up to the
191
     *        beginning MIME boundary's headers
192
     * @return boolean if the end boundary is found
193
     */
194 3
    protected function readPartContent($handle, Message $message, MimePart $part, $boundary, $skipPart)
195
    {
196 3
        $start = ftell($handle);
197 3
        $boundaryLength = 0;
198 3
        $endBoundaryFound = false;
199 3
        if ($boundary !== null) {
0 ignored issues
show
introduced by
The condition $boundary !== null is always true.
Loading history...
200 2
            $this->findPartBoundaries($handle, $boundary, $boundaryLength, $endBoundaryFound);
201
        } else {
202 1
            fseek($handle, 0, SEEK_END);
203
        }
204 3
        $type = $part->getHeaderValue('Content-Type', 'text/plain');
205 3
        if (!$skipPart || preg_match('~multipart/\w+~i', $type)) {
206 3
            $this->attachStreamHandles($handle, $part, $message, $start, $boundaryLength);
0 ignored issues
show
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

206
            $this->attachStreamHandles(/** @scrutinizer ignore-type */ $handle, $part, $message, $start, $boundaryLength);
Loading history...
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

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