Completed
Push — master ( c7cf7f...702f4a )
by Zaahid
02:37
created

MessageParser   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 335
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 12
Bugs 2 Features 2
Metric Value
wmc 37
c 12
b 2
f 2
lcom 1
cbo 4
dl 0
loc 335
rs 8.6

14 Methods

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