Introduce mbox support
[gpl/argeo-slc.git] / ext / javax.mail.mbox / src / com / sun / mail / mbox / MboxFolder.java
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxFolder.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxFolder.java
new file mode 100644 (file)
index 0000000..340b84f
--- /dev/null
@@ -0,0 +1,1164 @@
+/*
+ * Copyright (c) 1997, 2018 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 com.sun.mail.mbox;
+
+import javax.mail.*;
+import javax.mail.event.*;
+import javax.mail.internet.*;
+import javax.mail.util.*;
+import java.io.*;
+import java.util.*;
+import com.sun.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 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 MboxStore mstore;
+    private 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;
+    }
+
+    public char getSeparator() {
+       return File.separatorChar;
+    }
+
+    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<String>();
+       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;
+    }
+
+    public String getName() {
+       if (name == null)
+           return "";
+       else if (is_inbox)
+           return "INBOX";
+       else
+           return folder.getName();
+    }
+
+    public String getFullName() {
+       if (name == null)
+           return "";
+       else
+           return name;
+    }
+
+    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());
+    }
+
+    public boolean exists() {
+       return folder.exists();
+    }
+
+    public int getType() {
+       if (folder.isDirectory())
+           return HOLDS_FOLDERS;
+       else
+           return HOLDS_MESSAGES;
+    }
+
+    public Flags getPermanentFlags() {
+       return MboxStore.permFlags;
+    }
+
+    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;
+    }
+
+    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);
+    }
+
+    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;
+    }
+
+    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;
+    }
+
+    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");
+    }
+
+    public boolean isOpen() {
+        return opened;
+    }
+
+    /*
+     * Open the folder in the specified mode.
+     */
+    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<MessageMetadata>();
+       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
+    }
+
+    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 e) {
+           throw e;
+       } catch (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 me) {
+           throw me;
+       } catch (IOException ioe) {
+           throw ioe;
+       }
+    }
+
+    /**
+     * 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);
+    }
+
+    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.
+     */
+    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);
+    }
+
+    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");
+    }
+
+    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.
+     */
+    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.
+     */
+    public URLName getURLName() {
+       // XXX - note:  this should not be done this way with the
+       // new javax.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);
+       try {
+           sl = fdir.list();
+       } catch (SecurityException e) {
+           return;     // can't read it, ignore it
+       }
+
+       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);
+    }
+}