Completed
Push — master ( 74fe50...0f19ea )
by Zaahid
09:04
created

MessageParser::readPartContent()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 15
cts 15
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 14
nc 4
nop 5
crap 4
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") {
1 ignored issue
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $boundary instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
123 2
                break;
124 2
            } elseif ($test === "--$boundary--") {
1 ignored issue
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $boundary instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
125 2
                $endBoundaryFound = true;
126 2
                break;
127
            }
128 2
        } while (!feof($handle));
129 2
    }
130
    
131
    /**
132
     * Reads the content of a mime part up to a boundary, or the entire message
133
     * if no boundary is specified.
134
     * 
135
     * readPartContent may be called to skip to the first boundary to read its
136
     * headers, in which case $skipPart should be true.
137
     * 
138
     * If the end boundary is found, the method returns true.
139
     * 
140
     * @param resource $handle the input stream resource
141
     * @param \ZBateson\MailMimeParser\Message $message the current Message
142
     *        object
143
     * @param \ZBateson\MailMimeParser\Message\MimePart $part the current MimePart
144
     *        object to load the content into.
145
     * @param string $boundary the MIME boundary
146
     * @param boolean $skipPart pass true if the intention is to read up to the
147
     *        beginning MIME boundary's headers
148
     * @return boolean if the end boundary is found
149
     */
150 3
    protected function readPartContent($handle, Message $message, MimePart $part, $boundary, $skipPart)
151
    {
152 3
        $start = ftell($handle);
153 3
        $boundaryLength = 0;
154 3
        $endBoundaryFound = false;
155 3
        if ($boundary !== null) {
156 2
            $this->findPartBoundaries($handle, $boundary, $boundaryLength, $endBoundaryFound);
157 2
        } else {
158 1
            fseek($handle, 0, SEEK_END);
159
        }
160 3
        $type = $part->getHeaderValue('Content-Type', 'text/plain');
161 3
        if (!$skipPart || preg_match('~multipart/\w+~i', $type)) {
162 3
            $end = ftell($handle) - $boundaryLength;
163 3
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
164 3
            $message->addPart($part);
165 3
        }
166 3
        return $endBoundaryFound;
167
    }
168
    
169
    /**
170
     * Returns the boundary from the parent MimePart, or the current boundary if
171
     * $parent is null
172
     * 
173
     * @param string $curBoundary
174
     * @param \ZBateson\MailMimeParser\Message\MimePart $parent
175
     * @return string
176
     */
177 2
    private function getParentBoundary($curBoundary, MimePart $parent = null)
178
    {
179 2
        return $parent !== null ?
180 2
            $parent->getHeaderParameter('Content-Type', 'boundary') :
181 2
            $curBoundary;
182
    }
183
    
184
    /**
185
     * Instantiates and returns a new MimePart setting the part's parent to
186
     * either the passed $parent, or $message if $parent is null.
187
     * 
188
     * @param \ZBateson\MailMimeParser\Message $message
189
     * @param \ZBateson\MailMimeParser\Message\MimePart $parent
190
     * @return \ZBateson\MailMimeParser\Message\MimePart
191
     */
192 3
    private function newMimePartForMessage(Message $message, MimePart $parent = null)
193
    {
194 3
        $nextPart = $this->partFactory->newMimePart();
195 3
        $nextPart->setParent($parent === null ? $message : $parent);
196 3
        return $nextPart;
197
    }
198
    
199
    /**
200
     * Keeps reading if an end boundary is found, to find the parent's boundary
201
     * and the part's content.
202
     * 
203
     * @param resource $handle
204
     * @param \ZBateson\MailMimeParser\Message $message
205
     * @param \ZBateson\MailMimeParser\Message\MimePart $parent
206
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
207
     * @param string $boundary
208
     * @param bool $skipFirst
209
     * @return \ZBateson\MailMimeParser\Message\MimePart
210
     */
211 3
    private function readMimeMessageBoundaryParts(
212
        $handle,
213
        Message $message,
214
        MimePart $parent,
215
        MimePart $part,
216
        $boundary,
217
        $skipFirst
218
    ) {
219 3
        $skipPart = $skipFirst;
220 3
        while ($this->readPartContent($handle, $message, $part, $boundary, $skipPart) && $parent !== null) {
221 2
            $parent = $parent->getParent();
222
            // $boundary used by next call to readPartContent
223 2
            $boundary = $this->getParentBoundary($boundary, $parent);
224 2
            $skipPart = true;
225 2
        }
226 3
        return $this->newMimePartForMessage($message, $parent);
227
    }
228
    
229
    /**
230
     * Finds the boundaries for the current MimePart, reads its content and
231
     * creates and returns the next part, setting its parent part accordingly.
232
     * 
233
     * @param resource $handle The handle to read from
234
     * @param \ZBateson\MailMimeParser\Message $message The current Message
235
     * @param \ZBateson\MailMimeParser\Message\MimePart $part 
236
     * @return MimePart
237
     */
238 3
    protected function readMimeMessagePart($handle, Message $message, MimePart $part)
239
    {
240 3
        $boundary = $part->getHeaderParameter('Content-Type', 'boundary');
241 3
        $skipFirst = true;
242 3
        $parent = $part;
243
244 3
        if ($boundary === null || !$part->isMultiPart()) {
245
            // either there is no boundary (possibly no parent boundary either) and message is read
246
            // till the end, or we're in a boundary already and content should be read till the parent
247
            // boundary is reached
248 3
            if ($part->getParent() !== null) {
249 2
                $parent = $part->getParent();
250 2
                $boundary = $parent->getHeaderParameter('Content-Type', 'boundary');
251 2
            }
252 3
            $skipFirst = false;
253 3
        }
254 3
        return $this->readMimeMessageBoundaryParts($handle, $message, $parent, $part, $boundary, $skipFirst);
255
    }
256
    
257
    /**
258
     * Extracts the filename and end position of a UUEncoded part.
259
     * 
260
     * The filename is set to the passed $nextFilename parameter.  The end
261
     * position is returned.
262
     * 
263
     * @param resource $handle the current file handle
264
     * @param int &$nextMode is assigned the value of the next file mode or null
265
     *        if not found
266
     * @param string &$nextFilename is assigned the value of the next filename
267
     *        or null if not found
268
     * @param int &$end assigned the offset position within the passed resource
269
     *        $handle of the end of the uuencoded part
270
     */
271 2
    private function findNextUUEncodedPartPosition($handle)
272
    {
273 2
        $end = ftell($handle);
274
        do {
275 2
            $line = trim(fgets($handle));
276 2
            $matches = null;
277 2
            if (preg_match('/^begin [0-7]{3} .*$/', $line, $matches)) {
278 1
                fseek($handle, $end);
279 1
                break;
280
            }
281 2
            $end = ftell($handle);
282 2
        } while (!feof($handle));
283 2
        return $end;
284
    }
285
    
286
    /**
287
     * Reads one part of a UUEncoded message and adds it to the passed Message
288
     * as a MimePart.
289
     * 
290
     * The method reads up to the first 'begin' part of the message, or to the
291
     * end of the message if no 'begin' exists.
292
     * 
293
     * @param resource $handle
294
     * @param \ZBateson\MailMimeParser\Message $message
295
     * @return string
296
     */
297 2
    protected function readUUEncodedOrPlainTextPart($handle, Message $message)
298
    {
299 2
        $start = ftell($handle);
300 2
        $line = trim(fgets($handle));
301 2
        $end = $this->findNextUUEncodedPartPosition($handle);
302
        
303 2
        $part = null;
304 2
        if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) {
305 1
            $mode = $matches[1];
306 1
            $filename = $matches[2];
307 1
            $part = $this->partFactory->newUUEncodedPart($mode, $filename);
308 1
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
309 1
        } else {
310 1
            $part = $this->partFactory->newNonMimePart();
311 1
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
312
        }
313 2
        $message->addPart($part);
314 2
    }
315
    
316
    /**
317
     * Reads the message from the input stream $handle into $message.
318
     * 
319
     * The method will loop to read headers and find and parse multipart-mime
320
     * message parts and uuencoded attachments (as mime-parts), adding them to
321
     * the passed Message object.
322
     * 
323
     * @param resource $handle
324
     * @param \ZBateson\MailMimeParser\Message $message
325
     */
326 5
    protected function read($handle, Message $message)
327
    {
328 5
        $part = $message;
329 5
        $this->readHeaders($handle, $message);
330
        do {
331 5
            if (!$message->isMime()) {
332 2
                $this->readUUEncodedOrPlainTextPart($handle, $message);
333 2
            } else {
334 3
                $part = $this->readMimeMessagePart($handle, $message, $part);
335 3
                $this->readHeaders($handle, $part);
336
            }
337 5
        } while (!feof($handle));
338 5
    }
339
}
340