Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.54% covered (success)
97.54%
238 / 244
96.00% covered (success)
96.00%
48 / 50
CRAP
0.00% covered (danger)
0.00%
0 / 1
Message
97.54% covered (success)
97.54%
238 / 244
96.00% covered (success)
96.00%
48 / 50
131
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 load
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 setSubject
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTo
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setCc
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setBcc
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setFrom
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setReplyTo
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setSender
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setReturnPath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setBody
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addText
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 addHtml
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 attachFile
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 attachFileFromStream
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 addPart
100.00% covered (success)
100.00%
3 / 3
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
 getSubject
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBcc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFrom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReplyTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSender
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReturnPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasCc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasBcc
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasFrom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasReplyTo
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSender
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasReturnPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAttachments
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getBody
83.33% covered (success)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
6.17
 getBoundary
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPart
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBoundary
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 generateBoundary
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 renderAsLines
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 toByteStream
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 parseFromFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parse
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
1 / 1
22
 decodeText
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 parseAddresses
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
17
 parseNameAndEmail
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 validateContentType
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
8
 __clone
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
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 */
14namespace Pop\Mail;
15
16use Pop\Mail\Message\Part;
17use Pop\Mail\Message\Simple;
18use Pop\Mail\Message\Text;
19use Pop\Mail\Message\Html;
20use Pop\Mail\Message\Attachment;
21use Pop\Mail\Message\AbstractMessage;
22use Pop\Mail\Message\AbstractPart;
23use Pop\Mail\Message\PartInterface;
24use Pop\Mime\Part\Header\Value;
25
26/**
27 * Message class
28 *
29 * @category   Pop
30 * @package    Pop\Mail
31 * @author     Nick Sagona, III <dev@nolainteractive.com>
32 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
33 * @license    http://www.popphp.org/license     New BSD License
34 * @version    4.0.0
35 */
36class Message extends AbstractMessage
37{
38
39    /**
40     * Message newline constant
41     * @var string
42     */
43    const CRLF = "\r\n";
44
45    /**
46     * Message parts
47     * @var array
48     */
49    protected array $parts = [];
50
51    /**
52     * Message addresses
53     * @var array
54     */
55    protected array $addresses = [
56        'To'          => [],
57        'CC'          => [],
58        'BCC'         => [],
59        'From'        => [],
60        'Reply-To'    => [],
61        'Sender'      => [],
62        'Return-Path' => []
63    ];
64
65    /**
66     * Message boundary
67     * @var ?string
68     */
69    protected ?string $boundary = null;
70
71    /**
72     * Constructor
73     *
74     * Instantiate the message object
75     *
76     * @param ?string $subject
77     */
78    public function __construct(?string $subject = null)
79    {
80        parent::__construct();
81
82        if ($subject !== null) {
83            $this->setSubject($subject);
84        }
85    }
86
87    /**
88     * Load a message from a string source or file on disk
89     *
90     * @param  string $message
91     * @throws Exception
92     * @return Message
93     */
94    public static function load(string $message): Message
95    {
96        if (is_string($message) && (str_contains($message, 'Subject:'))) {
97            return self::parse($message);
98        } else if (file_exists($message)) {
99            return self::parseFromFile($message);
100        } else {
101            throw new Exception('Error: Unable to parse message content');
102        }
103    }
104
105    /**
106     * Set Subject
107     *
108     * @param  string $subject
109     * @return AbstractMessage
110     */
111    public function setSubject(string $subject): AbstractMessage
112    {
113        return $this->addHeader('Subject', $subject);
114    }
115
116    /**
117     * Set To
118     *
119     * @param  mixed $to
120     * @return AbstractMessage
121     */
122    public function setTo(mixed $to): AbstractMessage
123    {
124        if ($to instanceof Value) {
125            $to = (string)$to;
126        }
127        $this->addresses['To'] = $this->parseAddresses($to, true);
128        return $this->addHeader('To', $this->parseAddresses($to));
129    }
130
131    /**
132     * Set CC
133     *
134     * @param  mixed $cc
135     * @return AbstractMessage
136     */
137    public function setCc(mixed $cc): AbstractMessage
138    {
139        if ($cc instanceof Value) {
140            $cc = (string)$cc;
141        }
142        $this->addresses['CC'] = $this->parseAddresses($cc, true);
143        return $this->addHeader('CC', $this->parseAddresses($cc));
144    }
145
146    /**
147     * Set BCC
148     *
149     * @param  mixed $bcc
150     * @return AbstractMessage
151     */
152    public function setBcc(mixed $bcc): AbstractMessage
153    {
154        if ($bcc instanceof Value) {
155            $bcc = (string)$bcc;
156        }
157        $this->addresses['BCC'] = $this->parseAddresses($bcc, true);
158        return $this->addHeader('BCC', $this->parseAddresses($bcc));
159    }
160
161    /**
162     * Set From
163     *
164     * @param  mixed $from
165     * @return AbstractMessage
166     */
167    public function setFrom(mixed $from): AbstractMessage
168    {
169        if ($from instanceof Value) {
170            $from = (string)$from;
171        }
172        $this->addresses['From'] = $this->parseAddresses($from, true);
173        return $this->addHeader('From', $this->parseAddresses($from));
174    }
175
176    /**
177     * Set Reply-To
178     *
179     * @param  mixed $replyTo
180     * @return AbstractMessage
181     */
182    public function setReplyTo(mixed $replyTo): AbstractMessage
183    {
184        if ($replyTo instanceof Value) {
185            $replyTo = (string)$replyTo;
186        }
187        $this->addresses['Reply-To'] = $this->parseAddresses($replyTo, true);
188        return $this->addHeader('Reply-To', $this->parseAddresses($replyTo));
189    }
190
191    /**
192     * Set Sender
193     *
194     * @param  mixed $sender
195     * @return AbstractMessage
196     */
197    public function setSender(mixed $sender): AbstractMessage
198    {
199        if ($sender instanceof Value) {
200            $sender = (string)$sender;
201        }
202        $this->addresses['Sender'] = $this->parseAddresses($sender, true);
203        return $this->addHeader('Sender', $this->parseAddresses($sender));
204    }
205
206    /**
207     * Set Return-Path
208     *
209     * @param  mixed $returnPath
210     * @return AbstractMessage
211     */
212    public function setReturnPath(mixed $returnPath): AbstractMessage
213    {
214        if ($returnPath instanceof Value) {
215            $returnPath = (string)$returnPath;
216        }
217        $this->addresses['Return-Path'] = $this->parseAddresses($returnPath, true);
218        return $this->addHeader('Return-Path', $this->parseAddresses($returnPath));
219    }
220
221    /**
222     * Set body
223     *
224     * @param  mixed $body
225     * @return Message
226     */
227    public function setBody(mixed $body): Message
228    {
229        if (!($body instanceof PartInterface)) {
230            $body = new Text($body);
231        }
232        return $this->addPart($body);
233    }
234
235    /**
236     * Add text message part
237     *
238     * @param  mixed $text
239     * @return Message
240     */
241    public function addText(mixed $text): Message
242    {
243        if (!($text instanceof Text) && is_string($text)) {
244            $text = new Text($text);
245        }
246        return $this->addPart($text);
247    }
248
249    /**
250     * Add HTML message part
251     *
252     * @param  mixed $html
253     * @return Message
254     */
255    public function addHtml(mixed $html): Message
256    {
257        if (!($html instanceof Html) && is_string($html)) {
258            $html = new Html($html);
259        }
260        return $this->addPart($html);
261    }
262
263    /**
264     * Attach file message part
265     *
266     * @param  string $file
267     * @param  string $encoding
268     * @return Message
269     */
270    public function attachFile(string $file, string $encoding = AbstractPart::BASE64): Message
271    {
272        if (!($file instanceof Attachment)) {
273            $options = [
274                'encoding' => $encoding,
275                'chunk'    => true
276            ];
277            $file = Attachment::createFromFile($file, $options);
278        }
279        return $this->addPart($file);
280    }
281
282    /**
283     * Attach file message part from stream
284     *
285     * @param  string $stream
286     * @param  string $basename
287     * @param  string $encoding
288     * @return Message
289     */
290    public function attachFileFromStream(
291        string $stream, string $basename = 'file.tmp', string $encoding = AbstractPart::BASE64
292    ): Message
293    {
294        $options = [
295            'basename' => $basename,
296            'encoding' => $encoding,
297            'chunk'    => true
298        ];
299        $file = Attachment::createFromStream($stream, $options);
300        return $this->addPart($file);
301    }
302
303    /**
304     * Add message part
305     *
306     * @param  PartInterface $part
307     * @return Message
308     */
309    public function addPart(PartInterface $part): Message
310    {
311        $this->parts[] = $part;
312        $this->validateContentType();
313        return $this;
314    }
315
316    /**
317     * Remove header
318     *
319     * @param  string $header
320     * @return Message
321     */
322    public function removeHeader(string $header): Message
323    {
324        if (isset($this->headers[$header])) {
325            unset($this->headers[$header]);
326        }
327        return $this;
328    }
329
330    /**
331     * Get subject
332     *
333     * @return ?string
334     */
335    public function getSubject(): ?string
336    {
337        return $this->getHeader('Subject');
338    }
339
340    /**
341     * Get To
342     *
343     * @return array
344     */
345    public function getTo(): array
346    {
347        return $this->addresses['To'];
348    }
349
350    /**
351     * Get CC
352     *
353     * @return array
354     */
355    public function getCc(): array
356    {
357        return $this->addresses['CC'];
358    }
359
360    /**
361     * Get BCC
362     *
363     * @return array
364     */
365    public function getBcc(): array
366    {
367        return $this->addresses['BCC'];
368    }
369
370    /**
371     * Get From
372     *
373     * @return array
374     */
375    public function getFrom(): array
376    {
377        return $this->addresses['From'];
378    }
379
380    /**
381     * Get Reply-To
382     *
383     * @return array
384     */
385    public function getReplyTo(): array
386    {
387        return $this->addresses['Reply-To'];
388    }
389
390    /**
391     * Get Sender
392     *
393     * @return array
394     */
395    public function getSender(): array
396    {
397        return $this->addresses['Sender'];
398    }
399
400    /**
401     * Get Return-Path
402     *
403     * @return array
404     */
405    public function getReturnPath(): array
406    {
407        return $this->addresses['Return-Path'];
408    }
409
410    /**
411     * Has To
412     *
413     * @return bool
414     */
415    public function hasTo(): bool
416    {
417        return !empty($this->addresses['To']);
418    }
419
420    /**
421     * Has CC
422     *
423     * @return bool
424     */
425    public function hasCc(): bool
426    {
427        return !empty($this->addresses['CC']);
428    }
429
430    /**
431     * Has BCC
432     *
433     * @return bool
434     */
435    public function hasBcc(): bool
436    {
437        return !empty($this->addresses['BCC']);
438    }
439
440    /**
441     * Has From
442     *
443     * @return bool
444     */
445    public function hasFrom(): bool
446    {
447        return !empty($this->addresses['From']);
448    }
449
450    /**
451     * Has Reply-To
452     *
453     * @return bool
454     */
455    public function hasReplyTo(): bool
456    {
457        return !empty($this->addresses['Reply-To']);
458    }
459
460    /**
461     * Has Sender
462     *
463     * @return bool
464     */
465    public function hasSender(): bool
466    {
467        return !empty($this->addresses['Sender']);
468    }
469
470    /**
471     * Has Return-Path
472     *
473     * @return bool
474     */
475    public function hasReturnPath(): bool
476    {
477        return !empty($this->addresses['Return-Path']);
478    }
479
480    /**
481     * Has attachments
482     *
483     * @return bool
484     */
485    public function hasAttachments(): bool
486    {
487        foreach ($this->parts as $part) {
488            if ($part instanceof Attachment) {
489                return true;
490            }
491        }
492
493        return false;
494    }
495
496    /**
497     * Get message body
498     *
499     * @return ?string
500     */
501    public function getBody(): ?string
502    {
503        $body  = null;
504
505        if (count($this->parts) > 1) {
506            foreach ($this->parts as $part) {
507                $body .= '--' . $this->getBoundary() . self::CRLF . $part . self::CRLF;
508            }
509            $body .= '--' . $this->getBoundary() . '--';
510        } else if (count($this->parts) == 1) {
511            $part  = $this->parts[0];
512            if (($part instanceof Text) || ($part instanceof Html)) {
513                $body .= $part->getBody() . self::CRLF;
514            } else {
515                $body .= '--' . $this->getBoundary() . self::CRLF . $part . self::CRLF;
516                $body .= '--' . $this->getBoundary() . '--';
517            }
518        }
519
520        return $body;
521    }
522
523    /**
524     * Get message MIME boundary
525     *
526     * @return string
527     */
528    public function getBoundary(): string
529    {
530        if ($this->boundary === null) {
531            $this->generateBoundary();
532        }
533        return $this->boundary;
534    }
535
536    /**
537     * Get message part
538     *
539     * @param  int $i
540     * @return PartInterface|null
541     */
542    public function getPart(int $i): PartInterface|null
543    {
544        return $this->parts[$i] ?? null;
545    }
546
547    /**
548     * Get message parts
549     *
550     * @return array
551     */
552    public function getParts(): array
553    {
554        return $this->parts;
555    }
556
557    /**
558     * Set message MIME boundary
559     *
560     * @param  string $boundary
561     * @return Message
562     */
563    public function setBoundary(string $boundary): Message
564    {
565        $this->boundary = $boundary;
566        $this->addHeader('MIME-Version', '1.0');
567        return $this;
568    }
569
570    /**
571     * Generate message MIME boundary
572     *
573     * @return Message
574     */
575    public function generateBoundary(): Message
576    {
577        return $this->setBoundary(sha1(time()));
578    }
579
580    /**
581     * Render message
582     *
583     * @param  array $omitHeaders
584     * @return string
585     */
586    public function render(array $omitHeaders = []): string
587    {
588        return $this->getHeadersAsString($omitHeaders) . self::CRLF . $this->getBody();
589    }
590
591    /**
592     * Save message to file on disk
593     *
594     * @param  string $to
595     * @param  array  $omitHeaders
596     * @return void
597     */
598    public function save(string $to, array $omitHeaders = []): void
599    {
600        file_put_contents($to, $this->render($omitHeaders));
601    }
602
603    /**
604     * Render as an array of lines
605     *
606     * @param  array $omitHeaders
607     * @return array
608     */
609    public function renderAsLines(array $omitHeaders = []): array
610    {
611        $lines   = [];
612        $headers = explode(Message::CRLF, $this->getHeadersAsString($omitHeaders) . Message::CRLF);
613        $body    = explode(Message::CRLF, $this->getBody());
614
615        foreach ($headers as $header) {
616            $lines[] = trim($header);
617        }
618
619        foreach ($body as $line) {
620            $lines[] = trim($line);
621        }
622
623        return $lines;
624    }
625
626    /**
627     * Write this entire entity to a buffer
628     *
629     * @param  Transport\Smtp\Stream\BufferInterface $is
630     * @param  array $omitHeaders
631     * @return void
632     */
633    public function toByteStream(Transport\Smtp\Stream\BufferInterface $is, array $omitHeaders = []): void
634    {
635        $lines = $this->renderAsLines($omitHeaders);
636        foreach ($lines as $line) {
637            $is->write($line . self::CRLF);
638        }
639        $is->commit();
640    }
641
642    /**
643     * Parse message from file
644     *
645     * @param  string $file
646     * @throws Exception
647     * @return Message
648     */
649    public static function parseFromFile(string $file): Message
650    {
651        if (!file_exists($file)) {
652            throw new Exception("Error: The file '" . $file . "' does not exist.");
653        }
654
655        return self::parse(file_get_contents($file));
656    }
657
658    /**
659     * Parse message from string
660     *
661     * @param  string $stream
662     * @throws Exception
663     * @return Message
664     */
665    public static function parse(string $stream): Message
666    {
667        $parsedMessage = \Pop\Mime\Message::parseMessage($stream);
668        $message       = new self();
669
670        if ($parsedMessage->hasHeaders()) {
671            $headers = $parsedMessage->getHeaders();
672            foreach ($headers as $header => $value) {
673                if (count($value->getValues()) == 1) {
674                    switch (strtolower($header)) {
675                        case 'subject':
676                            $message->setSubject($value->getValue(0));
677                            break;
678                        case 'to':
679                            $message->setTo($value->getValue(0));
680                            break;
681                        case 'cc':
682                            $message->setCc($value->getValue(0));
683                            break;
684                        case 'bcc':
685                            $message->setBcc($value->getValue(0));
686                            break;
687                        case 'from':
688                            $message->setFrom($value->getValue(0));
689                            break;
690                        case 'reply-to':
691                            $message->setReplyTo($value->getValue(0));
692                            break;
693                        case 'sender':
694                            $message->setSender($value->getValue(0));
695                            break;
696                        case 'return-path':
697                            $message->setReturnPath($value->getValue(0));
698                            break;
699                        default:
700                            $message->addHeader($header, $value->getValue(0));
701                    }
702                }
703            }
704        }
705
706        if (empty($message->getSubject())) {
707            throw new Exception('Error: There is no subject in the message contents');
708        }
709
710        if (empty($message->getTo())) {
711            throw new Exception('Error: There is no to address in the message contents');
712        }
713
714        if ($parsedMessage->hasParts()) {
715            $parts = Part::parseParts($parsedMessage->getParts());
716
717            foreach ($parts as $part) {
718                if ($part->attachment) {
719                    $options = [
720                        'contentType' => $part->type,
721                        'basename'    => $part->basename,
722                        'encoding'    => AbstractPart::BASE64,
723                        'chunk'       => true
724                    ];
725                    $message->addPart(Attachment::createFromStream($part->content, $options));
726                } else if (!empty($part->type) && (stripos($part->type, 'html') !== false)) {
727                    $message->addPart(new Html($part->content));
728                } else if (!empty($part->type) && (stripos($part->type, 'text') !== false)) {
729                    $message->addPart(new Text($part->content));
730                } else {
731                    $message->addPart(new Simple($part->content));
732                }
733            }
734        }
735
736        return $message;
737    }
738
739    /**
740     * Decode text
741     *
742     * @param  string $text
743     * @return string
744     */
745    public static function decodeText(string $text): string
746    {
747        $decodedValues = imap_mime_header_decode($text);
748        $decoded       = '';
749
750        foreach ($decodedValues as $string) {
751            $decoded .= $string->text;
752        }
753
754        return $decoded;
755    }
756
757    /**
758     * Parse addresses
759     *
760     * @param  mixed $addresses
761     * @param  bool  $asArray
762     * @return string|array
763     */
764    public function parseAddresses(mixed $addresses, bool $asArray = false): string|array
765    {
766        $formatted = [];
767        $emails    = [];
768
769        if (is_array($addresses)) {
770            foreach ($addresses as $key => $value) {
771                if (($value instanceof \stdClass) && isset($value->mailbox) && isset($value->host)) {
772                    $formatted[]    = $value->mailbox . '@' . $value->host;
773                    $emails[$value->mailbox . '@' . $value->host] = null;
774                } else {
775                    // $key is email
776                    if (str_contains($key, '@')) {
777                        if (!empty($value) && !is_numeric($value)) {
778                            $formatted[]  = '"' . $value . '" <' . $key . '>';
779                            $emails[$key] = $value;
780                        } else {
781                            $formatted[]  = $key;
782                            $emails[$key] = null;
783                        }
784                    // $value is email
785                    } else if (str_contains($value, '@')) {
786                        if (!empty($key) && !is_numeric($key)) {
787                            $formatted[]    = '"' . $key . '" <' . $value . '>';
788                            $emails[$value] = $key;
789                        } else {
790                            $formatted[]    = $value;
791                            $emails[$value] = null;
792                        }
793                    }
794                }
795            }
796        } else if (is_string($addresses) && (str_contains($addresses, '@'))) {
797            $formatted = [$addresses];
798            if (str_contains($addresses, ',')) {
799                $addresses = explode(',', $addresses);
800                foreach ($addresses as $address) {
801                    $address = $this->parseNameAndEmail(trim($address));
802                    $emails[$address['email']] = $address['name'];
803                }
804            } else {
805                $address = $this->parseNameAndEmail(trim($addresses));
806                $emails[$address['email']] = $address['name'];
807            }
808        }
809
810        return ($asArray) ? $emails : implode(', ', $formatted);
811    }
812
813    /**
814     * Parse a name and email from an address string
815     *
816     * @param  string $address
817     * @return array
818     */
819    public function parseNameAndEmail(string $address): array
820    {
821        $name  = null;
822        $email = null;
823
824        if ((str_contains($address, '<')) && (str_contains($address, '>'))) {
825            $name  = trim(substr($address, 0, strpos($address, '<')));
826            $email = substr($address, (strpos($address, '<') + 1));
827            $email = trim(substr($email, 0, -1));
828        } else if (str_contains($address, '@')) {
829            $email = trim($address);
830        }
831
832        return ['name' => $name, 'email' => $email];
833    }
834
835    /**
836     * Validate content type based on message parts added to the message
837     *
838     * @return void
839     */
840    protected function validateContentType(): void
841    {
842        $hasText = false;
843        $hasHtml = false;
844        $hasFile = false;
845
846        foreach ($this->parts as $part) {
847            if ($part instanceof Text) {
848                $hasText = true;
849            }
850            if ($part instanceof Html) {
851                $hasHtml = true;
852            }
853            if ($part instanceof Attachment) {
854                $hasFile = true;
855            }
856        }
857
858        if (($hasText) && ($hasHtml)) {
859            $this->setContentType(
860                'multipart/alternative; boundary="' . $this->getBoundary() . '"' . self::CRLF .
861                'This is a multi-part message in MIME format.'
862            );
863            $this->setCharSet('');
864        } else if ($hasFile) {
865            $this->setContentType(
866                'multipart/mixed; boundary="' . $this->getBoundary() . '"' . self::CRLF .
867                'This is a multi-part message in MIME format.'
868            );
869            $this->setCharSet('');
870        }
871    }
872
873    /**
874     * Perform a "deep" clone of a message object
875     *
876     * @return void
877     */
878    public function __clone(): void
879    {
880        foreach($this as $key => $val) {
881            if (is_object($val) || (is_array($val))) {
882                $this->{$key} = unserialize(serialize($val));
883            }
884        }
885    }
886
887}