Read the message structure returend by the IMAP server
and build flat lists of content parts and attachments

@param rcube_message_part $structure Message structure node
@param bool               $recursive True when called recursively
964921
private function parse_structure($structure, $recursive = false)
    {
        // real content-type of message/rfc822 part
        if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype) {
            $mimetype = $structure->real_mimetype;

            // parse headers from message/rfc822 part
            if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) {
                list($headers, ) = explode("\r\n\r\n", $this->get_part_body($structure->mime_id, false, 32768));
                $structure->headers = rcube_mime::parse_headers($headers);

                if ($this->context == $structure->mime_id) {
                    $this->headers = rcube_message_header::from_array($structure->headers);
                }
            }
        }
        else {
            $mimetype = $structure->mimetype;
        }

        // show message headers
        if ($recursive && is_array($structure->headers) &&
            (isset($structure->headers['subject']) || $structure->headers['from'] || $structure->headers['to'])
        ) {
            $c = new stdClass;
            $c->type = 'headers';
            $c->headers = $structure->headers;
            $this->add_part($c);
        }

        // Allow plugins to handle message parts
        $plugin = $this->app->plugins->exec_hook('message_part_structure',
            array('object' => $this, 'structure' => $structure,
                'mimetype' => $mimetype, 'recursive' => $recursive));

        if ($plugin['abort']) {
            return;
        }

        $structure = $plugin['structure'];
        $mimetype  = $plugin['mimetype'];
        $recursive = $plugin['recursive'];

        list($message_ctype_primary, $message_ctype_secondary) = explode('/', $mimetype);

        // print body if message doesn't have multiple parts
        if ($message_ctype_primary == 'text' && !$recursive) {
            // parts with unsupported type add to attachments list
            if (!in_array($message_ctype_secondary, array('plain', 'html', 'enriched'))) {
                $this->add_part($structure, 'attachment');
                return;
            }

            $structure->type = 'content';
            $this->add_part($structure);

            // Parse simple (plain text) message body
            if ($message_ctype_secondary == 'plain') {
                foreach ((array)$this->uu_decode($structure) as $uupart) {
                    $this->mime_parts[$uupart->mime_id] = $uupart;
                    $this->add_part($uupart, 'attachment');
                }
            }
        }
        // the same for pgp signed messages
        else if ($mimetype == 'application/pgp' && !$recursive) {
            $structure->type = 'content';
            $this->add_part($structure);
        }
        // message contains (more than one!) alternative parts
        else if ($mimetype == 'multipart/alternative'
            && is_array($structure->parts) && count($structure->parts) > 1
        ) {
            // get html/plaintext parts, other add to attachments list
            foreach ($structure->parts as $p => $sub_part) {
                $sub_mimetype = $sub_part->mimetype;
                $is_multipart = preg_match('/^multipart\/(related|relative|mixed|alternative)/', $sub_mimetype);

                // skip empty text parts
                if (!$sub_part->size && !$is_multipart) {
                    continue;
                }

                // We've encountered (malformed) messages with more than
                // one text/plain or text/html part here. There's no way to choose
                // which one is better, so we'll display first of them and add
                // others as attachments (#1489358)

                // check if sub part is
                if ($is_multipart)
                    $related_part = $p;
                else if ($sub_mimetype == 'text/plain' && !$plain_part)
                    $plain_part = $p;
                else if ($sub_mimetype == 'text/html' && !$html_part) {
                    $html_part = $p;
                    $this->got_html_part = true;
                }
                else if ($sub_mimetype == 'text/enriched' && !$enriched_part)
                    $enriched_part = $p;
                else {
                    // add unsupported/unrecognized parts to attachments list
                    $this->add_part($sub_part, 'attachment');
                }
            }

            // parse related part (alternative part could be in here)
            if ($related_part !== null && !$this->parse_alternative) {
                $this->parse_alternative = true;
                $this->parse_structure($structure->parts[$related_part], true);
                $this->parse_alternative = false;

                // if plain part was found, we should unset it if html is preferred
                if ($this->opt['prefer_html'] && count($this->parts)) {
                    $plain_part = null;
                }
            }

            // choose html/plain part to print
            if ($html_part !== null && $this->opt['prefer_html']) {
                $print_part = $structure->parts[$html_part];
            }
            else if ($enriched_part !== null) {
                $print_part = $structure->parts[$enriched_part];
            }
            else if ($plain_part !== null) {
                $print_part = $structure->parts[$plain_part];
            }

            // add the right message body
            if (is_object($print_part)) {
                $print_part->type = 'content';

                // Allow plugins to handle also this part
                $plugin = $this->app->plugins->exec_hook('message_part_structure',
                    array('object' => $this, 'structure' => $print_part,
                        'mimetype' => $print_part->mimetype, 'recursive' => true));

                if (!$plugin['abort']) {
                    $this->add_part($print_part);
                }
            }
            // show plaintext warning
            else if ($html_part !== null && empty($this->parts)) {
                $c = new stdClass;
                $c->type            = 'content';
                $c->ctype_primary   = 'text';
                $c->ctype_secondary = 'plain';
                $c->mimetype        = 'text/plain';
                $c->realtype        = 'text/html';

                $this->add_part($c);
            }
        }
        // this is an ecrypted message -> create a plaintext body with the according message
        else if ($mimetype == 'multipart/encrypted') {
            $p = new stdClass;
            $p->type            = 'content';
            $p->ctype_primary   = 'text';
            $p->ctype_secondary = 'plain';
            $p->mimetype        = 'text/plain';
            $p->realtype        = 'multipart/encrypted';
            $p->mime_id         = $structure->mime_id;

            $this->add_part($p);

            // add encrypted payload part as attachment
            if (is_array($structure->parts)) {
                for ($i=0; $i < count($structure->parts); $i++) {
                    $subpart = $structure->parts[$i];
                    if ($subpart->mimetype == 'application/octet-stream' || !empty($subpart->filename)) {
                        $this->add_part($subpart, 'attachment');
                    }
                }
            }
        }
        // this is an S/MIME ecrypted message -> create a plaintext body with the according message
        else if ($mimetype == 'application/pkcs7-mime') {
            $p = new stdClass;
            $p->type            = 'content';
            $p->ctype_primary   = 'text';
            $p->ctype_secondary = 'plain';
            $p->mimetype        = 'text/plain';
            $p->realtype        = 'application/pkcs7-mime';
            $p->mime_id         = $structure->mime_id;

            $this->add_part($p);

            if (!empty($structure->filename)) {
                $this->add_part($structure, 'attachment');
            }
        }
        // message contains multiple parts
        else if (is_array($structure->parts) && !empty($structure->parts)) {
            // iterate over parts
            for ($i=0; $i < count($structure->parts); $i++) {
                $mail_part      = &$structure->parts[$i];
                $primary_type   = $mail_part->ctype_primary;
                $secondary_type = $mail_part->ctype_secondary;
                $part_mimetype  = $mail_part->mimetype;

                // multipart/alternative or message/rfc822
                if ($primary_type == 'multipart' || $part_mimetype == 'message/rfc822') {
                    // list message/rfc822 as attachment as well
                    if ($part_mimetype == 'message/rfc822') {
                        $this->add_part($mail_part, 'attachment');
                    }

                    $this->parse_structure($mail_part, true);
                }
                // part text/[plain|html] or delivery status
                else if ((($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment') ||
                    in_array($part_mimetype, array('message/delivery-status', 'text/rfc822-headers', 'message/disposition-notification'))
                ) {
                    // Allow plugins to handle also this part
                    $plugin = $this->app->plugins->exec_hook('message_part_structure',
                        array('object' => $this, 'structure' => $mail_part,
                            'mimetype' => $part_mimetype, 'recursive' => true));

                    if ($plugin['abort']) {
                        continue;
                    }

                    if ($part_mimetype == 'text/html' && $mail_part->size) {
                        $this->got_html_part = true;
                    }

                    $mail_part = $plugin['structure'];
                    list($primary_type, $secondary_type) = explode('/', $plugin['mimetype']);

                    // add text part if it matches the prefs
                    if (!$this->parse_alternative ||
                        ($secondary_type == 'html' && $this->opt['prefer_html']) ||
                        ($secondary_type == 'plain' && !$this->opt['prefer_html'])
                    ) {
                        $mail_part->type = 'content';
                        $this->add_part($mail_part);
                    }

                    // list as attachment as well
                    if (!empty($mail_part->filename)) {
                        $this->add_part($mail_part, 'attachment');
                    }
                }
                // ignore "virtual" protocol parts
                else if ($primary_type == 'protocol') {
                    continue;
                }
                // part is Microsoft Outlook TNEF (winmail.dat)
                else if ($part_mimetype == 'application/ms-tnef') {
                    $tnef_parts = (array) $this->tnef_decode($mail_part);
                    foreach ($tnef_parts as $tpart) {
                        $this->mime_parts[$tpart->mime_id] = $tpart;
                        $this->add_part($tpart, 'attachment');
                    }

                    // add winmail.dat to the list if it's content is unknown
                    if (empty($tnef_parts) && !empty($mail_part->filename)) {
                        $this->mime_parts[$mail_part->mime_id] = $mail_part;
                        $this->add_part($mail_part, 'attachment');
                    }
                }
                // part is a file/attachment
                else if (preg_match('/^(inline|attach)/', $mail_part->disposition) ||
                    $mail_part->headers['content-id'] ||
                    ($mail_part->filename &&
                        (empty($mail_part->disposition) || preg_match('/^[a-z0-9!#$&.+^_-]+$/i', $mail_part->disposition)))
                ) {
                    // skip apple resource forks
                    if ($message_ctype_secondary == 'appledouble' && $secondary_type == 'applefile')
                        continue;

                    // part belongs to a related message and is linked
                    if (preg_match('/^multipart\/(related|relative)/', $mimetype)
                        && ($mail_part->headers['content-id'] || $mail_part->headers['content-location'])
                    ) {
                        if ($mail_part->headers['content-id'])
                            $mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']);
                        if ($mail_part->headers['content-location'])
                            $mail_part->content_location = $mail_part->headers['content-base'] . $mail_part->headers['content-location'];

                        $this->add_part($mail_part, 'inline');
                    }
                    // regular attachment with valid content type
                    // (content-type name regexp according to RFC4288.4.2)
                    else if (preg_match('/^[a-z0-9!#$&.+^_-]+\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) {
                        $this->add_part($mail_part, 'attachment');
                    }
                    // attachment with invalid content type
                    // replace malformed content type with application/octet-stream (#1487767)
                    else if ($mail_part->filename) {
                        $mail_part->ctype_primary   = 'application';
                        $mail_part->ctype_secondary = 'octet-stream';
                        $mail_part->mimetype        = 'application/octet-stream';

                        $this->add_part($mail_part, 'attachment');
                    }
                }
                // calendar part not marked as attachment (#1490325)
                else if ($part_mimetype == 'text/calendar') {
                    if (!$mail_part->filename) {
                        $mail_part->filename = 'calendar.ics';
                    }

                    $this->add_part($mail_part, 'attachment');
                }
            }

            // if this was a related part try to resolve references
            if (preg_match('/^multipart\/(related|relative)/', $mimetype) && count($this->inline_parts)) {
                $a_replaces = array();
                $img_regexp = '/^image\/(gif|jpe?g|png|tiff|bmp|svg)/';

                foreach ($this->inline_parts as $inline_object) {
                    $part_url = $this->get_part_url($inline_object->mime_id, $inline_object->ctype_primary);
                    if (isset($inline_object->content_id))
                        $a_replaces['cid:'.$inline_object->content_id] = $part_url;
                    if ($inline_object->content_location) {
                        $a_replaces[$inline_object->content_location] = $part_url;
                    }

                    if (!empty($inline_object->filename)) {
                        // MS Outlook sends sometimes non-related attachments as related
                        // In this case multipart/related message has only one text part
                        // We'll add all such attachments to the attachments list
                        if (!isset($this->got_html_part)) {
                            $this->add_part($inline_object, 'attachment');
                        }
                        // MS Outlook sometimes also adds non-image attachments as related
                        // We'll add all such attachments to the attachments list
                        // Warning: some browsers support pdf in <img/>
                        else if (!preg_match($img_regexp, $inline_object->mimetype)) {
                            $this->add_part($inline_object, 'attachment');
                        }
                        // @TODO: we should fetch HTML body and find attachment's content-id
                        // to handle also image attachments without reference in the body
                        // @TODO: should we list all image attachments in text mode?
                    }
                }

                // add replace array to each content part
                // (will be applied later when part body is available)
                foreach ($this->parts as $i => $part) {
                    if ($part->type == 'content')
                        $this->parts[$i]->replaces = $a_replaces;
                }
            }
        }
        // message is a single part non-text
        else if ($structure->filename || preg_match('/^application\//i', $mimetype)) {
            $this->add_part($structure, 'attachment');
        }
    }
890039
protected function selectmultiple_controls(structure $structure) {
        $output = '';

        // Bulk action button delete and bulk action button cancel.
        $buttondeleteoptions = array(
            'type' => 'button',
            'id' => 'selectmultipledeletecommand',
            'value' => get_string('deleteselected', 'mod_quiz'),
            'class' => 'btn btn-secondary'
        );
        $buttoncanceloptions = array(
            'type' => 'button',
            'id' => 'selectmultiplecancelcommand',
            'value' => get_string('cancel', 'moodle'),
            'class' => 'btn btn-secondary'
        );

        $groupoptions = array(
            'class' => 'btn-group selectmultiplecommand actions',
            'role' => 'group'
        );

        $output .= html_writer::tag('div',
                        html_writer::tag('button', get_string('deleteselected', 'mod_quiz'), $buttondeleteoptions) .
                        " " .
                        html_writer::tag('button', get_string('cancel', 'moodle'),
                $buttoncanceloptions), $groupoptions);

        $toolbaroptions = array(
            'class' => 'btn-toolbar',
            'role' => 'toolbar',
            'aria-label' => get_string('selectmultipletoolbar', 'quiz'),
        );

        // Select all/deselect all questions.
        $buttonselectalloptions = array(
            'role' => 'button',
            'id' => 'questionselectall',
            'class' => 'btn btn-link'
        );
        $buttondeselectalloptions = array(
            'role' => 'button',
            'id' => 'questiondeselectall',
            'class' => 'btn btn-link'
        );
        $output .= html_writer::tag('div',
                html_writer::tag('div',
                        html_writer::link('#', get_string('selectall', 'quiz'), $buttonselectalloptions) .
                        html_writer::tag('span', "/", ['class' => 'separator']) .
                        html_writer::link('#', get_string('selectnone', 'quiz'), $buttondeselectalloptions),
                        array('class' => 'btn-group selectmultiplecommandbuttons')),
                $toolbaroptions);
        return $output;
    }
371892
public static function toObject($resource, $recursive = false) {
        if (static::isObject($resource)) {
            if (!$recursive) {
                return $resource;
            }

        } else if (static::isArray($resource)) {
            // Continue

        } else if (static::isJson($resource)) {
            $resource = json_decode($resource, true);

        } else if (static::isSerialized($resource)) {
            $resource = unserialize($resource);

        } else if (static::isXml($resource)) {
            $resource = static::xmlToArray(simplexml_load_string($resource));
        }

        return static::buildObject($resource);
    }
995542
private function prepareStructure($structure)
    {
        $newStructure = [];
        foreach ($structure as $path => $element) {
            $position = &$newStructure;
            foreach (explode('/', $path) as $part) {
                $position[$part] = [];
                $position = &$position[$part];
            }
            $position = strpos($path, DIRECTORY_SEPARATOR) === false ? [] : $position;
            $position = is_array($element) ? $this->prepareStructure($element) : (string) $element;
        }
        return $newStructure;
    }
1353313
public function save( &$structure )
    {
        if ( is_object( $structure ) )
        {
            $data = $structure;
        }
        else if ( is_array( $structure ) )
        {
            $data = (object) $structure;
        }
        else
        {
            return 0;
        }

        $result = 0;
        $rootId = isset( $data->rootId )        ? (int) $data->rootId   : null;
        $rules  = isset( $data->rules )         ? (array) $data->rules  : null;
        $extra  = isset( $data->extraContent )  ? $data->extraContent   : null;

        $result += $this->saveRules( $rootId ?: null, $rules );
        $result += $this->saveExtra( $rootId ?: null, $extra );
        return $result;
    }
1009688
protected function doRawList($dir, $recursive = false)
    {
        $connection = $this->connection;

        return $this->retry(function () use ($connection, $dir, $recursive) {
            $res = @ftp_rawlist($connection, $dir, $recursive);

            if (false === $res) {
                throw new \RuntimeException("Rawlist failed: ".$dir);
            }

            $items = array();

            foreach ($res as $child) {
                $chunks = preg_split("/\s+/", $child);
                list($item['rights'], $item['number'], $item['user'], $item['group'], $item['size'], $item['month'], $item['day'], $item['time']) = $chunks;
                $item['type'] = $chunks[0]{0}
                === 'd' ? 'directory' : 'file';
                array_splice($chunks, 0, 8);
                $item['name'] = implode(" ", $chunks);
                $item['pathname'] = \rtrim($dir, '/').'/'.$item['name'];
                $items[] = $item;
            }

            return $items;
        });
    }
836203
private function parse_body(&$body)
    {
        // Mail_mimeDecode need \r\n end-line, but gpg may return \n
        $body = preg_replace('/\r?\n/', "\r\n", $body);

        // parse the body into structure
        $struct = rcube_mime::parse_message($body);

        return $struct;
    }
22447
public function json($fields = array(), $recursive = true)
    {
        $list = array();
        foreach ($this->getModels() as $model) {
            $list[] = $model->json($fields, $recursive);
        }
        return $list;
    }
1119465
public function listContents($directory = '', $recursive = false)
    {
        [$filesystem, $directory, $prefix] = $this->getFilesystemAndPath($directory);
        $result = $filesystem->listContents($directory, $recursive);

        foreach ($result as &$file) {
            $file['filesystem'] = $prefix;
        }

        return $result;
    }
119908
private function lazyParseStructure(): void
    {
        if (true === $this->structureParsed) {
            return;
        }
        $this->structureParsed = true;

        $this->lazyLoadStructure();

        $this->type = self::$typesMap[$this->structure->type] ?? self::TYPE_UNKNOWN;

        // In our context, \ENCOTHER is as useful as an uknown encoding
        $this->encoding = self::$encodingsMap[$this->structure->encoding] ?? self::ENCODING_UNKNOWN;
        $this->subtype  = $this->structure->subtype;

        if (isset($this->structure->bytes)) {
            $this->bytes = $this->structure->bytes;
        }
        if ($this->structure->ifdisposition) {
            $this->disposition = $this->structure->disposition;
        }
        if ($this->structure->ifdescription) {
            $this->description = $this->structure->description;
        }

        $this->parameters = new Parameters();
        if ($this->structure->ifparameters) {
            $this->parameters->add($this->structure->parameters);
        }

        if ($this->structure->ifdparameters) {
            $this->parameters->add($this->structure->dparameters);
        }

        // When the message is not multipart and the body is the attachment content
        // Prevents infinite recursion
        if (self::isAttachment($this->structure) && !$this instanceof Attachment) {
            $this->parts[] = new Attachment($this->resource, $this->getNumber(), '1', $this->structure);
        }

        if (isset($this->structure->parts)) {
            $parts = $this->structure->parts;
            // https://secure.php.net/manual/en/function.imap-fetchbody.php#89002
            if ($this instanceof Attachment && $this->isEmbeddedMessage() && 1 === \count($parts) && \TYPEMULTIPART === $parts[0]->type) {
                $parts = $parts[0]->parts;
            }
            foreach ($parts as $key => $partStructure) {
                $partNumber = (!$this instanceof Message) ? $this->partNumber . '.' : '';
                $partNumber .= (string) ($key + 1);

                $newPartClass = self::isAttachment($partStructure)
                    ? Attachment::class
                    : SimplePart::class
                ;

                $this->parts[] = new $newPartClass($this->resource, $this->getNumber(), $partNumber, $partStructure);
            }
        }
    }
1145646
public function getMessageStructure()
	{
		if( !$this->structure )
			$this->structure	= imap_fetchstructure( $this->stream, $this->messageNumber );
		return $this->structure;
	}
665820
public static function readStructure()
    {
        if(count(self::$structure) == 0 ){
            if(self::USE_CACHE == false){
                //rebuild structure each time
                self::$structure = self::buildStructures();

            } else {
                //cache management
                try {
                    self::$structure = \common_cache_FileCache::singleton()->get(self::CACHE_KEY);
                } catch (\common_cache_NotFoundException $e) {
                    self::$structure = self::buildStructures();
                    \common_cache_FileCache::singleton()->put(self::$structure, self::CACHE_KEY);
                }

            }
        }
        return self::$structure;
    }
1301669
public function readDirectory($directory, $recursive = true)
    {
        if (is_dir($directory) === false) {
            return false;
        }

        try {
            $resource = opendir($directory);
            $contents = array();

            while (false !== ($item = readdir($resource)))
            {
                if (in_array($item, array('.', '..'))
                    || substr($item, 0, 1) == '_') {
                    continue;
                }

                $absoluteItem = $directory . DIRECTORY_SEPARATOR . $item;
                if ($recursive === true && is_dir($absoluteItem)) {
                    $contents[$item] = $this->readDirectory($absoluteItem, $recursive);
                } else {
                    $contents[$item] = $absoluteItem;
                }
            }
        } catch(\Exception $e) {
            return false;
        }

        return $contents;
    }
960461
public function load($data, $recursive = false)
    {
        foreach ($data as $name => $value) {
            $name = trim($name);

            if (is_numeric($name)) {
                continue;
            }

            $this->$name = $recursive && \is_array($value) ? static::create($value, $recursive) : $value;
        }

        return $this;
    }
489479
private function implodeRlpParts(StructureInterface $structure, array $parts, $separator = '-')
    {
        $title = '';
        // concat rlp parts in sort of priority
        foreach ($structure->getPropertiesByTagName('sulu.rlp.part') as $property) {
            if (array_key_exists($property->getName(), $parts)) {
                $title = $parts[$property->getName()] . $separator . $title;
            }
        }
        $title = substr($title, 0, -1);

        return $title;
    }
1059
private function resolveView(StructureInterface $structure, array $data)
    {
        $structure->setWebspaceKey($this->getWebspaceKey());

        $view = [];
        foreach ($structure->getProperties(true) as $child) {
            if (array_key_exists($child->getName(), $data)) {
                $child->setValue($data[$child->getName()]);
            }

            $contentType = $this->contentTypeManager->get($child->getContentTypeName());
            $view[$child->getName()] = $contentType->getViewData($child);
        }

        return $view;
    }
223439
public function calculateHash($recursive = false) {
        if (!$this->loaded) {
            $this->hash = null;
        } else {
            $key = $this->name . $this->path . $this->comment;
            if ($this->tables) {
                foreach ($this->tables as $tbl) {
                    $table = $this->getTable($tbl->getName());
                    if ($recursive) {
                        $table->calculateHash($recursive);
                    }
                    $key .= $table->getHash();
                }
            }
            $this->hash = md5($key);
        }
    }