Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.96% covered (success)
97.96%
144 / 147
85.71% covered (success)
85.71%
6 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Message
97.96% covered (success)
97.96%
144 / 147
85.71% covered (success)
85.71%
6 / 7
61
0.00% covered (danger)
0.00%
0 / 1
 setMessageId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parseMessage
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 parseForm
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
10
 createForm
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
16
 parseHeaders
84.21% covered (success)
84.21%
16 / 19
0.00% covered (danger)
0.00%
0 / 1
7.19
 parseBody
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 parsePart
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
16
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Mime;
16
17use Pop\Mime\Part\Header;
18use Pop\Mime\Part\Body;
19
20/**
21 * MIME message class
22 *
23 * @category   Pop
24 * @package    Pop\Mime
25 * @author     Nick Sagona, III <dev@noladev.com>
26 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    3.0.0
29 */
30class Message extends Part
31{
32
33    /**
34     * Set the Message-ID header
35     *
36     * @param  ?string $id
37     * @param  ?string $domain
38     * @return Message
39     */
40    public function setMessageId(?string $id = null, ?string $domain = null): Message
41    {
42        $this->addHeader('Message-ID', $id ?? $this->generateId($domain));
43        return $this;
44    }
45
46    /**
47     * Parse message
48     *
49     * @param  string $messageString
50     * @return Message
51     */
52    public static function parseMessage(string $messageString): Message
53    {
54        $splitPos     = strpos($messageString, "\r\n\r\n");
55        $headerString = substr($messageString, 0, $splitPos);
56        $bodyString   = substr($messageString, $splitPos + 4);
57
58        $headers  = self::parseHeaders($headerString);
59        $boundary = null;
60        $parts    = [];
61
62        foreach ($headers as $header) {
63            foreach ($header->getValues() as $headerValue) {
64                if ($headerValue->hasParameter('boundary')) {
65                    $boundary = $headerValue->getParameter('boundary');
66                    break;
67                }
68            }
69        }
70
71        $partStrings = self::parseBody($bodyString, $boundary);
72
73        foreach ($partStrings as $partString) {
74            $parts[] = self::parsePart($partString);
75        }
76
77        $message = new self();
78
79        if (!empty($headers)) {
80            $message->addHeaders($headers);
81        }
82        if (!empty($parts)) {
83            $message->addParts($parts);
84        }
85
86        return $message;
87    }
88
89    /**
90     * Parse form data
91     *
92     * @param  string $formString
93     * @return array
94     */
95    public static function parseForm(string $formString): array
96    {
97        $form     = self::parseMessage($formString);
98        $formData = [];
99
100        foreach ($form->getParts() as $part) {
101            if (($part->hasHeader('Content-Disposition')) && (count($part->getHeader('Content-Disposition')->getValues()) == 1)) {
102                $disposition = $part->getHeader('Content-Disposition');
103                if (($disposition->hasValue('form-data')) && ($disposition->getValue(0)->hasParameter('name'))) {
104                    $name     = $disposition->getValue(0)->getParameter('name');
105                    $contents = $part->getContents();
106                    $filename = ($disposition->getValue(0)->hasParameter('filename')) ? $disposition->getValue(0)->getParameter('filename') : null;
107
108                    if (str_ends_with($name, '[]')) {
109                        $name = substr($name, 0, -2);
110                        if (!isset($formData[$name])) {
111                            $formData[$name] = [];
112                        }
113                        $formData[$name][] = $contents;
114                    } else {
115                        if ($filename !== null) {
116                            $formData[$name] = [
117                                'filename' => $filename,
118                                'contents' => $contents
119                            ];
120                        } else {
121                            $formData[$name] = $contents;
122                        }
123                    }
124                }
125            }
126        }
127
128        return $formData;
129    }
130
131    /**
132     * Create multipart form object
133     *
134     * @param  array $fields
135     * @return Message
136     */
137    public static function createForm(array $fields = []): Message
138    {
139        $message = new self();
140        $header  = new Header('Content-Type', new Header\Value('multipart/form-data', null, ['boundary' => $message->generateBoundary()]));
141        $message->addHeader($header);
142
143        if (!empty($fields)) {
144            foreach ($fields as $name => $value) {
145                if (is_array($value)) {
146                    // Is file
147                    if (isset($value['filename'])) {
148                        $parameters   = ['name' => $name];
149                        $contentType  = null;
150                        $fileContents = null;
151
152                        foreach ($value as $key => $val) {
153                            $key = strtolower($key);
154                            if ($key == 'filename') {
155                                $parameters['filename'] = basename($val);
156                            }
157                            if (($key == 'content-type') || ($key == 'contenttype') ||
158                                ($key == 'mime-type') || ($key == 'mimetype') || ($key == 'mime')) {
159                                $contentType = $val;
160                            }
161                        }
162
163                        if (isset($value['contents'])) {
164                            $fileContents = $value['contents'];
165                        } else if (file_exists($value['filename'])) {
166                            $fileContents = file_get_contents($value['filename']);
167                        }
168
169                        $fieldPart = new Part(new Header('Content-Disposition', new Header\Value('form-data', null, $parameters)));
170                        if ($contentType !== null) {
171                            $fieldPart->addHeader('Content-Type', $contentType);
172                        }
173                        $fieldPart->setBody(new Body($fileContents));
174                        $message->addPart($fieldPart);
175                    } else {
176                        foreach ($value as $val) {
177                            $fieldPart = new Part(
178                                new Header('Content-Disposition', new Header\Value('form-data', null, ['name' => $name . '[]']))
179                            );
180                            $fieldPart->setBody(new Body($val, Body\Encoding::RAW_URL));
181                            $message->addPart($fieldPart);
182                        }
183                    }
184                } else {
185                    $fieldPart = new Part(new Header('Content-Disposition', new Header\Value('form-data', null, ['name' => $name])));
186                    $fieldPart->setBody(new Body($value, Body\Encoding::RAW_URL));
187                    $message->addPart($fieldPart);
188                }
189            }
190        }
191
192        return $message;
193    }
194
195    /**
196     * Parse message header string
197     *
198     * @param  string $headerString
199     * @return array
200     */
201    public static function parseHeaders(string $headerString): array
202    {
203        $headers  = [];
204        $unfolded = Header\Lexer::unfold($headerString);
205        $lines    = explode("\r\n", $unfolded);
206
207        $currentName  = null;
208        $currentLines = [];
209
210        foreach ($lines as $line) {
211            if ($line === '') {
212                continue;
213            }
214
215            $colonToken = Header\Lexer::findTopLevelDelimiter($line, ':');
216
217            if ($colonToken !== null) {
218                if ($currentName !== null) {
219                    $headers[] = Header::parse(implode("\r\n", $currentLines));
220                }
221                $currentName  = trim(substr($line, 0, $colonToken->start));
222                $currentLines = [$line];
223            } else if ($currentName !== null) {
224                $currentLines[] = $line;
225            }
226        }
227
228        if ($currentName !== null) {
229            $headers[] = Header::parse(implode("\r\n", $currentLines));
230        }
231
232        return $headers;
233    }
234
235    /**
236     * Parse message body string
237     *
238     * @param  string  $bodyString
239     * @param  ?string $boundary
240     * @return array
241     */
242    public static function parseBody(string $bodyString, ?string $boundary = null): array
243    {
244        $needle   = '--' . $boundary;
245        $splitPos = strpos($bodyString, $needle);
246
247        if ($splitPos !== false) {
248            $parts = explode($needle, $bodyString);
249            if ($splitPos > 0) {
250                unset($parts[0]);
251            }
252        } else {
253            $parts = [$bodyString];
254        }
255
256        return array_values(array_filter(array_map('trim', $parts), function ($value) {
257            return (!empty($value) && ($value != '--'));
258        }));
259    }
260
261    /**
262     * Parse message part string
263     *
264     * @param  string $partString
265     * @return Part|array
266     */
267    public static function parsePart(string $partString): Part|array
268    {
269        $headers = [];
270
271        $splitPos = strpos($partString, "\r\n\r\n");
272
273        if ($splitPos !== false) {
274            $headerString = substr($partString, 0, $splitPos);
275            $bodyString   = trim(substr($partString, $splitPos + 4));
276            $headers      = self::parseHeaders($headerString);
277        } else {
278            $bodyString   = trim($partString);
279        }
280
281        $part = new Part();
282
283        if (!empty($headers)) {
284            $part->addHeaders($headers);
285        }
286
287        $boundary = null;
288        foreach ($part->getHeaders() as $header) {
289            foreach ($header->getValues() as $headerValue) {
290                if ($headerValue->hasParameter('boundary')) {
291                    $boundary = $headerValue->getParameter('boundary');
292                    break;
293                }
294            }
295        }
296
297        if (!empty($bodyString)) {
298            if ($boundary !== null) {
299                $subPartStrings = self::parseBody($bodyString, $boundary);
300                $subParts       = [];
301
302                foreach ($subPartStrings as $subPartString) {
303                    $subParts[] = self::parsePart($subPartString);
304                }
305                return $subParts;
306            } else {
307                $encoding = null;
308                $isFile   = (($part->hasHeader('Content-Disposition')) &&
309                    ($part->getHeader('Content-Disposition')->isAttachment()));
310                $isForm   = (($part->hasHeader('Content-Disposition')) &&
311                    ($part->getHeader('Content-Disposition')->hasValue('form-data')));
312                if ($part->hasHeader('Content-Transfer-Encoding') && (count($part->getHeader('Content-Transfer-Encoding')->getValues()) == 1)) {
313                    $encoding = Body\Encoding::fromHeaderValue((string)$part->getHeader('Content-Transfer-Encoding')->getValue(0));
314                } else if ($isForm) {
315                    $encoding = Body\Encoding::RAW_URL;
316                }
317                $body = new Body($bodyString, $encoding);
318                if ($encoding !== null) {
319                    $body->setAsEncoded(true);
320                }
321                if ($isFile) {
322                    $body->setAsFile(true);
323                }
324                $part->setBody($body);
325            }
326        }
327
328        return $part;
329    }
330
331}