Introduce mbox support
authorMathieu Baudier <mbaudier@argeo.org>
Sat, 7 May 2022 09:23:25 +0000 (11:23 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Sat, 7 May 2022 09:23:25 +0000 (11:23 +0200)
32 files changed:
Makefile-ext.mk
ext/javax.mail.mbox/.classpath [new file with mode: 0644]
ext/javax.mail.mbox/.project [new file with mode: 0644]
ext/javax.mail.mbox/bnd.bnd [new file with mode: 0644]
ext/javax.mail.mbox/build.properties [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/ContentLengthCounter.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/ContentLengthUpdater.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/DefaultMailbox.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/FileInterface.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/InboxFile.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/LineCounter.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/MailFile.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/Mailbox.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxFolder.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxMessage.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxProvider.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxStore.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/MessageLoader.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/NewlineOutputStream.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/SolarisMailbox.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/SunOSMailbox.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/SunV3BodyPart.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/SunV3Multipart.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/TempFile.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/UNIXFile.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/UNIXFolder.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/mbox/UNIXInbox.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/remote/POP3RemoteProvider.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/remote/POP3RemoteStore.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/remote/RemoteDefaultFolder.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/remote/RemoteInbox.java [new file with mode: 0644]
ext/javax.mail.mbox/src/com/sun/mail/remote/RemoteStore.java [new file with mode: 0644]

index 11868d49fc6c43a4c28f1928f71b2dbdb5b24f0f..f671f28227d20daf403017171cdf661ca33cc18f 100644 (file)
@@ -7,6 +7,7 @@ A2_CATEGORY = org.argeo.tp
 
 BUNDLES = \
 ext/org.argeo.ext.slf4j \
+ext/javax.mail.mbox \
 
 clean:
        rm -rf $(BUILD_BASE)
diff --git a/ext/javax.mail.mbox/.classpath b/ext/javax.mail.mbox/.classpath
new file mode 100644 (file)
index 0000000..e801ebf
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/ext/javax.mail.mbox/.project b/ext/javax.mail.mbox/.project
new file mode 100644 (file)
index 0000000..b037595
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>javax.mail.mbox</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/ext/javax.mail.mbox/bnd.bnd b/ext/javax.mail.mbox/bnd.bnd
new file mode 100644 (file)
index 0000000..496cdb1
--- /dev/null
@@ -0,0 +1,7 @@
+Export-Package: com.sun.mail.*;version="1.6.7"
+
+Import-Package: \
+javax.mail.event,\
+*
+
+Bundle-Version : 1.6.7
diff --git a/ext/javax.mail.mbox/build.properties b/ext/javax.mail.mbox/build.properties
new file mode 100644 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/ContentLengthCounter.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/ContentLengthCounter.java
new file mode 100644 (file)
index 0000000..f01aa59
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * 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 java.io.*;
+
+/**
+ * Count the number of bytes in the body of the message written to the stream.
+ */
+class ContentLengthCounter extends OutputStream {
+    private long size = 0;
+    private boolean inHeader = true;
+    private int lastb1 = -1, lastb2 = -1;
+
+    public void write(int b) throws IOException {
+       if (inHeader) {
+           // if line terminator is CR
+           if (b == '\r' && lastb1 == '\r')
+               inHeader = false;
+           else if (b == '\n') {
+               // if line terminator is \n
+               if (lastb1 == '\n')
+                   inHeader = false;
+               // if line terminator is CRLF
+               else if (lastb1 == '\r' && lastb2 == '\n')
+                   inHeader = false;
+           }
+           lastb2 = lastb1;
+           lastb1 = b;
+       } else
+           size++;
+    }
+
+    public void write(byte[] b) throws IOException {
+       if (inHeader)
+           super.write(b);
+       else
+           size += b.length;
+    }
+
+    public void write(byte[] b, int off, int len) throws IOException {
+       if (inHeader)
+           super.write(b, off, len);
+       else
+           size += len;
+    }
+
+    public long getSize() {
+       return size;
+    }
+
+    /*
+    public static void main(String argv[]) throws Exception {
+       int b;
+       ContentLengthCounter os = new ContentLengthCounter();
+       while ((b = System.in.read()) >= 0)
+           os.write(b);
+       System.out.println("size " + os.getSize());
+    }
+    */
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/ContentLengthUpdater.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/ContentLengthUpdater.java
new file mode 100644 (file)
index 0000000..b6d4408
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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 java.io.*;
+
+/**
+ * Update the Content-Length header in the message written to the stream.
+ */
+class ContentLengthUpdater extends FilterOutputStream {
+    private String contentLength;
+    private boolean inHeader = true;
+    private boolean sawContentLength = false;
+    private int lastb1 = -1, lastb2 = -1;
+    private StringBuilder line = new StringBuilder();
+
+    public ContentLengthUpdater(OutputStream os, long contentLength) {
+       super(os);
+       this.contentLength = "Content-Length: " + contentLength;
+    }
+
+    public void write(int b) throws IOException {
+       if (inHeader) {
+           String eol = "\n";
+           // First, determine if we're still in the header.
+           if (b == '\r') {
+               // if line terminator is CR
+               if (lastb1 == '\r') {
+                   inHeader = false;
+                   eol = "\r";
+               // else, if line terminator is CRLF
+               } else if (lastb1 == '\n' && lastb2 == '\r') {
+                   inHeader = false;
+                   eol = "\r\n";
+               }
+           // else, if line terminator is \n
+           } else if (b == '\n') {
+               if (lastb1 == '\n') {
+                   inHeader = false;
+                   eol = "\n";
+               }
+           }
+
+           // If we're no longer in the header, and we haven't seen
+           // a Content-Length header yet, it's time to put one out.
+           if (!inHeader && !sawContentLength) {
+               out.write(contentLength.getBytes("iso-8859-1"));
+               out.write(eol.getBytes("iso-8859-1"));
+           }
+
+           // If we have a full line, see if it's a Content-Length header.
+           if (b == '\r' || (b == '\n' && lastb1 != '\r')) {
+               if (line.toString().regionMatches(true, 0,
+                                       "content-length:", 0, 15)) {
+                   // yup, got it
+                   sawContentLength = true;
+                   // put out the new version
+                   out.write(contentLength.getBytes("iso-8859-1"));
+               } else {
+                   // not a Content-Length header, just write it out
+                   out.write(line.toString().getBytes("iso-8859-1"));
+               }
+               line.setLength(0);      // clear buffer for next line
+           }
+           if (b == '\r' || b == '\n')
+               out.write(b);   // write out line terminator immediately
+           else
+               line.append((char)b);   // accumulate characters of the line
+
+           // rotate saved characters for next time through loop
+           lastb2 = lastb1;
+           lastb1 = b;
+       } else
+           out.write(b);               // not in the header, just write it out
+    }
+
+    public void write(byte[] b) throws IOException {
+       if (inHeader)
+           write(b, 0, b.length);
+       else
+           out.write(b);
+    }
+
+    public void write(byte[] b, int off, int len) throws IOException {
+       if (inHeader) {
+           for (int i = 0 ; i < len ; i++) {
+               write(b[off + i]);
+           }
+       } else
+           out.write(b, off, len);
+    }
+
+    // for testing
+    public static void main(String argv[]) throws Exception {
+       int b;
+       ContentLengthUpdater os =
+           new ContentLengthUpdater(System.out, Long.parseLong(argv[0]));
+       while ((b = System.in.read()) >= 0)
+           os.write(b);
+       os.flush();
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/DefaultMailbox.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/DefaultMailbox.java
new file mode 100644 (file)
index 0000000..bac0b05
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * 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 java.io.*;
+
+public class DefaultMailbox extends Mailbox {
+    private final String home;
+
+    private static final boolean homeRelative =
+                               Boolean.getBoolean("mail.mbox.homerelative");
+
+    public DefaultMailbox() {
+       home = System.getProperty("user.home");
+    }
+
+    public MailFile getMailFile(String user, String folder) {
+       return new DefaultMailFile(filename(user, folder));
+    }
+
+    public String filename(String user, String folder) {
+       try {
+           char c = folder.charAt(0);
+           if (c == File.separatorChar) {
+               return folder;
+           } else if (c == '~') {
+               int i = folder.indexOf(File.separatorChar);
+               String tail = "";
+               if (i > 0) {
+                   tail = folder.substring(i);
+                   folder = folder.substring(0, i);
+               }
+               return home + tail;
+           } else {
+               if (folder.equalsIgnoreCase("INBOX"))
+                   folder = "INBOX";
+               if (homeRelative)
+                   return home + File.separator + folder;
+               else
+                   return folder;
+           }
+       } catch (StringIndexOutOfBoundsException e) {
+           return folder;
+       }
+    }
+}
+
+class DefaultMailFile extends File implements MailFile {
+    protected transient RandomAccessFile file;
+
+    private static final long serialVersionUID = 3713116697523761684L;
+
+    DefaultMailFile(String name) {
+       super(name);
+    }
+
+    public boolean lock(String mode) {
+       try {
+           file = new RandomAccessFile(this, mode);
+           return true;
+       } catch (FileNotFoundException fe) {
+           return false;
+       } catch (IOException ie) {
+           file = null;
+           return false;
+       }
+    }
+
+    public void unlock() { 
+       if (file != null) {
+           try {
+               file.close();
+           } catch (IOException e) {
+               // ignore it
+           }
+           file = null;
+       }
+    }
+
+    public void touchlock() {
+    }
+
+    public FileDescriptor getFD() {
+       if (file == null)
+           return null;
+       try {
+           return file.getFD();
+       } catch (IOException e) {
+           return null;
+       }
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/FileInterface.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/FileInterface.java
new file mode 100644 (file)
index 0000000..891f0d5
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * 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 java.io.File;
+import java.io.FilenameFilter;
+
+public interface FileInterface {
+    /**
+     * Gets the name of the file. This method does not include the
+     * directory.
+     * @return the file name.
+     */
+    public String getName();
+
+    /**
+     * Gets the path of the file.
+     * @return the file path.
+     */
+    public String getPath();
+
+    /**
+     * Gets the absolute path of the file.
+     * @return the absolute file path.
+     */
+    public String getAbsolutePath();
+
+    /**
+     * Gets the official, canonical path of the File.
+     * @return canonical path
+     */
+    // XXX - JDK1.1
+    // public String getCanonicalPath();
+
+    /**
+     * Gets the name of the parent directory.
+     * @return the parent directory, or null if one is not found.
+     */
+    public String getParent();
+
+    /**
+     * Returns a boolean indicating whether or not a file exists.
+     */
+    public boolean exists();
+
+    /**
+     * Returns a boolean indicating whether or not a writable file 
+     * exists. 
+     */
+    public boolean canWrite();
+
+    /**
+     * Returns a boolean indicating whether or not a readable file 
+     * exists.
+     */
+    public boolean canRead();
+
+    /**
+     * Returns a boolean indicating whether or not a normal file 
+     * exists.
+     */
+    public boolean isFile();
+
+    /**
+     * Returns a boolean indicating whether or not a directory file 
+     * exists.
+     */
+    public boolean isDirectory();
+
+    /**
+     * Returns a boolean indicating whether the file name is absolute.
+     */
+    public boolean isAbsolute();
+
+    /**
+     * Returns the last modification time. The return value should
+     * only be used to compare modification dates. It is meaningless
+     * as an absolute time.
+     */
+    public long lastModified();
+
+    /**
+     * Returns the length of the file. 
+     */
+    public long length();
+
+    /**
+     * Creates a directory and returns a boolean indicating the
+     * success of the creation.  Will return false if the directory already
+     * exists.
+     */
+    public boolean mkdir();
+
+    /**
+     * Renames a file and returns a boolean indicating whether 
+     * or not this method was successful.
+     * @param dest the new file name
+     */
+    public boolean renameTo(File dest);
+
+    /**
+     * Creates all directories in this path.  This method 
+     * returns true if the target (deepest) directory was created,
+     * false if the target directory was not created (e.g., if it
+     * existed previously).
+     */
+    public boolean mkdirs();
+
+    /**
+     * Lists the files in a directory. Works only on directories.
+     * @return an array of file names.  This list will include all
+     * files in the directory except the equivalent of "." and ".." .
+     */
+    public String[] list();
+
+    /**
+     * Uses the specified filter to list files in a directory. 
+     * @param filter the filter used to select file names
+     * @return the filter selected files in this directory.
+     * @see FilenameFilter
+     */
+    public String[] list(FilenameFilter filter);
+
+    /**
+     * Deletes the specified file. Returns true
+     * if the file could be deleted.
+     */
+    public boolean delete();
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/InboxFile.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/InboxFile.java
new file mode 100644 (file)
index 0000000..e4fffd5
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+public interface InboxFile extends MailFile {
+    public boolean openLock(String mode);
+    public void closeLock();
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/LineCounter.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/LineCounter.java
new file mode 100644 (file)
index 0000000..1ae4084
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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 java.io.*;
+
+/**
+ * Count number of lines output.
+ */
+class LineCounter extends FilterOutputStream {
+    private int lastb = -1;
+    protected int lineCount;
+
+    public LineCounter(OutputStream os) {
+       super(os);
+    }
+
+    public void write(int b) throws IOException {
+       // If we have a full line, count it.
+       if (b == '\r' || (b == '\n' && lastb != '\r'))
+           lineCount++;
+       out.write(b);
+       lastb = b;
+    }
+
+    public void write(byte[] b) throws IOException {
+       write(b, 0, b.length);
+    }
+
+    public void write(byte[] b, int off, int len) throws IOException {
+       for (int i = 0 ; i < len ; i++) {
+           write(b[off + i]);
+       }
+    }
+
+    public int getLineCount() {
+       return lineCount;
+    }
+
+    // for testing
+    public static void main(String argv[]) throws Exception {
+       int b;
+       LineCounter os =
+           new LineCounter(System.out);
+       while ((b = System.in.read()) >= 0)
+           os.write(b);
+       os.flush();
+       System.out.println(os.getLineCount());
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/MailFile.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/MailFile.java
new file mode 100644 (file)
index 0000000..f790fce
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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 java.io.FileDescriptor;
+
+public interface MailFile extends FileInterface {
+    public boolean lock(String mode);
+    public void unlock();
+    public void touchlock();
+    public FileDescriptor getFD();
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/Mailbox.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/Mailbox.java
new file mode 100644 (file)
index 0000000..b902121
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+public abstract class Mailbox {
+    /**
+     * Return a MailFile object for the specified user's folder.
+     */
+    public abstract MailFile getMailFile(String user, String folder);
+
+    /**
+     * Return the file name corresponding to a folder with the given name.
+     */
+    public abstract String filename(String user, String folder);
+}
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);
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxMessage.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxMessage.java
new file mode 100644 (file)
index 0000000..db57a26
--- /dev/null
@@ -0,0 +1,532 @@
+/*
+ * 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 java.io.*;
+import java.util.StringTokenizer;
+import java.util.Date;
+import java.text.SimpleDateFormat;
+import javax.activation.*;
+import javax.mail.*;
+import javax.mail.internet.*;
+import javax.mail.event.MessageChangedEvent;
+import com.sun.mail.util.LineInputStream;
+
+/**
+ * This class represents an RFC822 style email message that resides in a file.
+ *
+ * @author Bill Shannon
+ */
+
+public class MboxMessage extends MimeMessage {
+
+    boolean writable = false;
+    // original msg flags, used by MboxFolder to detect modification
+    Flags origFlags;
+    /*
+     * A UNIX From line looks like:
+     * From address Day Mon DD HH:MM:SS YYYY
+     */
+    String unix_from;
+    InternetAddress unix_from_user;
+    Date rcvDate;
+    int lineCount = -1;
+    private static OutputStream nullOutputStream = new OutputStream() {
+       public void write(int b) { }
+       public void write(byte[] b, int off, int len) { }
+    };
+
+    /**
+     * Construct an MboxMessage from the InputStream.
+     */
+    public MboxMessage(Session session, InputStream is)
+                               throws MessagingException, IOException {
+       super(session);
+       BufferedInputStream bis;
+       if (is instanceof BufferedInputStream)
+           bis = (BufferedInputStream)is;
+       else
+           bis = new BufferedInputStream(is);
+       LineInputStream dis = new LineInputStream(bis);
+       bis.mark(1024);
+       String line = dis.readLine();
+       if (line != null && line.startsWith("From "))
+           this.unix_from = line;
+       else
+           bis.reset();
+       parse(bis);
+       saved = true;
+    }
+
+    /**
+     * Construct an MboxMessage using the given InternetHeaders object
+     * and content from an InputStream.
+     */
+    public MboxMessage(MboxFolder folder, InternetHeaders hdrs, InputStream is,
+                               int msgno, String unix_from, boolean writable)
+                               throws MessagingException {
+       super(folder, hdrs, null, msgno);
+       setFlagsFromHeaders();
+       origFlags = getFlags();
+       this.unix_from = unix_from;
+       this.writable = writable;
+       this.contentStream = is;
+    }
+
+    /**
+     * Returns the "From" attribute. The "From" attribute contains
+     * the identity of the person(s) who wished this message to 
+     * be sent. <p>
+     * 
+     * If our superclass doesn't have a value, we return the address
+     * from the UNIX From line.
+     *
+     * @return          array of Address objects
+     * @exception       MessagingException
+     */
+    public Address[] getFrom() throws MessagingException {
+       Address[] ret = super.getFrom();
+       if (ret == null) {
+           InternetAddress ia = getUnixFrom();
+           if (ia != null)
+               ret = new InternetAddress[] { ia };
+       }
+       return ret;
+    }
+
+    /**
+     * Returns the address from the UNIX "From" line.
+     *
+     * @return          UNIX From address
+     * @exception       MessagingException
+     */
+    public synchronized InternetAddress getUnixFrom()
+                               throws MessagingException {
+       if (unix_from_user == null && unix_from != null) {
+           int i;
+           // find the space after the address, before the date
+           i = unix_from.indexOf(' ', 5);
+           if (i > 5) {
+               try {
+                   unix_from_user =
+                       new InternetAddress(unix_from.substring(5, i));
+               } catch (AddressException e) {
+                   // ignore it
+               }
+           }
+       }
+       return unix_from_user != null ?
+               (InternetAddress)unix_from_user.clone() : null;
+    }
+
+    private String getUnixFromLine() {
+       if (unix_from != null)
+           return unix_from;
+       String from = "unknown";
+       try {
+           Address[] froma = getFrom();
+           if (froma != null && froma.length > 0 &&
+                   froma[0] instanceof InternetAddress)
+               from = ((InternetAddress)froma[0]).getAddress();
+       } catch (MessagingException ex) { }
+       Date d = null;
+       try {
+           d = getSentDate();
+       } catch (MessagingException ex) {
+           // ignore
+       }
+       if (d == null)
+           d = new Date();
+       // From shannon Mon Jun 10 12:06:52 2002
+       SimpleDateFormat fmt = new SimpleDateFormat("EEE LLL dd HH:mm:ss yyyy");
+       return "From " + from + " " + fmt.format(d);
+    }
+
+    /**
+     * Get the date this message was received, from the UNIX From line.
+     *
+     * @return          the date this message was received
+     * @exception       MessagingException
+     */
+    @SuppressWarnings("deprecation")   // for Date constructor
+    public Date getReceivedDate() throws MessagingException {
+       if (rcvDate == null && unix_from != null) {
+           int i;
+           // find the space after the address, before the date
+           i = unix_from.indexOf(' ', 5);
+           if (i > 5) {
+               try {
+                   rcvDate = new Date(unix_from.substring(i));
+               } catch (IllegalArgumentException iae) {
+                   // ignore it
+               }
+           }
+       }
+       return rcvDate == null ? null : new Date(rcvDate.getTime());
+    }
+
+    /**
+     * Return the number of lines for the content of this message.
+     * Return -1 if this number cannot be determined. <p>
+     *
+     * Note that this number may not be an exact measure of the 
+     * content length and may or may not account for any transfer 
+     * encoding of the content. <p>
+     *
+     * This implementation returns -1.
+     *
+     * @return          number of lines in the content.
+     * @exception      MessagingException
+     */  
+    public int getLineCount() throws MessagingException {
+       if (lineCount < 0 && isMimeType("text/plain")) {
+           LineCounter lc = null;
+           // writeTo will set the SEEN flag, remember the original state
+           boolean seen = isSet(Flags.Flag.SEEN);
+           try {
+               lc = new LineCounter(nullOutputStream);
+               getDataHandler().writeTo(lc);
+               lineCount = lc.getLineCount();
+           } catch (IOException ex) {
+               // ignore it, can't happen
+           } finally {
+               try {
+                   if (lc != null)
+                       lc.close();
+               } catch (IOException ex) {
+                   // can't happen
+               }
+           }
+           if (!seen)
+               setFlag(Flags.Flag.SEEN, false);
+       }
+       return lineCount;
+     }
+
+    /**
+     * Set the specified flags on this message to the specified value.
+     *
+     * @param flags    the flags to be set
+     * @param set      the value to be set
+     */
+    public void setFlags(Flags newFlags, boolean set)
+                               throws MessagingException {
+       Flags oldFlags = (Flags)flags.clone();
+       super.setFlags(newFlags, set);
+       if (!flags.equals(oldFlags)) {
+           setHeadersFromFlags(this);
+           if (folder != null)
+               ((MboxFolder)folder).notifyMessageChangedListeners(
+                               MessageChangedEvent.FLAGS_CHANGED, this);
+       }
+    }
+
+    /**
+     * Return the content type, mapping from SunV3 types to MIME types
+     * as necessary.
+     */
+    public String getContentType()  throws MessagingException {
+       String ct = super.getContentType();
+       if (ct.indexOf('/') < 0)
+           ct = SunV3BodyPart.MimeV3Map.toMime(ct);
+       return ct;
+    }
+
+    /**
+     * Produce the raw bytes of the content. This method is used during
+     * parsing, to create a DataHandler object for the content. Subclasses
+     * that can provide a separate input stream for just the message 
+     * content might want to override this method. <p>
+     *
+     * This implementation just returns a ByteArrayInputStream constructed
+     * out of the <code>content</code> byte array.
+     *
+     * @see #content
+     */
+    protected InputStream getContentStream() throws MessagingException {
+       if (folder != null)
+           ((MboxFolder)folder).checkOpen();
+       if (isExpunged())
+           throw new MessageRemovedException("mbox message expunged");
+       if (!isSet(Flags.Flag.SEEN))
+           setFlag(Flags.Flag.SEEN, true);
+       return super.getContentStream();
+    }
+
+    /**                                                            
+     * Return a DataHandler for this Message's content.
+     * If this is a SunV3 multipart message, handle it specially.
+     *
+     * @exception      MessagingException
+     */
+    public synchronized DataHandler getDataHandler() 
+               throws MessagingException {
+       if (dh == null) {
+           // XXX - Following is a kludge to avoid having to register
+           // the "multipart/x-sun-attachment" data type with the JAF.
+           String ct = getContentType();
+           if (ct.equalsIgnoreCase("multipart/x-sun-attachment"))
+               dh = new DataHandler(
+                   new SunV3Multipart(new MimePartDataSource(this)), ct);
+           else
+               return super.getDataHandler();  // will set "dh"
+       }
+       return dh;
+    }
+
+    // here only to allow package private access from MboxFolder
+    protected void setMessageNumber(int msgno) {
+       super.setMessageNumber(msgno);
+    }
+
+    // here to synchronize access to expunged field
+    public synchronized boolean isExpunged() {
+       return super.isExpunged();
+    }
+
+    // here to synchronize and to allow access from MboxFolder
+    protected synchronized void setExpunged(boolean expunged) {
+       super.setExpunged(expunged);
+    }
+
+    // XXX - We assume that only body parts that are part of a SunV3
+    // multipart will use the SunV3 headers (X-Sun-Content-Length,
+    // X-Sun-Content-Lines, X-Sun-Data-Type, X-Sun-Encoding-Info,
+    // X-Sun-Data-Description, X-Sun-Data-Name) so we don't handle
+    // them here.
+
+    /**
+     * Set the flags for this message based on the Status,
+     * X-Status, and X-Dt-Delete-Time headers.
+     *
+     * SIMS 2.0:
+     * "X-Status: DFAT", deleted, flagged, answered, draft.
+     * Unset flags represented as "$".
+     * User flags not supported.
+     *
+     * University of Washington IMAP server:
+     * "X-Status: DFAT", deleted, flagged, answered, draft.
+     * Unset flags not present.
+     * "X-Keywords: userflag1 userflag2"
+     */
+    private synchronized void setFlagsFromHeaders() {
+       flags = new Flags(Flags.Flag.RECENT);
+       try {
+           String s = getHeader("Status", null);
+           if (s != null) {
+               if (s.indexOf('R') >= 0)
+                   flags.add(Flags.Flag.SEEN);
+               if (s.indexOf('O') >= 0)
+                   flags.remove(Flags.Flag.RECENT);
+           }
+           s = getHeader("X-Dt-Delete-Time", null);    // set by dtmail
+           if (s != null)
+               flags.add(Flags.Flag.DELETED);
+           s = getHeader("X-Status", null);            // set by IMAP server
+           if (s != null) {
+               if (s.indexOf('D') >= 0)
+                   flags.add(Flags.Flag.DELETED);
+               if (s.indexOf('F') >= 0)
+                   flags.add(Flags.Flag.FLAGGED);
+               if (s.indexOf('A') >= 0)
+                   flags.add(Flags.Flag.ANSWERED);
+               if (s.indexOf('T') >= 0)
+                   flags.add(Flags.Flag.DRAFT);
+           }
+           s = getHeader("X-Keywords", null);          // set by IMAP server
+           if (s != null) {
+               StringTokenizer st = new StringTokenizer(s);
+               while (st.hasMoreTokens())
+                   flags.add(st.nextToken());
+           }
+       } catch (MessagingException e) {
+           // ignore it
+       }
+    }
+
+    /**
+     * Set the various header fields that represent the message flags.
+     */
+    static void setHeadersFromFlags(MimeMessage msg) {
+       try {
+           Flags flags = msg.getFlags();
+           StringBuilder status = new StringBuilder();
+           if (flags.contains(Flags.Flag.SEEN))
+               status.append('R');
+           if (!flags.contains(Flags.Flag.RECENT))
+               status.append('O');
+           if (status.length() > 0)
+               msg.setHeader("Status", status.toString());
+           else
+               msg.removeHeader("Status");
+
+           boolean sims = false;
+           String s = msg.getHeader("X-Status", null);
+           // is it a SIMS 2.0 format X-Status header?
+           sims = s != null && s.length() == 4 && s.indexOf('$') >= 0;
+           status.setLength(0);
+           if (flags.contains(Flags.Flag.DELETED))
+               status.append('D');
+           else if (sims)
+               status.append('$');
+           if (flags.contains(Flags.Flag.FLAGGED))
+               status.append('F');
+           else if (sims)
+               status.append('$');
+           if (flags.contains(Flags.Flag.ANSWERED))
+               status.append('A');
+           else if (sims)
+               status.append('$');
+           if (flags.contains(Flags.Flag.DRAFT))
+               status.append('T');
+           else if (sims)
+               status.append('$');
+           if (status.length() > 0)
+               msg.setHeader("X-Status", status.toString());
+           else
+               msg.removeHeader("X-Status");
+
+           String[] userFlags = flags.getUserFlags();
+           if (userFlags.length > 0) {
+               status.setLength(0);
+               for (int i = 0; i < userFlags.length; i++)
+                   status.append(userFlags[i]).append(' ');
+               status.setLength(status.length() - 1);  // smash trailing space
+               msg.setHeader("X-Keywords", status.toString());
+           }
+           if (flags.contains(Flags.Flag.DELETED)) {
+               s = msg.getHeader("X-Dt-Delete-Time", null);
+               if (s == null)
+                   // XXX - should be time
+                   msg.setHeader("X-Dt-Delete-Time", "1");
+           }
+       } catch (MessagingException e) {
+           // ignore it
+       }
+    }
+
+    protected void updateHeaders() throws MessagingException {
+       super.updateHeaders();
+       setHeadersFromFlags(this);
+    }
+
+    /**
+     * Save any changes made to this message.
+     */
+    public void saveChanges() throws MessagingException {
+       if (folder != null)
+           ((MboxFolder)folder).checkOpen();
+       if (isExpunged())
+           throw new MessageRemovedException("mbox message expunged");
+       if (!writable)
+           throw new MessagingException("Message is read-only");
+
+       super.saveChanges();
+
+       try {
+           /*
+            * Count the size of the body, in order to set the Content-Length
+            * header.  (Should we only do this to update an existing
+            * Content-Length header?)
+            * XXX - We could cache the content bytes here, for use later
+            * in writeTo.
+            */
+           ContentLengthCounter cos = new ContentLengthCounter();
+           OutputStream os = new NewlineOutputStream(cos);
+           super.writeTo(os);
+           os.flush();
+           setHeader("Content-Length", String.valueOf(cos.getSize()));
+           // setContentSize((int)cos.getSize());
+       } catch (MessagingException e) {
+           throw e;
+       } catch (Exception e) {
+           throw new MessagingException("unexpected exception " + e);
+       }
+    }
+
+    /**
+     * Expose modified flag to MboxFolder.
+     */
+    boolean isModified() {
+       return modified;
+    }
+
+    /**
+     * Put out a byte stream suitable for saving to a file.
+     * XXX - ultimately implement "ignore headers" here?
+     */
+    public void writeToFile(OutputStream os) throws IOException {
+       try {
+           if (getHeader("Content-Length") == null) {
+               /*
+                * Count the size of the body, in order to set the
+                * Content-Length header.
+                */
+               ContentLengthCounter cos = new ContentLengthCounter();
+               OutputStream oos = new NewlineOutputStream(cos);
+               super.writeTo(oos, null);
+               oos.flush();
+               setHeader("Content-Length", String.valueOf(cos.getSize()));
+               // setContentSize((int)cos.getSize());
+           }
+
+           os = new NewlineOutputStream(os, true);
+           PrintStream pos = new PrintStream(os, false, "iso-8859-1");
+
+           pos.println(getUnixFromLine());
+           super.writeTo(pos, null);
+           pos.flush();
+       } catch (MessagingException e) {
+           throw new IOException("unexpected exception " + e);
+       }
+    }
+
+    public void writeTo(OutputStream os, String[] ignoreList)
+                               throws IOException, MessagingException {
+       // set the SEEN flag now, which will normally be set by
+       // getContentStream, so it will show up in our headers
+       if (!isSet(Flags.Flag.SEEN))
+           setFlag(Flags.Flag.SEEN, true);
+       super.writeTo(os, ignoreList);
+    }
+
+    /**
+     * Interpose on superclass method to make sure folder is still open
+     * and message hasn't been expunged.
+     */
+    public String[] getHeader(String name)
+                       throws MessagingException {
+       if (folder != null)
+           ((MboxFolder)folder).checkOpen();
+       if (isExpunged())
+           throw new MessageRemovedException("mbox message expunged");
+       return super.getHeader(name);
+    }
+
+    /**
+     * Interpose on superclass method to make sure folder is still open
+     * and message hasn't been expunged.
+     */
+    public String getHeader(String name, String delimiter)
+                               throws MessagingException {
+       if (folder != null)
+           ((MboxFolder)folder).checkOpen();
+       if (isExpunged())
+           throw new MessageRemovedException("mbox message expunged");
+       return super.getHeader(name, delimiter);
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxProvider.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxProvider.java
new file mode 100644 (file)
index 0000000..d0b056d
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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.Provider;
+
+/**
+ * The Mbox protocol provider.
+ */
+public class MboxProvider extends Provider {
+    public MboxProvider() {
+       super(Provider.Type.STORE, "mbox", MboxStore.class.getName(),
+           "Oracle", null);
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxStore.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxStore.java
new file mode 100644 (file)
index 0000000..1ce39bc
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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 java.io.*;
+import javax.mail.*;
+
+public class MboxStore extends Store {
+
+    String user;
+    String home;
+    Mailbox mb;
+    static Flags permFlags;
+
+    static {
+       // we support all flags
+       permFlags = new Flags();
+       permFlags.add(Flags.Flag.SEEN);
+       permFlags.add(Flags.Flag.RECENT);
+       permFlags.add(Flags.Flag.DELETED);
+       permFlags.add(Flags.Flag.FLAGGED);
+       permFlags.add(Flags.Flag.ANSWERED);
+       permFlags.add(Flags.Flag.DRAFT);
+       permFlags.add(Flags.Flag.USER);
+    }
+
+    public MboxStore(Session session, URLName url) {
+       super(session, url);
+
+       // XXX - handle security exception
+       user = System.getProperty("user.name");
+       home = System.getProperty("user.home");
+       String os = System.getProperty("os.name");
+       try {
+           String cl = "com.sun.mail.mbox." + os + "Mailbox";
+           mb = (Mailbox)Class.forName(cl).
+                                       getDeclaredConstructor().newInstance();
+       } catch (Exception e) {
+           mb = new DefaultMailbox();
+       }
+    }
+
+    /**
+     * Since we do not have any authentication
+     * to do and we do not want a dialog put up asking the user for a 
+     * password we always succeed in connecting.
+     * But if we're given a password, that means the user is
+     * doing something wrong so fail the request.
+     */
+    protected boolean protocolConnect(String host, int port, String user,
+                               String passwd) throws MessagingException {
+
+       if (passwd != null)
+           throw new AuthenticationFailedException(
+                               "mbox does not allow passwords");
+       // XXX - should we use the user?
+       return true;
+    }
+
+    protected void setURLName(URLName url) {
+       // host, user, password, and file don't matter so we strip them out
+       if (url != null && (url.getUsername() != null ||
+                           url.getHost() != null ||
+                           url.getFile() != null))
+           url = new URLName(url.getProtocol(), null, -1, null, null, null);
+       super.setURLName(url);
+    }
+
+
+    public Folder getDefaultFolder() throws MessagingException {
+       checkConnected();
+
+       return new MboxFolder(this, null);
+    }
+
+    public Folder getFolder(String name) throws MessagingException {
+       checkConnected();
+
+       return new MboxFolder(this, name);
+    }
+
+    public Folder getFolder(URLName url) throws MessagingException {
+       checkConnected();
+       return getFolder(url.getFile());
+    }
+
+    private void checkConnected() throws MessagingException {
+       if (!isConnected())
+           throw new MessagingException("Not connected");
+    }
+
+    MailFile getMailFile(String folder) {
+       return mb.getMailFile(user, folder);
+    }
+
+    Session getSession() {
+       return session;
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/MessageLoader.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/MessageLoader.java
new file mode 100644 (file)
index 0000000..b5ecaaa
--- /dev/null
@@ -0,0 +1,285 @@
+/*
+ * 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 java.io.*;
+import java.util.*;
+
+/**
+ * A support class that contains the state and logic needed when
+ * loading messages from a folder.
+ */
+final class MessageLoader {
+    private final TempFile temp;
+    private FileInputStream fis = null;
+    private OutputStream fos = null;
+    private int pos, len;      // position in and length of buffer
+    private long off;          // current offset in temp file
+    private long prevend;      // the end of the previous message in temp file
+    private MboxFolder.MessageMetadata md;
+    private byte[] buf = null;
+    // the length of the longest header we'll need to look at
+    private static final int LINELEN = "Content-Length: XXXXXXXXXX".length();
+    private char[] line;
+
+    public MessageLoader(TempFile temp) {
+       this.temp = temp;
+    }
+
+    /**
+     * Load messages from the given file descriptor, starting at the
+     * specified offset, adding the MessageMetadata to the list. <p>
+     *
+     * The data is assumed to be in UNIX mbox format, with newlines
+     * only as the line terminators.
+     */
+    public int load(FileDescriptor fd, long offset,
+                               List<MboxFolder.MessageMetadata> msgs)
+                               throws IOException {
+       // XXX - could allocate and deallocate buffers here
+       int loaded = 0;
+       try {
+           fis = new FileInputStream(fd);
+           if (fis.skip(offset) != offset)
+               throw new EOFException("Failed to skip to offset " + offset);
+           this.off = prevend = temp.length();
+           pos = len = 0;
+           line = new char[LINELEN];
+           buf = new byte[64 * 1024];
+           fos = temp.getAppendStream();
+           int n;
+           // keep loading messages as long as we have headers
+           while ((n = skipHeader(loaded == 0)) >= 0) {
+               long start;
+               if (n == 0) {
+                   // didn't find a Content-Length, skip the body
+                   start = skipBody();
+                   if (start < 0) {
+                       // md.end = -1;
+                       md.dataend = -1;
+                       msgs.add(md);
+                       loaded++;
+                       break;
+                   }
+                   md.dataend = start;
+               } else {
+                   // skip over the body
+                   skip(n);
+                   md.dataend = off;
+                   int b;
+                   // skip any blank lines after the body
+                   while ((b = get()) >= 0) {
+                       if (b != '\n')
+                           break;
+                   }
+                   start = off;
+                   if (b >= 0)
+                       start--;        // back up one byte if not at EOF
+               }
+               // md.end = start;
+               prevend = start;
+               msgs.add(md);
+               loaded++;
+           }
+       } finally {
+           try {
+               fis.close();
+           } catch (IOException ex) {
+               // ignore
+           }
+           try {
+               fos.close();
+           } catch (IOException ex) {
+               // ignore
+           }
+           line = null;
+           buf = null;
+       }
+       return loaded;
+    }
+
+    /**
+     * Skip over the message header, returning the content length
+     * of the body, or 0 if no Content-Length header was seen.
+     * Update the MessageMetadata based on the headers seen.
+     * return -1 on EOF.
+     */
+    private int skipHeader(boolean first)  throws IOException {
+       int clen = 0;
+       boolean bol = true;
+       int lpos = -1;
+       int b;
+       boolean saw_unix_from = false;
+       int lineno = 0;
+       md = new MboxFolder.MessageMetadata();
+       md.start = prevend;
+       md.recent = true;
+       while ((b = get()) >= 0) {
+           if (bol) {
+               if (b == '\n')
+                   break;
+               lpos = 0;
+           }
+           if (b == '\n') {
+               bol = true;
+               lineno++;
+               // newline at end of line, was the line one of the headers
+               // we're looking for?
+               if (lpos > 7) {
+                   // XXX - make this more efficient?
+                   String s = new String(line, 0, lpos);
+                   // fast check for Content-Length header
+                   if (lineno == 1 && line[0] == 'F' && isPrefix(s, "From ")) {
+                       saw_unix_from = true;
+                   } else if (line[7] == '-' &&
+                               isPrefix(s, "Content-Length:")) {
+                       s = s.substring(15).trim();
+                       try {
+                           clen = Integer.parseInt(s);
+                       } catch (NumberFormatException ex) {
+                           // ignore it
+                       }
+                   // fast check for Status header
+                   } else if ((line[1] == 't' || line[1] == 'T') &&
+                               isPrefix(s, "Status:")) {
+                       if (s.indexOf('O') >= 0)
+                           md.recent = false;
+                   // fast check for X-Status header
+                   } else if ((line[3] == 't' || line[3] == 'T') &&
+                               isPrefix(s, "X-Status:")) {
+                       if (s.indexOf('D') >= 0)
+                           md.deleted = true;
+                   // fast check for X-Dt-Delete-Time header
+                   } else if (line[4] == '-' &&
+                               isPrefix(s, "X-Dt-Delete-Time:")) {
+                       md.deleted = true;
+                   // fast check for X-IMAP header
+                   } else if (line[5] == 'P' && s.startsWith("X-IMAP:")) {
+                       md.imap = true;
+                   }
+               }
+           } else {
+               // accumlate data in line buffer
+               bol = false;
+               if (lpos < 0)   // ignoring this line
+                   continue;
+               if (lpos == 0 && (b == ' ' || b == '\t'))
+                   lpos = -1;  // ignore continuation lines
+               else if (lpos < line.length)
+                   line[lpos++] = (char)b;
+           }
+       }
+
+       // if we hit EOF, or this is the first message we're loading and
+       // it doesn't have a UNIX From line, return EOF.
+       // (After the first message, UNIX From lines are seen by skipBody
+       // to terminate the message.)
+       if (b < 0 || (first && !saw_unix_from))
+           return -1;
+       else
+           return clen;
+    }
+
+    /**
+     * Does "s" start with "pre", ignoring case?
+     */
+    private static final boolean isPrefix(String s, String pre) {
+       return s.regionMatches(true, 0, pre, 0, pre.length());
+    }
+
+    /**
+     * Skip over the body of the message looking for a line that starts
+     * with "From ".  If found, return the offset of the beginning of
+     * that line.  Return -1 on EOF.
+     */
+    private long skipBody() throws IOException {
+       boolean bol = true;
+       int lpos = -1;
+       long loff = off;
+       int b;
+       while ((b = get()) >= 0) {
+           if (bol) {
+               lpos = 0;
+               loff = off - 1;
+           }
+           if (b == '\n') {
+               bol = true;
+               if (lpos >= 5) {        // have enough data to test?
+                   if (line[0] == 'F' && line[1] == 'r' && line[2] == 'o' &&
+                       line[3] == 'm' && line[4] == ' ')
+                       return loff;
+               }
+           } else {
+               bol = false;
+               if (lpos < 0)
+                   continue;
+               if (lpos == 0 && b != 'F')
+                   lpos = -1;          // ignore lines that don't start with F
+               else if (lpos < 5)      // only need first 5 chars to test
+                   line[lpos++] = (char)b;
+           }
+       }
+       return -1;
+    }
+
+    /**
+     * Skip "n" bytes, returning how much we were able to skip.
+     */
+    private final int skip(int n) throws IOException {
+       int n0 = n;
+       if (pos + n < len) {
+           pos += n;   // can do it all within this buffer
+           off += n;
+       } else {
+           do {
+               n -= (len - pos);       // skip rest of this buffer
+               off += (len - pos);
+               fill();
+               if (len <= 0)   // ran out of data
+                   return n0 - n;
+           } while (n > len);
+           pos += n;
+           off += n;
+       }
+       return n0;
+    }
+
+    /**
+     * Return the next byte.
+     */
+    private final int get() throws IOException {
+       if (pos >= len)
+           fill();
+       if (pos >= len)
+           return -1;
+       else {
+           off++;
+           return buf[pos++] & 0xff;
+       }
+    }
+
+    /**
+     * Fill our buffer with more data.
+     * Every buffer we read is also written to the temp file.
+     */
+    private final void fill() throws IOException {
+       len = fis.read(buf);
+       pos = 0;
+       if (len > 0)
+           fos.write(buf, 0, len);
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/NewlineOutputStream.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/NewlineOutputStream.java
new file mode 100644 (file)
index 0000000..f4a31cb
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * 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 java.io.*;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Convert the various newline conventions to the local platform's
+ * newline convention.  Optionally, make sure the output ends with
+ * a blank line.
+ */
+public class NewlineOutputStream extends FilterOutputStream {
+    private int lastb = -1;
+    private int bol = 1; // number of times in a row we're at beginning of line
+    private final boolean endWithBlankLine;
+    private static final byte[] newline;
+
+    static {
+       String s = null;
+       try {
+           s = System.lineSeparator();
+       } catch (SecurityException sex) {
+           // ignore, should never happen
+       }
+       if (s == null || s.length() <= 0)
+           s = "\n";
+       newline = s.getBytes(StandardCharsets.ISO_8859_1);
+    }
+
+    public NewlineOutputStream(OutputStream os) {
+       this(os, false);
+    }
+
+    public NewlineOutputStream(OutputStream os, boolean endWithBlankLine) {
+       super(os);
+       this.endWithBlankLine = endWithBlankLine;
+    }
+
+    public void write(int b) throws IOException {
+       if (b == '\r') {
+           out.write(newline);
+           bol++;
+       } else if (b == '\n') {
+           if (lastb != '\r') {
+               out.write(newline);
+               bol++;
+           }
+       } else {
+           out.write(b);
+           bol = 0;    // no longer at beginning of line
+       }
+       lastb = b;
+    }
+
+    public void write(byte b[]) throws IOException {
+       write(b, 0, b.length);
+    }
+
+    public void write(byte b[], int off, int len) throws IOException {
+       for (int i = 0 ; i < len ; i++) {
+           write(b[off + i]);
+       }
+    }
+
+    public void flush() throws IOException {
+       if (endWithBlankLine) {
+           if (bol == 0) {
+               // not at bol, return to bol and add a blank line
+               out.write(newline);
+               out.write(newline);
+           } else if (bol == 1) {
+               // at bol, add a blank line
+               out.write(newline);
+           }
+       }
+       bol = 2;
+       out.flush();
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/SolarisMailbox.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/SolarisMailbox.java
new file mode 100644 (file)
index 0000000..84f3abc
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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 java.io.File;
+
+public class SolarisMailbox extends Mailbox {
+    private final String home;
+    private final String user;
+
+    private static final boolean homeRelative =
+                               Boolean.getBoolean("mail.mbox.homerelative");
+
+    public SolarisMailbox() {
+       String h = System.getenv("HOME");
+       if (h == null)
+           h = System.getProperty("user.home");
+       home = h;
+       user = System.getProperty("user.name");
+    }
+
+    public MailFile getMailFile(String user, String folder) {
+       if (folder.equalsIgnoreCase("INBOX"))
+           return new UNIXInbox(user, filename(user, folder));
+       else
+           return new UNIXFolder(filename(user, folder));
+    }
+
+    /**
+     * Given a name of a mailbox folder, expand it to a full path name.
+     */
+    public String filename(String user, String folder) {
+       try {
+           switch (folder.charAt(0)) {
+           case '/':
+               return folder;
+           case '~':
+               int i = folder.indexOf(File.separatorChar);
+               String tail = "";
+               if (i > 0) {
+                   tail = folder.substring(i);
+                   folder = folder.substring(0, i);
+               }
+               if (folder.length() == 1)
+                   return home + tail;
+               else
+                   return "/home/" + folder.substring(1) + tail;       // XXX
+           default:
+               if (folder.equalsIgnoreCase("INBOX")) {
+                   if (user == null)   // XXX - should never happen
+                       user = this.user;
+                   String inbox = System.getenv("MAIL");
+                   if (inbox == null)
+                       inbox = "/var/mail/" + user;
+                   return inbox;
+               } else {
+                   if (homeRelative)
+                       return home + File.separator + folder;
+                   else
+                       return folder;
+               }
+           }
+       } catch (StringIndexOutOfBoundsException e) {
+           return folder;
+       }
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/SunOSMailbox.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/SunOSMailbox.java
new file mode 100644 (file)
index 0000000..55230d1
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+public class SunOSMailbox extends SolarisMailbox {
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/SunV3BodyPart.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/SunV3BodyPart.java
new file mode 100644 (file)
index 0000000..189ff85
--- /dev/null
@@ -0,0 +1,314 @@
+/*
+ * 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.internet.*;
+import javax.activation.*;
+import java.io.*;
+
+/**
+ * This class represents a SunV3 BodyPart.
+ *
+ * @author Bill Shannon
+ * @see javax.mail.Part
+ * @see javax.mail.internet.MimePart
+ * @see javax.mail.internet.MimeBodyPart
+ */
+
+public class SunV3BodyPart extends MimeBodyPart {
+    /**
+     * Constructs a SunV3BodyPart using the given header and
+     * content bytes. <p>
+     *
+     * Used by providers.
+     *
+     * @param  headers The header of this part
+     * @param  content bytes representing the body of this part.
+     */
+    public SunV3BodyPart(InternetHeaders headers, byte[] content) 
+                       throws MessagingException {
+       super(headers, content);
+    }
+
+    /**
+     * Return the size of the content of this BodyPart in bytes.
+     * Return -1 if the size cannot be determined. <p>
+     *
+     * Note that this number may not be an exact measure of the
+     * content size and may or may not account for any transfer
+     * encoding of the content. <p>
+     *
+     * @return size in bytes
+     */
+    public int getSize() throws MessagingException {
+       String s = getHeader("X-Sun-Content-Length", null);
+       try {
+           return Integer.parseInt(s);
+       } catch (NumberFormatException ex) {
+           return -1;
+       }
+    }
+
+    /**
+     * Return the number of lines for the content of this Part.
+     * Return -1 if this number cannot be determined. <p>
+     *
+     * Note that this number may not be an exact measure of the 
+     * content length and may or may not account for any transfer 
+     * encoding of the content. 
+     */  
+     public int getLineCount() throws MessagingException {
+       String s = getHeader("X-Sun-Content-Lines", null);
+       try {
+           return Integer.parseInt(s);
+       } catch (NumberFormatException ex) {
+           return -1;
+       }
+    }
+
+    /*
+     * This is just enough to get us going.
+     *
+     * For more complete transformation from V3 to MIME, refer to
+     * sun_att.c from the Sun IMAP server code.
+     */
+    static class MimeV3Map {
+       String mime;
+       String v3;
+
+       MimeV3Map(String mime, String v3) {
+           this.mime = mime;
+           this.v3 = v3;
+       }
+
+       private static MimeV3Map[] mimeV3Table = new MimeV3Map[] {
+           new MimeV3Map("text/plain", "text"),
+           new MimeV3Map("text/plain", "default"),
+           new MimeV3Map("multipart/x-sun-attachment", "X-sun-attachment"),
+           new MimeV3Map("application/postscript", "postscript-file"),
+           new MimeV3Map("image/gif", "gif-file")
+           // audio-file
+           // cshell-script
+       };
+
+       // V3 Content-Type to MIME Content-Type
+       static String toMime(String s) {
+           for (int i = 0; i < mimeV3Table.length; i++) {
+               if (mimeV3Table[i].v3.equalsIgnoreCase(s))
+                   return mimeV3Table[i].mime;
+           }
+           return "application/x-" + s;
+       }
+
+       // MIME Content-Type to V3 Content-Type
+       static String toV3(String s) {
+           for (int i = 0; i < mimeV3Table.length; i++) {
+               if (mimeV3Table[i].mime.equalsIgnoreCase(s))
+                   return mimeV3Table[i].v3;
+           }
+           return s;
+       }
+    }
+
+    /**
+     * Returns the value of the RFC822 "Content-Type" header field.
+     * This represents the content-type of the content of this
+     * BodyPart. This value must not be null. If this field is
+     * unavailable, "text/plain" should be returned. <p>
+     *
+     * This implementation uses <code>getHeader(name)</code>
+     * to obtain the requisite header field.
+     *
+     * @return Content-Type of this BodyPart
+     */
+    public String getContentType() throws MessagingException {
+       String ct = getHeader("Content-Type", null);
+       if (ct == null)
+           ct = getHeader("X-Sun-Data-Type", null);
+       if (ct == null)
+           ct = "text/plain";
+       else if (ct.indexOf('/') < 0)
+           ct = MimeV3Map.toMime(ct);
+       return ct;
+    }
+
+    /**
+     * Returns the value of the "Content-Transfer-Encoding" header
+     * field. Returns <code>null</code> if the header is unavailable
+     * or its value is absent. <p>
+     *
+     * This implementation uses <code>getHeader(name)</code>
+     * to obtain the requisite header field.
+     *
+     * @see #headers
+     */
+    public String getEncoding() throws MessagingException {
+       String enc = super.getEncoding();
+       if (enc == null)
+           enc = getHeader("X-Sun-Encoding-Info", null);
+       return enc;
+    }
+
+    /**
+     * Returns the "Content-Description" header field of this BodyPart.
+     * This typically associates some descriptive information with 
+     * this part. Returns null if this field is unavailable or its
+     * value is absent. <p>
+     *
+     * If the Content-Description field is encoded as per RFC 2047,
+     * it is decoded and converted into Unicode. If the decoding or 
+     * conversion fails, the raw data is returned as-is <p>
+     *
+     * This implementation uses <code>getHeader(name)</code>
+     * to obtain the requisite header field.
+     * 
+     * @return content-description
+     */
+    public String getDescription() throws MessagingException {
+       String desc = super.getDescription();
+       if (desc == null)
+           desc = getHeader("X-Sun-Data-Description", null);
+       return desc;
+    }
+
+    /**
+     * Set the "Content-Description" header field for this BodyPart.
+     * If the description parameter is <code>null</code>, then any 
+     * existing "Content-Description" fields are removed. <p>
+     *
+     * If the description contains non US-ASCII characters, it will 
+     * be encoded using the platform's default charset. If the 
+     * description contains only US-ASCII characters, no encoding 
+     * is done and it is used as-is.
+     * 
+     * @param description content-description
+     * @exception      IllegalWriteException if the underlying
+     *                 implementation does not support modification
+     * @exception      IllegalStateException if this BodyPart is
+     *                 obtained from a READ_ONLY folder.
+     */
+    public void setDescription(String description) throws MessagingException {
+       throw new MethodNotSupportedException("SunV3BodyPart not writable");
+    }
+
+    /**
+     * Set the "Content-Description" header field for this BodyPart.
+     * If the description parameter is <code>null</code>, then any 
+     * existing "Content-Description" fields are removed. <p>
+     *
+     * If the description contains non US-ASCII characters, it will 
+     * be encoded using the specified charset. If the description 
+     * contains only US-ASCII characters, no encoding  is done and 
+     * it is used as-is
+     * 
+     * @param  description     Description
+     * @param  charset         Charset for encoding
+     * @exception      IllegalWriteException if the underlying
+     *                 implementation does not support modification
+     * @exception      IllegalStateException if this BodyPart is
+     *                 obtained from a READ_ONLY folder.
+     */
+    public void setDescription(String description, String charset) 
+               throws MessagingException {
+       throw new MethodNotSupportedException("SunV3BodyPart not writable");
+    }
+
+    /**
+     * Get the filename associated with this BodyPart. <p>
+     *
+     * Returns the value of the "filename" parameter from the
+     * "Content-Disposition" header field of this BodyPart. If its
+     * not available, returns the value of the "name" parameter from
+     * the "Content-Type" header field of this BodyPart.
+     * Returns <code>null</code> if both are absent.
+     *
+     * @return filename
+     */
+    public String getFileName() throws MessagingException {
+       String name = super.getFileName();
+       if (name == null)
+           name = getHeader("X-Sun-Data-Name", null);
+       return name;
+    }
+
+    /**
+     * Set the filename associated with this BodyPart, if possible. <p>
+     *
+     * Sets the "filename" parameter of the "Content-Disposition"
+     * header field of this BodyPart.
+     *
+     * @exception      IllegalWriteException if the underlying
+     *                 implementation does not support modification
+     * @exception      IllegalStateException if this BodyPart is
+     *                 obtained from a READ_ONLY folder.
+     */
+    public void setFileName(String filename) throws MessagingException {
+       throw new MethodNotSupportedException("SunV3BodyPart not writable");
+    }
+
+    /**
+     * This method provides the mechanism to set this BodyPart's content.
+     * The given DataHandler object should wrap the actual content.
+     * 
+     * @param   dh      The DataHandler for the content
+     * @exception       IllegalWriteException if the underlying
+     *                         implementation does not support modification
+     * @exception      IllegalStateException if this BodyPart is
+     *                 obtained from a READ_ONLY folder.
+     */                 
+    public void setDataHandler(DataHandler dh) 
+               throws MessagingException {
+       throw new MethodNotSupportedException("SunV3BodyPart not writable");
+    }
+
+    /**
+     * Output the BodyPart as a RFC822 format stream.
+     *
+     * @exception MessagingException
+     * @exception IOException  if an error occurs writing to the
+     *                         stream or if an error is generated
+     *                         by the javax.activation layer.
+     * @see javax.activation.DataHandler#writeTo()
+     */
+    public void writeTo(OutputStream os)
+                               throws IOException, MessagingException {
+       throw new MethodNotSupportedException("SunV3BodyPart writeTo");
+    }
+
+    /**
+     * This is the method that has the 'smarts' to query the 'content'
+     * and update the appropriate headers. Typical headers that get
+     * set here are: Content-Type, Content-Encoding, boundary (for
+     * multipart). Now, the tricky part here is when to actually
+     * activate this method:
+     *
+     * - A Message being crafted by a mail-application will certainly
+     * need to activate this method at some point to fill up its internal
+     * headers. Typically this is triggered off by our writeTo() method.
+     *
+     * - A message read-in from a MessageStore will have obtained
+     * all its headers from the store, and so does'nt need this.
+     * However, if this message is editable and if any edits have
+     * been made to either the content or message-structure, we might
+     * need to resync our headers. Typically this is triggered off by
+     * the Message.saveChanges() methods.
+     */
+    protected void updateHeaders() throws MessagingException {
+       throw new MethodNotSupportedException("SunV3BodyPart updateHeaders");
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/SunV3Multipart.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/SunV3Multipart.java
new file mode 100644 (file)
index 0000000..8a642f1
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * 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.internet.*;
+import javax.activation.*;
+import java.util.*;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import com.sun.mail.util.LineInputStream;
+
+/**
+ * The SunV3Multipart class is an implementation of the abstract Multipart
+ * class that uses SunV3 conventions for the multipart data. <p>
+ *
+ * @author  Bill Shannon
+ */
+
+public class SunV3Multipart extends MimeMultipart {
+    private boolean parsing;
+
+    /**
+     * Constructs a SunV3Multipart object and its bodyparts from the 
+     * given DataSource. <p>
+     *
+     * @param  ds      DataSource, can be a MultipartDataSource
+     */
+    public SunV3Multipart(DataSource ds) throws MessagingException {
+       super(ds);
+    }
+
+    /**
+     * Set the subtype.  Throws MethodNotSupportedException.
+     *
+     * @param  subtype         Subtype
+     */
+    public void setSubType(String subtype) throws MessagingException {
+       throw new MethodNotSupportedException(
+               "can't change SunV3Multipart subtype");
+    }
+
+    /**
+     * Get the BodyPart referred to by the given ContentID (CID). 
+     * Throws MethodNotSupportException.
+     */
+    public synchronized BodyPart getBodyPart(String CID) 
+                       throws MessagingException {
+       throw new MethodNotSupportedException(
+               "SunV3Multipart doesn't support Content-ID");
+    }
+
+    /**
+     * Update headers.  Throws MethodNotSupportException.
+     */
+    protected void updateHeaders() throws MessagingException {
+       throw new MethodNotSupportedException("SunV3Multipart not writable");
+    }
+
+    /**
+     * Iterates through all the parts and outputs each SunV3 part
+     * separated by a boundary.
+     */
+    public void writeTo(OutputStream os)
+                               throws IOException, MessagingException {
+       throw new MethodNotSupportedException(
+               "SunV3Multipart writeTo not supported");
+    }
+
+    private static final String boundary = "----------";
+
+    /*
+     * Parse the contents of this multipart message and create the
+     * child body parts.
+     */
+    protected synchronized void parse() throws MessagingException {
+       /*
+        * If the data has already been parsed, or we're in the middle of
+        * parsing it, there's nothing to do.  The latter will occur when
+        * we call addBodyPart, which will call parse again.  We really
+        * want to be able to call super.super.addBodyPart.
+        */
+       if (parsed || parsing)
+           return;
+
+       InputStream in = null;
+
+       try {
+           in = ds.getInputStream();
+           if (!(in instanceof ByteArrayInputStream) &&
+               !(in instanceof BufferedInputStream))
+               in = new BufferedInputStream(in);
+       } catch (IOException ex) {
+           throw new MessagingException("No inputstream from datasource");
+       } catch (RuntimeException ex) {
+           throw new MessagingException("No inputstream from datasource");
+       }
+
+       byte[] bndbytes = boundary.getBytes(StandardCharsets.ISO_8859_1);
+       int bl = bndbytes.length;
+
+       String line;
+       parsing = true;
+       try {
+           /*
+            * Skip any kind of junk until we get to the first
+            * boundary line.
+            */
+           LineInputStream lin = new LineInputStream(in);
+           while ((line = lin.readLine()) != null) {
+               if (line.trim().equals(boundary))
+                   break;
+           }
+           if (line == null)
+               throw new MessagingException("Missing start boundary");
+
+           /*
+            * Read and process body parts until we see the
+            * terminating boundary line (or EOF).
+            */
+           for (;;) {
+               /*
+                * Collect the headers for this body part.
+                */
+               InternetHeaders headers = new InternetHeaders(in);
+
+               if (!in.markSupported())
+                   throw new MessagingException("Stream doesn't support mark");
+
+               ByteArrayOutputStream buf = new ByteArrayOutputStream();
+               int b;
+
+               /*
+                * Read and save the content bytes in buf.
+                */
+               while ((b = in.read()) >= 0) {
+                   if (b == '\r' || b == '\n') {
+                       /*
+                        * Found the end of a line, check whether the
+                        * next line is a boundary.
+                        */
+                       int i;
+                       in.mark(bl + 4 + 1);    // "4" for possible "--\r\n"
+                       if (b == '\r' && in.read() != '\n') {
+                           in.reset();
+                           in.mark(bl + 4);
+                       }
+                       // read bytes, matching against the boundary
+                       for (i = 0; i < bl; i++)
+                           if (in.read() != bndbytes[i])
+                               break;
+                       if (i == bl) {
+                           int b2 = in.read();
+                           // check for end of line
+                           if (b2 == '\n')
+                               break;  // got it!  break out of the while loop
+                           if (b2 == '\r') {
+                               in.mark(1);
+                               if (in.read() != '\n')
+                                   in.reset();
+                               break;  // got it!  break out of the while loop
+                           }
+                       }
+                       // failed to match, reset and proceed normally
+                       in.reset();
+                   }
+                   buf.write(b);
+               }
+
+               /*
+                * Create a SunV3BodyPart to represent this body part.
+                */
+               SunV3BodyPart body =
+                       new SunV3BodyPart(headers, buf.toByteArray());
+               addBodyPart(body);
+               if (b < 0)
+                   break;
+           }
+       } catch (IOException e) {
+           throw new MessagingException("IO Error");   // XXX
+       } finally {
+           parsing = false;
+       }
+
+       parsed = true;
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/TempFile.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/TempFile.java
new file mode 100644 (file)
index 0000000..25f4bd8
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2010, 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 java.util.*;
+import java.net.*;
+import java.io.*;
+import java.security.*;
+
+import com.sun.mail.util.PropUtil;
+import javax.mail.util.SharedFileInputStream;
+
+/**
+ * A temporary file used to cache messages.
+ */
+class TempFile {
+
+    private File file; // the temp file name
+    private WritableSharedFile sf;
+
+    /**
+     * Create a temp file in the specified directory (if not null).
+     * The file will be deleted when the JVM exits.
+     */
+    public TempFile(File dir) throws IOException {
+       file = File.createTempFile("mbox.", ".mbox", dir);
+       // XXX - need JDK 6 to set permissions on the file to owner-only
+       file.deleteOnExit();
+       sf = new WritableSharedFile(file);
+    }
+
+    /**
+     * Return a stream for appending to the temp file.
+     */
+    public AppendStream getAppendStream() throws IOException {
+       return sf.getAppendStream();
+    }
+
+    /**
+     * Return a stream for reading from part of the file.
+     */
+    public InputStream newStream(long start, long end) {
+       return sf.newStream(start, end);
+    }
+
+    public long length() {
+       return file.length();
+    }
+
+    /**
+     * Close and remove this temp file.
+     */
+    public void close() {
+       try {
+           sf.close();
+       } catch (IOException ex) {
+           // ignore it
+       }
+       file.delete();
+    }
+
+    protected void finalize() throws Throwable {
+       try {
+           close();
+       } finally {
+           super.finalize();
+       }
+    }
+}
+
+/**
+ * A subclass of SharedFileInputStream that also allows writing.
+ */
+class WritableSharedFile extends SharedFileInputStream {
+    private RandomAccessFile raf;
+    private AppendStream af;
+
+    public WritableSharedFile(File file) throws IOException {
+       super(file);
+       try {
+           raf = new RandomAccessFile(file, "rw");
+       } catch (IOException ex) {
+           // if anything goes wrong opening the writable file,
+           // close the readable file too
+           super.close();
+       }
+    }
+
+    /**
+     * Return the writable version of this file.
+     */
+    public RandomAccessFile getWritableFile() {
+       return raf;
+    }
+
+    /**
+     * Close the readable and writable files.
+     */
+    public void close() throws IOException {
+       try {
+           super.close();
+       } finally {
+           raf.close();
+       }
+    }
+
+    /**
+     * Update the size of the readable file after writing
+     * to the file.  Updates the length to be the current
+     * size of the file.
+     */
+    synchronized long updateLength() throws IOException {
+       datalen = in.length();
+       af = null;
+       return datalen;
+    }
+
+    /**
+     * Return a new AppendStream, but only if one isn't in active use.
+     */
+    public synchronized AppendStream getAppendStream() throws IOException {
+       if (af != null)
+           throw new IOException(
+               "file cache only supports single threaded access");
+       af = new AppendStream(this);
+       return af;
+    }
+}
+
+/**
+ * A stream for writing to the temp file, and when done
+ * can return a stream for reading the data just written.
+ * NOTE: We assume that only one thread is writing to the
+ * file at a time.
+ */
+class AppendStream extends OutputStream {
+    private final WritableSharedFile tf;
+    private RandomAccessFile raf;
+    private final long start;
+    private long end;
+
+    public AppendStream(WritableSharedFile tf) throws IOException {
+       this.tf = tf;
+       raf = tf.getWritableFile();
+       start = raf.length();
+       raf.seek(start);
+    }
+
+    public void write(int b) throws IOException {
+       raf.write(b);
+    }
+
+    public void write(byte[] b) throws IOException {
+       raf.write(b);
+    }
+
+    public void write(byte[] b, int off, int len) throws IOException {
+       raf.write(b, off, len);
+    }
+
+    public synchronized void close() throws IOException {
+       end = tf.updateLength();
+       raf = null;     // no more writing allowed
+    }
+
+    public synchronized InputStream getInputStream() throws IOException {
+       return tf.newStream(start, end);
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/UNIXFile.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/UNIXFile.java
new file mode 100644 (file)
index 0000000..ab1579e
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * 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 java.io.File;
+import java.io.FileDescriptor;
+import java.util.StringTokenizer;
+
+public class UNIXFile extends File {
+    protected static final boolean loaded;
+    protected static final int lockType;
+
+    private static final long serialVersionUID = -7972156315284146651L;
+
+    public UNIXFile(String name) {
+       super(name);
+    }
+
+    // lock type enum
+    protected static final int NONE = 0;
+    protected static final int NATIVE = 1;
+    protected static final int JAVA = 2;
+
+    static {
+       String lt = System.getProperty("mail.mbox.locktype", "native");
+       int type = NATIVE;
+       if (lt.equalsIgnoreCase("none"))
+           type = NONE;
+       else if (lt.equalsIgnoreCase("java"))
+           type = JAVA;
+       lockType = type;
+
+       boolean lloaded = false;
+       if (lockType == NATIVE) {
+           try {
+               System.loadLibrary("mbox");
+               lloaded = true;
+           } catch (UnsatisfiedLinkError e) {
+               String classpath = System.getProperty("java.class.path");
+               String sep = System.getProperty("path.separator");
+               String arch = System.getProperty("os.arch");
+               StringTokenizer st = new StringTokenizer(classpath, sep);
+               while (st.hasMoreTokens()) {
+                   String path = st.nextToken();
+                   if (path.endsWith("/classes") ||
+                           path.endsWith("/mail.jar") ||
+                           path.endsWith("/javax.mail.jar")) {
+                       int i = path.lastIndexOf('/');
+                       String libdir = path.substring(0, i + 1) + "lib/";
+                       String lib = libdir + arch + "/libmbox.so";
+                       try {
+                           System.load(lib);
+                           lloaded = true;
+                           break;
+                       } catch (UnsatisfiedLinkError e2) {
+                           lib = libdir + "libmbox.so";
+                           try {
+                               System.load(lib);
+                               lloaded = true;
+                               break;
+                           } catch (UnsatisfiedLinkError e3) {
+                               continue;
+                           }
+                       }
+                   }
+               }
+           }
+       }
+       loaded = lloaded;
+       if (loaded)
+           initIDs(FileDescriptor.class, FileDescriptor.in);
+    }
+
+    /**
+     * Return the access time of the file.
+     */
+    public static long lastAccessed(File file) {
+       return lastAccessed0(file.getPath());
+    }
+
+    public long lastAccessed() {
+       return lastAccessed0(getPath());
+    }
+
+    private static native void initIDs(Class<FileDescriptor> fdClass,
+                                       FileDescriptor stdin);
+
+    /**
+     * Lock the file referred to by fd.  The string mode is "r"
+     * for a read lock or "rw" for a write lock.  Don't block
+     * if lock can't be acquired.
+     */
+    public static boolean lock(FileDescriptor fd, String mode) {
+       return lock(fd, mode, false);
+    }
+
+    /**
+     * Lock the file referred to by fd.  The string mode is "r"
+     * for a read lock or "rw" for a write lock.  If block is set,
+     * block waiting for the lock if necessary.
+     */
+    private static boolean lock(FileDescriptor fd, String mode, boolean block) {
+       //return loaded && lock0(fd, mode);
+       if (loaded) {
+           boolean ret;
+           //System.out.println("UNIXFile.lock(" + fd + ", " + mode + ")");
+           ret = lock0(fd, mode, block);
+           //System.out.println("UNIXFile.lock returns " + ret);
+           return ret;
+       }
+       return false;
+    }
+
+    private static native boolean lock0(FileDescriptor fd, String mode,
+                                                               boolean block);
+
+    public static native long lastAccessed0(String name);
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/UNIXFolder.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/UNIXFolder.java
new file mode 100644 (file)
index 0000000..13dc30a
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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 java.io.*;
+
+public class UNIXFolder extends UNIXFile implements MailFile {
+    protected transient RandomAccessFile file;
+
+    private static final long serialVersionUID = -254578891263785591L;
+
+    public UNIXFolder(String name) {
+       super(name);
+    }
+
+    public boolean lock(String mode) {
+       try {
+           file = new RandomAccessFile(this, mode);
+           switch (lockType) {
+           case NONE:
+               return true;
+           case NATIVE:
+           default:
+               return UNIXFile.lock(file.getFD(), mode);
+           case JAVA:
+               return file.getChannel().
+                   tryLock(0L, Long.MAX_VALUE, !mode.equals("rw")) != null;
+           }
+       } catch (FileNotFoundException fe) {
+           return false;
+       } catch (IOException ie) {
+           file = null;
+           return false;
+       }
+    }
+
+    public void unlock() { 
+       if (file != null) {
+           try {
+               file.close();
+           } catch (IOException e) {
+               // ignore it
+           }
+           file = null;
+       }
+    }
+
+    public void touchlock() {
+    }
+
+    public FileDescriptor getFD() {
+       if (file == null)
+           return null;
+       try {
+           return file.getFD();
+       } catch (IOException e) {
+           return null;
+       }
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/mbox/UNIXInbox.java b/ext/javax.mail.mbox/src/com/sun/mail/mbox/UNIXInbox.java
new file mode 100644 (file)
index 0000000..70ce865
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * 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 java.io.*;
+
+public class UNIXInbox extends UNIXFolder implements InboxFile {
+    private final String user;
+
+    private static final long serialVersionUID = 651261842162777620L;
+
+    /*
+     * Superclass UNIXFile loads the library containing all the
+     * native code and sets the "loaded" flag if successful.
+     */
+
+    public UNIXInbox(String user, String name) {
+       super(name);
+       this.user = user;
+       if (user == null)
+           throw new NullPointerException("user name is null in UNIXInbox");
+    }
+
+    public boolean lock(String mode) {
+       if (lockType == NATIVE) {
+           if (!loaded)
+               return false;
+           if (!maillock(user, 5))
+               return false;
+       }
+       if (!super.lock(mode)) {
+           if (loaded)
+               mailunlock();
+           return false;
+       }
+       return true;
+    }
+
+    public void unlock() { 
+       super.unlock();
+       if (loaded)
+           mailunlock();
+    }
+
+    public void touchlock() {
+       if (loaded)
+           touchlock0();
+    }
+
+    private transient RandomAccessFile lockfile; // the user's ~/.Maillock file
+    private transient String lockfileName;     // its name
+
+    public boolean openLock(String mode) {
+       if (mode.equals("r"))
+           return true;
+       if (lockfileName == null) {
+           String home = System.getProperty("user.home");
+           lockfileName = home + File.separator + ".Maillock";
+       }
+       try {
+           lockfile = new RandomAccessFile(lockfileName, mode);
+           boolean ret;
+           switch (lockType) {
+           case NONE:
+               ret = true;
+               break;
+           case NATIVE:
+           default:
+               ret = UNIXFile.lock(lockfile.getFD(), mode);
+               break;
+           case JAVA:
+               ret = lockfile.getChannel().
+                   tryLock(0L, Long.MAX_VALUE, !mode.equals("rw")) != null;
+               break;
+           }
+           if (!ret)
+               closeLock();
+           return ret;
+       } catch (IOException ex) {
+       }
+       return false;
+    }
+
+    public void closeLock() {
+       if (lockfile == null)
+           return;
+       try {
+           lockfile.close();
+       } catch (IOException ex) {
+       } finally {
+           lockfile = null;
+       }
+    }
+
+    public boolean equals(Object o) {
+       if (!(o instanceof UNIXInbox))
+           return false;
+       UNIXInbox other = (UNIXInbox)o;
+       return user.equals(other.user) && super.equals(other);
+    }
+
+    public int hashCode() {
+       return super.hashCode() + user.hashCode();
+    }
+
+    private native boolean maillock(String user, int retryCount);
+    private native void mailunlock();
+    private native void touchlock0();
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/remote/POP3RemoteProvider.java b/ext/javax.mail.mbox/src/com/sun/mail/remote/POP3RemoteProvider.java
new file mode 100644 (file)
index 0000000..e5e3987
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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.remote;
+
+import javax.mail.Provider;
+
+/**
+ * The POP3 remote protocol provider.
+ */
+public class POP3RemoteProvider extends Provider {
+    public POP3RemoteProvider() {
+       super(Provider.Type.STORE, "pop3remote",
+           POP3RemoteStore.class.getName(), "Oracle", null);
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/remote/POP3RemoteStore.java b/ext/javax.mail.mbox/src/com/sun/mail/remote/POP3RemoteStore.java
new file mode 100644 (file)
index 0000000..fa6b1d4
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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.remote;
+
+import javax.mail.*;
+import com.sun.mail.pop3.POP3Store;
+
+/**
+ * A local store that uses POP3 to populate the INBOX.
+ *
+ * @author      Bill Shannon
+ */
+public class POP3RemoteStore extends RemoteStore {
+
+    public POP3RemoteStore(Session session, URLName url) {
+       super(session, url);
+    }
+
+    protected Store getRemoteStore(Session session, URLName url) {
+       return new POP3Store(session, url);
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/remote/RemoteDefaultFolder.java b/ext/javax.mail.mbox/src/com/sun/mail/remote/RemoteDefaultFolder.java
new file mode 100644 (file)
index 0000000..81c0f90
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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.remote;
+
+import javax.mail.*;
+import com.sun.mail.mbox.*;
+
+/**
+ * The default folder for the "remote" protocol.
+ *
+ * @author Bill Shannon
+ */
+public class RemoteDefaultFolder extends MboxFolder {
+
+    protected RemoteDefaultFolder(RemoteStore store, String name) {
+       super(store, name);
+    }
+
+    /**
+     * Depending on the name of the requested folder, create an
+     * appropriate <code>Folder</code> subclass.  If the name is
+     * <code>null</code>, create a <code>RemoteDefaultFolder</code>.
+     * If the name is "INBOX" (ignoring case), create a
+     * <code>RemoteInbox</code>.  Otherwise, create an <code>MboxFolder</code>.
+     *
+     * @return the new <code>Folder</code>
+     */
+    protected Folder createFolder(Store store, String name) {
+       if (name == null)
+           return new RemoteDefaultFolder((RemoteStore)store, null);
+       else if (name.equalsIgnoreCase("INBOX"))
+           return new RemoteInbox((RemoteStore)store, name);
+       else
+           return new MboxFolder((MboxStore)store, name);
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/remote/RemoteInbox.java b/ext/javax.mail.mbox/src/com/sun/mail/remote/RemoteInbox.java
new file mode 100644 (file)
index 0000000..5bfd232
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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.remote;
+
+import javax.mail.*;
+import com.sun.mail.mbox.*;
+
+/**
+ * A remote Inbox folder.  The data is actually managed by our subclass
+ * (<code>MboxFolder</code>).  We fetch data from the remote Inbox and
+ * add it to the local Inbox.
+ *
+ * @author Bill Shannon
+ */
+
+public class RemoteInbox extends MboxFolder {
+
+    private RemoteStore mstore;
+
+    protected RemoteInbox(RemoteStore store, String name) {
+       super(store, name);
+       this.mstore = store;
+    }
+
+    /**
+     * Poll the remote store for any new messages.
+     */
+    public synchronized boolean hasNewMessages() {
+       try {
+           mstore.updateInbox();
+       } catch (MessagingException ex) {
+           // ignore it
+       }
+       return super.hasNewMessages();
+    }
+
+    /**
+     * Open the folder in the specified mode.
+     * Poll the remote store for any new messages first.
+     */
+    public synchronized void open(int mode) throws MessagingException {
+       mstore.updateInbox();
+       super.open(mode);
+    }
+
+    /**
+     * Return the number of messages in this folder.
+     * Poll the remote store for any new messages first.
+     */
+    public synchronized int getMessageCount() throws MessagingException {
+       mstore.updateInbox();
+       return super.getMessageCount();
+    }
+}
diff --git a/ext/javax.mail.mbox/src/com/sun/mail/remote/RemoteStore.java b/ext/javax.mail.mbox/src/com/sun/mail/remote/RemoteStore.java
new file mode 100644 (file)
index 0000000..faf9dd4
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * 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.remote;
+
+import java.io.*;
+import javax.mail.*;
+import com.sun.mail.mbox.*;
+
+/**
+ * A wrapper around a local <code>MboxStore</code> that fetches data
+ * from the Inbox in a remote store and adds it to our local Inbox.
+ */
+public abstract class RemoteStore extends MboxStore {
+
+    protected Store remoteStore;
+    protected Folder remoteInbox;
+    protected Folder inbox;
+    protected String host, user, password;
+    protected int port;
+    protected long lastUpdate = 0;
+
+    public RemoteStore(Session session, URLName url) {
+       super(session, url);
+       remoteStore = getRemoteStore(session, url);
+    }
+
+    /**
+     * Subclasses override this method to return the appropriate
+     * <code>Store</code> object.  This method will be called by
+     * the <code>RemoteStore</code> constructor.
+     */
+    protected abstract Store getRemoteStore(Session session, URLName url);
+
+    /**
+     * Connect to the store.
+     */
+    public void connect(String host, int port, String user, String password)
+                       throws MessagingException {
+       this.host = host;
+       this.port = port;
+       this.user = user;
+       this.password = password;
+       updateInbox();
+    }
+
+    /**
+     * Fetch any new mail in the remote INBOX and add it to the local INBOX.
+     */
+    protected void updateInbox() throws MessagingException {
+       // is it time to do an update yet?
+       // XXX - polling frequency, rules, etc. should be in properties
+       if (System.currentTimeMillis() < lastUpdate + (5 * 1000))
+           return;
+       try {
+           /*
+            * Connect to the remote store, using the saved
+            * authentication information.
+            */
+           remoteStore.connect(host, port, user, password);
+
+           /*
+            * If this store isn't connected yet, do it now, because
+            * it needs to be connected to get the INBOX folder.
+            */
+           if (!isConnected())
+               super.connect(host, port, user, password);
+           if (remoteInbox == null)
+               remoteInbox = remoteStore.getFolder("INBOX");
+           if (inbox == null)
+               inbox = getFolder("INBOX");
+           remoteInbox.open(Folder.READ_WRITE);
+           Message[] msgs = remoteInbox.getMessages();
+           inbox.appendMessages(msgs);
+           remoteInbox.setFlags(msgs, new Flags(Flags.Flag.DELETED), true);
+           remoteInbox.close(true);
+           remoteStore.close();
+       } catch (MessagingException ex) {
+           try {
+               if (remoteInbox != null && remoteInbox.isOpen())
+                   remoteInbox.close(false);
+           } finally {
+               if (remoteStore != null && remoteStore.isConnected())
+                   remoteStore.close();
+           }
+           throw ex;
+       }
+    }
+
+    public Folder getDefaultFolder() throws MessagingException {
+       checkConnected();
+
+       return new RemoteDefaultFolder(this, null);
+    }
+
+    public Folder getFolder(String name) throws MessagingException {
+       checkConnected();
+
+       if (name.equalsIgnoreCase("INBOX"))
+           return new RemoteInbox(this, name);
+       else
+           return super.getFolder(name);
+    }
+
+    public Folder getFolder(URLName url) throws MessagingException {
+       checkConnected();
+       return getFolder(url.getFile());
+    }
+
+    private void checkConnected() throws MessagingException {
+       if (!isConnected())
+           throw new MessagingException("Not connected");
+    }
+}