Completed
Push — master ( 69e914...42b3e6 )
by Zaahid
02:52
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
     * Reads header lines up to an empty line, adding them to the passed $part.
66
     * 
67
     * @param resource $handle the resource handle to read from
68
     * @param \ZBateson\MailMimeParser\MimePart $part the current part to add
69
     *        headers to
70
     */
71
    protected function readHeaders($handle, MimePart $part)
72
    {
73
        $header = '';
74
        do {
75
            $line = fgets($handle, 1000);
76
            if ($line[0] !== "\t" && $line[0] !== ' ') {
77
                if (!empty($header) && strpos($header, ':') !== false) {
78
                    $a = explode(':', $header, 2);
79
                    $part->setRawHeader($a[0], trim($a[1]));
80
                }
81
                $header = '';
82
            } else {
83
                $line = ' ' . ltrim($line);
84
            }
85
            $header .= rtrim($line, "\r\n");
86
        } while (!empty($header));
87
    }
88
    
89
    /**
90
     * Finds the end of the Mime part at the current read position in $handle
91
     * and sets $boundaryLength to the number of bytes in the part, and
92
     * $endBoundaryFound to true if it's an 'end' boundary, meaning there are no
93
     * further parts for the current mime part (ends with --).
94
     * 
95
     * @param resource $handle
96
     * @param string $boundary
97
     * @param int $boundaryLength
98
     * @param boolean $endBoundaryFound
99
     */
100
    private function findPartBoundaries($handle, $boundary, &$boundaryLength, &$endBoundaryFound)
101
    {
102
        do {
103
            $line = fgets($handle);
104
            $boundaryLength = strlen($line);
105
            $test = rtrim($line);
106
            if ($test === "--$boundary") {
107
                break;
108
            } elseif ($test === "--$boundary--") {
109
                $endBoundaryFound = true;
110
                break;
111
            }
112
        } while (!feof($handle));
113
    }
114
    
115
    /**
116
     * Reads the content of a mime part up to a boundary, or the entire message
117
     * if no boundary is specified.
118
     * 
119
     * readPartContent may be called to skip to the first boundary to read its
120
     * headers, in which case $skipPart should be true.
121
     * 
122
     * If the end boundary is found, the method returns true.
123
     * 
124
     * @param resource $handle the input stream resource
125
     * @param \ZBateson\MailMimeParser\Message $message the current Message
126
     *        object
127
     * @param \ZBateson\MailMimeParser\MimePart $part the current MimePart
128
     *        object to load the content into.
129
     * @param string $boundary the MIME boundary
130
     * @param boolean $skipPart pass true if the intention is to read up to the
131
     *        beginning MIME boundary's headers
132
     * @return boolean if the end boundary is found
133
     */
134
    protected function readPartContent($handle, Message $message, MimePart $part, $boundary, $skipPart)
135
    {
136
        $start = ftell($handle);
137
        $boundaryLength = 0;
138
        $endBoundaryFound = false;
139
        if (!empty($boundary)) {
140
            $this->findPartBoundaries($handle, $boundary, $boundaryLength, $endBoundaryFound);
141
        } else {
142
            fseek($handle, 0, SEEK_END);
143
        }
144
        if (!$skipPart) {
145
            $end = ftell($handle) - $boundaryLength;
146
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
147
            $message->addPart($part);
148
        }
149
        return $endBoundaryFound;
150
    }
151
    
152
    /**
153
     * Keeps reading if an end boundary is found, to find the parent's boundary
154
     * and the part's content.
155
     * 
156
     * @param resource $handle
157
     * @param \ZBateson\MailMimeParser\Message $message
158
     * @param \ZBateson\MailMimeParser\MimePart $parent
159
     * @param \ZBateson\MailMimeParser\MimePart $part
160
     * @param string $boundary
161
     * @param bool $skipFirst
162
     * @return \ZBateson\MailMimeParser\MimePart
163
     */
164
    private function readMimeMessageBoundaryParts(
165
        $handle,
166
        Message $message,
167
        MimePart $parent,
168
        MimePart $part,
169
        $boundary,
170
        $skipFirst
171
    ) {
172
        $skipPart = $skipFirst;
173
        while ($this->readPartContent($handle, $message, $part, $boundary, $skipPart) && $parent !== null) {
174
            $parent = $parent->getParent();
175
            if ($parent !== null) {
176
                $boundary = $parent->getHeaderParameter('Content-Type', 'boundary');
177
            }
178
            $skipPart = true;
179
        }
180
        $nextPart = $this->partFactory->newMimePart();
181
        if ($parent === null) {
182
            $parent = $message;
183
        }
184
        $nextPart->setParent($parent);
185
        return $nextPart;
186
    }
187
    
188
    /**
189
     * Finds the boundaries for the current MimePart, reads its content and
190
     * creates and returns the next part, setting its parent part accordingly.
191
     * 
192
     * @param resource $handle The handle to read from
193
     * @param \ZBateson\MailMimeParser\Message $message The current Message
194
     * @param \ZBateson\MailMimeParser\MimePart $part 
195
     * @return MimePart
196
     */
197
    protected function readMimeMessagePart($handle, Message $message, MimePart $part)
198
    {
199
        $boundary = $part->getHeaderParameter('Content-Type', 'boundary');
200
        $skipFirst = true;
201
        $parent = $part;
202
203
        if (empty($boundary) || !preg_match('~multipart/\w+~i', $part->getHeaderValue('Content-Type'))) {
204
            // either there is no boundary (possibly no parent boundary either) and message is read
205
            // till the end, or we're in a boundary already and content should be read till the parent
206
            // boundary is reached
207
            if ($part->getParent() !== null) {
208
                $parent = $part->getParent();
209
                $boundary = $parent->getHeaderParameter('Content-Type', 'boundary');
210
            }
211
            $skipFirst = false;
212
        }
213
        return $this->readMimeMessageBoundaryParts($handle, $message, $parent, $part, $boundary, $skipFirst);
214
    }
215
    
216
    /**
217
     * Extracts the filename and end position of a UUEncoded part.
218
     * 
219
     * The filename is set to the passed $nextFilename parameter.  The end
220
     * position is returned.
221
     * 
222
     * @param resource $handle the current file handle
223
     * @param int &$nextMode is assigned the value of the next file mode or null
224
     *        if not found
225
     * @param string &$nextFilename is assigned the value of the next filename
226
     *        or null if not found
227
     * @param int &$end assigned the offset position within the passed resource
228
     *        $handle of the end of the uuencoded part
229
     */
230
    private function findNextUUEncodedPartPosition($handle)
231
    {
232
        $end = ftell($handle);
233
        do {
234
            $line = trim(fgets($handle));
235
            $matches = null;
236
            if (preg_match('/^begin [0-7]{3} .*$/', $line, $matches)) {
237
                fseek($handle, $end);
238
                break;
239
            }
240
            $end = ftell($handle);
241
        } while (!feof($handle));
242
        return $end;
243
    }
244
    
245
    /**
246
     * Reads one part of a UUEncoded message and adds it to the passed Message
247
     * as a MimePart.
248
     * 
249
     * The method reads up to the first 'begin' part of the message, or to the
250
     * end of the message if no 'begin' exists.
251
     * 
252
     * @param resource $handle
253
     * @param \ZBateson\MailMimeParser\Message $message
254
     * @return string
255
     */
256
    protected function readUUEncodedOrPlainTextPart($handle, Message $message)
257
    {
258
        $start = ftell($handle);
259
        $line = trim(fgets($handle));
260
        $end = $this->findNextUUEncodedPartPosition($handle);
261
        
262
        $part = null;
263
        if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) {
264
            $mode = $matches[1];
265
            $filename = $matches[2];
266
            $part = $this->partFactory->newUUEncodedPart($mode, $filename);
267
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
268
        } else {
269
            $part = $this->partFactory->newNonMimePart();
270
            $this->partStreamRegistry->attachPartStreamHandle($part, $message, $start, $end);
271
        }
272
        $message->addPart($part);
273
    }
274
    
275
    /**
276
     * Reads the message from the input stream $handle into $message.
277
     * 
278
     * The method will loop to read headers and find and parse multipart-mime
279
     * message parts and uuencoded attachments (as mime-parts), adding them to
280
     * the passed Message object.
281
     * 
282
     * @param resource $handle
283
     * @param \ZBateson\MailMimeParser\Message $message
284
     */
285
    protected function read($handle, Message $message)
286
    {
287
        $part = $message;
288
        $this->readHeaders($handle, $message);
289
        $contentType = $part->getHeaderValue('Content-Type');
290
        $mimeVersion = $part->getHeaderValue('Mime-Version');
291
        do {
292
            if ($part === $message && $contentType === null && $mimeVersion === null) {
293
                $this->readUUEncodedOrPlainTextPart($handle, $message);
294
            } else {
295
                $part = $this->readMimeMessagePart($handle, $message, $part);
296
                $this->readHeaders($handle, $part);
297
            }
298
        } while (!feof($handle));
299
    }
300
}
301