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