Completed
Push — master ( 147214...53e4d6 )
by Zaahid
09:05
created

MimePart::writeTo()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
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 9
            fclose($this->handle);
76 9
        }
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
        $key = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
89 74
        if ($part->getHeaderValue('Content-Disposition') === null && !$part->isMultiPart()) {
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
        unset($this->mimeToPart[$part->getHeaderValue('Content-Type', 'text/plain')]);
108 7
        $this->parts = $partsArray;
109 7
    }
110
    
111
    /**
112
     * Returns the non-text, non-HTML part at the given 0-based index, or null
113
     * if none is set.
114
     * 
115
     * @param int $index
116
     * @return \ZBateson\MailMimeParser\MimePart
117
     */
118 2
    public function getPart($index)
119
    {
120 2
        if (!isset($this->parts[$index])) {
121
            return null;
122
        }
123 2
        return $this->parts[$index];
124
    }
125
    
126
    /**
127
     * Returns all attachment parts.
128
     * 
129
     * @return \ZBateson\MailMimeParser\MimePart[]
130
     */
131 22
    public function getAllParts()
132
    {
133 22
        return $this->parts;
134
    }
135
    
136
    /**
137
     * Returns the number of attachments available.
138
     * 
139
     * @return int
140
     */
141 1
    public function getPartCount()
142
    {
143 1
        return count($this->parts);
144
    }
145
    
146
    /**
147
     * Returns the part associated with the passed mime type if it exists.
148
     * 
149
     * @param string $mimeType
150
     * @return \ZBateson\MailMimeParser\MimePart or null
151
     */
152 18
    public function getPartByMimeType($mimeType)
153
    {
154 18
        $key = strtolower($mimeType);
155 18
        if (isset($this->mimeToPart[$key])) {
156 18
            return $this->mimeToPart[$key];
157
        }
158
        return null;
159
    }
160
    
161
    /**
162
     * Returns true if there's a content stream associated with the part.
163
     * 
164
     * @return boolean
165
     */
166 3
    public function hasContent()
167
    {
168 3
        if (!empty($this->handle)) {
169 3
            return true;
170
        }
171 1
        return false;
172
    }
173
174
    /**
175
     * Returns true if this part's mime type is multipart/*
176
     * 
177
     * @return bool
178
     */
179 74
    public function isMultiPart()
180
    {
181 74
        return preg_match(
182 74
            '~multipart/\w+~i',
183 74
            $this->getHeaderValue('Content-Type', 'text/plain')
184 74
        );
185
    }
186
187
    /**
188
     * Attaches the resource handle for the part's content.  The attached handle
189
     * is closed when the MimePart object is destroyed.
190
     * 
191
     * @param resource $contentHandle
192
     */
193 76
    public function attachContentResourceHandle($contentHandle)
194
    {
195 76
        if ($this->handle !== null && $this->handle !== $contentHandle) {
196 2
            fclose($this->handle);
197 2
        }
198 76
        $this->handle = $contentHandle;
199 76
    }
200
    
201
    /**
202
     * 
203
     */
204 8
    protected function detachContentResourceHandle()
205
    {
206 8
        $this->handle = null;
207 8
    }
208
    
209
    /**
210
     * Sets the content of the part to the passed string (effectively creates
211
     * a php://temp stream with the passed content and calls
212
     * attachContentResourceHandle with the opened stream).
213
     * 
214
     * @param string $string
215
     */
216 6
    public function setContent($string)
217
    {
218 6
        $handle = fopen('php://temp', 'r+');
219 6
        fwrite($handle, $string);
220 6
        rewind($handle);
221 6
        $this->attachContentResourceHandle($handle);
222 6
    }
223
    
224
    /**
225
     * Returns the resource stream handle for the part's content.
226
     * 
227
     * The resource is automatically closed by MimePart's destructor and should
228
     * not be closed otherwise.
229
     * 
230
     * @return resource
231
     */
232 72
    public function getContentResourceHandle()
233
    {
234 72
        return $this->handle;
235
    }
236
    
237
    /**
238
     * Shortcut to reading stream content and assigning it to a string.  Returns
239
     * null if the part doesn't have a content stream.
240
     * 
241
     * @return string
242
     */
243 2
    public function getContent()
244
    {
245 2
        if ($this->hasContent()) {
246 2
            return stream_get_contents($this->handle);
247
        }
248
        return null;
249
    }
250
    
251
    /**
252
     * Adds a header with the given $name and $value.
253
     * 
254
     * Creates a new \ZBateson\MailMimeParser\Header\AbstractHeader object and
255
     * registers it as a header.
256
     * 
257
     * @param string $name
258
     * @param string $value
259
     */
260 77
    public function setRawHeader($name, $value)
261
    {
262 77
        $this->headers[strtolower($name)] = $this->headerFactory->newInstance($name, $value);
263 77
    }
264
    
265
    /**
266
     * Removes the header with the given name
267
     * 
268
     * @param string $name
269
     */
270 5
    public function removeHeader($name)
271
    {
272 5
        unset($this->headers[strtolower($name)]);
273 5
    }
274
    
275
    /**
276
     * Returns the AbstractHeader object for the header with the given $name
277
     * 
278
     * Note that mime headers aren't case sensitive.
279
     * 
280
     * @param string $name
281
     * @return \ZBateson\MailMimeParser\Header\Header
282
     */
283 79
    public function getHeader($name)
284
    {
285 79
        if (isset($this->headers[strtolower($name)])) {
286 77
            return $this->headers[strtolower($name)];
287
        }
288 75
        return null;
289
    }
290
    
291
    /**
292
     * Returns the string value for the header with the given $name.
293
     * 
294
     * Note that mime headers aren't case sensitive.
295
     * 
296
     * @param string $name
297
     * @param string $defaultValue
298
     * @return string
299
     */
300 76
    public function getHeaderValue($name, $defaultValue = null)
301
    {
302 76
        $header = $this->getHeader($name);
303 76
        if (!empty($header)) {
304 75
            return $header->getValue();
305
        }
306 74
        return $defaultValue;
307
    }
308
    
309
    /**
310
     * Returns the full array of headers for this part.
311
     * 
312
     * @return \ZBateson\MailMimeParser\Header\AbstractHeader[]
313
     */
314 1
    public function getHeaders()
315
    {
316 1
        return $this->headers;
317
    }
318
    
319
    /**
320
     * Returns a parameter of the header $header, given the parameter named
321
     * $param.
322
     * 
323
     * Only headers of type
324
     * \ZBateson\MailMimeParser\Header\ParameterHeader have parameters.
325
     * Content-Type and Content-Disposition are examples of headers with
326
     * parameters. "Charset" is a common parameter of Content-Type.
327
     * 
328
     * @param string $header
329
     * @param string $param
330
     * @param string $defaultValue
331
     * @return string
332
     */
333 76
    public function getHeaderParameter($header, $param, $defaultValue = null)
334
    {
335 76
        $obj = $this->getHeader($header);
336 76
        if ($obj && $obj instanceof ParameterHeader) {
337 75
            return $obj->getValueFor($param, $defaultValue);
338
        }
339 5
        return $defaultValue;
340
    }
341
    
342
    /**
343
     * Sets the parent part.
344
     * 
345
     * @param \ZBateson\MailMimeParser\MimePart $part
346
     */
347 73
    public function setParent(MimePart $part)
348
    {
349 73
        $this->parent = $part;
350 73
    }
351
    
352
    /**
353
     * Returns this part's parent.
354
     * 
355
     * @return \ZBateson\MailMimeParser\MimePart
356
     */
357 73
    public function getParent()
358
    {
359 73
        return $this->parent;
360
    }
361
    
362
    /**
363
     * Sets up a mailmimeparser-encode stream filter on the passed stream
364
     * resource handle if applicable and returns a reference to the filter.
365
     * 
366
     * @param resource $handle
367
     * @return resource a reference to the appended stream filter or null
368
     */
369 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...
370
    {
371 71
        $contentType = strtolower($this->getHeaderValue('Content-Type', 'text/plain'));
372 71
        if (strpos($contentType, 'text/') === 0) {
373 69
            return stream_filter_append(
374 69
                $this->handle,
375 69
                'mailmimeparser-encode',
376 69
                STREAM_FILTER_READ,
377
                [
378 69
                    'charset' => 'UTF-8',
379 69
                    'to' => $this->getHeaderParameter('Content-Type', 'charset', 'ISO-8859-1')
380 69
                ]
381 69
            );
382
        }
383 41
        return null;
384
    }
385
    
386
    /**
387
     * Appends a stream filter the passed resource handle based on the type of
388
     * encoding for the current mime part.
389
     * 
390
     * Unfortunately PHP seems to error out allocating memory for
391
     * stream_filter_make_writable in Base64EncodeStreamFilter using
392
     * STREAM_FILTER_WRITE, and HHVM doesn't seem to remove the filter properly
393
     * for STREAM_FILTER_READ, so the function appends a read filter on
394
     * $fromHandle if running through 'php', and a write filter on $toHandle if
395
     * using HHVM.
396
     * 
397
     * @param resource $fromHandle
398
     * @param resource $toHandle
399
     * @param \ZBateson\MailMimeParser\Stream\StreamLeftover $leftovers
400
     * @return resource the stream filter
401
     */
402 71
    private function setTransferEncodingFilterOnStream($fromHandle, $toHandle, StreamLeftover $leftovers)
403
    {
404 71
        $encoding = strtolower($this->getHeaderValue('Content-Transfer-Encoding'));
405
        $params = [
406 71
            'line-length' => 76,
407 71
            'line-break-chars' => "\r\n",
408 71
            'leftovers' => $leftovers,
409 71
            'filename' => $this->getHeaderParameter(
410 71
                'Content-Type',
411 71
                'name',
412
                'null'
413 71
            )
414 71
        ];
415
        $typeToEncoding = [
416 71
            'quoted-printable' => 'convert.quoted-printable-encode',
417 71
            'base64' => 'convert.base64-encode',
418 71
            'x-uuencode' => 'mailmimeparser-uuencode',
419 71
        ];
420 71
        if (isset($typeToEncoding[$encoding])) {
421 61
            if (defined('HHVM_VERSION')) {
422
                return stream_filter_append(
423
                    $toHandle,
424
                    $typeToEncoding[$encoding],
425
                    STREAM_FILTER_WRITE,
426
                    $params
427
                );
428
            } else {
429 61
                return stream_filter_append(
430 61
                    $fromHandle,
431 61
                    $typeToEncoding[$encoding],
432 61
                    STREAM_FILTER_READ,
433
                    $params
434 61
                );
435
            }
436
        }
437 40
        return null;
438
    }
439
    
440
    /**
441
     * Returns true if the content-transfer-encoding header of the current part
442
     * is set to 'x-uuencode'.
443
     * 
444
     * @return bool
445
     */
446 71
    private function isUUEncoded()
447
    {
448 71
        $encoding = strtolower($this->getHeaderValue('Content-Transfer-Encoding'));
449 71
        return ($encoding === 'x-uuencode');
450
    }
451
    
452
    /**
453
     * Copies the content of the $fromHandle stream into the $toHandle stream,
454
     * maintaining the current read position in $fromHandle and writing
455
     * uuencode headers.
456
     * 
457
     * @param resource $fromHandle
458
     * @param resource $toHandle
459
     * @param bool $isUUEncoded
460
     */
461 71
    private function copyContentStream($fromHandle, $toHandle, $isUUEncoded)
0 ignored issues
show
Unused Code introduced by
The parameter $isUUEncoded 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...
462
    {
463 71
        $pos = ftell($fromHandle);
464 71
        rewind($fromHandle);
465
        // changed from stream_copy_to_stream because hhvm seems to stop before
466
        // end of file for some reason
467 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...
468 71
            fwrite($toHandle, $read);
469 71
        }
470 71
        fseek($fromHandle, $pos);
471 71
    }
472
    
473
    /**
474
     * Writes out headers and follows them with an empty line.
475
     * 
476
     * @param resource $handle
477
     */
478 71
    protected function writeHeadersTo($handle)
479
    {
480 71
        foreach ($this->headers as $header) {
481 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...
482 71
        }
483 71
        fwrite($handle, "\r\n");
484 71
    }
485
    
486
    /**
487
     * Writes out the content portion of the mime part based on the headers that
488
     * are set on the part, taking care of character/content-transfer encoding.
489
     * 
490
     * @param resource $handle
491
     */
492 71
    protected function writeContentTo($handle)
493
    {
494 71
        if (!empty($this->handle)) {
495 71
            $filter = $this->setCharsetStreamFilterOnStream($handle);
496 71
            $leftovers = new StreamLeftover();
497 71
            $encodingFilter = $this->setTransferEncodingFilterOnStream($this->handle, $handle, $leftovers);
498 71
            $this->copyContentStream($this->handle, $handle, $this->isUUEncoded());
499 71
            if ($encodingFilter !== null) {
500 61
                fflush($handle);
501 61
                stream_filter_remove($encodingFilter);
502 61
                if (!empty($leftovers->encodedValue)) {
503 39
                    fwrite($handle, $leftovers->encodedValue);
504 39
                }
505 61
            }
506 71
            if ($filter !== null) {
507 69
                stream_filter_remove($filter);
508 69
            }
509 71
        }
510 71
    }
511
    
512
    /**
513
     * Writes out the MimePart to the passed resource.
514
     * 
515
     * Takes care of character and content transfer encoding on the output based
516
     * on what headers are set.
517
     * 
518
     * @param resource $handle
519
     */
520 48
    protected function writeTo($handle)
521
    {
522 48
        $this->writeHeadersTo($handle);
523 48
        $this->writeContentTo($handle);
524 48
    }
525
}
526