Mail_mimeDecode::_parseHeaderValue()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 15
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 27
rs 9.4555
1
<?php
2
// +-----------------------------------------------------------------------+
3
// | Copyright (c) 2002  Richard Heyes                                     |
4
// | All rights reserved.                                                  |
5
// |                                                                       |
6
// | Redistribution and use in source and binary forms, with or without    |
7
// | modification, are permitted provided that the following conditions    |
8
// | are met:                                                              |
9
// |                                                                       |
10
// | o Redistributions of source code must retain the above copyright      |
11
// |   notice, this list of conditions and the following disclaimer.       |
12
// | o Redistributions in binary form must reproduce the above copyright   |
13
// |   notice, this list of conditions and the following disclaimer in the |
14
// |   documentation and/or other materials provided with the distribution.|
15
// | o The names of the authors may not be used to endorse or promote      |
16
// |   products derived from this software without specific prior written  |
17
// |   permission.                                                         |
18
// |                                                                       |
19
// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
20
// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
21
// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
22
// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
23
// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
24
// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
25
// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
26
// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
27
// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
28
// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
29
// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
30
// |                                                                       |
31
// +-----------------------------------------------------------------------+
32
// | Author: Richard Heyes <[email protected]>                           |
33
// +-----------------------------------------------------------------------+
34
35
require_once XHELP_PEAR_PATH . '/PEAR.php';
36
37
/**
38
 *  +----------------------------- IMPORTANT ------------------------------+
39
 *  | Usage of this class compared to native php extensions such as        |
40
 *  | mailparse or imap, is slow and may be feature deficient. If available|
41
 *  | you are STRONGLY recommended to use the php extensions.              |
42
 *  +----------------------------------------------------------------------+
43
 *
44
 * Mime Decoding class
45
 *
46
 * This class will parse a raw mime email and return
47
 * the structure. Returned structure is similar to
48
 * that returned by imap_fetchstructure().
49
 *
50
 * USAGE: (assume $input is your raw email)
51
 *
52
 * $decode = new Mail_mimeDecode($input, "\r\n");
53
 * $structure = $decode->decode();
54
 * print_r($structure);
55
 *
56
 * Or statically:
57
 *
58
 * $params['input'] = $input;
59
 * $structure = Mail_mimeDecode::decode($params);
60
 * print_r($structure);
61
 *
62
 * TODO:
63
 *  - Implement further content types, eg. multipart/parallel,
64
 *    perhaps even message/partial.
65
 *
66
 * @author  Richard Heyes <[email protected]>
67
 * @version $Revision: 1.2 $
68
 */
69
class Mail_mimeDecode extends PEAR
70
{
71
    /**
72
     * The raw email to decode
73
     * @var string
74
     */
75
    public $_input;
76
    /**
77
     * The header part of the input
78
     * @var string
79
     */
80
    public $_header;
81
    /**
82
     * The body part of the input
83
     * @var string
84
     */
85
    public $_body;
86
    /**
87
     * If an error occurs, this is used to store the message
88
     * @var string
89
     */
90
    public $_error;
91
    /**
92
     * Flag to determine whether to include bodies in the
93
     * returned object.
94
     * @var bool
95
     */
96
    public $_include_bodies;
97
    /**
98
     * Flag to determine whether to decode bodies
99
     * @var bool
100
     */
101
    public $_decode_bodies;
102
    /**
103
     * Flag to determine whether to decode headers
104
     * @var bool
105
     */
106
    public $_decode_headers;
107
    /**
108
     * If invoked from a class, $this will be set. This has problematic
109
     * connotations for calling decode() statically. Hence this variable
110
     * is used to determine if we are indeed being called statically or
111
     * via an object.
112
     */
113
    public $mailMimeDecode;
114
115
    /**
116
     * Constructor.
117
     *
118
     * Sets up the object, initialise the variables, and splits and
119
     * stores the header and body of the input.
120
     *
121
     * @param mixed $input
122
     */
123
    public function __construct($input)
124
    {
125
        [$header, $body] = $this->_splitBodyHeader($input);
126
127
        $this->_input          = $input;
128
        $this->_header         = $header;
129
        $this->_body           = $body;
130
        $this->_decode_bodies  = false;
131
        $this->_include_bodies = true;
132
133
        $this->mailMimeDecode = true;
134
    }
135
136
    /**
137
     * Begins the decoding process. If called statically
138
     * it will create an object and call the decode() method
139
     * of it.
140
     *
141
     * @param null|mixed $params
142
     * @return object Decoded results
143
     */
144
    public function decode($params = null)
145
    {
146
        // Have we been called statically? If so, create an object and pass details to that.
147
        if (!isset($this->mailMimeDecode) && isset($params['input'])) {
148
            $obj       = new self($params['input']);
149
            $structure = $obj->decode($params);
150
            // Called statically but no input
151
        } elseif (isset($this->mailMimeDecode)) {
152
            $this->_include_bodies = $params['include_bodies'] ?? false;
153
            $this->_decode_bodies  = $params['decode_bodies'] ?? false;
154
            $this->_decode_headers = $params['decode_headers'] ?? false;
155
156
            $structure = $this->_decode($this->_header, $this->_body);
0 ignored issues
show
Bug introduced by
$this->_header of type string is incompatible with the type array expected by parameter $headers of Mail_mimeDecode::_decode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
            $structure = $this->_decode(/** @scrutinizer ignore-type */ $this->_header, $this->_body);
Loading history...
157
            if (false === $structure) {
158
                $structure = $this->raiseError($this->_error);
159
            }
160
        } else {
161
            return PEAR::raiseError('Called statically and no input given');
162
            // Called via an object
163
        }
164
165
        return $structure;
166
    }
167
168
    /**
169
     * Performs the decoding. Decodes the body string passed to it
170
     * If it finds certain content-types it will call itself in a
171
     * recursive fashion
172
     *
173
     * @param array  $headers
174
     * @param string $body
175
     * @param string $default_ctype
176
     * @return object|false Results of decoding process
177
     */
178
    public function _decode(array $headers, string $body, string $default_ctype = 'text/plain')
179
    {
180
        $return  = new stdClass();
181
        $headers = $this->_parseHeaders($headers);
182
183
        foreach ($headers as $value) {
184
            if (isset($return->headers[mb_strtolower($value['name'])]) && !is_array($return->headers[mb_strtolower($value['name'])])) {
185
                $return->headers[mb_strtolower($value['name'])]   = [$return->headers[mb_strtolower($value['name'])]];
186
                $return->headers[mb_strtolower($value['name'])][] = $value['value'];
187
            } elseif (isset($return->headers[mb_strtolower($value['name'])])) {
188
                $return->headers[mb_strtolower($value['name'])][] = $value['value'];
189
            } else {
190
                $return->headers[mb_strtolower($value['name'])] = $value['value'];
191
            }
192
        }
193
194
        reset($headers);
195
        //        while (list($key, $value) = each($headers)) {
196
        foreach ($headers as $key => $value) {
197
            $headers[$key]['name'] = \mb_strtolower($value['name']);
198
            switch ($headers[$key]['name']) {
199
                case 'content-type':
200
                    $content_type = $this->_parseHeaderValue($headers[$key]['value']);
201
202
                    if (preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) {
203
                        $return->ctype_primary   = $regs[1];
204
                        $return->ctype_secondary = $regs[2];
205
                    }
206
207
                    if (isset($content_type['other'])) {
208
                        foreach ($content_type['other'] as $p_name => $p_value) {
209
                            $return->ctype_parameters[$p_name] = $p_value;
210
                        }
211
                    }
212
                    break;
213
                case 'content-disposition':
214
                    $content_disposition = $this->_parseHeaderValue($headers[$key]['value']);
215
                    $return->disposition = $content_disposition['value'];
216
                    if (isset($content_disposition['other'])) {
217
                        foreach ($content_disposition['other'] as $p_name => $p_value) {
218
                            $return->d_parameters[$p_name] = $p_value;
219
                        }
220
                    }
221
                    break;
222
                case 'content-transfer-encoding':
223
                    $content_transfer_encoding = $this->_parseHeaderValue($headers[$key]['value']);
224
                    break;
225
            }
226
        }
227
228
        if (isset($content_type)) {
229
            switch (mb_strtolower($content_type['value'])) {
230
                case 'text/plain':
231
                    $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
232
                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
233
                    break;
234
                case 'text/html':
235
                    $encoding = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
236
                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $encoding) : $body) : null;
237
                    break;
238
                case 'multipart/parallel':
239
                case 'multipart/report': // RFC1892
240
                case 'multipart/signed': // PGP
241
                case 'multipart/digest':
242
                case 'multipart/alternative':
243
                case 'multipart/related':
244
                case 'multipart/mixed':
245
                    if (!isset($content_type['other']['boundary'])) {
246
                        $this->_error = 'No boundary found for ' . $content_type['value'] . ' part';
247
248
                        return false;
249
                    }
250
251
                    $default_ctype = ('multipart/digest' === \mb_strtolower($content_type['value'])) ? 'message/rfc822' : 'text/plain';
252
253
                    $parts = $this->_boundarySplit($body, $content_type['other']['boundary']);
254
                    foreach ($parts as $iValue) {
255
                        [$part_header, $part_body] = $this->_splitBodyHeader($iValue);
256
                        $part = $this->_decode($part_header, $part_body, $default_ctype);
257
                        if (false == $part) {
258
                            $part = $this->raiseError($this->_error);
259
                        }
260
                        $return->parts[] = $part;
261
                    }
262
                    break;
263
                case 'message/rfc822':
264
                    $obj             = new self($body);
265
                    $return->parts[] = $obj->decode(['include_bodies' => $this->_include_bodies]);
266
                    unset($obj);
267
                    break;
268
                default:
269
                    if (!isset($content_transfer_encoding['value'])) {
270
                        $content_transfer_encoding['value'] = '7bit';
271
                    }
272
                    $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body, $content_transfer_encoding['value']) : $body) : null;
273
                    break;
274
            }
275
        } else {
276
            $ctype                   = explode('/', $default_ctype);
277
            $return->ctype_primary   = $ctype[0];
278
            $return->ctype_secondary = $ctype[1];
279
            $this->_include_bodies ? $return->body = ($this->_decode_bodies ? $this->_decodeBody($body) : $body) : null;
280
        }
281
282
        return $return;
283
    }
284
285
    /**
286
     * Given the output of the above function, this will return an
287
     * array of references to the parts, indexed by mime number.
288
     *
289
     * @param object $structure   The structure to go through
290
     * @param bool   $no_refs
291
     * @param string $mime_number Internal use only.
292
     * @param string $prepend
293
     * @return array  Mime numbers
294
     */
295
    public function &getMimeNumbers(object $structure, bool $no_refs, string $mime_number = '', string $prepend = ''): array
296
    {
297
        $return = [];
298
        if (!empty($structure->parts)) {
299
            if ('' != $mime_number) {
300
                $structure->mime_id              = $prepend . $mime_number;
301
                $return[$prepend . $mime_number] = &$structure;
302
            }
303
            foreach ($structure->parts as $i => $iValue) {
304
                if (!empty($structure->headers['content-type']) && 0 === mb_strpos(mb_strtolower($structure->headers['content-type']), 'message/')) {
305
                    $prepend      .= $mime_number . '.';
306
                    $_mime_number = '';
307
                } else {
308
                    $_mime_number = ('' == $mime_number ? $i + 1 : sprintf('%s.%s', $mime_number, $i + 1));
309
                }
310
311
                $arr = &self::getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend);
0 ignored issues
show
Bug Best Practice introduced by
The method Mail_mimeDecode::getMimeNumbers() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

311
                $arr = &self::/** @scrutinizer ignore-call */ getMimeNumbers($structure->parts[$i], $no_refs, $_mime_number, $prepend);
Loading history...
312
                foreach ($arr as $key => $val) {
313
                    $no_refs ? $return[$key] = '' : $return[$key] = &$val;
314
                }
315
            }
316
        } else {
317
            if ('' === $mime_number) {
318
                $mime_number = '1';
319
            }
320
            $structure->mime_id = $prepend . $mime_number;
321
            $no_refs ? $return[$prepend . $mime_number] = '' : $return[$prepend . $mime_number] = &$structure;
322
        }
323
324
        return $return;
325
    }
326
327
    /**
328
     * Given a string containing a header and body
329
     * section, this function will split them (at the first
330
     * blank line) and return them.
331
     *
332
     * @param mixed $input
333
     * @return array|false Contains header and body section
334
     */
335
    public function _splitBodyHeader($input)
336
    {
337
        if (preg_match("/^(.*?)\r?\n\r?\n(.*)/s", $input, $match)) {
338
            return [$match[1], $match[2]];
339
        }
340
        $this->_error = 'Could not split header and body';
341
342
        return false;
343
    }
344
345
    /**
346
     * Parse headers given in $input and return
347
     * as assoc array.
348
     *
349
     * @param mixed $input
350
     * @return array Contains parsed headers
351
     */
352
    public function _parseHeaders($input): array
353
    {
354
        if ('' !== $input) {
355
            // Unfold the input
356
            $input   = preg_replace("/\r?\n/", "\r\n", $input);
357
            $input   = preg_replace("/\r\n(\t| )+/", ' ', $input);
358
            $headers = explode("\r\n", trim($input));
359
360
            foreach ($headers as $value) {
361
                $hdr_name  = mb_substr($value, 0, $pos = mb_strpos($value, ':'));
362
                $hdr_value = mb_substr($value, $pos + 1);
363
                if (' ' === $hdr_value[0]) {
364
                    $hdr_value = mb_substr($hdr_value, 1);
365
                }
366
367
                $return[] = [
368
                    'name'  => $hdr_name,
369
                    'value' => $this->_decode_headers ? $this->_decodeHeader($hdr_value) : $hdr_value,
370
                ];
371
            }
372
        } else {
373
            $return = [];
374
        }
375
376
        return $return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $return does not seem to be defined for all execution paths leading up to this point.
Loading history...
377
    }
378
379
    /**
380
     * Function to parse a header value,
381
     * extract first part, and any secondary
382
     * parts (after ;) This function is not as
383
     * robust as it could be. Eg. header comments
384
     * in the wrong place will probably break it.
385
     *
386
     * @param mixed $input
387
     * @return array Contains parsed result
388
     */
389
    public function _parseHeaderValue($input): array
390
    {
391
        if (false !== ($pos = mb_strpos($input, ';'))) {
392
            $return['value'] = trim(mb_substr($input, 0, $pos));
0 ignored issues
show
Comprehensibility Best Practice introduced by
$return was never initialized. Although not strictly required by PHP, it is generally a good practice to add $return = array(); before regardless.
Loading history...
393
            $input           = trim(mb_substr($input, $pos + 1));
394
395
            if ('' !== $input) {
396
                // This splits on a semi-colon, if there's no preceeding backslash
397
                // Can't handle if it's in double quotes however. (Of course anyone
398
                // sending that needs a good slap).
399
                $parameters = preg_split('/\s*(?<!\\\\);\s*/i', $input);
400
401
                foreach ($parameters as $i => $iValue) {
402
                    $param_name  = mb_substr($iValue, 0, $pos = mb_strpos($iValue, '='));
403
                    $param_value = mb_substr($iValue, $pos + 1);
404
                    if ('"' === $param_value[0]) {
405
                        $param_value = mb_substr($param_value, 1, -1);
406
                    }
407
                    $return['other'][$param_name]                = $param_value;
408
                    $return['other'][mb_strtolower($param_name)] = $param_value;
409
                }
410
            }
411
        } else {
412
            $return['value'] = trim($input);
413
        }
414
415
        return $return;
416
    }
417
418
    /**
419
     * This function splits the input based
420
     * on the given boundary
421
     *
422
     * @param string $input
423
     * @param string $boundary
424
     * @return array Contains array of resulting mime parts
425
     */
426
    public function _boundarySplit(string $input, string $boundary): array
427
    {
428
        $tmp = explode('--' . $boundary, $input);
429
430
        for ($i = 1; $i < count($tmp) - 1; ++$i) {
431
            $parts[] = $tmp[$i];
432
        }
433
434
        return $parts;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $parts does not seem to be defined for all execution paths leading up to this point.
Loading history...
435
    }
436
437
    /**
438
     * Given a header, this function will decode it
439
     * according to RFC2047. Probably not *exactly*
440
     * conformant, but it does pass all the given
441
     * examples (in RFC2047).
442
     *
443
     * @param mixed $input
444
     * @return string Decoded header value
445
     */
446
    public function _decodeHeader($input): string
447
    {
448
        // Remove white space between encoded-words
449
        $input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);
450
451
        // For each encoded-word...
452
        while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
453
            $encoded  = $matches[1];
454
            $charset  = $matches[2];
0 ignored issues
show
Unused Code introduced by
The assignment to $charset is dead and can be removed.
Loading history...
455
            $encoding = $matches[3];
456
            $text     = $matches[4];
457
458
            switch (mb_strtolower($encoding)) {
459
                case 'b':
460
                    $text = base64_decode($text, true);
461
                    break;
462
                case 'q':
463
                    $text = str_replace('_', ' ', $text);
464
                    preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
465
                    foreach ($matches[1] as $value) {
466
                        $text = str_replace('=' . $value, chr(hexdec($value)), $text);
0 ignored issues
show
Bug introduced by
It seems like hexdec($value) can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

466
                        $text = str_replace('=' . $value, chr(/** @scrutinizer ignore-type */ hexdec($value)), $text);
Loading history...
467
                    }
468
                    break;
469
            }
470
471
            $input = str_replace($encoded, $text, $input);
472
        }
473
474
        return $input;
475
    }
476
477
    /**
478
     * Given a body string and an encoding type,
479
     * this function will decode and return it.
480
     *
481
     * @param mixed $input
482
     * @param mixed $encoding
483
     * @return string Decoded body
484
     */
485
    public function _decodeBody($input, $encoding = '7bit'): string
486
    {
487
        switch ($encoding) {
488
            case '7bit':
489
                return $input;
490
            case 'quoted-printable':
491
                return $this->_quotedPrintableDecode($input);
492
            case 'base64':
493
                return base64_decode($input, true);
494
            default:
495
                return $input;
496
        }
497
    }
498
499
    /**
500
     * Given a quoted-printable string, this
501
     * function will decode and return it.
502
     *
503
     * @param mixed $input
504
     * @return string Decoded body
505
     */
506
    public function _quotedPrintableDecode($input): string
507
    {
508
        // Remove soft line breaks
509
        $input = preg_replace("/=\r?\n/", '', $input);
510
511
        // Replace encoded characters
512
        $input = preg_replace('/=([a-f0-9]{2})/ie', "chr(hexdec('\\1'))", $input);
513
514
        return $input;
515
    }
516
517
    /**
518
     * Checks the input for uuencoded files and returns
519
     * an array of them. Can be called statically, eg:
520
     *
521
     * $files =& Mail_mimeDecode::uudecode($some_text);
522
     *
523
     * It will check for the begin 666 ... end syntax
524
     * however and won't just blindly decode whatever you
525
     * pass it.
526
     *
527
     * @param mixed $input
528
     * @return array Decoded bodies, filenames and permissions
529
     * @author Unknown
530
     */
531
    public function &uudecode($input): array
532
    {
533
        // Find all uuencoded sections
534
        preg_match_all("/begin ([0-7]{3}) (.+)\r?\n(.+)\r?\nend/Us", $input, $matches);
535
536
        for ($j = 0, $jMax = count($matches[3]); $j < $jMax; $j++) {
537
            $str      = $matches[3][$j];
538
            $filename = $matches[2][$j];
539
            $fileperm = $matches[1][$j];
540
541
            $file = '';
542
            $str  = preg_split("/\r?\n/", trim($str));
543
544
            foreach ($str as $i => $iValue) {
545
                $pos = 1;
546
                $d   = 0;
547
                $len = (((ord(mb_substr($iValue, 0, 1)) - 32) - ' ') & 077);
548
549
                while (($d + 3 <= $len) and ($pos + 4 <= mb_strlen($iValue))) {
550
                    $c0   = (ord(mb_substr($iValue, $pos, 1)) ^ 0x20);
551
                    $c1   = (ord(mb_substr($iValue, $pos + 1, 1)) ^ 0x20);
552
                    $c2   = (ord(mb_substr($iValue, $pos + 2, 1)) ^ 0x20);
553
                    $c3   = (ord(mb_substr($iValue, $pos + 3, 1)) ^ 0x20);
554
                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
555
556
                    $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
557
558
                    $file .= chr(((($c2 - ' ') & 077) << 6) | (($c3 - ' ') & 077));
559
560
                    $pos += 4;
561
                    $d   += 3;
562
                }
563
564
                if (($d + 2 <= $len) && ($pos + 3 <= mb_strlen($iValue))) {
565
                    $c0   = (ord(mb_substr($iValue, $pos, 1)) ^ 0x20);
566
                    $c1   = (ord(mb_substr($iValue, $pos + 1, 1)) ^ 0x20);
567
                    $c2   = (ord(mb_substr($iValue, $pos + 2, 1)) ^ 0x20);
568
                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
569
570
                    $file .= chr(((($c1 - ' ') & 077) << 4) | ((($c2 - ' ') & 077) >> 2));
571
572
                    $pos += 3;
573
                    $d   += 2;
574
                }
575
576
                if (($d + 1 <= $len) && ($pos + 2 <= mb_strlen($iValue))) {
577
                    $c0   = (ord(mb_substr($iValue, $pos, 1)) ^ 0x20);
578
                    $c1   = (ord(mb_substr($iValue, $pos + 1, 1)) ^ 0x20);
579
                    $file .= chr(((($c0 - ' ') & 077) << 2) | ((($c1 - ' ') & 077) >> 4));
580
                }
581
            }
582
            $files[] = ['filename' => $filename, 'fileperm' => $fileperm, 'filedata' => $file];
583
        }
584
585
        return $files;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $files does not seem to be defined for all execution paths leading up to this point.
Loading history...
586
    }
587
588
    /**
589
     * getSendArray() returns the arguments required for Mail::send()
590
     * used to build the arguments for a mail::send() call
591
     *
592
     * Usage:
593
     * $mailtext = Full email (for example generated by a template)
594
     * $decoder = new Mail_mimeDecode($mailtext);
595
     * $parts =  $decoder->getSendArray();
596
     * if (!PEAR::isError($parts) {
597
     *     list($recipents,$headers,$body) = $parts;
598
     *     $mail = Mail::factory('smtp');
599
     *     $mail->send($recipents,$headers,$body);
600
     * } else {
601
     *     echo $parts->message;
602
     * }
603
     * @return array|\PEAR_Error array of recipeint, headers,body or Pear_Error
604
     * @author Alan Knowles <[email protected]>
605
     */
606
    public function getSendArray()
607
    {
608
        // prevent warning if this is not set
609
        $this->_decode_headers = false;
610
        $headerlist            = $this->_parseHeaders($this->_header);
611
        $to                    = '';
612
        if (!$headerlist) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $headerlist of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
613
            return $this->raiseError('Message did not contain headers');
614
        }
615
        foreach ($headerlist as $item) {
616
            $header[$item['name']] = $item['value'];
617
            switch (mb_strtolower($item['name'])) {
618
                case 'to':
619
                case 'cc':
620
                case 'bcc':
621
                    $to = ',' . $item['value'];
622
                // no break
623
                default:
624
                    break;
625
            }
626
        }
627
        if ('' === $to) {
0 ignored issues
show
introduced by
The condition '' === $to is always true.
Loading history...
628
            return $this->raiseError('Message did not contain any recipents');
629
        }
630
        $to = mb_substr($to, 1);
631
632
        return [$to, $header, $this->_body];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $header seems to be defined by a foreach iteration on line 615. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
633
    }
634
635
    /**
636
     * Returns a xml copy of the output of
637
     * Mail_mimeDecode::decode. Pass the output in as the
638
     * argument. This function can be called statically. Eg:
639
     *
640
     * $output = $obj->decode();
641
     * $xml    = Mail_mimeDecode::getXML($output);
642
     *
643
     * The DTD used for this should have been in the package. Or
644
     * alternatively you can get it from cvs, or here:
645
     * https://www.phpguru.org/xmail/xmail.dtd.
646
     *
647
     * @param mixed $input
648
     * @return string XML version of input
649
     */
650
    public function getXML($input): string
651
    {
652
        $crlf   = "\r\n";
653
        $output = '<?xml version=\'1.0\'?>' . $crlf . '<!DOCTYPE email SYSTEM "https://www.phpguru.org/xmail/xmail.dtd">' . $crlf . '<email>' . $crlf . self::_getXML($input) . '</email>';
0 ignored issues
show
Bug Best Practice introduced by
The method Mail_mimeDecode::_getXML() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

653
        $output = '<?xml version=\'1.0\'?>' . $crlf . '<!DOCTYPE email SYSTEM "https://www.phpguru.org/xmail/xmail.dtd">' . $crlf . '<email>' . $crlf . self::/** @scrutinizer ignore-call */ _getXML($input) . '</email>';
Loading history...
654
655
        return $output;
656
    }
657
658
    /**
659
     * Function that does the actual conversion to xml. Does a single
660
     * mimepart at a time.
661
     *
662
     * @param mixed $input
663
     * @param mixed $indent
664
     * @return string XML version of input
665
     */
666
    public function _getXML($input, $indent = 1): string
667
    {
668
        $htab    = "\t";
669
        $crlf    = "\r\n";
670
        $output  = '';
671
        $headers = @(array)$input->headers;
672
673
        foreach ($headers as $hdr_name => $hdr_value) {
674
            // Multiple headers with this name
675
            if (is_array($hdr_value)) {
676
                for ($i = 0, $iMax = count($hdr_value); $i < $iMax; ++$i) {
677
                    $output .= self::_getXML_helper($hdr_name, $hdr_value[$i], $indent);
0 ignored issues
show
Bug Best Practice introduced by
The method Mail_mimeDecode::_getXML_helper() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

677
                    $output .= self::/** @scrutinizer ignore-call */ _getXML_helper($hdr_name, $hdr_value[$i], $indent);
Loading history...
678
                }
679
                // Only one header of this sort
680
            } else {
681
                $output .= self::_getXML_helper($hdr_name, $hdr_value, $indent);
682
            }
683
        }
684
685
        if (!empty($input->parts)) {
686
            for ($i = 0, $iMax = count($input->parts); $i < $iMax; ++$i) {
687
                $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf . self::_getXML($input->parts[$i], $indent + 1) . str_repeat($htab, $indent) . '</mimepart>' . $crlf;
0 ignored issues
show
Bug Best Practice introduced by
The method Mail_mimeDecode::_getXML() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

687
                $output .= $crlf . str_repeat($htab, $indent) . '<mimepart>' . $crlf . self::/** @scrutinizer ignore-call */ _getXML($input->parts[$i], $indent + 1) . str_repeat($htab, $indent) . '</mimepart>' . $crlf;
Loading history...
688
            }
689
        } elseif (isset($input->body)) {
690
            $output .= $crlf . str_repeat($htab, $indent) . '<body><![CDATA[' . $input->body . ']]></body>' . $crlf;
691
        }
692
693
        return $output;
694
    }
695
696
    /**
697
     * Helper function to _getXML(). Returns xml of a header.
698
     *
699
     * @param mixed $hdr_name
700
     * @param mixed $hdr_value
701
     * @param mixed $indent
702
     * @return string XML version of input
703
     */
704
    public function _getXML_helper($hdr_name, $hdr_value, $indent): string
705
    {
706
        $htab   = "\t";
707
        $crlf   = "\r\n";
708
        $return = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $return is dead and can be removed.
Loading history...
709
710
        $new_hdr_value = ('received' !== $hdr_name) ? self::_parseHeaderValue($hdr_value) : ['value' => $hdr_value];
0 ignored issues
show
Bug Best Practice introduced by
The method Mail_mimeDecode::_parseHeaderValue() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

710
        $new_hdr_value = ('received' !== $hdr_name) ? self::/** @scrutinizer ignore-call */ _parseHeaderValue($hdr_value) : ['value' => $hdr_value];
Loading history...
711
        $new_hdr_name  = str_replace(' ', '-', ucwords(str_replace('-', ' ', $hdr_name)));
712
713
        // Sort out any parameters
714
        if (!empty($new_hdr_value['other'])) {
715
            foreach ($new_hdr_value['other'] as $paramname => $paramvalue) {
716
                $params[] = str_repeat($htab, $indent)
717
                            . $htab
718
                            . '<parameter>'
719
                            . $crlf
720
                            . str_repeat($htab, $indent)
721
                            . $htab
722
                            . $htab
723
                            . '<paramname>'
724
                            . htmlspecialchars($paramname, ENT_QUOTES | ENT_HTML5)
725
                            . '</paramname>'
726
                            . $crlf
727
                            . str_repeat($htab, $indent)
728
                            . $htab
729
                            . $htab
730
                            . '<paramvalue>'
731
                            . htmlspecialchars($paramvalue, ENT_QUOTES | ENT_HTML5)
732
                            . '</paramvalue>'
733
                            . $crlf
734
                            . str_repeat($htab, $indent)
735
                            . $htab
736
                            . '</parameter>'
737
                            . $crlf;
738
            }
739
740
            $params = implode('', $params);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $params seems to be defined by a foreach iteration on line 715. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
741
        } else {
742
            $params = '';
743
        }
744
745
        $return = str_repeat($htab, $indent) . '<header>' . $crlf . str_repeat($htab, $indent) . $htab . '<headername>' . htmlspecialchars($new_hdr_name, ENT_QUOTES | ENT_HTML5) . '</headername>' . $crlf . str_repeat($htab, $indent) . $htab . '<headervalue>' . htmlspecialchars(
746
                $new_hdr_value['value'],
747
                ENT_QUOTES | ENT_HTML5
748
            ) . '</headervalue>' . $crlf . $params . str_repeat($htab, $indent) . '</header>' . $crlf;
749
750
        return $return;
751
    }
752
} // End of class
753