/*
 * 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.pop3;

import jakarta.mail.AuthenticationFailedException;
import jakarta.mail.Folder;
import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import jakarta.mail.Store;
import jakarta.mail.URLName;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xbib.net.mail.util.MailConnectException;
import org.xbib.net.mail.util.PropUtil;
import org.xbib.net.mail.util.SocketConnectException;

/**
 * A POP3 Message Store.  Contains only one folder, "INBOX".
 *
 * See the <a href="package-summary.html">org.xbib.net.mail.pop3</a> package
 * documentation for further information on the POP3 protocol provider. <p>
 *
 * @author Bill Shannon
 * @author John Mani
 */
public class POP3Store extends Store {

    private static final Logger logger = Logger.getLogger(POP3Store.class.getName());

    private String name = "pop3";        // my protocol name
    private int defaultPort = 110;        // default POP3 port
    private boolean isSSL = false;        // use SSL?

    private Protocol port = null;        // POP3 port for self
    private POP3Folder portOwner = null;    // folder owning port
    private String host = null;            // host
    private int portNum = -1;
    private String user = null;
    private String passwd = null;
    private boolean useStartTLS = false;
    private boolean requireStartTLS = false;
    private boolean usingSSL = false;
    private Map<String, String> capabilities;

    // following set here and accessed by other classes in this package
    volatile Constructor<?> messageConstructor = null;
    volatile boolean rsetBeforeQuit = false;
    volatile boolean disableTop = false;
    volatile boolean forgetTopHeaders = false;
    volatile boolean supportsUidl = true;
    volatile boolean cacheWriteTo = false;
    volatile boolean useFileCache = false;
    volatile File fileCacheDir = null;
    volatile boolean keepMessageContent = false;
    volatile boolean finalizeCleanClose = false;

    public POP3Store(Session session, URLName url) {
        this(session, url, "pop3", false);
    }

    public POP3Store(Session session, URLName url,
                     String name, boolean isSSL) {
        super(session, url);
        if (url != null)
            name = url.getProtocol();
        this.name = name;
        if (!isSSL)
            isSSL = PropUtil.getBooleanProperty(session.getProperties(),
                    "mail." + name + ".ssl.enable", false);
        if (isSSL)
            this.defaultPort = 995;
        else
            this.defaultPort = 110;
        this.isSSL = isSSL;

        rsetBeforeQuit = getBoolProp("rsetbeforequit");
        disableTop = getBoolProp("disabletop");
        forgetTopHeaders = getBoolProp("forgettopheaders");
        cacheWriteTo = getBoolProp("cachewriteto");
        useFileCache = getBoolProp("filecache.enable");
        String dir = session.getProperty("mail." + name + ".filecache.dir");
        if (dir != null && logger.isLoggable(Level.CONFIG))
            logger.config("mail." + name + ".filecache.dir: " + dir);
        if (dir != null)
            fileCacheDir = new File(dir);
        keepMessageContent = getBoolProp("keepmessagecontent");

        // mail.pop3.starttls.enable enables use of STLS command
        useStartTLS = getBoolProp("starttls.enable");

        // mail.pop3.starttls.required requires use of STLS command
        requireStartTLS = getBoolProp("starttls.required");

        // mail.pop3.finalizecleanclose requires clean close when finalizing
        finalizeCleanClose = getBoolProp("finalizecleanclose");

        String s = session.getProperty("mail." + name + ".message.class");
        if (s != null) {
            logger.log(Level.CONFIG, "message class: {0}", s);
            try {
                ClassLoader cl = this.getClass().getClassLoader();

                // now load the class
                Class<?> messageClass = null;
                try {
                    // First try the "application's" class loader.
                    // This should eventually be replaced by
                    // Thread.currentThread().getContextClassLoader().
                    messageClass = Class.forName(s, false, cl);
                } catch (ClassNotFoundException ex1) {
                    // That didn't work, now try the "system" class loader.
                    // (Need both of these because JDK 1.1 class loaders
                    // may not delegate to their parent class loader.)
                    messageClass = Class.forName(s);
                }

                Class<?>[] c = {Folder.class, int.class};
                messageConstructor = messageClass.getConstructor(c);
            } catch (Exception ex) {
                logger.log(Level.CONFIG, "failed to load message class", ex);
            }
        }
    }

    /**
     * Get the value of a boolean property.
     * Print out the value if logging is enabled.
     */
    private final synchronized boolean getBoolProp(String prop) {
        prop = "mail." + name + "." + prop;
        boolean val = PropUtil.getBooleanProperty(session.getProperties(),
                prop, false);
        if (logger.isLoggable(Level.CONFIG))
            logger.config(prop + ": " + val);
        return val;
    }

    /**
     * Get a reference to the session.
     */
    synchronized Session getSession() {
        return session;
    }

    @Override
    public synchronized boolean protocolConnect(String host, int portNum,
                                                   String user, String passwd) throws MessagingException {

        // check for non-null values of host, password, user
        if (host == null || passwd == null || user == null)
            return false;

        // if port is not specified, set it to value of mail.pop3.port
        // property if it exists, otherwise default to 110
        if (portNum == -1)
            portNum = PropUtil.getIntProperty(session.getProperties(),
                    "mail." + name + ".port", -1);

        if (portNum == -1)
            portNum = defaultPort;

        this.host = host;
        this.portNum = portNum;
        this.user = user;
        this.passwd = passwd;
        try {
            port = getPort(null);
        } catch (EOFException eex) {
            throw new AuthenticationFailedException(eex.getMessage());
        } catch (SocketConnectException scex) {
            throw new MailConnectException(scex);
        } catch (IOException ioex) {
            throw new MessagingException("Connect failed", ioex);
        }

        return true;
    }

    /**
     * Check whether this store is connected. Override superclass
     * method, to actually ping our server connection.
     */
    /*
     * Note that we maintain somewhat of an illusion of being connected
     * even if we're not really connected.  This is because a Folder
     * can use the connection and close it when it's done.  If we then
     * ask whether the Store's connected we want the answer to be true,
     * as long as we can reconnect at that point.  This means that we
     * need to be able to reconnect the Store on demand.
     */
    @Override
    public synchronized boolean isConnected() {
        if (!super.isConnected())
            // if we haven't been connected at all, don't bother with
            // the NOOP.
            return false;
        try {
            if (port == null)
                port = getPort(null);
            else if (!port.noop())
                throw new IOException("NOOP failed");
            return true;
        } catch (IOException ioex) {
            // no longer connected, close it down
            try {
                super.close();        // notifies listeners
            } catch (MessagingException mex) {
                // ignore it
            }
            return false;
        }
    }

    synchronized Protocol getPort(POP3Folder owner) throws IOException {
        Protocol p;

        // if we already have a port, remember who's using it
        if (port != null && portOwner == null) {
            portOwner = owner;
            return port;
        }

        // need a new port, create it and try to login
        p = new Protocol(host, portNum, session.getProperties(), "mail." + name, isSSL);

        if (useStartTLS || requireStartTLS) {
            if (p.hasCapability("STLS")) {
                if (p.stls()) {
                    // success, refresh capabilities
                    p.setCapabilities(p.capa());
                } else if (requireStartTLS) {
                    logger.fine("STLS required but failed");
                    throw cleanupAndThrow(p,
                            new EOFException("STLS required but failed"));
                }
            } else if (requireStartTLS) {
                logger.fine("STLS required but not supported");
                throw cleanupAndThrow(p,
                        new EOFException("STLS required but not supported"));
            }
        }

        capabilities = p.getCapabilities();    // save for later, may be null
        usingSSL = p.isSSL();            // in case anyone asks

        /*
         * If we haven't explicitly disabled use of the TOP command,
         * and the server has provided its capabilities,
         * and the server doesn't support the TOP command,
         * disable the TOP command.
         */
        if (!disableTop &&
                capabilities != null && !capabilities.containsKey("TOP")) {
            disableTop = true;
            logger.fine("server doesn't support TOP, disabling it");
        }

        supportsUidl = capabilities == null || capabilities.containsKey("UIDL");

        try {
            if (!authenticate(p, user, passwd))
                throw cleanupAndThrow(p, new EOFException("login failed"));
        } catch (EOFException ex) {
            throw cleanupAndThrow(p, ex);
        } catch (Exception ex) {
            throw cleanupAndThrow(p, new EOFException(ex.getMessage()));
        }


        /*
         * If a Folder closes the port, and then a Folder
         * is opened, the Store won't have a port.  In that
         * case, the getPort call will come from Folder.open,
         * but we need to keep track of the port in the Store
         * so that a later call to Folder.isOpen, which calls
         * Store.isConnected, will use the same port.
         */
        if (port == null && owner != null) {
            port = p;
            portOwner = owner;
        }
        if (portOwner == null)
            portOwner = owner;
        return p;
    }

    private static IOException cleanupAndThrow(Protocol p, IOException ife) {
        try {
            p.quit();
        } catch (Throwable thr) {
            if (isRecoverable(thr)) {
                ife.addSuppressed(thr);
            } else {
                thr.addSuppressed(ife);
                if (thr instanceof Error) {
                    throw (Error) thr;
                }
                if (thr instanceof RuntimeException) {
                    throw (RuntimeException) thr;
                }
                throw new RuntimeException("unexpected exception", thr);
            }
        }
        return ife;
    }

    /**
     * Authenticate to the server.
     *
     * XXX - This extensible authentication mechanism scheme was adapted
     * from the SMTPTransport class.  The work was done at the last
     * minute for the 1.6.5 release and so is not as clean as it
     * could be.  There's great confusion over boolean success/failure
     * return codes vs exceptions.  This should all be cleaned up at
     * some point, and more testing should be done, but I'm leaving
     * it in this "I believe it works" state for now.  I've tested
     * it with LOGIN, PLAIN, and XOAUTH2 mechanisms, the latter being
     * the primary motivation for the work right now.
     *
     * @param    p    the Protocol object to use
     * @param    user    the user to authenticate as
     * @param    passwd    the password for the user
     * @return true if authentication succeeds
     * @exception MessagingException    if authentication fails
     * @since Jakarta Mail 1.6.5
     */
    private boolean authenticate(Protocol p, String user, String passwd)
            throws MessagingException {
        // setting mail.pop3.auth.mechanisms controls which mechanisms will
        // be used, and in what order they'll be considered.  only the first
        // match is used.
        String mechs = session.getProperty("mail." + name + ".auth.mechanisms");
        boolean usingDefaultMechs = false;
        if (mechs == null) {
            mechs = p.getDefaultMechanisms();
            usingDefaultMechs = true;
        }

        String authzid =
                session.getProperty("mail." + name + ".sasl.authorizationid");
        if (authzid == null)
            authzid = user;
	/*
	 * XXX - maybe someday
	 *
	if (enableSASL) {
	    logger.fine("Authenticate with SASL");
	    try {
		if (sasllogin(getSASLMechanisms(), getSASLRealm(), authzid,
				user, passwd)) {
		    return true;	// success
		} else {
		    logger.fine("SASL authentication failed");
		    return false;
		}
	    } catch (UnsupportedOperationException ex) {
		logger.log(Level.FINE, "SASL support failed", ex);
		// if the SASL support fails, fall back to non-SASL
	    }
	}
	 */

        if (logger.isLoggable(Level.FINE))
            logger.fine("Attempt to authenticate using mechanisms: " + mechs);

        /*
         * Loop through the list of mechanisms supplied by the user
         * (or defaulted) and try each in turn.  If the server supports
         * the mechanism and we have an authenticator for the mechanism,
         * and it hasn't been disabled, use it.
         */
        StringTokenizer st = new StringTokenizer(mechs);
        while (st.hasMoreTokens()) {
            String m = st.nextToken();
            m = m.toUpperCase(Locale.ENGLISH);
            if (!p.supportsMechanism(m)) {
                logger.log(Level.FINE, "no authenticator for mechanism {0}", m);
                continue;
            }

            if (!p.supportsAuthentication(m)) {
                logger.log(Level.FINE, "mechanism {0} not supported by server",
                        m);
                continue;
            }

            /*
             * If using the default mechanisms, check if this one is disabled.
             */
            if (usingDefaultMechs) {
                String dprop = "mail." + name + ".auth." +
                        m.toLowerCase(Locale.ENGLISH) + ".disable";
                boolean disabled = PropUtil.getBooleanProperty(
                        session.getProperties(),
                        dprop, !p.isMechanismEnabled(m));
                if (disabled) {
                    if (logger.isLoggable(Level.FINE))
                        logger.fine("mechanism " + m +
                                " disabled by property: " + dprop);
                    continue;
                }
            }

            // only the first supported and enabled mechanism is used
            logger.log(Level.FINE, "Using mechanism {0}", m);
            String msg =
                    p.authenticate(m, host, authzid, user, passwd);
            if (msg != null)
                throw new AuthenticationFailedException(msg);
            return true;
        }

        // if no authentication mechanism found, fail
        throw new AuthenticationFailedException(
                "No authentication mechanisms supported by both server and client");
    }

    private static boolean isRecoverable(Throwable t) {
        return (t instanceof Exception) || (t instanceof LinkageError);
    }

    synchronized void closePort(POP3Folder owner) {
        if (portOwner == owner) {
            port = null;
            portOwner = null;
        }
    }

    @Override
    public synchronized void close() throws MessagingException {
        close(false);
    }

    synchronized void close(boolean force) throws MessagingException {
        try {
            if (port != null) {
                if (force)
                    port.close();
                else
                    port.quit();
            }
        } catch (IOException ioex) {
        } finally {
            port = null;

            // to set the state and send the closed connection event
            super.close();
        }
    }

    @Override
    public Folder getDefaultFolder() throws MessagingException {
        checkConnected();
        return new DefaultFolder(this);
    }

    /**
     * Only the name "INBOX" is supported.
     */
    @Override
    public Folder getFolder(String name) throws MessagingException {
        checkConnected();
        return new POP3Folder(this, name);
    }

    @Override
    public Folder getFolder(URLName url) throws MessagingException {
        checkConnected();
        return new POP3Folder(this, url.getFile());
    }

    /**
     * Return a Map of the capabilities the server provided,
     * as per RFC 2449.  If the server doesn't support RFC 2449,
     * an emtpy Map is returned.  The returned Map can not be modified.
     * The key to the Map is the upper case capability name as
     * a String.  The value of the entry is the entire String
     * capability line returned by the server. <p>
     *
     * For example, to check if the server supports the STLS capability, use:
     * <code>if (store.capabilities().containsKey("STLS")) ...</code>
     *
     * @return Map of capabilities
     * @exception MessagingException for failures
     * @since JavaMail 1.4.3
     */
    public Map<String, String> capabilities() throws MessagingException {
        Map<String, String> c;
        synchronized (this) {
            c = capabilities;
        }
        if (c != null)
            return Collections.unmodifiableMap(c);
        else
            return Collections.<String, String>emptyMap();
    }

    /**
     * Is this POP3Store using SSL to connect to the server?
     *
     * @return true if using SSL
     * @since JavaMail 1.4.6
     */
    public synchronized boolean isSSL() {
        return usingSSL;
    }

    private void checkConnected() throws MessagingException {
        if (!super.isConnected())
            throw new MessagingException("Not connected");
    }
}
