Passed
Push — master ( 2ea88e...404a7f )
by Zaahid
03:05
created

MessageParser::addToParent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 1
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
            $part->setRawHeader($a[0], trim($a[1]));
80 5
        }
81 5
    }
82
    
83
    /**
84
     * Reads header lines up to an empty line, adding them to the passed $part.
85
     * 
86
     * @param resource $handle the resource handle to read from
87
     * @param \ZBateson\MailMimeParser\Message\MimePart $part the current part to add
88
     *        headers to
89
     */
90 5
    protected function readHeaders($handle, MimePart $part)
91
    {
92 5
        $header = '';
93
        do {
94 5
            $line = fgets($handle, 1000);
95 5
            if ($line[0] !== "\t" && $line[0] !== ' ') {
96 5
                $this->addRawHeaderToPart($header, $part);
97 5
                $header = '';
98 5
            } else {
99 1
                $line = "\r\n" . $line;
100
            }
101 5
            $header .= rtrim($line, "\r\n");
102 5
        } while ($header !== '');
103 5
    }
104
    
105
    /**
106
     * Finds the end of the Mime part at the current read position in $handle
107
     * and sets $boundaryLength to the number of bytes in the part, and
108
     * $endBoundaryFound to true if it's an 'end' boundary, meaning there are no
109
     * further parts for the current mime part (ends with --).
110
     * 
111
     * @param resource $handle
112
     * @param string $boundary
113
     * @param int $boundaryLength
114
     * @param boolean $endBoundaryFound
115
     */
116 2
    private function findPartBoundaries($handle, $boundary, &$boundaryLength, &$endBoundaryFound)
117
    {
118
        do {
119 2
            $line = fgets($handle);
120 2
            $boundaryLength = strlen($line);
121 2
            $test = rtrim($line);
122 2
            if ($test === "--$boundary") {
123 2
                break;
124 2
            } elseif ($test === "--$boundary--") {
125 2
                $endBoundaryFound = true;
126 2
                break;
127
            }
128 2
        } while (!feof($handle));
129 2
    }
130
    
131
    /**
132
     * Adds the part to its parent.
133
     * 
134
     * @param MimePart $part
135
     */
136 3
    private function addToParent(MimePart $part)
137
    {
138 3
        if ($part->getParent() !== null) {
139 2
            $part->getParent()->addPart($part);
140 2
        }
141 3
    }
142
    
143
    /**
144
     * 
145
     * 
146
     * @param type $handle
147
     * @param MimePart $part
148
     * @param Message $message
149
     * @param type $contentStartPos
150
     * @param type $boundaryLength
151
     */
152 3
    protected function attachStreamHandles($handle, MimePart $part, Message $message, $contentStartPos, $boundaryLength)
153
    {
154 3
        $end = ftell($handle) - $boundaryLength;
155 3
        $this->partStreamRegistry->attachContentPartStreamHandle($part, $message, $contentStartPos, $end);
156 3
        $this->partStreamRegistry->attachOriginalPartStreamHandle($part, $message, $part->startHandlePosition, $end);
0 ignored issues
show
Bug introduced by
The property startHandlePosition does not seem to exist. Did you mean handle?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
157
        
158 3
        if ($part->getParent() !== null) {
159
            do {
160 2
                $end = ftell($handle);
161 2
            } while (!feof($handle) && rtrim(fgets($handle)) === '');
162 2
            fseek($handle, $end, SEEK_SET);
163 2
            $this->partStreamRegistry->attachOriginalPartStreamHandle(
164 2
                $part->getParent(),
165 2
                $message,
166 2
                $part->getParent()->startHandlePosition,
0 ignored issues
show
Bug introduced by
The property startHandlePosition does not seem to exist. Did you mean handle?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$start is of type integer, but the function expects a object<ZBateson\MailMimeParser\Message\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
$boundaryLength is of type integer, but the function expects a object<ZBateson\MailMimeParser\Message\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
366 5
        $this->readHeaders($handle, $message);
367
        do {
368 5
            if (!$message->isMime()) {
369 2
                $this->readUUEncodedOrPlainTextPart($handle, $message);
370 2
            } else {
371 3
                $part = $this->readMimeMessagePart($handle, $message, $part);
372 3
                $part->startHandlePosition = ftell($handle);
0 ignored issues
show
Bug introduced by
The property startHandlePosition does not seem to exist. Did you mean handle?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
373 3
                $this->readHeaders($handle, $part);
374
            }
375 5
        } while (!feof($handle));
376 5
    }
377
}
378