Completed
Push — master ( 373de6...b74d12 )
by Zaahid
03:50
created

MimePart::removeHeader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
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 82
    public function __construct(HeaderFactory $headerFactory)
65
    {
66 82
        $this->headerFactory = $headerFactory;
67 82
    }
68
69
    /**
70
     * Closes the attached resource handle.
71
     */
72 82
    public function __destruct()
73
    {
74 82
        if ($this->handle !== null) {
75 8
            fclose($this->handle);
76 8
        }
77 82
    }
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 74
    public function addPart(MimePart $part)
86
    {
87 74
        $this->parts[] = $part;
88 74
        if ($part->getHeaderValue('Content-Disposition') === null && !$part->isMultiPart()) {
89 71
            $key = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
90 71
            $this->mimeToPart[$key] = $part;
91 71
        }
92 74
    }
93
94
    /**
95
     * Unregisters the child part from this part.
96
     *
97
     * @param \ZBateson\MailMimeParser\MimePart $part
98
     */
99 7
    public function removePart(MimePart $part)
100
    {
101 7
        $partsArray = [];
102 7
        foreach ($this->parts as $apart) {
103 7
            if ($apart !== $part) {
104 5
                $partsArray[] = $apart;
105 5
            }
106 7
        }
107 7
        $key = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
108 7
        unset($this->mimeToPart[$key]);
109 7
        $this->parts = $partsArray;
110 7
    }
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 2
    public function getPart($index)
120
    {
121 2
        if (!isset($this->parts[$index])) {
122
            return null;
123
        }
124 2
        return $this->parts[$index];
125
    }
126
127
    /**
128
     * Returns all attachment parts.
129
     *
130
     * @return \ZBateson\MailMimeParser\MimePart[]
131
     */
132 22
    public function getAllParts()
133
    {
134 22
        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 72
    public function hasContent()
168
    {
169 72
        if (!empty($this->handle)) {
170 72
            return true;
171
        }
172 22
        return false;
173
    }
174
175
    /**
176
     * Returns true if this part's mime type is multipart/*
177
     *
178
     * @return bool
179
     */
180 74
    public function isMultiPart()
181
    {
182 74
        return preg_match(
183 74
            '~multipart/\w+~i',
184 74
            $this->getHeaderValue('Content-Type', 'text/plain')
185 74
        );
186
    }
187
    
188
    /**
189
     * Returns true if this part's mime type is text/*
190
     * 
191
     * @return bool
192
     */
193 71
    public function isTextPart()
194
    {
195 71
        return preg_match(
196 71
            '~text/\w+~i',
197 71
            $this->getHeaderValue('Content-Type', 'text/plain')
198 71
        );
199
    }
200
201
    /**
202
     * Attaches the resource handle for the part's content.  The attached handle
203
     * is closed when the MimePart object is destroyed.
204
     *
205
     * @param resource $contentHandle
206
     */
207 76
    public function attachContentResourceHandle($contentHandle)
208
    {
209 76
        if ($this->handle !== null && $this->handle !== $contentHandle) {
210 2
            fclose($this->handle);
211 2
        }
212 76
        $this->handle = $contentHandle;
213 76
    }
214
215
    /**
216
     *
217
     */
218 8
    protected function detachContentResourceHandle()
219
    {
220 8
        $this->handle = null;
221 8
    }
222
223
    /**
224
     * Sets the content of the part to the passed string (effectively creates
225
     * a php://temp stream with the passed content and calls
226
     * attachContentResourceHandle with the opened stream).
227
     *
228
     * @param string $string
229
     */
230 6
    public function setContent($string)
231
    {
232 6
        $handle = fopen('php://temp', 'r+');
233 6
        fwrite($handle, $string);
234 6
        rewind($handle);
235 6
        $this->attachContentResourceHandle($handle);
236 6
    }
237
238
    /**
239
     * Returns the resource stream handle for the part's content.
240
     *
241
     * The resource is automatically closed by MimePart's destructor and should
242
     * not be closed otherwise.
243
     *
244
     * @return resource
245
     */
246 72
    public function getContentResourceHandle()
247
    {
248 72
        return $this->handle;
249
    }
250
251
    /**
252
     * Shortcut to reading stream content and assigning it to a string.  Returns
253
     * null if the part doesn't have a content stream.
254
     *
255
     * @return string
256
     */
257 2
    public function getContent()
258
    {
259 2
        if ($this->hasContent()) {
260 2
            return stream_get_contents($this->handle);
261
        }
262
        return null;
263
    }
264
265
    /**
266
     * Adds a header with the given $name and $value.
267
     *
268
     * Creates a new \ZBateson\MailMimeParser\Header\AbstractHeader object and
269
     * registers it as a header.
270
     *
271
     * @param string $name
272
     * @param string $value
273
     */
274 77
    public function setRawHeader($name, $value)
275
    {
276 77
        $this->headers[strtolower($name)] = $this->headerFactory->newInstance($name, $value);
277 77
    }
278
279
    /**
280
     * Removes the header with the given name
281
     *
282
     * @param string $name
283
     */
284 5
    public function removeHeader($name)
285
    {
286 5
        unset($this->headers[strtolower($name)]);
287 5
    }
288
289
    /**
290
     * Returns the AbstractHeader object for the header with the given $name
291
     *
292
     * Note that mime headers aren't case sensitive.
293
     *
294
     * @param string $name
295
     * @return \ZBateson\MailMimeParser\Header\AbstractHeader
296
     */
297 79
    public function getHeader($name)
298
    {
299 79
        if (isset($this->headers[strtolower($name)])) {
300 77
            return $this->headers[strtolower($name)];
301
        }
302 75
        return null;
303
    }
304
305
    /**
306
     * Returns the string value for the header with the given $name.
307
     *
308
     * Note that mime headers aren't case sensitive.
309
     *
310
     * @param string $name
311
     * @param string $defaultValue
312
     * @return string
313
     */
314 76
    public function getHeaderValue($name, $defaultValue = null)
315
    {
316 76
        $header = $this->getHeader($name);
317 76
        if (!empty($header)) {
318 75
            return $header->getValue();
319
        }
320 74
        return $defaultValue;
321
    }
322
323
    /**
324
     * Returns the full array of headers for this part.
325
     *
326
     * @return \ZBateson\MailMimeParser\Header\AbstractHeader[]
327
     */
328 1
    public function getHeaders()
329
    {
330 1
        return $this->headers;
331
    }
332
333
    /**
334
     * Returns a parameter of the header $header, given the parameter named
335
     * $param.
336
     *
337
     * Only headers of type
338
     * \ZBateson\MailMimeParser\Header\ParameterHeader have parameters.
339
     * Content-Type and Content-Disposition are examples of headers with
340
     * parameters. "Charset" is a common parameter of Content-Type.
341
     *
342
     * @param string $header
343
     * @param string $param
344
     * @param string $defaultValue
345
     * @return string
346
     */
347 76
    public function getHeaderParameter($header, $param, $defaultValue = null)
348
    {
349 76
        $obj = $this->getHeader($header);
350 76
        if ($obj && $obj instanceof ParameterHeader) {
351 75
            return $obj->getValueFor($param, $defaultValue);
352
        }
353 5
        return $defaultValue;
354
    }
355
356
    /**
357
     * Sets the parent part.
358
     *
359
     * @param \ZBateson\MailMimeParser\MimePart $part
360
     */
361 73
    public function setParent(MimePart $part)
362
    {
363 73
        $this->parent = $part;
364 73
    }
365
366
    /**
367
     * Returns this part's parent.
368
     *
369
     * @return \ZBateson\MailMimeParser\MimePart
370
     */
371 73
    public function getParent()
372
    {
373 73
        return $this->parent;
374
    }
375
376
    /**
377
     * Sets up a mailmimeparser-encode stream filter on the passed stream
378
     * resource handle if applicable and returns a reference to the filter.
379
     *
380
     * @param resource $handle
381
     * @return resource a reference to the appended stream filter or null
382
     */
383 71
    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...
384
    {
385 71
        $contentType = strtolower($this->getHeaderValue('Content-Type', 'text/plain'));
386 71
        if (strpos($contentType, 'text/') === 0) {
387 69
            return stream_filter_append(
388 69
                $this->handle,
389 69
                'mailmimeparser-encode',
390 69
                STREAM_FILTER_READ,
391
                [
392 69
                    'charset' => 'UTF-8',
393 69
                    'to' => $this->getHeaderParameter('Content-Type', 'charset', 'ISO-8859-1')
394 69
                ]
395 69
            );
396
        }
397 41
        return null;
398
    }
399
400
    /**
401
     * Appends a stream filter the passed resource handle based on the type of
402
     * encoding for the current mime part.
403
     *
404
     * Unfortunately PHP seems to error out allocating memory for
405
     * stream_filter_make_writable in Base64EncodeStreamFilter using
406
     * STREAM_FILTER_WRITE, and HHVM doesn't seem to remove the filter properly
407
     * for STREAM_FILTER_READ, so the function appends a read filter on
408
     * $fromHandle if running through 'php', and a write filter on $toHandle if
409
     * using HHVM.
410
     *
411
     * @param resource $fromHandle
412
     * @param resource $toHandle
413
     * @param \ZBateson\MailMimeParser\Stream\StreamLeftover $leftovers
414
     * @return resource the stream filter
415
     */
416 71
    private function setTransferEncodingFilterOnStream($fromHandle, $toHandle, StreamLeftover $leftovers)
417
    {
418 71
        $encoding = strtolower($this->getHeaderValue('Content-Transfer-Encoding'));
419
        $params = [
420 71
            'line-length' => 76,
421 71
            'line-break-chars' => "\r\n",
422 71
            'leftovers' => $leftovers,
423 71
            'filename' => $this->getHeaderParameter(
424 71
                'Content-Type',
425 71
                'name',
426
                'null'
427 71
            )
428 71
        ];
429
        $typeToEncoding = [
430 71
            'quoted-printable' => 'convert.quoted-printable-encode',
431 71
            'base64' => 'convert.base64-encode',
432 71
            'x-uuencode' => 'mailmimeparser-uuencode',
433 71
        ];
434 71
        if (isset($typeToEncoding[$encoding])) {
435 61
            if (defined('HHVM_VERSION')) {
436
                return stream_filter_append(
437
                    $toHandle,
438
                    $typeToEncoding[$encoding],
439
                    STREAM_FILTER_WRITE,
440
                    $params
441
                );
442
            } else {
443 61
                return stream_filter_append(
444 61
                    $fromHandle,
445 61
                    $typeToEncoding[$encoding],
446 61
                    STREAM_FILTER_READ,
447
                    $params
448 61
                );
449
            }
450
        }
451 40
        return null;
452
    }
453
454
    /**
455
     * Returns true if the content-transfer-encoding header of the current part
456
     * is set to 'x-uuencode'.
457
     *
458
     * @return bool
459
     */
460
    private function isUUEncoded()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
461
    {
462
        $encoding = strtolower($this->getHeaderValue('Content-Transfer-Encoding'));
463
        return ($encoding === 'x-uuencode');
464
    }
465
466
    /**
467
     * Filters out single line feed (CR or LF) characters from text input and
468
     * replaces them with CRLF, assigning the result to $read.  Also trims out
469
     * any starting and ending CRLF characters in the stream.
470
     *
471
     * @param string $read the read string, and where the result will be written
472
     *        to
473
     * @param bool $first set to true if this is the first set of read
474
     *        characters from the stream (ltrims CRLF)
475
     * @param string $lastChars contains any CRLF characters from the last $read
476
     *        line if it ended with a CRLF (because they're trimmed from the
477
     *        end, and get prepended to $read).
478
     */
479 69
    private function filterTextBeforeCopying(&$read, &$first, &$lastChars)
480
    {
481 69
        if ($first) {
482 69
            $first = false;
483 69
            $read = ltrim($read, "\r\n");
484 69
        }
485 69
        $read = $lastChars . $read;
486 69
        $read = preg_replace('/\r\n|\r|\n/', "\r\n", $read);
487 69
        $lastChars = '';
488 69
        $matches = null;
489 69
        if (preg_match('/[\r\n]+$/', $read, $matches)) {
490 65
            $lastChars = $matches[0];
491 65
            $read = rtrim($read, "\r\n");
492 65
        }
493 69
    }
494
495
    /**
496
     * Copies the content of the $fromHandle stream into the $toHandle stream,
497
     * maintaining the current read position in $fromHandle and writing
498
     * uuencode headers.
499
     *
500
     * @param resource $fromHandle
501
     * @param resource $toHandle
502
     */
503 71
    private function copyContentStream($fromHandle, $toHandle)
504
    {
505 71
        $pos = ftell($fromHandle);
506 71
        rewind($fromHandle);
507
        // changed from stream_copy_to_stream because hhvm seems to stop before
508
        // end of file for some reason
509 71
        $lastChars = '';
510 71
        $first = true;
511 71
        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...
512 71
            if ($this->isTextPart()) {
513 69
                $this->filterTextBeforeCopying($read, $first, $lastChars);
514 69
            }
515 71
            fwrite($toHandle, $read);
516 71
        }
517 71
        fseek($fromHandle, $pos);
518 71
    }
519
520
    /**
521
     * Writes out headers and follows them with an empty line.
522
     *
523
     * @param resource $handle
524
     */
525 71
    protected function writeHeadersTo($handle)
526
    {
527 71
        foreach ($this->headers as $header) {
528 71
            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...
529 71
        }
530 71
        fwrite($handle, "\r\n");
531 71
    }
532
533
    /**
534
     * Writes out the content portion of the mime part based on the headers that
535
     * are set on the part, taking care of character/content-transfer encoding.
536
     *
537
     * @param resource $handle
538
     */
539 71
    protected function writeContentTo($handle)
540
    {
541 71
        if (!empty($this->handle)) {
542 71
            $filter = $this->setCharsetStreamFilterOnStream($handle);
543 71
            $leftovers = new StreamLeftover();
544 71
            $encodingFilter = $this->setTransferEncodingFilterOnStream($this->handle, $handle, $leftovers);
545 71
            $this->copyContentStream($this->handle, $handle);
546 71
            if ($encodingFilter !== null) {
547 61
                fflush($handle);
548 61
                stream_filter_remove($encodingFilter);
549 61
                fwrite($handle, $leftovers->encodedValue);
550 61
            }
551 71
            if ($filter !== null) {
552 69
                stream_filter_remove($filter);
553 69
            }
554 71
        }
555 71
    }
556
557
    /**
558
     * Writes out the MimePart to the passed resource.
559
     *
560
     * Takes care of character and content transfer encoding on the output based
561
     * on what headers are set.
562
     *
563
     * @param resource $handle
564
     */
565 48
    protected function writeTo($handle)
566
    {
567 48
        $this->writeHeadersTo($handle);
568 48
        $this->writeContentTo($handle);
569 48
    }
570
}
571