/*
 * Copyright (c) 1997, 2024 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.mbox;

import jakarta.mail.Address;
import jakarta.mail.Flags;
import jakarta.mail.Folder;
import jakarta.mail.FolderNotFoundException;
import jakarta.mail.Message;
import jakarta.mail.MessageRemovedException;
import jakarta.mail.MessagingException;
import jakarta.mail.URLName;
import jakarta.mail.event.ConnectionEvent;
import jakarta.mail.event.FolderEvent;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.InternetHeaders;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.SharedInputStream;
import jakarta.mail.util.SharedByteArrayInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;
import org.xbib.net.mail.util.LineInputStream;

/**
 * This class represents a mailbox file containing RFC822 style email messages.
 *
 * @author John Mani
 * @author Bill Shannon
 */

public class MboxFolder extends Folder {

    private final String name;    // null => the default folder
    private boolean is_inbox = false;
    private int total;        // total number of messages in mailbox
    private volatile boolean opened = false;
    private List<MessageMetadata> messages;
    private TempFile temp;
    private final MboxStore mstore;
    private final MailFile folder;
    private long file_size;    // the size the last time we read or wrote it
    private long saved_file_size; // size at the last open, close, or expunge
    private boolean special_imap_message;

    private static final boolean homeRelative =
            Boolean.getBoolean("mail.mbox.homerelative");

    /**
     * Metadata for each message, to avoid instantiating MboxMessage
     * objects for messages we're not going to look at. <p>
     *
     * MboxFolder keeps an array of these objects corresponding to
     * each message in the folder.  Access to the array elements is
     * synchronized, but access to contents of this object is not.
     * The metadata stored here is only accessed if the message field
     * is null; otherwise the MboxMessage object contains the metadata.
     */
    static final class MessageMetadata {
        public long start;    // offset in temp file of start of this message
        // public long end;	// offset in temp file of end of this message
        public long dataend;    // offset of end of message data, <= "end"
        public MboxMessage message;    // the message itself
        public boolean recent;    // message is recent?
        public boolean deleted;    // message is marked deleted?
        public boolean imap;    // special imap message?
    }

    public MboxFolder(MboxStore store, String name) {
        super(store);
        this.mstore = store;
        this.name = name;

        if (name != null && name.equalsIgnoreCase("INBOX"))
            is_inbox = true;

        folder = mstore.getMailFile(name == null ? "~" : name);
        if (folder.exists())
            saved_file_size = folder.length();
        else
            saved_file_size = -1;
    }

    @Override
    public char getSeparator() {
        return File.separatorChar;
    }

    @Override
    public Folder[] list(String pattern) throws MessagingException {
        if (!folder.isDirectory())
            throw new MessagingException("not a directory");

        if (name == null)
            return list(null, pattern, true);
        else
            return list(name + File.separator, pattern, false);
    }

    /*
     * Version of list shared by MboxStore and MboxFolder.
     */
    protected Folder[] list(String ref, String pattern, boolean fromStore)
            throws MessagingException {
        if (ref != null && ref.length() == 0)
            ref = null;
        int i;
        String refdir = null;
        String realdir = null;

        pattern = canonicalize(ref, pattern);
        if ((i = indexOfAny(pattern, "%*")) >= 0) {
            refdir = pattern.substring(0, i);
        } else {
            refdir = pattern;
        }
        if ((i = refdir.lastIndexOf(File.separatorChar)) >= 0) {
            // get rid of anything after directory name
            refdir = refdir.substring(0, i + 1);
            realdir = mstore.mb.filename(mstore.user, refdir);
        } else if (refdir.length() == 0 || refdir.charAt(0) != '~') {
            // no separator and doesn't start with "~" => home or cwd
            refdir = null;
            if (homeRelative)
                realdir = mstore.home;
            else
                realdir = ".";
        } else {
            realdir = mstore.mb.filename(mstore.user, refdir);
        }
        List<String> flist = new ArrayList<>();
        listWork(realdir, refdir, pattern, fromStore ? 0 : 1, flist);
        if (Match.path("INBOX", pattern, '\0'))
            flist.add("INBOX");

        Folder[] fl = new Folder[flist.size()];
        for (i = 0; i < fl.length; i++) {
            fl[i] = createFolder(mstore, flist.get(i));
        }
        return fl;
    }

    @Override
    public String getName() {
        if (name == null)
            return "";
        else if (is_inbox)
            return "INBOX";
        else
            return folder.getName();
    }

    @Override
    public String getFullName() {
        if (name == null)
            return "";
        else
            return name;
    }

    @Override
    public Folder getParent() {
        if (name == null)
            return null;
        else if (is_inbox)
            return createFolder(mstore, null);
        else
            // XXX - have to recognize other folders under default folder
            return createFolder(mstore, folder.getParent());
    }

    @Override
    public boolean exists() {
        return folder.exists();
    }

    @Override
    public int getType() {
        if (folder.isDirectory())
            return HOLDS_FOLDERS;
        else
            return HOLDS_MESSAGES;
    }

    @Override
    public Flags getPermanentFlags() {
        return MboxStore.permFlags;
    }

    @Override
    public synchronized boolean hasNewMessages() {
        if (folder instanceof UNIXFile) {
            UNIXFile f = (UNIXFile) folder;
            if (f.length() > 0) {
                long atime = f.lastAccessed();
                long mtime = f.lastModified();
//System.out.println(name + " atime " + atime + " mtime " + mtime);
                return atime < mtime;
            }
            return false;
        }
        long current_size;
        if (folder.exists())
            current_size = folder.length();
        else
            current_size = -1;
        // if we've never opened the folder, remember the size now
        // (will cause us to return false the first time)
        if (saved_file_size < 0)
            saved_file_size = current_size;
        return current_size > saved_file_size;
    }

    @Override
    public synchronized Folder getFolder(String name)
            throws MessagingException {
        if (folder.exists() && !folder.isDirectory())
            throw new MessagingException("not a directory");
        return createFolder(mstore,
                (this.name == null ? "~" : this.name) + File.separator + name);
    }

    @Override
    public synchronized boolean create(int type) throws MessagingException {
        switch (type) {
            case HOLDS_FOLDERS:
                if (!folder.mkdirs()) {
                    return false;
                }
                break;

            case HOLDS_MESSAGES:
                if (folder.exists()) {
                    return false;
                }
                try {
                    (new FileOutputStream((File) folder)).close();
                } catch (FileNotFoundException fe) {
                    File parent = new File(folder.getParent());
                    if (!parent.mkdirs())
                        throw new
                                MessagingException("can't create folder: " + name);
                    try {
                        (new FileOutputStream((File) folder)).close();
                    } catch (IOException ex3) {
                        throw new
                                MessagingException("can't create folder: " + name, ex3);
                    }
                } catch (IOException e) {
                    throw new
                            MessagingException("can't create folder: " + name, e);
                }
                break;

            default:
                throw new MessagingException("type not supported");
        }
        notifyFolderListeners(FolderEvent.CREATED);
        return true;
    }

    @Override
    public synchronized boolean delete(boolean recurse)
            throws MessagingException {
        checkClosed();
        if (name == null)
            throw new MessagingException("can't delete default folder");
        boolean ret = true;
        if (recurse && folder.isDirectory())
            ret = delete(new File(folder.getPath()));
        if (ret && folder.delete()) {
            notifyFolderListeners(FolderEvent.DELETED);
            return true;
        }
        return false;
    }

    /**
     * Recursively delete the specified file/directory.
     */
    private boolean delete(File f) {
        File[] files = f.listFiles();
        boolean ret = true;
        for (int i = 0; ret && i < files.length; i++) {
            if (files[i].isDirectory())
                ret = delete(files[i]);
            else
                ret = files[i].delete();
        }
        return ret;
    }

    @Override
    public synchronized boolean renameTo(Folder f)
            throws MessagingException {
        checkClosed();
        if (name == null)
            throw new MessagingException("can't rename default folder");
        if (!(f instanceof MboxFolder))
            throw new MessagingException("can't rename to: " + f.getName());
        String newname = ((MboxFolder) f).folder.getPath();
        if (folder.renameTo(new File(newname))) {
            notifyFolderRenamedListeners(f);
            return true;
        }
        return false;
    }

    /* Ensure the folder is open */
    void checkOpen() throws IllegalStateException {
        if (!opened)
            throw new IllegalStateException("Folder is not Open");
    }

    /* Ensure the folder is not open */
    private void checkClosed() throws IllegalStateException {
        if (opened)
            throw new IllegalStateException("Folder is Open");
    }

    /*
     * Check that the given message number is within the range
     * of messages present in this folder. If the message
     * number is out of range, we check to see if new messages
     * have arrived.
     */
    private void checkRange(int msgno) throws MessagingException {
        if (msgno < 1) // message-numbers start at 1
            throw new IndexOutOfBoundsException("message number < 1");

        if (msgno <= total)
            return;

        // Out of range, let's check if there are any new messages.
        getMessageCount();

        if (msgno > total) // Still out of range ? Throw up ...
            throw new IndexOutOfBoundsException(msgno + " > " + total);
    }

    /* Ensure the folder is open & readable */
    private void checkReadable() throws IllegalStateException {
        if (!opened || (mode != READ_ONLY && mode != READ_WRITE))
            throw new IllegalStateException("Folder is not Readable");
    }

    /* Ensure the folder is open & writable */
    private void checkWritable() throws IllegalStateException {
        if (!opened || mode != READ_WRITE)
            throw new IllegalStateException("Folder is not Writable");
    }

    @Override
    public boolean isOpen() {
        return opened;
    }

    /*
     * Open the folder in the specified mode.
     */
    @Override
    @SuppressWarnings("fallthrough")
    public synchronized void open(int mode) throws MessagingException {
        if (opened)
            throw new IllegalStateException("Folder is already Open");

        if (!folder.exists())
            throw new FolderNotFoundException(this, "Folder doesn't exist: " +
                    folder.getPath());
        this.mode = mode;
        switch (mode) {
            case READ_WRITE:
            default:
                if (!folder.canWrite())
                    throw new MessagingException("Open Failure, can't write: " +
                            folder.getPath());
                // fall through...

            case READ_ONLY:
                if (!folder.canRead())
                    throw new MessagingException("Open Failure, can't read: " +
                            folder.getPath());
                break;
        }

        if (is_inbox && folder instanceof InboxFile) {
            InboxFile inf = (InboxFile) folder;
            if (!inf.openLock(mode == READ_WRITE ? "rw" : "r"))
                throw new MessagingException("Failed to lock INBOX");
        }
        if (!folder.lock("r"))
            throw new MessagingException("Failed to lock folder: " + name);
        messages = new ArrayList<>();
        total = 0;
        Message[] msglist = null;
        try {
            temp = new TempFile(null);
            saved_file_size = folder.length();
            msglist = load(0L, false);
        } catch (IOException e) {
            throw new MessagingException("IOException", e);
        } finally {
            folder.unlock();
        }
        notifyConnectionListeners(ConnectionEvent.OPENED);
        if (msglist != null)
            notifyMessageAddedListeners(msglist);
        opened = true;        // now really opened
    }

    @Override
    public synchronized void close(boolean expunge) throws MessagingException {
        checkOpen();

        try {
            if (mode == READ_WRITE) {
                try {
                    writeFolder(true, expunge);
                } catch (IOException e) {
                    throw new MessagingException("I/O Exception", e);
                }
            }
            messages = null;
        } finally {
            opened = false;
            if (is_inbox && folder instanceof InboxFile) {
                InboxFile inf = (InboxFile) folder;
                inf.closeLock();
            }
            temp.close();
            temp = null;
            notifyConnectionListeners(ConnectionEvent.CLOSED);
        }
    }

    /**
     * Re-write the folder with the current contents of the messages.
     * If closing is true, turn off the RECENT flag.  If expunge is
     * true, don't write out deleted messages (only used from close()
     * when the message cache won't be accessed again).
     *
     * Return the number of messages written.
     */
    protected int writeFolder(boolean closing, boolean expunge)
            throws IOException, MessagingException {

        /*
         * First, see if there have been any changes.
         */
        int modified = 0, deleted = 0, recent = 0;
        for (int msgno = 1; msgno <= total; msgno++) {
            MessageMetadata md = messages.get(messageIndexOf(msgno));
            MboxMessage msg = md.message;
            if (msg != null) {
                Flags flags = msg.getFlags();
                if (msg.isModified() || !msg.origFlags.equals(flags))
                    modified++;
                if (flags.contains(Flags.Flag.DELETED))
                    deleted++;
                if (flags.contains(Flags.Flag.RECENT))
                    recent++;
            } else {
                if (md.deleted)
                    deleted++;
                if (md.recent)
                    recent++;
            }
        }
        if ((!closing || recent == 0) && (!expunge || deleted == 0) &&
                modified == 0)
            return 0;

        /*
         * Have to save any new mail that's been appended to the
         * folder since we last loaded it.
         */
        if (!folder.lock("rw"))
            throw new MessagingException("Failed to lock folder: " + name);
        int oldtotal = total;    // XXX
        Message[] msglist = null;
        if (folder.length() != file_size)
            msglist = load(file_size, !closing);
        // don't use the folder's FD, need to re-open in order to trunc the file
        OutputStream os =
                new BufferedOutputStream(new FileOutputStream((File) folder));
        int wr = 0;
        boolean keep = true;
        try {
            if (special_imap_message)
                appendStream(getMessageStream(0), os);
            for (int msgno = 1; msgno <= total; msgno++) {
                MessageMetadata md = messages.get(messageIndexOf(msgno));
                MboxMessage msg = md.message;
                if (msg != null) {
                    if (expunge && msg.isSet(Flags.Flag.DELETED))
                        continue;    // skip it;
                    if (closing && msgno <= oldtotal &&
                            msg.isSet(Flags.Flag.RECENT))
                        msg.setFlag(Flags.Flag.RECENT, false);
                    writeMboxMessage(msg, os);
                } else {
                    if (expunge && md.deleted)
                        continue;    // skip it;
                    if (closing && msgno <= oldtotal && md.recent) {
                        // have to instantiate message so that we can
                        // clear the recent flag
                        msg = (MboxMessage) getMessage(msgno);
                        msg.setFlag(Flags.Flag.RECENT, false);
                        writeMboxMessage(msg, os);
                    } else {
                        appendStream(getMessageStream(msgno), os);
                    }
                }
                folder.touchlock();
                wr++;
            }
            // If no messages in the mailbox, and we're closing,
            // maybe we should remove the mailbox.
            if (wr == 0 && closing) {
                String skeep = ((MboxStore) store).getSession().
                        getProperty("mail.mbox.deleteEmpty");
                if (skeep != null && skeep.equalsIgnoreCase("true"))
                    keep = false;
            }
        } catch (IOException | MessagingException e) {
            throw e;
        } catch (Exception e) {
            e.printStackTrace();
            throw new MessagingException("unexpected exception " + e);
        } finally {
            // close the folder, flushing out the data
            try {
                os.close();
                file_size = saved_file_size = folder.length();
                if (!keep) {
                    folder.delete();
                    file_size = 0;
                }
            } catch (IOException ex) {
            }

            if (keep) {
                // make sure the access time is greater than the mod time
                // XXX - would be nice to have utime()
                try {
                    Thread.sleep(1000);        // sleep for a second
                } catch (InterruptedException ex) {
                }
                InputStream is = null;
                try {
                    is = new FileInputStream((File) folder);
                    is.read();    // read a byte
                } catch (IOException ex) {
                }    // ignore errors
                try {
                    if (is != null)
                        is.close();
                    is = null;
                } catch (IOException ex) {
                }    // ignore errors
            }

            folder.unlock();
            if (msglist != null)
                notifyMessageAddedListeners(msglist);
        }
        return wr;
    }

    /**
     * Append the input stream to the output stream, closing the
     * input stream when done.
     */
    private static final void appendStream(InputStream is, OutputStream os)
            throws IOException {
        try {
            byte[] buf = new byte[64 * 1024];
            int len;
            while ((len = is.read(buf)) > 0)
                os.write(buf, 0, len);
        } finally {
            is.close();
        }
    }

    /**
     * Write a MimeMessage to the specified OutputStream in a
     * format suitable for a UNIX mailbox, i.e., including a correct
     * Content-Length header and with the local platform's line
     * terminating convention. <p>
     *
     * If the message is really a MboxMessage, use its writeToFile
     * method, which has access to the UNIX From line.  Otherwise, do
     * all the work here, creating an appropriate UNIX From line.
     */
    public static void writeMboxMessage(MimeMessage msg, OutputStream os)
            throws IOException, MessagingException {
        try {
            if (msg instanceof MboxMessage) {
                ((MboxMessage) msg).writeToFile(os);
            } else {
                // XXX - modify the message to preserve the flags in headers
                MboxMessage.setHeadersFromFlags(msg);
                ContentLengthCounter cos = new ContentLengthCounter();
                NewlineOutputStream nos = new NewlineOutputStream(cos);
                msg.writeTo(nos);
                nos.flush();
                os = new NewlineOutputStream(os, true);
                os = new ContentLengthUpdater(os, cos.getSize());
                PrintStream pos = new PrintStream(os, false, "iso-8859-1");
                pos.println(getUnixFrom(msg));
                msg.writeTo(pos);
                pos.flush();
            }
        } catch (MessagingException | IOException me) {
            throw me;
        }
    }

    /**
     * Construct an appropriately formatted UNIX From line using
     * the sender address and the date in the message.
     */
    protected static String getUnixFrom(MimeMessage msg) {
        Address[] afrom;
        String from;
        Date ddate;
        String date;
        try {
            if ((afrom = msg.getFrom()) == null ||
                    !(afrom[0] instanceof InternetAddress) ||
                    (from = ((InternetAddress) afrom[0]).getAddress()) == null)
                from = "UNKNOWN";
            if ((ddate = msg.getReceivedDate()) == null ||
                    (ddate = msg.getSentDate()) == null)
                ddate = new Date();
        } catch (MessagingException e) {
            from = "UNKNOWN";
            ddate = new Date();
        }
        date = ddate.toString();
        // date is of the form "Sat Aug 12 02:30:00 PDT 1995"
        // need to strip out the timezone
        return "From " + from + " " +
                date.substring(0, 20) + date.substring(24);
    }

    @Override
    public synchronized int getMessageCount() throws MessagingException {
        if (!opened)
            return -1;

        boolean locked = false;
        Message[] msglist = null;
        try {
            if (folder.length() != file_size) {
                if (!folder.lock("r"))
                    throw new MessagingException("Failed to lock folder: " +
                            name);
                locked = true;
                msglist = load(file_size, true);
            }
        } catch (IOException e) {
            throw new MessagingException("I/O Exception", e);
        } finally {
            if (locked) {
                folder.unlock();
                if (msglist != null)
                    notifyMessageAddedListeners(msglist);
            }
        }
        return total;
    }

    /**
     * Get the specified message.  Note that messages are numbered
     * from 1.
     */
    @Override
    public synchronized Message getMessage(int msgno)
            throws MessagingException {
        checkReadable();
        checkRange(msgno);

        MessageMetadata md = messages.get(messageIndexOf(msgno));
        MboxMessage m = md.message;
        if (m == null) {
            InputStream is = getMessageStream(msgno);
            try {
                m = loadMessage(is, msgno, mode == READ_WRITE);
            } catch (IOException ex) {
                MessagingException mex =
                        new MessageRemovedException("mbox message gone", ex);
                throw mex;
            }
            md.message = m;
        }
        return m;
    }

    private final int messageIndexOf(int msgno) {
        return special_imap_message ? msgno : msgno - 1;
    }

    private InputStream getMessageStream(int msgno) {
        int index = messageIndexOf(msgno);
        MessageMetadata md = messages.get(index);
        return temp.newStream(md.start, md.dataend);
    }

    @Override
    public synchronized void appendMessages(Message[] msgs)
            throws MessagingException {
        if (!folder.lock("rw"))
            throw new MessagingException("Failed to lock folder: " + name);

        OutputStream os = null;
        boolean err = false;
        try {
            os = new BufferedOutputStream(
                    new FileOutputStream(((File) folder).getPath(), true));
            // XXX - should use getAbsolutePath()?
            for (int i = 0; i < msgs.length; i++) {
                if (msgs[i] instanceof MimeMessage) {
                    writeMboxMessage((MimeMessage) msgs[i], os);
                } else {
                    err = true;
                    continue;
                }
                folder.touchlock();
            }
        } catch (IOException e) {
            throw new MessagingException("I/O Exception", e);
        } catch (MessagingException e) {
            throw e;
        } catch (Exception e) {
            e.printStackTrace();
            throw new MessagingException("unexpected exception " + e);
        } finally {
            if (os != null)
                try {
                    os.close();
                } catch (IOException e) {
                    // ignored
                }
            folder.unlock();
        }
        if (opened)
            getMessageCount();    // loads new messages as a side effect
        if (err)
            throw new MessagingException("Can't append non-Mime message");
    }

    @Override
    public synchronized Message[] expunge() throws MessagingException {
        checkWritable();

        /*
         * First, write out the folder to make sure we have permission,
         * disk space, etc.
         */
        int wr = total;        // number of messages written out
        try {
            wr = writeFolder(false, true);
        } catch (IOException e) {
            throw new MessagingException("expunge failed", e);
        }
        if (wr == total)    // wrote them all => nothing expunged
            return new Message[0];

        /*
         * Now, actually get rid of the expunged messages.
         */
        int del = 0;
        Message[] msglist = new Message[total - wr];
        int msgno = 1;
        while (msgno <= total) {
            MessageMetadata md = messages.get(messageIndexOf(msgno));
            MboxMessage msg = md.message;
            if (msg != null) {
                if (msg.isSet(Flags.Flag.DELETED)) {
                    msg.setExpunged(true);
                    msglist[del] = msg;
                    del++;
                    messages.remove(messageIndexOf(msgno));
                    total--;
                } else {
                    msg.setMessageNumber(msgno);    // update message number
                    msgno++;
                }
            } else {
                if (md.deleted) {
                    // have to instantiate it for the notification
                    msg = (MboxMessage) getMessage(msgno);
                    msg.setExpunged(true);
                    msglist[del] = msg;
                    del++;
                    messages.remove(messageIndexOf(msgno));
                    total--;
                } else {
                    msgno++;
                }
            }
        }
        if (del != msglist.length)        // this is really an assert
            throw new MessagingException("expunge delete count wrong");
        notifyMessageRemovedListeners(true, msglist);
        return msglist;
    }

    /*
     * Load more messages from the folder starting at the specified offset.
     */
    private Message[] load(long offset, boolean notify)
            throws MessagingException, IOException {
        int oldtotal = total;
        MessageLoader loader = new MessageLoader(temp);
        int loaded = loader.load(folder.getFD(), offset, messages);
        total += loaded;
        file_size = folder.length();

        if (offset == 0 && loaded > 0) {
            /*
             * If the first message is the special message that the
             * IMAP server adds to the mailbox, remember that we've
             * seen it so it won't be shown to the user.
             */
            MessageMetadata md = messages.get(0);
            if (md.imap) {
                special_imap_message = true;
                total--;
            }
        }
        if (notify) {
            Message[] msglist = new Message[total - oldtotal];
            for (int i = oldtotal, j = 0; i < total; i++, j++)
                msglist[j] = getMessage(i + 1);
            return msglist;
        } else
            return null;
    }

    /**
     * Parse the input stream and return an appropriate message object.
     * The InputStream must be a SharedInputStream.
     */
    private MboxMessage loadMessage(InputStream is, int msgno,
                                    boolean writable) throws MessagingException, IOException {
        LineInputStream in = new LineInputStream(is);

        /*
         * Read lines until a UNIX From line,
         * skipping blank lines.
         */
        String line;
        String unix_from = null;
        while ((line = in.readLine()) != null) {
            if (line.trim().length() == 0)
                continue;
            if (line.startsWith("From ")) {
                /*
                 * A UNIX From line looks like:
                 * From address Day Mon DD HH:MM:SS YYYY
                 */
                unix_from = line;
                int i;
                // find the space after the address, before the date
                i = unix_from.indexOf(' ', 5);
                if (i < 0)
                    continue;    // not a valid UNIX From line
                break;
            }
            throw new MessagingException("Garbage in mailbox: " + line);
        }

        if (unix_from == null)
            throw new EOFException("end of mailbox");

        /*
         * Now load the RFC822 headers into an InternetHeaders object.
         */
        InternetHeaders hdrs = new InternetHeaders(is);

        // the rest is the message content
        SharedInputStream sis = (SharedInputStream) is;
        InputStream stream = sis.newStream(sis.getPosition(), -1);
        return new MboxMessage(this, hdrs, stream, msgno, unix_from, writable);
    }

    /*
     * Only here to make accessible to MboxMessage.
     */
    @Override
    protected void notifyMessageChangedListeners(int type, Message m) {
        super.notifyMessageChangedListeners(type, m);
    }


    /**
     * this is an exact duplicate of the Folder.getURL except it doesn't
     * add a beginning '/' to the URLName.
     */
    @Override
    public URLName getURLName() {
        // XXX - note:  this should not be done this way with the
        // new jakarta.mail apis.

        URLName storeURL = getStore().getURLName();
        if (name == null)
            return storeURL;

        char separator = getSeparator();
        String fullname = getFullName();
        StringBuilder encodedName = new StringBuilder();

        // We need to encode each of the folder's names, and replace
        // the store's separator char with the URL char '/'.
        StringTokenizer tok = new StringTokenizer(
                fullname, Character.toString(separator), true);

        while (tok.hasMoreTokens()) {
            String s = tok.nextToken();
            if (s.charAt(0) == separator)
                encodedName.append("/");
            else
                // XXX - should encode, but since there's no decoder...
                //encodedName.append(java.net.URLEncoder.encode(s));
                encodedName.append(s);
        }

        return new URLName(storeURL.getProtocol(), storeURL.getHost(),
                storeURL.getPort(), encodedName.toString(),
                storeURL.getUsername(),
                null /* no password */);
    }

    /**
     * Create an MboxFolder object, or a subclass thereof.
     * Can be overridden by subclasses of MboxFolder so that
     * the appropriate subclass is created by the list method.
     */
    protected Folder createFolder(MboxStore store, String name) {
        return new MboxFolder(store, name);
    }

    /*
     * Support routines for list().
     */

    /**
     * Return a canonicalized pattern given a reference name and a pattern.
     */
    private static String canonicalize(String ref, String pat) {
        if (ref == null)
            return pat;
        try {
            if (pat.length() == 0) {
                return ref;
            } else if (pat.charAt(0) == File.separatorChar) {
                return ref.substring(0, ref.indexOf(File.separatorChar)) + pat;
            } else {
                return ref + pat;
            }
        } catch (StringIndexOutOfBoundsException e) {
            return pat;
        }
    }

    /**
     * Return the first index of any of the characters in "any" in "s",
     * or -1 if none are found.
     *
     * This should be a method on String.
     */
    private static int indexOfAny(String s, String any) {
        try {
            int len = s.length();
            for (int i = 0; i < len; i++) {
                if (any.indexOf(s.charAt(i)) >= 0)
                    return i;
            }
            return -1;
        } catch (StringIndexOutOfBoundsException e) {
            return -1;
        }
    }

    /**
     * The recursive part of generating the list of mailboxes.
     * realdir is the full pathname to the directory to search.
     * dir is the name the user uses, often a relative name that's
     * relative to the user's home directory.  dir (if not null) always
     * has a trailing file separator character.
     *
     * @param realdir real pathname of directory to start looking in
     * @param dir     user's name for realdir
     * @param pat     pattern to match against
     * @param level   level of the directory hierarchy we're in
     * @param flist   list to which to add folder names that match
     */
    // Derived from the c-client listWork() function.
    private void listWork(String realdir, String dir, String pat,
                          int level, List<String> flist) {
        String[] sl;
        File fdir = new File(realdir);
        sl = fdir.list();

        if (level == 0 && dir != null &&
                Match.path(dir, pat, File.separatorChar))
            flist.add(dir);

        if (sl == null)
            return;    // nothing return, we're done

        if (realdir.charAt(realdir.length() - 1) != File.separatorChar)
            realdir += File.separator;

        for (int i = 0; i < sl.length; i++) {
            if (sl[i].charAt(0) == '.')
                continue;    // ignore all "dot" files for now
            String md = realdir + sl[i];
            File mf = new File(md);
            if (!mf.exists())
                continue;
            String name;
            if (dir != null)
                name = dir + sl[i];
            else
                name = sl[i];
            if (mf.isDirectory()) {
                if (Match.path(name, pat, File.separatorChar)) {
                    flist.add(name);
                    name += File.separator;
                } else {
                    name += File.separator;
                    if (Match.path(name, pat, File.separatorChar))
                        flist.add(name);
                }
                if (Match.dir(name, pat, File.separatorChar))
                    listWork(md, name, pat, level + 1, flist);
            } else {
                if (Match.path(name, pat, File.separatorChar))
                    flist.add(name);
            }
        }
    }
}

/**
 * Pattern matching support class for list().
 * Should probably be more public.
 */
// Translated from the c-client functions pmatch_full() and dmatch().
class Match {
    /**
     * Pathname pattern match
     *
     * @param s     base string
     * @param pat   pattern string
     * @param delim delimiter character
     * @return true if base matches pattern
     */
    static public boolean path(String s, String pat, char delim) {
        try {
            return path(s, 0, s.length(), pat, 0, pat.length(), delim);
        } catch (StringIndexOutOfBoundsException e) {
            return false;
        }
    }

    static private boolean path(String s, int s_index, int s_len,
                                String pat, int p_index, int p_len, char delim)
            throws StringIndexOutOfBoundsException {

        while (p_index < p_len) {
            char c = pat.charAt(p_index);
            switch (c) {
                case '%':
                    if (++p_index >= p_len)        // % at end of pattern
                        // ok if no delimiters
                        return delim == 0 || s.indexOf(delim, s_index) < 0;
                    // scan remainder until delimiter
                    do {
                        if (path(s, s_index, s_len, pat, p_index, p_len, delim))
                            return true;
                    } while (s.charAt(s_index) != delim && ++s_index < s_len);
                    // ran into a delimiter or ran out of string without a match
                    return false;

                case '*':
                    if (++p_index >= p_len)        // end of pattern?
                        return true;        // unconditional match
                    do {
                        if (path(s, s_index, s_len, pat, p_index, p_len, delim))
                            return true;
                    } while (++s_index < s_len);
                    // ran out of string without a match
                    return false;

                default:
                    // if ran out of string or no match, fail
                    if (s_index >= s_len || c != s.charAt(s_index))
                        return false;

                    // try the next string and pattern characters
                    s_index++;
                    p_index++;
            }
        }
        return s_index >= s_len;
    }

    /**
     * Directory pattern match
     *
     * @param s   base string
     * @param pat pattern string
     * @return true if base is a matching directory of pattern
     */
    static public boolean dir(String s, String pat, char delim) {
        try {
            return dir(s, 0, s.length(), pat, 0, pat.length(), delim);
        } catch (StringIndexOutOfBoundsException e) {
            return false;
        }
    }

    static private boolean dir(String s, int s_index, int s_len,
                               String pat, int p_index, int p_len, char delim)
            throws StringIndexOutOfBoundsException {

        while (p_index < p_len) {
            char c = pat.charAt(p_index);
            switch (c) {
                case '%':
                    if (s_index >= s_len)        // end of base?
                        return true;        // subset match
                    if (++p_index >= p_len)        // % at end of pattern?
                        return false;        // no inferiors permitted
                    do {
                        if (dir(s, s_index, s_len, pat, p_index, p_len, delim))
                            return true;
                    } while (s.charAt(s_index) != delim && ++s_index < s_len);

                    if (s_index + 1 == s_len)    // s ends with a delimiter
                        return true;        // must be a subset of pattern
                    return dir(s, s_index, s_len, pat, p_index, p_len, delim);

                case '*':
                    return true;            // unconditional match

                default:
                    if (s_index >= s_len)        // end of base?
                        return c == delim;        // matched if at delimiter

                    if (c != s.charAt(s_index))
                        return false;

                    // try the next string and pattern characters
                    s_index++;
                    p_index++;
            }
        }
        return s_index >= s_len;
    }
}

/**
 * A ByteArrayOutputStream that allows us to share the byte array
 * rather than copy it.  Eventually could replace this with something
 * that doesn't require a single contiguous byte array.
 */
class SharedByteArrayOutputStream extends ByteArrayOutputStream {
    public SharedByteArrayOutputStream(int size) {
        super(size);
    }

    public InputStream toStream() {
        return new SharedByteArrayInputStream(buf, 0, count);
    }
}
