Completed
Push — master ( ab502e...07a33d )
by Zaahid
06:14
created

MessageParser   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 321
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 100%

Importance

Changes 14
Bugs 2 Features 2
Metric Value
wmc 35
c 14
b 2
f 2
lcom 1
cbo 4
dl 0
loc 321
ccs 114
cts 114
cp 1
rs 9

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A parse() 0 6 1
A addRawHeaderToPart() 0 7 3
A findPartBoundaries() 0 14 4
A readPartContent() 0 17 3
A getParentBoundary() 0 6 2
A newMimePartForMessage() 0 6 2
A readMimeMessageBoundaryParts() 0 17 3
A readMimeMessagePart() 0 18 4
A readHeaders() 0 14 4
A findNextUUEncodedPartPosition() 0 14 3
A readUUEncodedOrPlainTextPart() 0 18 2
A read() 0 13 3
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;
8
9
use ZBateson\MailMimeParser\Stream\PartStreamRegistry;
10
11
/**
12
 * Parses a mail mime message into its component parts.  To invoke, call
13
 * MailMimeParser::parse.
14
 *
15
 * @author Zaahid Bateson
16
 */
17
class MessageParser
18
{
19
    /**
20
     * @var \ZBateson\MailMimeParser\Message the Message object that the read
21
     * mail mime message will be parsed into
22
     */
23
    protected $message;
24
    
25
    /**
26
     * @var \ZBateson\MailMimeParser\MimePartFactory the MimePartFactory object
27
     * used to create parts.
28
     */
29
    protected $partFactory;
30
    
31
    /**
32
     * @var \ZBateson\MailMimeParser\Stream\PartStreamRegistry the
33
     *      PartStreamRegistry 
34
     * object used to register stream parts.
35
     */
36
    protected $partStreamRegistry;
37
    
38
    /**
39
     * Sets up the parser with its dependencies.
40
     * 
41
     * @param \ZBateson\MailMimeParser\Message $m
42
     * @param \ZBateson\MailMimeParser\MimePartFactory $pf
43
     * @param \ZBateson\MailMimeParser\Stream\PartStreamRegistry $psr
44
     */
45 5
    public function __construct(Message $m, MimePartFactory $pf, PartStreamRegistry $psr)
46
    {
47 5
        $this->message = $m;
48 5
        $this->partFactory = $pf;
49 5
        $this->partStreamRegistry = $psr;
50 5
    }
51
    
52
    /**
53
     * Parses the passed stream handle into the ZBateson\MailMimeParser\Message
54
     * object and returns it.
55
     * 
56
     * @param resource $fhandle the resource handle to the input stream of the
57
     *        mime message
58
     * @return \ZBateson\MailMimeParser\Message
59
     */
60 5
    public function parse($fhandle)
61
    {
62 5
        $this->partStreamRegistry->register($this->message->getObjectId(), $fhandle);
63 5
        $this->read($fhandle, $this->message);
64 5
        return $this->message;
65
    }
66
    
67
    /**
68
     * Ensures the header isn't empty, and contains a colon character, then
69
     * splits it and assigns it to $part
70
     * 
71
     * @param string $header
72
     * @param \ZBateson\MailMimeParser\MimePart $part
73
     */
74 5
    private function addRawHeaderToPart($header, MimePart $part)
75
    {
76 5
        if (!empty($header) && strpos($header, ':') !== false) {
77 5
            $a = explode(':', $header, 2);
78 5
            $part->setRawHeader($a[0], trim($a[1]));
79 5
        }
80 5
    }
81
    
82
    /**
83
     * Reads header lines up to an empty line, adding them to the passed $part.
84
     * 
85
     * @param resource $handle the resource handle to read from
86
     * @param \ZBateson\MailMimeParser\MimePart $part the current part to add
87
     *        headers to
88
     */
89 5
    protected function readHeaders($handle, MimePart $part)
90
    {
91 5
        $header = '';
92
        do {
93 5
            $line = fgets($handle, 1000);
94 5
            if ($line[0] !== "\t" && $line[0] !== ' ') {
95 5
                $this->addRawHeaderToPart($header, $part);
96 5
                $header = '';
97 5
            } else {
98 1
                $line = ' ' . ltrim($line);
99
            }
100 5
            $header .= rtrim($line, "\r\n");
101 5
        } while (!empty($header));
102 5
    }
103
    
104
    /**
105
     * Finds the end of the Mime part at the current read position in $handle
106
     * and sets $boundaryLength to the number of bytes in the part, and
107
     * $endBoundaryFound to true if it's an 'end' boundary, meaning there are no
108
     * further parts for the current mime part (ends with --).
109
     * 
110
     * @param resource $handle
111
     * @param string $boundary
112
     * @param int $boundaryLength
113
     * @param boolean $endBoundaryFound
114
     */
115 2
    private function findPartBoundaries($handle, $boundary, &$boundaryLength, &$endBoundaryFound)
116
    {
117
        do {
118 2
            $line = fgets($handle);
119 2
            $boundaryLength = strlen($line);
120 2
            $test = rtrim($line);
121 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...
122 2
                break;
123 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...
124 2
                $endBoundaryFound = true;
125 2
                break;
126
            }
127 2
        } while (!feof($handle));
128 2
    }
129
    
130
    /**
131
     * Reads the content of a mime part up to a boundary, or the entire message
132
     * if no boundary is specified.
133
     * 
134
     * readPartContent may be called to skip to the first boundary to read its
135
     * headers, in which case $skipPart should be true.
136
     * 
137
     * If the end boundary is found, the method returns true.
138
     * 
139
     * @param resource $handle the input stream resource
140
     * @param \ZBateson\MailMimeParser\Message $message the current Message
141
     *        object
142
     * @param \ZBateson\MailMimeParser\MimePart $part the current MimePart
143
     *        object to load the content into.
144
     * @param string $boundary the MIME boundary
145
     * @param boolean $skipPart pass true if the intention is to read up to the
146
     *        beginning MIME boundary's headers
147
     * @return boolean if the end boundary is found
148
     */
149 3
    protected function readPartContent($handle, Message $message, MimePart $part, $boundary, $skipPart)
150
    {
151 3
        $start = ftell($handle);
152 3
        $boundaryLength = 0;
153 3
        $endBoundaryFound = false;
154 3
        if (!empty($boundary)) {
155 2
            $this->findPartBoundaries($handle, $boundary, $boundaryLength, $endBoundaryFound);
156 2
        } else {
157 1
            fseek($handle, 0, SEEK_END);
158
        }
159 3
        if (!$skipPart) {
160 3
            $end = ftell($handle) - $boundaryLength;
161 3
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
162 3
            $message->addPart($part);
163 3
        }
164 3
        return $endBoundaryFound;
165
    }
166
    
167
    /**
168
     * Returns the boundary from the parent MimePart, or the current boundary if
169
     * $parent is null
170
     * 
171
     * @param string $curBoundary
172
     * @param \ZBateson\MailMimeParser\MimePart $parent
173
     * @return string
174
     */
175 2
    private function getParentBoundary($curBoundary, MimePart $parent = null)
176
    {
177 2
        return $parent !== null ?
178 2
            $parent->getHeaderParameter('Content-Type', 'boundary') :
179 2
            $curBoundary;
180
    }
181
    
182
    /**
183
     * Instantiates and returns a new MimePart setting the part's parent to
184
     * either the passed $parent, or $message if $parent is null.
185
     * 
186
     * @param \ZBateson\MailMimeParser\Message $message
187
     * @param \ZBateson\MailMimeParser\MimePart $parent
188
     * @return \ZBateson\MailMimeParser\MimePart
189
     */
190 3
    private function newMimePartForMessage(Message $message, MimePart $parent = null)
191
    {
192 3
        $nextPart = $this->partFactory->newMimePart();
193 3
        $nextPart->setParent($parent === null ? $message : $parent);
194 3
        return $nextPart;
195
    }
196
    
197
    /**
198
     * Keeps reading if an end boundary is found, to find the parent's boundary
199
     * and the part's content.
200
     * 
201
     * @param resource $handle
202
     * @param \ZBateson\MailMimeParser\Message $message
203
     * @param \ZBateson\MailMimeParser\MimePart $parent
204
     * @param \ZBateson\MailMimeParser\MimePart $part
205
     * @param string $boundary
206
     * @param bool $skipFirst
207
     * @return \ZBateson\MailMimeParser\MimePart
208
     */
209 3
    private function readMimeMessageBoundaryParts(
210
        $handle,
211
        Message $message,
212
        MimePart $parent,
213
        MimePart $part,
214
        $boundary,
215
        $skipFirst
216
    ) {
217 3
        $skipPart = $skipFirst;
218 3
        while ($this->readPartContent($handle, $message, $part, $boundary, $skipPart) && $parent !== null) {
219 2
            $parent = $parent->getParent();
220
            // $boundary used by next call to readPartContent
221 2
            $boundary = $this->getParentBoundary($boundary, $parent);
222 2
            $skipPart = true;
223 2
        }
224 3
        return $this->newMimePartForMessage($message, $parent);
225
    }
226
    
227
    /**
228
     * Finds the boundaries for the current MimePart, reads its content and
229
     * creates and returns the next part, setting its parent part accordingly.
230
     * 
231
     * @param resource $handle The handle to read from
232
     * @param \ZBateson\MailMimeParser\Message $message The current Message
233
     * @param \ZBateson\MailMimeParser\MimePart $part 
234
     * @return MimePart
235
     */
236 3
    protected function readMimeMessagePart($handle, Message $message, MimePart $part)
237
    {
238 3
        $boundary = $part->getHeaderParameter('Content-Type', 'boundary');
239 3
        $skipFirst = true;
240 3
        $parent = $part;
241
242 3
        if (empty($boundary) || !preg_match('~multipart/\w+~i', $part->getHeaderValue('Content-Type'))) {
243
            // either there is no boundary (possibly no parent boundary either) and message is read
244
            // till the end, or we're in a boundary already and content should be read till the parent
245
            // boundary is reached
246 3
            if ($part->getParent() !== null) {
247 2
                $parent = $part->getParent();
248 2
                $boundary = $parent->getHeaderParameter('Content-Type', 'boundary');
249 2
            }
250 3
            $skipFirst = false;
251 3
        }
252 3
        return $this->readMimeMessageBoundaryParts($handle, $message, $parent, $part, $boundary, $skipFirst);
253
    }
254
    
255
    /**
256
     * Extracts the filename and end position of a UUEncoded part.
257
     * 
258
     * The filename is set to the passed $nextFilename parameter.  The end
259
     * position is returned.
260
     * 
261
     * @param resource $handle the current file handle
262
     * @param int &$nextMode is assigned the value of the next file mode or null
263
     *        if not found
264
     * @param string &$nextFilename is assigned the value of the next filename
265
     *        or null if not found
266
     * @param int &$end assigned the offset position within the passed resource
267
     *        $handle of the end of the uuencoded part
268
     */
269 2
    private function findNextUUEncodedPartPosition($handle)
270
    {
271 2
        $end = ftell($handle);
272
        do {
273 2
            $line = trim(fgets($handle));
274 2
            $matches = null;
275 2
            if (preg_match('/^begin [0-7]{3} .*$/', $line, $matches)) {
276 1
                fseek($handle, $end);
277 1
                break;
278
            }
279 2
            $end = ftell($handle);
280 2
        } while (!feof($handle));
281 2
        return $end;
282
    }
283
    
284
    /**
285
     * Reads one part of a UUEncoded message and adds it to the passed Message
286
     * as a MimePart.
287
     * 
288
     * The method reads up to the first 'begin' part of the message, or to the
289
     * end of the message if no 'begin' exists.
290
     * 
291
     * @param resource $handle
292
     * @param \ZBateson\MailMimeParser\Message $message
293
     * @return string
294
     */
295 2
    protected function readUUEncodedOrPlainTextPart($handle, Message $message)
296
    {
297 2
        $start = ftell($handle);
298 2
        $line = trim(fgets($handle));
299 2
        $end = $this->findNextUUEncodedPartPosition($handle);
300
        
301 2
        $part = null;
302 2
        if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) {
303 1
            $mode = $matches[1];
304 1
            $filename = $matches[2];
305 1
            $part = $this->partFactory->newUUEncodedPart($mode, $filename);
306 1
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
307 1
        } else {
308 1
            $part = $this->partFactory->newNonMimePart();
309 1
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
310
        }
311 2
        $message->addPart($part);
312 2
    }
313
    
314
    /**
315
     * Reads the message from the input stream $handle into $message.
316
     * 
317
     * The method will loop to read headers and find and parse multipart-mime
318
     * message parts and uuencoded attachments (as mime-parts), adding them to
319
     * the passed Message object.
320
     * 
321
     * @param resource $handle
322
     * @param \ZBateson\MailMimeParser\Message $message
323
     */
324 5
    protected function read($handle, Message $message)
325
    {
326 5
        $part = $message;
327 5
        $this->readHeaders($handle, $message);
328
        do {
329 5
            if (!$message->isMime()) {
330 2
                $this->readUUEncodedOrPlainTextPart($handle, $message);
331 2
            } else {
332 3
                $part = $this->readMimeMessagePart($handle, $message, $part);
333 3
                $this->readHeaders($handle, $part);
334
            }
335 5
        } while (!feof($handle));
336 5
    }
337
}
338