Completed
Push — master ( b2e1e5...74fe50 )
by Zaahid
03:27
created

MimePart::getPartCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
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\Header\HeaderFactory;
10
use ZBateson\MailMimeParser\Header\ParameterHeader;
11
use ZBateson\MailMimeParser\Stream\StreamLeftover;
12
13
/**
14
 * Represents a single part of a multi-part mime message.
15
 *
16
 * A MimePart object may have any number of child parts, or may be a child
17
 * itself with its own parent or parents.
18
 *
19
 * The content of the part can be read from its PartStream resource handle,
20
 * accessible via MimePart::getContentResourceHanlde.
21
 *
22
 * @author Zaahid Bateson
23
 */
24
class MimePart
25
{
26
    /**
27
     * @var \ZBateson\MailMimeParser\Header\HeaderFactory the HeaderFactory
28
     *      object used for created headers
29
     */
30
    protected $headerFactory;
31
32
    /**
33
     * @var \ZBateson\MailMimeParser\Header\AbstractHeader[] array of header
34
     * objects
35
     */
36
    protected $headers;
37
38
    /**
39
     * @var \ZBateson\MailMimeParser\MimePart parent part
40
     */
41
    protected $parent;
42
43
    /**
44
     * @var resource the content's resource handle
45
     */
46
    protected $handle;
47
48
    /**
49
     * @var \ZBateson\MailMimeParser\MimePart[] array of parts in this message
50
     */
51
    protected $parts = [];
52
53
    /**
54
     * @var \ZBateson\MailMimeParser\MimePart[] Maps mime types to parts for
55
     * looking up in getPartByMimeType
56
     */
57
    protected $mimeToPart = [];
58
59
    /**
60
     * Sets up class dependencies.
61
     *
62
     * @param HeaderFactory $headerFactory
63
     */
64 88
    public function __construct(HeaderFactory $headerFactory)
65
    {
66 88
        $this->headerFactory = $headerFactory;
67 88
    }
68
69
    /**
70
     * Closes the attached resource handle.
71
     */
72 88
    public function __destruct()
73
    {
74 88
        if (is_resource($this->handle)) {
75 9
            fclose($this->handle);
76 9
        }
77 88
    }
78
79
    /**
80
     * Adds the passed part to the parts array, and registers non-attachment/
81
     * non-multipart parts by their content type.
82
     *
83
     * @param \ZBateson\MailMimeParser\MimePart $part
84
     */
85 80
    public function addPart(MimePart $part)
86
    {
87 80
        $this->parts[] = $part;
88 80
        if ($part->getHeaderValue('Content-Disposition') === null && !$part->isMultiPart()) {
89 76
            $key = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
90 76
            $this->mimeToPart[$key] = $part;
91 76
        }
92 80
    }
93
94
    /**
95
     * Unregisters the child part from this part.
96
     *
97
     * @param \ZBateson\MailMimeParser\MimePart $part
98
     */
99 8
    public function removePart(MimePart $part)
100
    {
101 8
        $partsArray = [];
102 8
        foreach ($this->parts as $apart) {
103 8
            if ($apart !== $part) {
104 6
                $partsArray[] = $apart;
105 6
            }
106 8
        }
107 8
        $key = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
108 8
        unset($this->mimeToPart[$key]);
109 8
        $this->parts = $partsArray;
110 8
    }
111
112
    /**
113
     * Returns the non-text, non-HTML part at the given 0-based index, or null
114
     * if none is set.
115
     *
116
     * @param int $index
117
     * @return \ZBateson\MailMimeParser\MimePart
118
     */
119 3
    public function getPart($index)
120
    {
121 3
        if (!isset($this->parts[$index])) {
122
            return null;
123
        }
124 3
        return $this->parts[$index];
125
    }
126
127
    /**
128
     * Returns all attachment parts.
129
     *
130
     * @return \ZBateson\MailMimeParser\MimePart[]
131
     */
132 25
    public function getAllParts()
133
    {
134 25
        return $this->parts;
135
    }
136
137
    /**
138
     * Returns the number of attachments available.
139
     *
140
     * @return int
141
     */
142 1
    public function getPartCount()
143
    {
144 1
        return count($this->parts);
145
    }
146
147
    /**
148
     * Returns the part associated with the passed mime type if it exists.
149
     *
150
     * @param string $mimeType
151
     * @return \ZBateson\MailMimeParser\MimePart or null
152
     */
153 18
    public function getPartByMimeType($mimeType)
154
    {
155 18
        $key = strtolower($mimeType);
156 18
        if (isset($this->mimeToPart[$key])) {
157 18
            return $this->mimeToPart[$key];
158
        }
159
        return null;
160
    }
161
162
    /**
163
     * Returns true if there's a content stream associated with the part.
164
     *
165
     * @return boolean
166
     */
167 78
    public function hasContent()
168
    {
169 78
        if ($this->handle !== null) {
170 78
            return true;
171
        }
172 24
        return false;
173
    }
174
175
    /**
176
     * Returns true if this part's mime type is multipart/*
177
     *
178
     * @return bool
179
     */
180 80
    public function isMultiPart()
181
    {
182 80
        return preg_match(
183 80
            '~multipart/\w+~i',
184 80
            $this->getHeaderValue('Content-Type', 'text/plain')
185 80
        );
186
    }
187
    
188
    /**
189
     * Returns true if this part's mime type is text/plain, text/html or has a
190
     * text/* and has a defined 'charset' attribute.
191
     * 
192
     * @return bool
193
     */
194 80
    public function isTextPart()
195
    {
196 80
        $type = $this->getHeaderValue('Content-Type', 'text/plain');
197 80
        if ($type === 'text/html' || $type === 'text/plain') {
198 69
            return true;
199
        }
200 49
        $charset = $this->getHeaderParameter('Content-Type', 'charset');
201 49
        return ($charset !== null && preg_match(
202 4
            '~text/\w+~i',
203 4
            $this->getHeaderValue('Content-Type', 'text/plain')
204 49
        ));
205
    }
206
207
    /**
208
     * Attaches the resource handle for the part's content.  The attached handle
209
     * is closed when the MimePart object is destroyed.
210
     *
211
     * @param resource $contentHandle
212
     */
213 82
    public function attachContentResourceHandle($contentHandle)
214
    {
215 82
        if ($this->handle !== null && $this->handle !== $contentHandle) {
216 2
            fclose($this->handle);
217 2
        }
218 82
        $this->handle = $contentHandle;
219 82
    }
220
221
    /**
222
     *
223
     */
224 11
    protected function detachContentResourceHandle()
225
    {
226 11
        $this->handle = null;
227 11
    }
228
229
    /**
230
     * Sets the content of the part to the passed string (effectively creates
231
     * a php://temp stream with the passed content and calls
232
     * attachContentResourceHandle with the opened stream).
233
     *
234
     * @param string $string
235
     */
236 8
    public function setContent($string)
237
    {
238 8
        $handle = fopen('php://temp', 'r+');
239 8
        fwrite($handle, $string);
240 8
        rewind($handle);
241 8
        $this->attachContentResourceHandle($handle);
242 8
    }
243
244
    /**
245
     * Returns the resource stream handle for the part's content or null if not
246
     * set.  rewind() is called on the stream before returning it.
247
     *
248
     * The resource is automatically closed by MimePart's destructor and should
249
     * not be closed otherwise.
250
     *
251
     * @return resource
252
     */
253 76
    public function getContentResourceHandle()
254
    {
255 76
        if (is_resource($this->handle)) {
256 76
            rewind($this->handle);
257 76
        }
258 76
        return $this->handle;
259
    }
260
261
    /**
262
     * Shortcut to reading stream content and assigning it to a string.  Returns
263
     * null if the part doesn't have a content stream.
264
     *
265
     * @return string
266
     */
267 3
    public function getContent()
268
    {
269 3
        if ($this->hasContent()) {
270 3
            return stream_get_contents($this->handle);
271
        }
272
        return null;
273
    }
274
275
    /**
276
     * Adds a header with the given $name and $value.
277
     *
278
     * Creates a new \ZBateson\MailMimeParser\Header\AbstractHeader object and
279
     * registers it as a header.
280
     *
281
     * @param string $name
282
     * @param string $value
283
     */
284 83
    public function setRawHeader($name, $value)
285
    {
286 83
        $this->headers[strtolower($name)] = $this->headerFactory->newInstance($name, $value);
287 83
    }
288
289
    /**
290
     * Removes the header with the given name
291
     *
292
     * @param string $name
293
     */
294 7
    public function removeHeader($name)
295
    {
296 7
        unset($this->headers[strtolower($name)]);
297 7
    }
298
299
    /**
300
     * Returns the AbstractHeader object for the header with the given $name
301
     *
302
     * Note that mime headers aren't case sensitive.
303
     *
304
     * @param string $name
305
     * @return \ZBateson\MailMimeParser\Header\AbstractHeader
306
     */
307 85
    public function getHeader($name)
308
    {
309 85
        if (isset($this->headers[strtolower($name)])) {
310 83
            return $this->headers[strtolower($name)];
311
        }
312 80
        return null;
313
    }
314
315
    /**
316
     * Returns the string value for the header with the given $name.
317
     *
318
     * Note that mime headers aren't case sensitive.
319
     *
320
     * @param string $name
321
     * @param string $defaultValue
322
     * @return string
323
     */
324 82
    public function getHeaderValue($name, $defaultValue = null)
325
    {
326 82
        $header = $this->getHeader($name);
327 82
        if ($header !== null) {
328 81
            return $header->getValue();
329
        }
330 79
        return $defaultValue;
331
    }
332
333
    /**
334
     * Returns the full array of headers for this part.
335
     *
336
     * @return \ZBateson\MailMimeParser\Header\AbstractHeader[]
337
     */
338 1
    public function getHeaders()
339
    {
340 1
        return $this->headers;
341
    }
342
343
    /**
344
     * Returns a parameter of the header $header, given the parameter named
345
     * $param.
346
     *
347
     * Only headers of type
348
     * \ZBateson\MailMimeParser\Header\ParameterHeader have parameters.
349
     * Content-Type and Content-Disposition are examples of headers with
350
     * parameters. "Charset" is a common parameter of Content-Type.
351
     *
352
     * @param string $header
353
     * @param string $param
354
     * @param string $defaultValue
355
     * @return string
356
     */
357 82
    public function getHeaderParameter($header, $param, $defaultValue = null)
358
    {
359 82
        $obj = $this->getHeader($header);
360 82
        if ($obj && $obj instanceof ParameterHeader) {
361 81
            return $obj->getValueFor($param, $defaultValue);
362
        }
363 7
        return $defaultValue;
364
    }
365
366
    /**
367
     * Sets the parent part.
368
     *
369
     * @param \ZBateson\MailMimeParser\MimePart $part
370
     */
371 79
    public function setParent(MimePart $part)
372
    {
373 79
        $this->parent = $part;
374 79
    }
375
376
    /**
377
     * Returns this part's parent.
378
     *
379
     * @return \ZBateson\MailMimeParser\MimePart
380
     */
381 79
    public function getParent()
382
    {
383 79
        return $this->parent;
384
    }
385
386
    /**
387
     * Sets up a mailmimeparser-encode stream filter on the passed stream
388
     * resource handle if applicable and returns a reference to the filter.
389
     *
390
     * @param resource $handle
391
     * @return resource a reference to the appended stream filter or null
392
     */
393 77
    private function setCharsetStreamFilterOnStream($handle)
0 ignored issues
show
Unused Code introduced by
The parameter $handle is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
394
    {
395 77
        if ($this->isTextPart()) {
396 71
            return stream_filter_append(
397 71
                $this->handle,
398 71
                'mailmimeparser-encode',
399 71
                STREAM_FILTER_READ,
400
                [
401 71
                    'charset' => 'UTF-8',
402 71
                    'to' => $this->getHeaderParameter('Content-Type', 'charset', 'ISO-8859-1')
403 71
                ]
404 71
            );
405
        }
406 46
        return null;
407
    }
408
409
    /**
410
     * Appends a stream filter the passed resource handle based on the type of
411
     * encoding for the current mime part.
412
     *
413
     * Unfortunately PHP seems to error out allocating memory for
414
     * stream_filter_make_writable in Base64EncodeStreamFilter using
415
     * STREAM_FILTER_WRITE, and HHVM doesn't seem to remove the filter properly
416
     * for STREAM_FILTER_READ, so the function appends a read filter on
417
     * $fromHandle if running through 'php', and a write filter on $toHandle if
418
     * using HHVM.
419
     *
420
     * @param resource $fromHandle
421
     * @param resource $toHandle
422
     * @param \ZBateson\MailMimeParser\Stream\StreamLeftover $leftovers
423
     * @return resource the stream filter
424
     */
425 77
    private function setTransferEncodingFilterOnStream($fromHandle, $toHandle, StreamLeftover $leftovers)
426
    {
427 77
        $encoding = strtolower($this->getHeaderValue('Content-Transfer-Encoding'));
428
        $params = [
429 77
            'line-length' => 76,
430 77
            'line-break-chars' => "\r\n",
431 77
            'leftovers' => $leftovers,
432 77
            'filename' => $this->getHeaderParameter(
433 77
                'Content-Type',
434 77
                'name',
435
                'null'
436 77
            )
437 77
        ];
438
        $typeToEncoding = [
439 77
            'quoted-printable' => 'convert.quoted-printable-encode',
440 77
            'base64' => 'convert.base64-encode',
441 77
            'x-uuencode' => 'mailmimeparser-uuencode',
442 77
        ];
443 77
        if (isset($typeToEncoding[$encoding])) {
444 65
            if (defined('HHVM_VERSION')) {
445
                return stream_filter_append(
446
                    $toHandle,
447
                    $typeToEncoding[$encoding],
448
                    STREAM_FILTER_WRITE,
449
                    $params
450
                );
451
            } else {
452 65
                return stream_filter_append(
453 65
                    $fromHandle,
454 65
                    $typeToEncoding[$encoding],
455 65
                    STREAM_FILTER_READ,
456
                    $params
457 65
                );
458
            }
459
        }
460 43
        return null;
461
    }
462
463
    /**
464
     * Returns true if the content-transfer-encoding header of the current part
465
     * is set to 'x-uuencode'.
466
     *
467
     * @return bool
468
     */
469
    private function isUUEncoded()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
470
    {
471
        $encoding = strtolower($this->getHeaderValue('Content-Transfer-Encoding'));
472
        return ($encoding === 'x-uuencode');
473
    }
474
475
    /**
476
     * Filters out single line feed (CR or LF) characters from text input and
477
     * replaces them with CRLF, assigning the result to $read.  Also trims out
478
     * any starting and ending CRLF characters in the stream.
479
     *
480
     * @param string $read the read string, and where the result will be written
481
     *        to
482
     * @param bool $first set to true if this is the first set of read
483
     *        characters from the stream (ltrims CRLF)
484
     * @param string $lastChars contains any CRLF characters from the last $read
485
     *        line if it ended with a CRLF (because they're trimmed from the
486
     *        end, and get prepended to $read).
487
     */
488 71
    private function filterTextBeforeCopying(&$read, &$first, &$lastChars)
489
    {
490 71
        if ($first) {
491 71
            $first = false;
492 71
            $read = ltrim($read, "\r\n");
493 71
        }
494 71
        $read = $lastChars . $read;
495 71
        $read = preg_replace('/\r\n|\r|\n/', "\r\n", $read);
496 71
        $lastChars = '';
497 71
        $matches = null;
498 71
        if (preg_match('/[\r\n]+$/', $read, $matches)) {
499 67
            $lastChars = $matches[0];
500 67
            $read = rtrim($read, "\r\n");
501 67
        }
502 71
    }
503
504
    /**
505
     * Copies the content of the $fromHandle stream into the $toHandle stream,
506
     * maintaining the current read position in $fromHandle and writing
507
     * uuencode headers.
508
     *
509
     * @param resource $fromHandle
510
     * @param resource $toHandle
511
     */
512 77
    private function copyContentStream($fromHandle, $toHandle)
513
    {
514 77
        $pos = ftell($fromHandle);
515 77
        rewind($fromHandle);
516
        // changed from stream_copy_to_stream because hhvm seems to stop before
517
        // end of file for some reason
518 77
        $lastChars = '';
519 77
        $first = true;
520 77
        while (($read = fread($fromHandle, 1024)) != false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $read = fread($fromHandle, 1024) of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
521 77
            if ($this->isTextPart()) {
522 71
                $this->filterTextBeforeCopying($read, $first, $lastChars);
523 71
            }
524 77
            fwrite($toHandle, $read);
525 77
        }
526 77
        fseek($fromHandle, $pos);
527 77
    }
528
529
    /**
530
     * Writes out headers and follows them with an empty line.
531
     *
532
     * @param resource $handle
533
     */
534 77
    protected function writeHeadersTo($handle)
535
    {
536 77
        foreach ($this->headers as $header) {
537 77
            fwrite($handle, "$header\r\n");
1 ignored issue
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $header 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...
538 77
        }
539 77
        fwrite($handle, "\r\n");
540 77
    }
541
542
    /**
543
     * Writes out the content portion of the mime part based on the headers that
544
     * are set on the part, taking care of character/content-transfer encoding.
545
     *
546
     * @param resource $handle
547
     */
548 77
    protected function writeContentTo($handle)
549
    {
550 77
        if ($this->handle !== null) {
551 77
            $filter = $this->setCharsetStreamFilterOnStream($handle);
552 77
            $leftovers = new StreamLeftover();
553 77
            $encodingFilter = $this->setTransferEncodingFilterOnStream($this->handle, $handle, $leftovers);
554 77
            $this->copyContentStream($this->handle, $handle);
555 77
            if ($encodingFilter !== null) {
556 65
                fflush($handle);
557 65
                stream_filter_remove($encodingFilter);
558 65
                fwrite($handle, $leftovers->encodedValue);
559 65
            }
560 77
            if ($filter !== null) {
561 71
                stream_filter_remove($filter);
562 71
            }
563 77
        }
564 77
    }
565
566
    /**
567
     * Writes out the MimePart to the passed resource.
568
     *
569
     * Takes care of character and content transfer encoding on the output based
570
     * on what headers are set.
571
     *
572
     * @param resource $handle
573
     */
574 51
    protected function writeTo($handle)
575
    {
576 51
        $this->writeHeadersTo($handle);
577 51
        $this->writeContentTo($handle);
578 51
    }
579
}
580