Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
165 / 165
100.00% covered (success)
100.00%
35 / 35
CRAP
100.00% covered (success)
100.00%
1 / 1
Part
100.00% covered (success)
100.00%
165 / 165
100.00% covered (success)
100.00%
35 / 35
105
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 addHeader
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 addHeaders
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getHeaders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeadersAsArray
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasHeaders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeHeader
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setBody
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 addFile
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addPart
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addParts
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 getParts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasParts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSubType
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSubType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSubType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBoundary
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getBoundary
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasBoundary
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 generateBoundary
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasAttachment
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 hasAttachments
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getAttachments
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getContentType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getFilename
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
21
 getContents
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 renderHeaders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderParts
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
 renderBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
9
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2023 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Mime;
15
16/**
17 * MIME message part class
18 *
19 * @category   Pop
20 * @package    Pop\Mime
21 * @author     Nick Sagona, III <dev@nolainteractive.com>
22 * @copyright  Copyright (c) 2009-2023 NOLA Interactive, LLC. (http://www.nolainteractive.com)
23 * @license    http://www.popphp.org/license     New BSD License
24 * @version    1.1.0
25 */
26class Part
27{
28
29    /**
30     * Headers
31     * @var array
32     */
33    protected $headers = [];
34
35    /**
36     * Body
37     * @var Part\Body
38     */
39    protected $body = null;
40
41    /**
42     * Nested parts
43     * @var array
44     */
45    protected $parts = [];
46
47    /**
48     * Sub-type
49     * @var string
50     */
51    protected $subType = null;
52
53    /**
54     * Boundary
55     * @var string
56     */
57    protected $boundary = null;
58
59    /**
60     * Constructor
61     *
62     * Instantiate the mime part object
63     *
64     */
65    public function __construct()
66    {
67        $args = func_get_args();
68        foreach ($args as $arg) {
69            if (is_array($arg)) {
70                foreach ($arg as $a) {
71                    if ($a instanceof Part\Header) {
72                        $this->addHeader($a);
73                    } else if ($a instanceof Part) {
74                        $this->addPart($a);
75                    }
76                }
77            } else if ($arg instanceof Part\Header) {
78                $this->addHeader($arg);
79            } else if ($arg instanceof Part\Body) {
80                $this->setBody($arg);
81            }
82        }
83    }
84
85    /**
86     * Add a header
87     *
88     * @param  Part\Header|string $header
89     * @param  string             $value
90     * @return Part
91     */
92    public function addHeader($header, $value = null)
93    {
94        if ($header instanceof Part\Header) {
95            $this->headers[$header->getName()] = $header;
96        } else {
97            $this->headers[$header] = new Part\Header($header, $value);
98        }
99
100        return $this;
101    }
102
103    /**
104     * Add headers
105     *
106     * @param  array $headers
107     * @return Part
108     */
109    public function addHeaders(array $headers)
110    {
111        foreach ($headers as $header => $value) {
112            if ($value instanceof Part\Header) {
113                $this->addHeader($value);
114            } else {
115                $this->addHeader($header, $value);
116            }
117        }
118        return $this;
119    }
120
121    /**
122     * Get headers
123     *
124     * @return array
125     */
126    public function getHeaders()
127    {
128        return $this->headers;
129    }
130
131    /**
132     * Get headers as array
133     *
134     * @return array
135     */
136    public function getHeadersAsArray()
137    {
138        $headers = [];
139
140        foreach ($this->headers as $header) {
141            $headerValue = $header->getValue();
142            if ($header->hasParameters()) {
143                $headerValue .= '; ' . $header->getParametersAsString();
144            }
145            $headers[$header->getName()] = $headerValue;
146        }
147
148        return $headers;
149    }
150
151    /**
152     * Get headers
153     *
154     * @param  string $name
155     * @return Part\Header
156     */
157    public function getHeader($name)
158    {
159        return (isset($this->headers[$name])) ? $this->headers[$name] : null;
160    }
161
162    /**
163     * Has header
164     *
165     * @param  string $name
166     * @return boolean
167     */
168    public function hasHeader($name)
169    {
170        return (isset($this->headers[$name]));
171    }
172
173    /**
174     * Has headers
175     *
176     * @return boolean
177     */
178    public function hasHeaders()
179    {
180        return (count($this->headers) > 0);
181    }
182
183    /**
184     * Remove header
185     *
186     * @param  string $name
187     * @return Part
188     */
189    public function removeHeader($name)
190    {
191        if (isset($this->headers[$name])) {
192            unset($this->headers[$name]);
193        }
194        return $this;
195    }
196
197    /**
198     * Set body
199     *
200     * @param  Part\Body|string $body
201     * @return Part
202     */
203    public function setBody($body)
204    {
205        $this->body = ($body instanceof Part\Body) ? $body : new Part\Body($body);
206        return $this;
207    }
208
209    /**
210     * Add file as body
211     *
212     * @param  string      $file
213     * @param  string      $disposition
214     * @param  string      $encoding
215     * @param  int|boolean $split
216     * @return Part
217     */
218    public function addFile($file, $disposition = 'attachment', $encoding = Part\Body::BASE64, $split = true)
219    {
220        if (null !== $disposition) {
221            $header = new Part\Header('Content-Disposition', $disposition);
222            $header->addParameter('filename', basename($file));
223            $this->addHeader($header);
224        }
225        $this->body = new Part\Body();
226        $this->body->setContentFromFile($file, $encoding, $split);
227        return $this;
228    }
229
230    /**
231     * Get body
232     *
233     * @return Part\Body
234     */
235    public function getBody()
236    {
237        return $this->body;
238    }
239
240    /**
241     * Has body
242     *
243     * @return boolean
244     */
245    public function hasBody()
246    {
247        return (null !== $this->body);
248    }
249
250    /**
251     * Add a nested part
252     *
253     * @param  Part $part
254     * @return Part
255     */
256    public function addPart(Part $part)
257    {
258        $this->parts[] = $part;
259        return $this;
260    }
261
262    /**
263     * Add nested parts
264     *
265     * @param  array $parts
266     * @return Part
267     */
268    public function addParts(array $parts)
269    {
270        foreach ($parts as $part) {
271            if (is_array($part)) {
272                $file = false;
273                foreach ($part as $p) {
274                    if (($p->hasBody()) && ($p->getBody()->isFile())) {
275                        $file = true;
276                    }
277                }
278                $subType  = ($file) ? 'mixed' : 'alternative';
279                $subParts = new Part();
280                $subParts->setSubType($subType);
281                $subParts->addParts($part);
282                $this->addPart($subParts);
283            } else {
284                $this->addPart($part);
285            }
286        }
287        return $this;
288    }
289
290    /**
291     * Get nested parts
292     *
293     * @return array
294     */
295    public function getParts()
296    {
297        return $this->parts;
298    }
299
300    /**
301     * Has nested parts
302     *
303     * @return boolean
304     */
305    public function hasParts()
306    {
307        return (count($this->parts) > 0);
308    }
309
310    /**
311     * Set sub-type
312     *
313     * @param  string $subType
314     * @return Part
315     */
316    public function setSubType($subType)
317    {
318        $this->subType = $subType;
319        return $this;
320    }
321
322    /**
323     * Get sub-type
324     *
325     * @return string
326     */
327    public function getSubType()
328    {
329        return $this->subType;
330    }
331
332    /**
333     * Has sub-type
334     *
335     * @return boolean
336     */
337    public function hasSubType()
338    {
339        return (null !== $this->subType);
340    }
341
342    /**
343     * Set boundary
344     *
345     * @param  string $boundary
346     * @return Part
347     */
348    public function setBoundary($boundary)
349    {
350        $this->boundary = $boundary;
351        return $this;
352    }
353
354    /**
355     * Get boundary
356     *
357     * @return string
358     */
359    public function getBoundary()
360    {
361        return $this->boundary;
362    }
363
364    /**
365     * Has boundary
366     *
367     * @return boolean
368     */
369    public function hasBoundary()
370    {
371        return (null !== $this->boundary);
372    }
373
374    /**
375     * Generate boundary
376     *
377     * @return string
378     */
379    public function generateBoundary()
380    {
381        $this->setBoundary(sha1(uniqid()));
382        return $this->boundary;
383    }
384
385    /**
386     * Has attachment (check via a header)
387     *
388     * @return boolean
389     */
390    public function hasAttachment()
391    {
392        $result = false;
393
394        foreach ($this->headers as $header) {
395            if ($header->isAttachment()) {
396                $result = true;
397                break;
398            }
399        }
400
401        if ((!$result) && ($this->hasParts())) {
402            $result = $this->hasAttachments();
403        }
404
405        return $result;
406    }
407
408    /**
409     * Does message have attachments (check via parts)
410     *
411     * @return boolean
412     */
413    public function hasAttachments()
414    {
415        foreach ($this->parts as $part) {
416            if ($part->hasAttachment()) {
417                return true;
418            }
419        }
420        return false;
421    }
422
423    /**
424     * Get attachments
425     *
426     * @return array
427     */
428    public function getAttachments()
429    {
430        $attachments = [];
431
432        foreach ($this->parts as $part) {
433            if ($part->getBody()->isFile()) {
434                $attachments[] = $part;
435            }
436        }
437
438        return $attachments;
439    }
440
441    /**
442     * Get content-type
443     *
444     * @return string
445     */
446    public function getContentType()
447    {
448        $contentType = null;
449
450        if ($this->hasHeader('Content-Type')) {
451            $contentType = $this->getHeader('Content-Type')->getValue();
452        }
453
454        return $contentType;
455    }
456
457    /**
458     * Get attachment filename
459     *
460     * @return string
461     */
462    public function getFilename()
463    {
464        $filename = null;
465
466        if ($this->getBody()->isFile()) {
467            // Check Content-Disposition header (standard)
468            if ($this->hasHeader('Content-Disposition')) {
469                $header = $this->getHeader('Content-Disposition');
470                if ($header->hasParameter('filename')) {
471                    $filename = $header->getParameter('filename');
472                } else if ($header->hasParameter('name')) {
473                    $filename = $header->getParameter('name');
474                }
475            }
476
477            // Else, check Content-Type header (non-standard)
478            if (null === $filename) {
479                if ($this->hasHeader('Content-Type')) {
480                    $header = $this->getHeader('Content-Type');
481                    if ($header->hasParameter('filename')) {
482                        $filename = $header->getParameter('filename');
483                    } else if ($header->hasParameter('name')) {
484                        $filename = $header->getParameter('name');
485                    }
486                }
487            }
488
489            // Else, check Content-Description header (non-standard)
490            if (null === $filename) {
491                if ($this->hasHeader('Content-Description')) {
492                    $header = $this->getHeader('Content-Description');
493                    if ($header->hasParameter('filename')) {
494                        $filename = $header->getParameter('filename');
495                    } else if ($header->hasParameter('name')) {
496                        $filename = $header->getParameter('name');
497                    }
498                }
499            }
500        }
501
502        // Decode filename, if encoded
503        if ((null !== $filename) && (function_exists('imap_mime_header_decode')) &&
504            ((strpos($filename, 'UTF') !== false) || (strpos($filename, 'ISO') !== false) ||
505                (strpos($filename, '?') !== false) || (strpos($filename, '=') !== false))) {
506            $filenameAry = imap_mime_header_decode($filename);
507            if (isset($filenameAry[0]) && isset($filenameAry[0]->text)) {
508                $filename = $filenameAry[0]->text;
509            }
510        }
511
512        return $filename;
513    }
514
515    /**
516     * Get contents (decoded)
517     *
518     * @return mixed
519     */
520    public function getContents()
521    {
522        $content = $this->body->getContent();
523
524        if ($this->body->isEncoded()) {
525            if ($this->body->isBase64Encoding()) {
526                $content = base64_decode($content);
527            } else if ($this->body->isQuotedEncoding()) {
528                $content = quoted_printable_decode($content);
529            } else if ($this->body->isUrlEncoding()) {
530                $content = urldecode($content);
531            } else if ($this->body->isRawUrlEncoding()) {
532                $content = rawurldecode($content);
533            }
534        }
535
536        return $content;
537    }
538
539    /**
540     * Render the part headers
541     *
542     * @return string
543     */
544    public function renderHeaders()
545    {
546        return implode("\r\n", $this->headers) . "\r\n\r\n";
547    }
548
549    /**
550     * Render the parts
551     *
552     * @param  boolean $preamble
553     * @return string
554     */
555    public function renderParts($preamble = true)
556    {
557        $parts = '';
558
559        $boundary = (!$this->hasBoundary()) ? $this->generateBoundary() : $this->boundary;
560        if (!($this->hasHeader('Content-Type')) && ($this->hasSubType())) {
561            $this->addHeader(
562                new Part\Header('Content-Type', 'multipart/' . $this->subType . '; boundary=' . $boundary)
563            );
564        }
565        if ($this->hasHeaders()) {
566            $parts .= $this->renderHeaders();
567        }
568        if ($preamble) {
569            $parts .= "This is a multi-part message in MIME format.\r\n";
570        }
571        foreach ($this->parts as $part) {
572            $parts .= "--" . $boundary . "\r\n" . $part . "\r\n";
573        }
574
575        $parts .= "--" . $boundary . "--\r\n";
576
577        return $parts;
578    }
579
580    /**
581     * Render the part body
582     *
583     * @return string
584     */
585    public function renderBody()
586    {
587        return $this->body->render();
588    }
589
590    /**
591     * Render the part
592     *
593     * @param  boolean $preamble
594     * @return string
595     */
596    public function render($preamble = true)
597    {
598        $messagePart = '';
599
600        if ($this->hasParts()) {
601            $messagePart .= $this->renderParts($preamble);
602        } else if ($this->hasBody()) {
603            if (($this->body->isFile()) && (!$this->hasHeader('Content-Transfer-Encoding'))) {
604                $encoding = ($this->body->isBase64Encoding()) ? 'base64' : 'binary';
605                $this->addHeader(
606                    new Part\Header('Content-Transfer-Encoding', $encoding)
607                );
608            } else if ((!$this->hasHeader('Content-Transfer-Encoding')) && $this->body->isQuotedEncoding()) {
609                $this->addHeader(
610                    new Part\Header('Content-Transfer-Encoding', 'quoted-printable')
611                );
612            }
613            if ($this->hasHeaders()) {
614                $messagePart .= $this->renderHeaders();
615            }
616            $messagePart .= $this->renderBody();
617        }
618
619        return $messagePart;
620    }
621
622    /**
623     * Render the part
624     *
625     * @return string
626     */
627    public function __toString()
628    {
629        return $this->render();
630    }
631
632}