Seditio Source
Root |
./othercms/dotclear-2.22/inc/libs/clearbricks/mail.mime/class.mime.message.php
<?php
/**
 * @class mimeMessage
 *
 * @package Clearbricks
 * @subpackage Mail
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright GPL-2.0-only
 */
class mimeMessage
{
    protected
$from_email       = null;
    protected
$from_name        = null;
    protected
$ctype_primary    = null;
    protected
$ctype_secondary  = null;
    protected
$ctype_parameters = [];
    protected
$d_parameters     = [];
    protected
$disposition      = null;
    protected
$headers          = [];
    protected
$body             = null;
    protected
$parts            = [];

    public function
__construct($message)
    {
        [
$header, $body] = $this->splitBodyHeader($message);
       
$this->decode($header, $body);
    }

    public function
getMainBody()
    {
       
# Look for the main body part (text/plain or text/html)
       
if ($this->body && $this->ctype_primary == 'text' && $this->ctype_secondary == 'plain') {
            return
$this->body;
        }

        foreach (
$this->parts as $part) {
            if ((
$body = $part->getBody()) !== null) {
                return
$body;
            }
        }
    }

    public function
getBody()
    {
        return
$this->body;
    }

    public function
getHeaders()
    {
        return
$this->headers;
    }

    public function
getHeader($hdr, $only_last = false)
    {
       
$hdr = strtolower($hdr);

        if (!isset(
$this->headers[$hdr])) {
            return;
        }

        if (
$only_last && is_array($this->headers[$hdr])) {
           
$r = $this->headers[$hdr];

            return
array_pop($r);
        }

        return
$this->headers[$hdr];
    }

    public function
getFrom()
    {
        return [
$this->from_email, $this->from_name];
    }

    public function
getAllFiles()
    {
       
$parts = [];
        foreach (
$this->parts as $part) {
           
$body     = $part->getBody();
           
$filename = $part->getFileName();
            if (
$body && $filename) {
               
$parts[] = [
                   
'filename' => $filename,
                   
'content'  => $part->getBody(),
                ];
            } else {
               
$parts = array_merge($parts, $part->getAllFiles());
            }
        }

        return
$parts;
    }

    public function
getFileName()
    {
        if (isset(
$this->ctype_parameters['name'])) {
            return
$this->ctype_parameters['name'];
        }

        if (isset(
$this->d_parameters['filename'])) {
            return
$this->d_parameters['filename'];
        }
    }

    protected function
decode($headers, $body, $default_ctype = 'text/plain')
    {
       
$headers = $this->parseHeaders($headers);

        foreach (
$headers as $v) {
            if (isset(
$this->headers[strtolower($v['name'])]) && !is_array($this->headers[strtolower($v['name'])])) {
               
$this->headers[strtolower($v['name'])]   = [$this->headers[strtolower($v['name'])]];
               
$this->headers[strtolower($v['name'])][] = $v['value'];
            } elseif (isset(
$this->headers[strtolower($v['name'])])) {
               
$this->headers[strtolower($v['name'])][] = $v['value'];
            } else {
               
$this->headers[strtolower($v['name'])] = $v['value'];
            }
        }

        foreach (
$headers as $k => $v) {
           
$headers[$k]['name'] = strtolower($headers[$k]['name']);
            switch (
$headers[$k]['name']) {
                case
'from':
                    [
$this->from_name, $this->from_email] = $this->decodeSender($headers[$k]['value']);

                    break;

                case
'content-type':
                   
$content_type = $this->parseHeaderValue($headers[$k]['value']);

                    if (
preg_match('/([0-9a-z+.-]+)\/([0-9a-z+.-]+)/i', $content_type['value'], $regs)) {
                       
$this->ctype_primary   = strtolower($regs[1]);
                       
$this->ctype_secondary = strtolower($regs[2]);
                    }

                    if (isset(
$content_type['other'])) {
                        foreach (
$content_type['other'] as $p_name => $p_value) {
                           
$this->ctype_parameters[$p_name] = $p_value;
                        }
                    }

                    break;

                case
'content-disposition':
                   
$content_disposition = $this->parseHeaderValue($headers[$k]['value']);
                   
$this->disposition   = $content_disposition['value'];
                    if (isset(
$content_disposition['other'])) {
                        foreach (
$content_disposition['other'] as $p_name => $p_value) {
                           
$this->d_parameters[$p_name] = $p_value;
                        }
                    }

                    break;

                case
'content-transfer-encoding':
                   
$content_transfer_encoding = $this->parseHeaderValue($headers[$k]['value']);

                    break;
            }
        }

        if (isset(
$content_type)) {
            switch (
strtolower($content_type['value'])) {
                case
'text/plain':
                case
'text/html':
                   
$encoding   = isset($content_transfer_encoding) ? $content_transfer_encoding['value'] : '7bit';
                   
$charset    = $this->ctype_parameters['charset'] ?? null;
                   
$this->body = $this->decodeBody($body, $encoding);
                   
$this->body = text::cleanUTF8(@text::toUTF8($this->body, $charset));

                    break;

                case
'multipart/parallel':
                case
'multipart/appledouble': // Appledouble mail
               
case 'multipart/report':      // RFC1892
               
case 'multipart/signed':      // PGP
               
case 'multipart/digest':
                case
'multipart/alternative':
                case
'multipart/related':
                case
'multipart/mixed':
                    if (!isset(
$content_type['other']['boundary'])) {
                        throw new
Exception('No boundary found');
                    }

                   
$default_ctype = (strtolower($content_type['value']) === 'multipart/digest') ? 'message/rfc822' : 'text/plain';

                   
$parts = $this->boundarySplit($body, $content_type['other']['boundary']);
                    for (
$i = 0; $i < count($parts); $i++) {
                       
$this->parts[] = new self($parts[$i]);
                    }

                    break;

                case
'message/rfc822':
                   
$this->parts[] = new self($body);

                    break;

                default:
                    if (!isset(
$content_transfer_encoding['value'])) {
                       
$content_transfer_encoding['value'] = '7bit';
                    }
                   
$this->body = $this->decodeBody($body, $content_transfer_encoding['value']);

                    break;
            }
        } else {
           
$ctype                 = explode('/', $default_ctype);
           
$this->ctype_primary   = $ctype[0];
           
$this->ctype_secondary = $ctype[1];
           
$this->body            = $this->decodeBody($body, '7bit');
           
$this->body            = text::cleanUTF8(@text::toUTF8($this->body));
        }
    }

    protected function
splitBodyHeader($input)
    {
        if (
preg_match('/^(.*?)\r?\n\r?\n(.*)/s', $input, $match)) {
            return [
$match[1], $match[2]];
        }
       
# No body found
       
return [$input, ''];
    }

    protected function
parseHeaders($input)
    {
        if (!
$input) {
            return [];
        }

       
# Unfold the input
       
$input   = preg_replace("/\r?\n/", "\r\n", $input);
       
$input   = preg_replace("/\r\n(\t| )+/", ' ', $input);
       
$headers = explode("\r\n", trim($input));

       
$res = [];

       
# Remove first From line if exists
       
if (strpos($headers[0], 'From ') === 0) {
           
array_shift($headers);
        }

        foreach (
$headers as $value) {
           
$hdr_name = substr($value, 0, $pos = strpos($value, ':'));

           
$hdr_value = substr($value, $pos + 1);

            if (
$hdr_value[0] == ' ') {
               
$hdr_value = substr($hdr_value, 1);
            }

           
$res[] = [
               
'name'  => $hdr_name,
               
'value' => $this->decodeHeader($hdr_value),
            ];
        }

        return
$res;
    }

    protected function
parseHeaderValue($input)
    {
        if ((
$pos = strpos($input, ';')) !== false) {
           
$return['value'] = trim(substr($input, 0, $pos));
           
$input           = trim(substr($input, $pos + 1));

            if (
strlen($input) > 0) {
               
# This splits on a semi-colon, if there's no preceeding backslash
                # Now works with quoted values; had to glue the \; breaks in PHP
                # the regex is already bordering on incomprehensible
               
$splitRegex = '/([^;\'"]*[\'"]([^\'"]*([^\'"]*)*)[\'"][^;\'"]*|([^;]+))(;|$)/';
               
preg_match_all($splitRegex, $input, $matches);
               
$parameters = [];
                for (
$i = 0; $i < count($matches[0]); $i++) {
                   
$param = $matches[0][$i];
                    while (
substr($param, -2) == '\;') {
                       
$param .= $matches[0][++$i];
                    }
                   
$parameters[] = $param;
                }

                for (
$i = 0; $i < count($parameters); $i++) {
                   
$param_name  = trim(substr($parameters[$i], 0, $pos = strpos($parameters[$i], '=')), "'\";\t\\ ");
                   
$param_value = trim(str_replace('\;', ';', substr($parameters[$i], $pos + 1)), "'\";\t\\ ");
                    if (
$param_value[0] == '"') {
                       
$param_value = substr($param_value, 1, -1);
                    }

                    if (
preg_match('/\*$/', $param_name)) {
                       
$return['other'][strtolower(substr($param_name, 0, -1))] = $this->parserHeaderSpecialValue($param_value);
                    } else {
                       
$return['other'][strtolower($param_name)] = $param_value;
                    }
                }
            }
        } else {
           
$return['value'] = trim((string) $input);
        }

        return
$return;
    }

    protected function
parserHeaderSpecialValue($value)
    {
        if (
strpos($value, "''") === false) {
            return
$value;
        }

        [
$charset, $value] = explode("''", $value);

        return @
text::toUTF8(rawurldecode($value), $charset);
    }

    protected function
boundarySplit($input, $boundary)
    {
       
$parts = [];

       
$bs_possible = substr($boundary, 2, -2);
       
$bs_check    = '\"' . $bs_possible . '\"';

        if (
$boundary == $bs_check) {
           
$boundary = $bs_possible;
        }

       
$tmp = explode('--' . $boundary, $input);

        for (
$i = 1; $i < count($tmp) - 1; $i++) {
           
$parts[] = $tmp[$i];
        }

        return
$parts;
    }

    protected function
decodeHeader($input)
    {
       
# Remove white space between encoded-words
       
$input = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $input);

       
# Non encoded
       
if (!preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input)) {
            return @
text::toUTF8($input);
        }

       
# For each encoded-word...
       
while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $input, $matches)) {
           
$encoded  = $matches[1];
           
$charset  = $matches[2];
           
$encoding = $matches[3];
           
$text     = $matches[4];

            switch (
strtolower($encoding)) {
                case
'b':
                   
$text = base64_decode($text);

                    break;

                case
'q':
                   
$text = str_replace('_', ' ', $text);
                   
preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
                    foreach (
$matches[1] as $value) {
                       
$text = str_replace('=' . $value, chr(hexdec($value)), $text);
                    }

                    break;
            }
           
$text  = @text::toUTF8($text, $charset);
           
$input = str_replace($encoded, $text, $input);
        }

        return
text::cleanUTF8($input);
    }

    protected function
decodeSender($sender)
    {
        if (
preg_match('/([\'|"])?(.*)(?(1)[\'|"])\s+<([\w\-=!#$%^*\'+\\.={}|?~]+@[\w\-=!#$%^*\'+\\.={}|?~]+[\w\-=!#$%^*\'+\\={}|?~])>/', $sender, $matches)) {
           
# Match address in the form: Name <email@host>
           
$result[0] = $matches[2];
           
$result[1] = $matches[sizeof($matches) - 1];
        } elseif (
preg_match('/([\w\-=!#$%^*\'+\\.={}|?~]+@[\w\-=!#$%^*\'+\\.={}|?~]+[\w\-=!#$%^*\'+\\={}|?~])\s+\((.*)\)/', $sender, $matches)) {
           
# Match address in the form: email@host (Name)
           
$result[0] = $matches[1];
           
$result[1] = $matches[2];
        } else {
           
# Only the email address present
           
$result[0] = $sender;
           
$result[1] = $sender;
        }

       
$result[0] = str_replace('"', '', $result[0]);
       
$result[0] = str_replace("'", '', $result[0]);

        return
$result;
    }

    protected function
decodeBody($input, $encoding = '7bit')
    {
        switch (
strtolower($encoding)) {
            case
'7bit':
                return
$input;

            case
'quoted-printable':
                return
$this->quotedPrintableDecode($input);

            case
'base64':
                return
base64_decode($input);

            default:
                return
$input;
        }
    }

    protected function
quotedPrintableDecode($input)
    {
       
// Remove soft line breaks
       
$input = preg_replace("/=\r?\n/", '', $input);

       
// Replace encoded characters
       
$input = preg_replace_callback('/=([a-f0-9]{2})/i', [$this, 'quotedPrintableDecodeHandler'], $input);

        return
$input;
    }

    protected function
quotedPrintableDecodeHandler($m)
    {
        return
chr(hexdec($m[1]));
    }
}