Completed
Push — master ( 42b3e6...c7cf7f )
by Zaahid
02:29
created

MessageParser::readUUEncodedOrPlainTextPart()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 18
rs 9.4285
cc 2
eloc 14
nc 2
nop 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;
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
     * Keeps reading if an end boundary is found, to find the parent's boundary
166
     * and the part's content.
167
     * 
168
     * @param resource $handle
169
     * @param \ZBateson\MailMimeParser\Message $message
170
     * @param \ZBateson\MailMimeParser\MimePart $parent
171
     * @param \ZBateson\MailMimeParser\MimePart $part
172
     * @param string $boundary
173
     * @param bool $skipFirst
174
     * @return \ZBateson\MailMimeParser\MimePart
175
     */
176
    private function readMimeMessageBoundaryParts(
177
        $handle,
178
        Message $message,
179
        MimePart $parent,
180
        MimePart $part,
181
        $boundary,
182
        $skipFirst
183
    ) {
184
        $skipPart = $skipFirst;
185
        while ($this->readPartContent($handle, $message, $part, $boundary, $skipPart) && $parent !== null) {
186
            $parent = $parent->getParent();
187
            // $boundary used by next call to readPartContent
188
            $boundary = $parent !== null ?
189
                $parent->getHeaderParameter('Content-Type', 'boundary') :
190
                $boundary;
191
            $skipPart = true;
192
        }
193
        $nextPart = $this->partFactory->newMimePart();
194
        $nextPart->setParent($parent === null ? $message : $parent);
195
        return $nextPart;
196
    }
197
    
198
    /**
199
     * Finds the boundaries for the current MimePart, reads its content and
200
     * creates and returns the next part, setting its parent part accordingly.
201
     * 
202
     * @param resource $handle The handle to read from
203
     * @param \ZBateson\MailMimeParser\Message $message The current Message
204
     * @param \ZBateson\MailMimeParser\MimePart $part 
205
     * @return MimePart
206
     */
207
    protected function readMimeMessagePart($handle, Message $message, MimePart $part)
208
    {
209
        $boundary = $part->getHeaderParameter('Content-Type', 'boundary');
210
        $skipFirst = true;
211
        $parent = $part;
212
213
        if (empty($boundary) || !preg_match('~multipart/\w+~i', $part->getHeaderValue('Content-Type'))) {
214
            // either there is no boundary (possibly no parent boundary either) and message is read
215
            // till the end, or we're in a boundary already and content should be read till the parent
216
            // boundary is reached
217
            if ($part->getParent() !== null) {
218
                $parent = $part->getParent();
219
                $boundary = $parent->getHeaderParameter('Content-Type', 'boundary');
220
            }
221
            $skipFirst = false;
222
        }
223
        return $this->readMimeMessageBoundaryParts($handle, $message, $parent, $part, $boundary, $skipFirst);
224
    }
225
    
226
    /**
227
     * Extracts the filename and end position of a UUEncoded part.
228
     * 
229
     * The filename is set to the passed $nextFilename parameter.  The end
230
     * position is returned.
231
     * 
232
     * @param resource $handle the current file handle
233
     * @param int &$nextMode is assigned the value of the next file mode or null
234
     *        if not found
235
     * @param string &$nextFilename is assigned the value of the next filename
236
     *        or null if not found
237
     * @param int &$end assigned the offset position within the passed resource
238
     *        $handle of the end of the uuencoded part
239
     */
240
    private function findNextUUEncodedPartPosition($handle)
241
    {
242
        $end = ftell($handle);
243
        do {
244
            $line = trim(fgets($handle));
245
            $matches = null;
246
            if (preg_match('/^begin [0-7]{3} .*$/', $line, $matches)) {
247
                fseek($handle, $end);
248
                break;
249
            }
250
            $end = ftell($handle);
251
        } while (!feof($handle));
252
        return $end;
253
    }
254
    
255
    /**
256
     * Reads one part of a UUEncoded message and adds it to the passed Message
257
     * as a MimePart.
258
     * 
259
     * The method reads up to the first 'begin' part of the message, or to the
260
     * end of the message if no 'begin' exists.
261
     * 
262
     * @param resource $handle
263
     * @param \ZBateson\MailMimeParser\Message $message
264
     * @return string
265
     */
266
    protected function readUUEncodedOrPlainTextPart($handle, Message $message)
267
    {
268
        $start = ftell($handle);
269
        $line = trim(fgets($handle));
270
        $end = $this->findNextUUEncodedPartPosition($handle);
271
        
272
        $part = null;
273
        if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) {
274
            $mode = $matches[1];
275
            $filename = $matches[2];
276
            $part = $this->partFactory->newUUEncodedPart($mode, $filename);
277
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
278
        } else {
279
            $part = $this->partFactory->newNonMimePart();
280
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
281
        }
282
        $message->addPart($part);
283
    }
284
    
285
    /**
286
     * Reads the message from the input stream $handle into $message.
287
     * 
288
     * The method will loop to read headers and find and parse multipart-mime
289
     * message parts and uuencoded attachments (as mime-parts), adding them to
290
     * the passed Message object.
291
     * 
292
     * @param resource $handle
293
     * @param \ZBateson\MailMimeParser\Message $message
294
     */
295
    protected function read($handle, Message $message)
296
    {
297
        $part = $message;
298
        $this->readHeaders($handle, $message);
299
        $contentType = $part->getHeaderValue('Content-Type');
300
        $mimeVersion = $part->getHeaderValue('Mime-Version');
301
        $isMimeNotDefined = ($part === $message && $contentType === null && $mimeVersion === null);
302
        do {
303
            if ($isMimeNotDefined) {
304
                $this->readUUEncodedOrPlainTextPart($handle, $message);
305
            } else {
306
                $part = $this->readMimeMessagePart($handle, $message, $part);
307
                $this->readHeaders($handle, $part);
308
            }
309
        } while (!feof($handle));
310
    }
311
}
312