/*
 * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.xbib.net.mail.imap.protocol;

import jakarta.mail.internet.ParameterList;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xbib.net.mail.iap.ParsingException;
import org.xbib.net.mail.iap.Response;
import org.xbib.net.mail.util.PropUtil;

/**
 * A BODYSTRUCTURE response.
 *
 * @author John Mani
 * @author Bill Shannon
 */

public class BODYSTRUCTURE implements Item {
    
    private static final Logger logger = Logger.getLogger(BODYSTRUCTURE.class.getName());

    static final char[] name =
            {'B', 'O', 'D', 'Y', 'S', 'T', 'R', 'U', 'C', 'T', 'U', 'R', 'E'};
    public int msgno;

    public String type;        // Type
    public String subtype;    // Subtype
    public String encoding;    // Encoding
    public int lines = -1;    // Size in lines
    public int size = -1;    // Size in bytes
    public String disposition;    // Disposition
    public String id;        // Content-ID
    public String description;    // Content-Description
    public String md5;        // MD-5 checksum
    public String attachment;    // Attachment name
    public ParameterList cParams; // Body parameters
    public ParameterList dParams; // Disposition parameters
    public String[] language;    // Language
    public BODYSTRUCTURE[] bodies; // array of BODYSTRUCTURE objects
    //  for multipart & message/rfc822
    public ENVELOPE envelope;    // for message/rfc822

    private static int SINGLE = 1;
    private static int MULTI = 2;
    private static int NESTED = 3;
    private int processedType;    // MULTI | SINGLE | NESTED

    // special debugging output to debug parsing errors
    private static final boolean parseDebug =
            PropUtil.getBooleanSystemProperty("mail.imap.parse.debug", false);


    public BODYSTRUCTURE(FetchResponse r) throws ParsingException {
        if (parseDebug)
            logger.log(Level.FINEST, "DEBUG IMAP: parsing BODYSTRUCTURE");
        msgno = r.getNumber();
        if (parseDebug)
            logger.log(Level.FINEST, "DEBUG IMAP: msgno " + msgno);

        r.skipSpaces();

        if (r.readByte() != '(')
            throw new ParsingException(
                    "BODYSTRUCTURE parse error: missing ``('' at start");

        if (r.peekByte() == '(') { // multipart
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: parsing multipart");
            type = "multipart";
            processedType = MULTI;
            List<BODYSTRUCTURE> v = new ArrayList<>(1);
            do {
                v.add(new BODYSTRUCTURE(r));
                /*
                 * Even though the IMAP spec says there can't be any spaces
                 * between parts, some servers erroneously put a space in
                 * here.  In the spirit of "be liberal in what you accept",
                 * we skip it.
                 */
                r.skipSpaces();
            } while (r.peekByte() == '(');

            // setup bodies.
            bodies = v.toArray(new BODYSTRUCTURE[0]);

            subtype = r.readString(); // subtype
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: subtype " + subtype);

            if (r.isNextNonSpace(')')) { // done
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: parse DONE");
                return;
            }

            // Else, we have extension data

            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: parsing extension data");
            // Body parameters
            cParams = parseParameters(r);
            if (r.isNextNonSpace(')')) { // done
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: body parameters DONE");
                return;
            }

            // Disposition
            byte b = r.peekByte();
            if (b == '(') {
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: parse disposition");
                r.readByte();
                disposition = r.readString();
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: disposition " +
                            disposition);
                dParams = parseParameters(r);
                if (!r.isNextNonSpace(')')) // eat the end ')'
                    throw new ParsingException(
                            "BODYSTRUCTURE parse error: " +
                                    "missing ``)'' at end of disposition in multipart");
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: disposition DONE");
            } else if (b == 'N' || b == 'n') {
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: disposition NIL");
                r.skip(3); // skip 'NIL'
            } else {
		/*
		throw new ParsingException(
		    "BODYSTRUCTURE parse error: " +
		    type + "/" + subtype + ": " +
		    "bad multipart disposition, b " + b);
		*/
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: bad multipart disposition" +
                            ", applying Exchange bug workaround");
                description = r.readString();
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: multipart description " +
                            description);
                // Throw away whatever comes after it, since we have no
                // idea what it's supposed to be
                while (r.readByte() == ' ')
                    parseBodyExtension(r);
                return;
            }

            // RFC3501 allows no body-fld-lang after body-fld-disp,
            // even though RFC2060 required it
            if (r.isNextNonSpace(')')) {
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: no body-fld-lang");
                return; // done
            }

            // Language
            if (r.peekByte() == '(') { // a list follows
                language = r.readStringList();
                if (parseDebug)
                    logger.log(Level.FINEST, 
                            "DEBUG IMAP: language len " + language.length);
            } else {
                String l = r.readString();
                if (l != null) {
                    String[] la = {l};
                    language = la;
                    if (parseDebug)
                        logger.log(Level.FINEST, "DEBUG IMAP: language " + l);
                }
            }

            // RFC3501 defines an optional "body location" next,
            // but for now we ignore it along with other extensions.

            // Throw away any further extension data
            while (r.readByte() == ' ')
                parseBodyExtension(r);
        } else if (r.peekByte() == ')') {    // (illegal) empty body
            /*
             * Domino will fail to return the body structure of nested messages.
             * Fake it by providing an empty message.  Could probably do better
             * with more work...
             */
	    /*
	     * XXX - this prevents the exception, but without the exception
	     * the application has no way to know the data from the message
	     * is missing.
	     *
	    if (parseDebug)
		logger.log(Level.FINEST, "DEBUG IMAP: empty body, fake it");
	    r.readByte();
	    type = "text";
	    subtype = "plain";
	    lines = 0;
	    size = 0;
	     */
            throw new ParsingException(
                    "BODYSTRUCTURE parse error: missing body content");
        } else { // Single part
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: single part");
            type = r.readString();
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: type " + type);
            processedType = SINGLE;
            subtype = r.readString();
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: subtype " + subtype);

            // SIMS 4.0 returns NIL for a Content-Type of "binary", fix it here
            if (type == null) {
                type = "application";
                subtype = "octet-stream";
            }
            cParams = parseParameters(r);
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: cParams " + cParams);
            id = r.readString();
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: id " + id);
            description = r.readString();
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: description " + description);
            /*
             * XXX - Work around bug in Exchange 2010 that
             *       returns unquoted string.
             */
            encoding = r.readAtomString();
            if (encoding != null && encoding.equalsIgnoreCase("NIL")) {
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: NIL encoding" +
                            ", applying Exchange bug workaround");
                encoding = null;
            }
            /*
             * XXX - Work around bug in office365.com that returns
             *	     a string with a trailing space in some cases.
             */
            if (encoding != null)
                encoding = encoding.trim();
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: encoding " + encoding);
            size = r.readNumber();
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: size " + size);
            if (size < 0)
                throw new ParsingException(
                        "BODYSTRUCTURE parse error: bad ``size'' element");

            // "text/*" & "message/rfc822" types have additional data ..
            if (type.equalsIgnoreCase("text")) {
                lines = r.readNumber();
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: lines " + lines);
                if (lines < 0)
                    throw new ParsingException(
                            "BODYSTRUCTURE parse error: bad ``lines'' element");
            } else if (type.equalsIgnoreCase("message") &&
                    subtype.equalsIgnoreCase("rfc822")) {
                // Nested message
                processedType = NESTED;
                // The envelope comes next, but sadly Gmail handles nested
                // messages just like simple body parts and fails to return
                // the envelope and body structure of the message (sort of
                // like IMAP4 before rev1).
                r.skipSpaces();
                if (r.peekByte() == '(') {    // the envelope follows
                    envelope = new ENVELOPE(r);
                    if (parseDebug)
                        logger.log(Level.FINEST, 
                                "DEBUG IMAP: got envelope of nested message");
                    BODYSTRUCTURE[] bs = {new BODYSTRUCTURE(r)};
                    bodies = bs;
                    lines = r.readNumber();
                    if (parseDebug)
                        logger.log(Level.FINEST, "DEBUG IMAP: lines " + lines);
                    if (lines < 0)
                        throw new ParsingException(
                                "BODYSTRUCTURE parse error: bad ``lines'' element");
                } else {
                    if (parseDebug)
                        logger.log(Level.FINEST, "DEBUG IMAP: " +
                                "missing envelope and body of nested message");
                }
            } else {
                // Detect common error of including lines element on other types
                r.skipSpaces();
                byte bn = r.peekByte();
                if (Character.isDigit((char) bn)) // number
                    throw new ParsingException(
                            "BODYSTRUCTURE parse error: server erroneously " +
                                    "included ``lines'' element with type " +
                                    type + "/" + subtype);
            }

            if (r.isNextNonSpace(')')) {
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: parse DONE");
                return; // done
            }

            // Optional extension data

            // MD5
            md5 = r.readString();
            if (r.isNextNonSpace(')')) {
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: no MD5 DONE");
                return; // done
            }

            // Disposition
            byte b = r.readByte();
            if (b == '(') {
                disposition = r.readString();
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: disposition " +
                            disposition);
                dParams = parseParameters(r);
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: dParams " + dParams);
                if (!r.isNextNonSpace(')')) // eat the end ')'
                    throw new ParsingException(
                            "BODYSTRUCTURE parse error: " +
                                    "missing ``)'' at end of disposition");
            } else if (b == 'N' || b == 'n') {
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: disposition NIL");
                r.skip(2); // skip 'NIL'
            } else {
                throw new ParsingException(
                        "BODYSTRUCTURE parse error: " +
                                type + "/" + subtype + ": " +
                                "bad single part disposition, b " + b);
            }

            if (r.isNextNonSpace(')')) {
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: disposition DONE");
                return; // done
            }

            // Language
            if (r.peekByte() == '(') { // a list follows
                language = r.readStringList();
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: language len " +
                            language.length);
            } else { // protocol is unnessarily complex here
                String l = r.readString();
                if (l != null) {
                    String[] la = {l};
                    language = la;
                    if (parseDebug)
                        logger.log(Level.FINEST, "DEBUG IMAP: language " + l);
                }
            }

            // RFC3501 defines an optional "body location" next,
            // but for now we ignore it along with other extensions.

            // Throw away any further extension data
            while (r.readByte() == ' ')
                parseBodyExtension(r);
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: all DONE");
        }
    }

    public boolean isMulti() {
        return processedType == MULTI;
    }

    public boolean isSingle() {
        return processedType == SINGLE;
    }

    public boolean isNested() {
        return processedType == NESTED;
    }

    private ParameterList parseParameters(Response r)
            throws ParsingException {
        r.skipSpaces();

        ParameterList list = null;
        byte b = r.readByte();
        if (b == '(') {
            list = new ParameterList();
            do {
                String name = r.readString();
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: parameter name " + name);
                if (name == null)
                    throw new ParsingException(
                            "BODYSTRUCTURE parse error: " +
                                    type + "/" + subtype + ": " +
                                    "null name in parameter list");
                String value = r.readString();
                if (parseDebug)
                    logger.log(Level.FINEST, "DEBUG IMAP: parameter value " + value);
                if (value == null) {    // work around buggy servers
                    if (parseDebug)
                        logger.log(Level.FINEST, "DEBUG IMAP: NIL parameter value" +
                                ", applying Exchange bug workaround");
                    value = "";
                }
                list.set(name, value);
            } while (!r.isNextNonSpace(')'));
            list.combineSegments();
        } else if (b == 'N' || b == 'n') {
            if (parseDebug)
                logger.log(Level.FINEST, "DEBUG IMAP: parameter list NIL");
            r.skip(2);
        } else
            throw new ParsingException("Parameter list parse error");

        return list;
    }

    private void parseBodyExtension(Response r) throws ParsingException {
        r.skipSpaces();

        byte b = r.peekByte();
        if (b == '(') {
            r.skip(1); // skip '('
            do {
                parseBodyExtension(r);
            } while (!r.isNextNonSpace(')'));
        } else if (Character.isDigit((char) b)) // number
            r.readNumber();
        else // nstring
            r.readString();
    }
}
