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