Completed
Push — master ( 6512b0...b69ec3 )
by Zaahid
03:33
created

MessageParser::readMimeMessagePart()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

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