diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle
index 2b98eff..cbfd0c4 100644
--- a/gradle/test/junit5.gradle
+++ b/gradle/test/junit5.gradle
@@ -1,5 +1,6 @@
dependencies {
testImplementation testLibs.junit.jupiter.api
+ testImplementation testLibs.junit.jupiter.params
testImplementation testLibs.hamcrest
testRuntimeOnly testLibs.junit.jupiter.engine
testRuntimeOnly testLibs.junit.jupiter.platform.launcher
diff --git a/net-mail/build.gradle b/net-mail/build.gradle
new file mode 100644
index 0000000..d9584ab
--- /dev/null
+++ b/net-mail/build.gradle
@@ -0,0 +1,14 @@
+dependencies {
+ api project(':net-security-auth')
+}
+
+def moduleName = 'org.xbib.net.mail.test'
+def patchArgs = ['--patch-module', "$moduleName=" + files(sourceSets.test.resources.srcDirs).asPath ]
+
+tasks.named('compileTestJava') {
+ options.compilerArgs += patchArgs
+}
+
+tasks.named('test') {
+ jvmArgs += patchArgs
+}
diff --git a/net-mail/src/main/java/jakarta/activation/ActivationDataFlavor.java b/net-mail/src/main/java/jakarta/activation/ActivationDataFlavor.java
new file mode 100644
index 0000000..fd548c7
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/ActivationDataFlavor.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+/**
+ * The ActivationDataFlavor class is similar to the JDK's
+ * java.awt.datatransfer.DataFlavor class. It allows
+ * Jakarta Activation to
+ * set all three values stored by the DataFlavor class via a new
+ * constructor. It also contains improved MIME parsing in the equals
+ * method. Except for the improved parsing, its semantics are
+ * identical to that of the JDK's DataFlavor class.
+ */
+
+public class ActivationDataFlavor {
+
+ /*
+ * Raison d'etre:
+ *
+ * The DataFlavor class included in JDK 1.1 has several limitations
+ * including poor MIME type parsing, and the limitation of
+ * only supporting serialized objects and InputStreams as
+ * representation objects. This class 'fixes' that.
+ */
+
+ private final String mimeType;
+ private MimeType mimeObject = null;
+ private String humanPresentableName;
+ private Class> representationClass = null;
+
+ /**
+ * Construct an ActivationDataFlavor that represents an arbitrary
+ * Java object.
+ *
+ * The returned ActivationDataFlavor will have the following
+ * characteristics:
+ *
+ * representationClass = representationClass
+ * mimeType = mimeType
+ * humanName = humanName
+ *
+ * @param representationClass the class used in this ActivationDataFlavor
+ * @param mimeType the MIME type of the data represented by this class
+ * @param humanPresentableName the human presentable name of the flavor
+ */
+ public ActivationDataFlavor(Class> representationClass,
+ String mimeType, String humanPresentableName) {
+ this.mimeType = mimeType;
+ this.humanPresentableName = humanPresentableName;
+ this.representationClass = representationClass;
+ }
+
+ /**
+ * Construct an ActivationDataFlavor that represents a MimeType.
+ *
+ * The returned ActivationDataFlavor will have the following
+ * characteristics:
+ *
+ * If the mimeType is "application/x-java-serialized-object;
+ * class=", the result is the same as calling new
+ * ActivationDataFlavor(Class.forName()) as above.
+ *
+ * otherwise:
+ *
+ * representationClass = InputStream
+ * mimeType = mimeType
+ *
+ * @param representationClass the class used in this ActivationDataFlavor
+ * @param humanPresentableName the human presentable name of the flavor
+ */
+ public ActivationDataFlavor(Class> representationClass,
+ String humanPresentableName) {
+ this.mimeType = "application/x-java-serialized-object";
+ this.representationClass = representationClass;
+ this.humanPresentableName = humanPresentableName;
+ }
+
+ /**
+ * Construct an ActivationDataFlavor that represents a MimeType.
+ *
+ * The returned ActivationDataFlavor will have the following
+ * characteristics:
+ *
+ * If the mimeType is "application/x-java-serialized-object; class=",
+ * the result is the same as calling new
+ * ActivationDataFlavor(Class.forName()) as above, otherwise:
+ *
+ * representationClass = InputStream
+ * mimeType = mimeType
+ *
+ * @param mimeType the MIME type of the data represented by this class
+ * @param humanPresentableName the human presentable name of the flavor
+ */
+ public ActivationDataFlavor(String mimeType, String humanPresentableName) {
+ this.mimeType = mimeType;
+ try {
+ this.representationClass = Class.forName("java.io.InputStream");
+ } catch (ClassNotFoundException ex) {
+ // XXX - should never happen, ignore it
+ }
+ this.humanPresentableName = humanPresentableName;
+ }
+
+ /**
+ * Return the MIME type for this ActivationDataFlavor.
+ *
+ * @return the MIME type
+ */
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ /**
+ * Return the representation class.
+ *
+ * @return the representation class
+ */
+ public Class> getRepresentationClass() {
+ return representationClass;
+ }
+
+ /**
+ * Return the Human Presentable name.
+ *
+ * @return the human presentable name
+ */
+ public String getHumanPresentableName() {
+ return humanPresentableName;
+ }
+
+ /**
+ * Set the human presentable name.
+ *
+ * @param humanPresentableName the name to set
+ */
+ public void setHumanPresentableName(String humanPresentableName) {
+ this.humanPresentableName = humanPresentableName;
+ }
+
+ /**
+ * Compares the ActivationDataFlavor passed in with this
+ * ActivationDataFlavor; calls the isMimeTypeEqual method.
+ *
+ * @param dataFlavor the ActivationDataFlavor to compare with
+ * @return true if the MIME type and representation class
+ * are the same
+ */
+ public boolean equals(ActivationDataFlavor dataFlavor) {
+ return (isMimeTypeEqual(dataFlavor.mimeType) &&
+ dataFlavor.getRepresentationClass() == representationClass);
+ }
+
+ /**
+ * @param o the Object to compare with
+ * @return true if the object is also an ActivationDataFlavor
+ * and is equal to this
+ */
+ @Override
+ public boolean equals(Object o) {
+ return ((o instanceof ActivationDataFlavor) &&
+ equals((ActivationDataFlavor) o));
+ }
+
+ /**
+ * Returns hash code for this ActivationDataFlavor.
+ * For two equal ActivationDataFlavors, hash codes are equal.
+ * For the String
+ * that matches ActivationDataFlavor.equals(String), it is not
+ * guaranteed that ActivationDataFlavor's hash code is equal
+ * to the hash code of the String.
+ *
+ * @return a hash code for this ActivationDataFlavor
+ */
+ public int hashCode() {
+ int total = 0;
+
+ if (representationClass != null) {
+ total += representationClass.hashCode();
+ }
+
+ // XXX - MIME type equality is too complicated so we don't
+ // include it in the hashCode
+
+ return total;
+ }
+
+ /**
+ * Is the string representation of the MIME type passed in equivalent
+ * to the MIME type of this ActivationDataFlavor.
+ *
+ * ActivationDataFlavor delegates the comparison of MIME types to
+ * the MimeType class included as part of Jakarta Activation.
+ *
+ * @param mimeType the MIME type
+ * @return true if the same MIME type
+ */
+ public boolean isMimeTypeEqual(String mimeType) {
+ MimeType mt = null;
+ try {
+ if (mimeObject == null)
+ mimeObject = new MimeType(this.mimeType);
+ mt = new MimeType(mimeType);
+ } catch (MimeTypeParseException e) {
+ // something didn't parse, do a crude comparison
+ return this.mimeType.equalsIgnoreCase(mimeType);
+ }
+
+ return mimeObject.match(mt);
+ }
+
+}
diff --git a/net-mail/src/main/java/jakarta/activation/CommandInfo.java b/net-mail/src/main/java/jakarta/activation/CommandInfo.java
new file mode 100644
index 0000000..8b58daf
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/CommandInfo.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * The CommandInfo class is used by CommandMap implementations to
+ * describe the results of command requests. It provides the requestor
+ * with both the verb requested, as well as an instance of the
+ * bean. There is also a method that will return the name of the
+ * class that implements the command but it is not guaranteed to
+ * return a valid value. The reason for this is to allow CommandMap
+ * implmentations that subclass CommandInfo to provide special
+ * behavior. For example a CommandMap could dynamically generate
+ * JavaBeans. In this case, it might not be possible to create an
+ * object with all the correct state information solely from the class
+ * name.
+ */
+
+public class CommandInfo {
+ private String verb;
+ private String className;
+
+ /**
+ * The Constructor for CommandInfo.
+ *
+ * @param verb The command verb this CommandInfo decribes.
+ * @param className The command's fully qualified class name.
+ */
+ public CommandInfo(String verb, String className) {
+ this.verb = verb;
+ this.className = className;
+ }
+
+ /**
+ * Return the command verb.
+ *
+ * @return the command verb.
+ */
+ public String getCommandName() {
+ return verb;
+ }
+
+ /**
+ * Return the command's class name. This method MAY return null in
+ * cases where a CommandMap subclassed CommandInfo for its
+ * own purposes. In other words, it might not be possible to
+ * create the correct state in the command by merely knowing
+ * its class name. DO NOT DEPEND ON THIS METHOD RETURNING
+ * A VALID VALUE!
+ *
+ * @return The class name of the command, or null
+ */
+ public String getCommandClass() {
+ return className;
+ }
+
+ /**
+ * Return the instantiated JavaBean component.
+ *
+ * If the current runtime environment supports
+ * Beans.instantiate,
+ * use it to instantiate the JavaBeans component. Otherwise, use
+ * {@link Class#forName(String)} Class.forName}.
+ *
+ * The component class needs to be public.
+ * On Java SE 9 and newer, if the component class is in a named module,
+ * it needs to be in an exported package.
+ *
+ * If the bean implements the jakarta.activation.CommandObject
+ * interface, call its setCommandContext method.
+ *
+ * If the DataHandler parameter is null, then the bean is
+ * instantiated with no data. NOTE: this may be useful
+ * if for some reason the DataHandler that is passed in
+ * throws IOExceptions when this method attempts to
+ * access its InputStream. It will allow the caller to
+ * retrieve a reference to the bean if it can be
+ * instantiated.
+ *
+ * If the bean does NOT implement the CommandObject interface,
+ * this method will check if it implements the
+ * java.io.Externalizable interface. If it does, the bean's
+ * readExternal method will be called if an InputStream
+ * can be acquired from the DataHandler.
+ *
+ * @param dh The DataHandler that describes the data to be
+ * passed to the command.
+ * @param loader The ClassLoader to be used to instantiate the bean.
+ * @return The bean
+ * @throws IOException for failures reading data
+ * @throws ClassNotFoundException if command object class can't
+ * be found
+ * @see CommandObject
+ */
+ public Object getCommandObject(DataHandler dh, ClassLoader loader)
+ throws IOException, ClassNotFoundException {
+ Object new_bean = null;
+
+ // try to instantiate the bean
+ new_bean = Beans.instantiate(loader, className);
+
+ // if we got one and it is a CommandObject
+ if (new_bean != null) {
+ if (new_bean instanceof CommandObject) {
+ ((CommandObject) new_bean).setCommandContext(verb, dh);
+ }
+ }
+ return new_bean;
+ }
+
+ /**
+ * Helper class to invoke Beans.instantiate reflectively or the equivalent
+ * with core reflection when module java.desktop is not readable.
+ */
+ private static final class Beans {
+ static final Method instantiateMethod;
+
+ static {
+ Method m;
+ try {
+ Class> c = Class.forName("java.beans.Beans");
+ m = c.getDeclaredMethod("instantiate", ClassLoader.class, String.class);
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ m = null;
+ }
+ instantiateMethod = m;
+ }
+
+ /**
+ * Equivalent to invoking java.beans.Beans.instantiate(loader, cn)
+ */
+ static Object instantiate(ClassLoader loader, String cn)
+ throws ClassNotFoundException {
+ if (instantiateMethod != null) {
+ // invoke Beans.instantiate
+ try {
+ return instantiateMethod.invoke(null, loader, cn);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ //
+ }
+
+ } else {
+
+ if (loader == null) {
+ loader = ClassLoader.getSystemClassLoader();
+ }
+ Class> beanClass = Class.forName(cn, true, loader);
+ try {
+ return beanClass.getDeclaredConstructor().newInstance();
+ } catch (Exception ex) {
+ throw new ClassNotFoundException(beanClass + ": " + ex, ex);
+ }
+
+ }
+ return null;
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/CommandMap.java b/net-mail/src/main/java/jakarta/activation/CommandMap.java
new file mode 100644
index 0000000..579f483
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/CommandMap.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+
+
+/**
+ * The CommandMap class provides an interface to a registry of
+ * command objects available in the system.
+ * Developers are expected to either use the CommandMap
+ * implementation included with this package (MailcapCommandMap) or
+ * develop their own. Note that some of the methods in this class are
+ * abstract.
+ */
+public abstract class CommandMap {
+ private static CommandMap defaultCommandMap = null;
+ private static Map map = new WeakHashMap<>();
+
+ /**
+ * Default (empty) constructor.
+ */
+ protected CommandMap() {
+ }
+
+ /**
+ * Get the default CommandMap.
+ *
+ *
+ *
In cases where a CommandMap instance has been previously set
+ * to some value (via setDefaultCommandMap)
+ * return the CommandMap.
+ *
+ * In cases where no CommandMap has been set, the CommandMap
+ * creates an instance of MailcapCommandMap and
+ * set that to the default, returning its value.
+ *
+ *
+ *
+ * @return the CommandMap
+ */
+ public static synchronized CommandMap getDefaultCommandMap() {
+ if (defaultCommandMap != null)
+ return defaultCommandMap;
+
+ // fetch per-thread-context-class-loader default
+ ClassLoader tccl = Util.getContextClassLoader();
+ CommandMap def = map.get(tccl);
+ if (def == null) {
+ def = new MailcapCommandMap();
+ map.put(tccl, def);
+ }
+ return def;
+ }
+
+ /**
+ * Set the default CommandMap. Reset the CommandMap to the default by
+ * calling this method with null.
+ *
+ * @param commandMap The new default CommandMap.
+ */
+ public static synchronized void setDefaultCommandMap(CommandMap commandMap) {
+ map.remove(Util.getContextClassLoader());
+ defaultCommandMap = commandMap;
+ }
+
+ /**
+ * Get the preferred command list from a MIME Type. The actual semantics
+ * are determined by the implementation of the CommandMap.
+ *
+ * @param mimeType the MIME type
+ * @return the CommandInfo classes that represent the command Beans.
+ */
+ abstract public CommandInfo[] getPreferredCommands(String mimeType);
+
+ /**
+ * Get the preferred command list from a MIME Type. The actual semantics
+ * are determined by the implementation of the CommandMap.
+ *
+ * The DataSource provides extra information, such as
+ * the file name, that a CommandMap implementation may use to further
+ * refine the list of commands that are returned. The implementation
+ * in this class simply calls the getPreferredCommands
+ * method that ignores this argument.
+ *
+ * @param mimeType the MIME type
+ * @param ds a DataSource for the data
+ * @return the CommandInfo classes that represent the command Beans.
+ * @since JAF 1.1
+ */
+ public CommandInfo[] getPreferredCommands(String mimeType, DataSource ds) {
+ return getPreferredCommands(mimeType);
+ }
+
+ /**
+ * Get all the available commands for this type. This method
+ * should return all the possible commands for this MIME type.
+ *
+ * @param mimeType the MIME type
+ * @return the CommandInfo objects representing all the commands.
+ */
+ abstract public CommandInfo[] getAllCommands(String mimeType);
+
+ /**
+ * Get all the available commands for this type. This method
+ * should return all the possible commands for this MIME type.
+ *
+ * The DataSource provides extra information, such as
+ * the file name, that a CommandMap implementation may use to further
+ * refine the list of commands that are returned. The implementation
+ * in this class simply calls the getAllCommands
+ * method that ignores this argument.
+ *
+ * @param mimeType the MIME type
+ * @param ds a DataSource for the data
+ * @return the CommandInfo objects representing all the commands.
+ * @since JAF 1.1
+ */
+ public CommandInfo[] getAllCommands(String mimeType, DataSource ds) {
+ return getAllCommands(mimeType);
+ }
+
+ /**
+ * Get the default command corresponding to the MIME type.
+ *
+ * @param mimeType the MIME type
+ * @param cmdName the command name
+ * @return the CommandInfo corresponding to the command.
+ */
+ abstract public CommandInfo getCommand(String mimeType, String cmdName);
+
+ /**
+ * Get the default command corresponding to the MIME type.
+ *
+ * The DataSource provides extra information, such as
+ * the file name, that a CommandMap implementation may use to further
+ * refine the command that is chosen. The implementation
+ * in this class simply calls the getCommand
+ * method that ignores this argument.
+ *
+ * @param mimeType the MIME type
+ * @param cmdName the command name
+ * @param ds a DataSource for the data
+ * @return the CommandInfo corresponding to the command.
+ * @since JAF 1.1
+ */
+ public CommandInfo getCommand(String mimeType, String cmdName,
+ DataSource ds) {
+ return getCommand(mimeType, cmdName);
+ }
+
+ /**
+ * Locate a DataContentHandler that corresponds to the MIME type.
+ * The mechanism and semantics for determining this are determined
+ * by the implementation of the particular CommandMap.
+ *
+ * @param mimeType the MIME type
+ * @return the DataContentHandler for the MIME type
+ */
+ abstract public DataContentHandler createDataContentHandler(String
+ mimeType);
+
+ /**
+ * Locate a DataContentHandler that corresponds to the MIME type.
+ * The mechanism and semantics for determining this are determined
+ * by the implementation of the particular CommandMap.
+ *
+ * The DataSource provides extra information, such as
+ * the file name, that a CommandMap implementation may use to further
+ * refine the choice of DataContentHandler. The implementation
+ * in this class simply calls the createDataContentHandler
+ * method that ignores this argument.
+ *
+ * @param mimeType the MIME type
+ * @param ds a DataSource for the data
+ * @return the DataContentHandler for the MIME type
+ * @since JAF 1.1
+ */
+ public DataContentHandler createDataContentHandler(String mimeType,
+ DataSource ds) {
+ return createDataContentHandler(mimeType);
+ }
+
+ /**
+ * Get all the MIME types known to this command map.
+ * If the command map doesn't support this operation,
+ * null is returned.
+ *
+ * @return array of MIME types as strings, or null if not supported
+ * @since JAF 1.1
+ */
+ public String[] getMimeTypes() {
+ return null;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/CommandObject.java b/net-mail/src/main/java/jakarta/activation/CommandObject.java
new file mode 100644
index 0000000..88cdfa1
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/CommandObject.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.IOException;
+
+/**
+ * JavaBeans components that are Jakarta Activation aware implement
+ * this interface to find out which command verb they're being asked
+ * to perform, and to obtain the DataHandler representing the
+ * data they should operate on. JavaBeans that don't implement
+ * this interface may be used as well. Such commands may obtain
+ * the data using the Externalizable interface, or using an
+ * application-specific method.
+ */
+public interface CommandObject {
+
+ /**
+ * Initialize the Command with the verb it is requested to handle
+ * and the DataHandler that describes the data it will
+ * operate on. NOTE: it is acceptable for the caller
+ * to pass null as the value for DataHandler.
+ *
+ * @param verb The Command Verb this object refers to.
+ * @param dh The DataHandler.
+ * @throws IOException for failures accessing data
+ */
+ void setCommandContext(String verb, DataHandler dh)
+ throws IOException;
+}
diff --git a/net-mail/src/main/java/jakarta/activation/DataContentHandler.java b/net-mail/src/main/java/jakarta/activation/DataContentHandler.java
new file mode 100644
index 0000000..f0d163b
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/DataContentHandler.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * The DataContentHandler interface is implemented by objects that can
+ * be used to extend the capabilities of the DataHandler's implementation
+ * of the Transferable interface. Through DataContentHandlers
+ * the framework can be extended to convert streams in to objects, and
+ * to write objects to streams.
+ *
+ * Applications don't generally call the methods in DataContentHandlers
+ * directly. Instead, an application calls the equivalent methods in
+ * DataHandler. The DataHandler will attempt to find an appropriate
+ * DataContentHandler that corresponds to its MIME type using the
+ * current DataContentHandlerFactory. The DataHandler then calls
+ * through to the methods in the DataContentHandler.
+ */
+
+public interface DataContentHandler {
+ /**
+ * Returns an array of ActivationDataFlavor objects indicating the flavors
+ * the data can be provided in. The array should be ordered according to
+ * preference for providing the data (from most richly descriptive to
+ * least descriptive).
+ *
+ * @return The ActivationDataFlavors.
+ */
+ ActivationDataFlavor[] getTransferDataFlavors();
+
+ /**
+ * Returns an object which represents the data to be transferred.
+ * The class of the object returned is defined by the representation class
+ * of the flavor.
+ *
+ * @param df The ActivationDataFlavor representing the requested type.
+ * @param ds The DataSource representing the data to be converted.
+ * @return The constructed Object.
+ * @throws IOException if the handler doesn't
+ * support the requested flavor
+ * @throws IOException if the data can't be accessed
+ */
+ Object getTransferData(ActivationDataFlavor df, DataSource ds)
+ throws IOException;
+
+ /**
+ * Return an object representing the data in its most preferred form.
+ * Generally this will be the form described by the first
+ * ActivationDataFlavor returned by the
+ * getTransferDataFlavors method.
+ *
+ * @param ds The DataSource representing the data to be converted.
+ * @return The constructed Object.
+ * @throws IOException if the data can't be accessed
+ */
+ Object getContent(DataSource ds) throws IOException;
+
+ /**
+ * Convert the object to a byte stream of the specified MIME type
+ * and write it to the output stream.
+ *
+ * @param obj The object to be converted.
+ * @param mimeType The requested MIME type of the resulting byte stream.
+ * @param os The output stream into which to write the converted
+ * byte stream.
+ * @throws IOException errors writing to the stream
+ */
+ void writeTo(Object obj, String mimeType, OutputStream os)
+ throws IOException;
+}
diff --git a/net-mail/src/main/java/jakarta/activation/DataContentHandlerFactory.java b/net-mail/src/main/java/jakarta/activation/DataContentHandlerFactory.java
new file mode 100644
index 0000000..8eb0dcc
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/DataContentHandlerFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+/**
+ * This interface defines a factory for DataContentHandlers. An
+ * implementation of this interface should map a MIME type into an
+ * instance of DataContentHandler. The design pattern for classes implementing
+ * this interface is the same as for the ContentHandler mechanism used in
+ * java.net.URL.
+ */
+
+public interface DataContentHandlerFactory {
+
+ /**
+ * Creates a new DataContentHandler object for the MIME type.
+ *
+ * @param mimeType the MIME type to create the DataContentHandler for.
+ * @return The new DataContentHandler, or null
+ * if none are found.
+ */
+ DataContentHandler createDataContentHandler(String mimeType);
+}
diff --git a/net-mail/src/main/java/jakarta/activation/DataHandler.java b/net-mail/src/main/java/jakarta/activation/DataHandler.java
new file mode 100644
index 0000000..ab60b8c
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/DataHandler.java
@@ -0,0 +1,857 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.URL;
+import java.nio.charset.Charset;
+
+/**
+ * The DataHandler class provides a consistent interface to data
+ * available in many different sources and formats.
+ * It manages simple stream to string conversions and related operations
+ * using DataContentHandlers.
+ * It provides access to commands that can operate on the data.
+ * The commands are found using a CommandMap.
+ *
+ * DataHandler and CommandMaps
+ * The DataHandler keeps track of the current CommandMap that it uses to
+ * service requests for commands (getCommand,
+ * getAllCommands, getPreferredCommands).
+ * Each instance of a DataHandler may have a CommandMap associated with
+ * it using the setCommandMap method. If a CommandMap was
+ * not set, DataHandler calls the getDefaultCommandMap
+ * method in CommandMap and uses the value it returns. See
+ * CommandMap for more information.
+ *
+ * DataHandler and URLs
+ * The current DataHandler implementation creates a private
+ * instance of URLDataSource when it is constructed with a URL.
+ *
+ * @see CommandMap
+ * @see DataContentHandler
+ * @see DataSource
+ * @see URLDataSource
+ */
+
+public class DataHandler {
+
+ // our transfer flavors
+ private static final ActivationDataFlavor[] emptyFlavors =
+ new ActivationDataFlavor[0];
+ // our DataContentHandlerFactory
+ private static DataContentHandlerFactory factory = null;
+ // Use the datasource to indicate whether we were started via the
+ // DataSource constructor or the object constructor.
+ private DataSource dataSource = null;
+ private DataSource objDataSource = null;
+ // The Object and mimetype from the constructor (if passed in).
+ // object remains null if it was instantiated with a
+ // DataSource.
+ private Object object = null;
+ private String objectMimeType = null;
+ // Keep track of the CommandMap
+ private CommandMap currentCommandMap = null;
+ private ActivationDataFlavor[] transferFlavors = emptyFlavors;
+ // our DataContentHandler
+ private DataContentHandler dataContentHandler = null;
+ private DataContentHandler factoryDCH = null;
+ private DataContentHandlerFactory oldFactory = null;
+ // the short representation of the ContentType (sans params)
+ private String shortType = null;
+
+ /**
+ * Create a DataHandler instance referencing the
+ * specified DataSource. The data exists in a byte stream form.
+ * The DataSource will provide an InputStream to access the data.
+ *
+ * @param ds the DataSource
+ */
+ public DataHandler(DataSource ds) {
+ // save a reference to the incoming DS
+ dataSource = ds;
+ oldFactory = factory; // keep track of the factory
+ }
+
+ /**
+ * Create a DataHandler instance representing an object
+ * of this MIME type. This constructor is
+ * used when the application already has an in-memory representation
+ * of the data in the form of a Java Object.
+ *
+ * @param obj the Java Object
+ * @param mimeType the MIME type of the object
+ */
+ public DataHandler(Object obj, String mimeType) {
+ object = obj;
+ objectMimeType = mimeType;
+ oldFactory = factory; // keep track of the factory
+ }
+
+ /**
+ * Create a DataHandler instance referencing a URL.
+ * The DataHandler internally creates a URLDataSource
+ * instance to represent the URL.
+ *
+ * @param url a URL object
+ */
+ public DataHandler(URL url) {
+ dataSource = new URLDataSource(url);
+ oldFactory = factory; // keep track of the factory
+ }
+
+ /**
+ * Sets the DataContentHandlerFactory. The DataContentHandlerFactory
+ * is called first to find DataContentHandlers.
+ * The DataContentHandlerFactory can only be set once.
+ *
+ * If the DataContentHandlerFactory has already been set,
+ * this method throws an Error.
+ *
+ * @param newFactory the DataContentHandlerFactory
+ * @throws Error if the factory has already been defined.
+ * @see DataContentHandlerFactory
+ */
+ public static synchronized void setDataContentHandlerFactory(
+ DataContentHandlerFactory newFactory) {
+ if (factory != null)
+ throw new Error("DataContentHandlerFactory already defined");
+ factory = newFactory;
+ }
+
+ /**
+ * Return the CommandMap for this instance of DataHandler.
+ */
+ private synchronized CommandMap getCommandMap() {
+ if (currentCommandMap != null)
+ return currentCommandMap;
+ else
+ return CommandMap.getDefaultCommandMap();
+ }
+
+ /**
+ * Set the CommandMap for use by this DataHandler.
+ * Setting it to null causes the CommandMap to revert
+ * to the CommandMap returned by the
+ * CommandMap.getDefaultCommandMap method.
+ * Changing the CommandMap, or setting it to null,
+ * clears out any data cached from the previous CommandMap.
+ *
+ * @param commandMap the CommandMap to use in this DataHandler
+ * @see CommandMap#setDefaultCommandMap
+ */
+ public synchronized void setCommandMap(CommandMap commandMap) {
+ if (commandMap != currentCommandMap || commandMap == null) {
+ // clear cached values...
+ transferFlavors = emptyFlavors;
+ dataContentHandler = null;
+
+ currentCommandMap = commandMap;
+ }
+ }
+
+ /**
+ * Return the DataSource associated with this instance
+ * of DataHandler.
+ *
+ * For DataHandlers that have been instantiated with a DataSource,
+ * this method returns the DataSource that was used to create the
+ * DataHandler object. In other cases the DataHandler
+ * constructs a DataSource from the data used to construct
+ * the DataHandler. DataSources created for DataHandlers not
+ * instantiated with a DataSource are cached for performance
+ * reasons.
+ *
+ * @return a valid DataSource object for this DataHandler
+ */
+ public DataSource getDataSource() {
+ if (dataSource == null) {
+ // create one on the fly
+ if (objDataSource == null)
+ objDataSource = new DataHandlerDataSource(this);
+ return objDataSource;
+ }
+ return dataSource;
+ }
+
+ /**
+ * Return the name of the data object. If this DataHandler
+ * was created with a DataSource, this method calls through
+ * to the DataSource.getName method, otherwise it
+ * returns null.
+ *
+ * @return the name of the object
+ */
+ public String getName() {
+ if (dataSource != null)
+ return dataSource.getName();
+ else
+ return null;
+ }
+
+ /**
+ * Return the MIME type of this object as retrieved from
+ * the source object. Note that this is the full
+ * type with parameters.
+ *
+ * @return the MIME type
+ */
+ public String getContentType() {
+ if (dataSource != null) // data source case
+ return dataSource.getContentType();
+ else
+ return objectMimeType; // obj/type case
+ }
+
+ /**
+ * Get the InputStream for this object.
+ *
+ * For DataHandlers instantiated with a DataSource, the DataHandler
+ * calls the DataSource.getInputStream method and
+ * returns the result to the caller.
+ *
+ * For DataHandlers instantiated with an Object, the DataHandler
+ * first attempts to find a DataContentHandler for the Object. If
+ * the DataHandler can not find a DataContentHandler for this MIME
+ * type, it throws an UnsupportedDataTypeException. If it is
+ * successful, it creates a pipe and a thread. The thread uses the
+ * DataContentHandler's writeTo method to write the
+ * stream data into one end of the pipe. The other end of the pipe
+ * is returned to the caller. Because a thread is created to copy
+ * the data, IOExceptions that may occur during the copy can not be
+ * propagated back to the caller. The result is an empty stream.
+ *
+ * @return the InputStream representing this data
+ * @throws IOException if an I/O error occurs
+ * @see DataContentHandler#writeTo
+ * @see UnsupportedDataTypeException
+ */
+ public InputStream getInputStream() throws IOException {
+ InputStream ins = null;
+
+ if (dataSource != null) {
+ ins = dataSource.getInputStream();
+ } else {
+ DataContentHandler dch = getDataContentHandler();
+ // we won't even try if we can't get a dch
+ if (dch == null)
+ throw new UnsupportedDataTypeException(
+ "no DCH for MIME type " + getBaseType());
+
+ if (dch instanceof ObjectDataContentHandler) {
+ if (((ObjectDataContentHandler) dch).getDCH() == null)
+ throw new UnsupportedDataTypeException(
+ "no object DCH for MIME type " + getBaseType());
+ }
+ // there is none but the default^^^^^^^^^^^^^^^^
+ final DataContentHandler fdch = dch;
+
+ // from bill s.
+ // ce n'est pas une pipe!
+ //
+ // NOTE: This block of code needs to throw exceptions, but
+ // can't because it is in another thread!!! ARG!
+ //
+ final PipedOutputStream pos = new PipedOutputStream();
+ PipedInputStream pin = new PipedInputStream(pos);
+ new Thread(
+ new Runnable() {
+ public void run() {
+ try {
+ fdch.writeTo(object, objectMimeType, pos);
+ } catch (IOException e) {
+
+ } finally {
+ try {
+ pos.close();
+ } catch (IOException ie) {
+ }
+ }
+ }
+ },
+ "DataHandler.getInputStream").start();
+ ins = pin;
+ }
+
+ return ins;
+ }
+
+ /**
+ * Write the data to an OutputStream.
+ *
+ * If the DataHandler was created with a DataSource, writeTo
+ * retrieves the InputStream and copies the bytes from the
+ * InputStream to the OutputStream passed in.
+ *
+ * If the DataHandler was created with an object, writeTo
+ * retrieves the DataContentHandler for the object's type.
+ * If the DataContentHandler was found, it calls the
+ * writeTo method on the DataContentHandler.
+ *
+ * @param os the OutputStream to write to
+ * @throws IOException if an I/O error occurs
+ */
+ public void writeTo(OutputStream os) throws IOException {
+ // for the DataSource case
+ if (dataSource != null) {
+ InputStream is = null;
+ byte[] data = new byte[8 * 1024];
+ int bytes_read;
+
+ is = dataSource.getInputStream();
+
+ try {
+ while ((bytes_read = is.read(data)) > 0) {
+ os.write(data, 0, bytes_read);
+ }
+ } finally {
+ is.close();
+ is = null;
+ }
+ } else { // for the Object case
+ DataContentHandler dch = getDataContentHandler();
+ dch.writeTo(object, objectMimeType, os);
+ }
+ }
+
+ /**
+ * Get an OutputStream for this DataHandler to allow overwriting
+ * the underlying data.
+ * If the DataHandler was created with a DataSource, the
+ * DataSource's getOutputStream method is called.
+ * Otherwise, null is returned.
+ *
+ * @return the OutputStream
+ * @throws IOException for failures creating the OutputStream
+ * @see DataSource#getOutputStream
+ * @see URLDataSource
+ */
+ public OutputStream getOutputStream() throws IOException {
+ if (dataSource != null)
+ return dataSource.getOutputStream();
+ else
+ return null;
+ }
+
+ /**
+ * Return the ActivationDataFlavors in which this data is available.
+ *
+ * Returns an array of ActivationDataFlavor objects indicating the flavors
+ * the data can be provided in. The array is usually ordered
+ * according to preference for providing the data, from most
+ * richly descriptive to least richly descriptive.
+ *
+ * The DataHandler attempts to find a DataContentHandler that
+ * corresponds to the MIME type of the data. If one is located,
+ * the DataHandler calls the DataContentHandler's
+ * getTransferDataFlavors method.
+ *
+ * If a DataContentHandler can not be located, and if the
+ * DataHandler was created with a DataSource (or URL), one
+ * ActivationDataFlavor is returned that represents this object's MIME type
+ * and the java.io.InputStream class. If the
+ * DataHandler was created with an object and a MIME type,
+ * getTransferDataFlavors returns one ActivationDataFlavor that represents
+ * this object's MIME type and the object's class.
+ *
+ * @return an array of data flavors in which this data can be transferred
+ * @see DataContentHandler#getTransferDataFlavors
+ */
+ public synchronized ActivationDataFlavor[] getTransferDataFlavors() {
+ if (factory != oldFactory) // if the factory has changed, clear cache
+ transferFlavors = emptyFlavors;
+
+ // if it's not set, set it...
+ if (transferFlavors == emptyFlavors)
+ transferFlavors = getDataContentHandler().getTransferDataFlavors();
+ if (transferFlavors == emptyFlavors)
+ return transferFlavors; // no need to clone an empty array
+ else
+ return transferFlavors.clone();
+ }
+
+ /**
+ * Returns whether the specified data flavor is supported
+ * for this object.
+ *
+ * This method iterates through the ActivationDataFlavors returned from
+ * getTransferDataFlavors, comparing each with
+ * the specified flavor.
+ *
+ * @param flavor the requested flavor for the data
+ * @return true if the data flavor is supported
+ * @see DataHandler#getTransferDataFlavors
+ */
+ public boolean isDataFlavorSupported(ActivationDataFlavor flavor) {
+ ActivationDataFlavor[] lFlavors = getTransferDataFlavors();
+
+ for (int i = 0; i < lFlavors.length; i++) {
+ if (lFlavors[i].equals(flavor))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns an object that represents the data to be
+ * transferred. The class of the object returned is defined by the
+ * representation class of the data flavor.
+ *
+ * For DataHandler's created with DataSources or URLs:
+ *
+ * The DataHandler attempts to locate a DataContentHandler
+ * for this MIME type. If one is found, the passed in ActivationDataFlavor
+ * and the type of the data are passed to its getTransferData
+ * method. If the DataHandler fails to locate a DataContentHandler
+ * and the flavor specifies this object's MIME type and the
+ * java.io.InputStream class, this object's InputStream
+ * is returned.
+ * Otherwise it throws an IOException.
+ *
+ * For DataHandler's created with Objects:
+ *
+ * The DataHandler attempts to locate a DataContentHandler
+ * for this MIME type. If one is found, the passed in ActivationDataFlavor
+ * and the type of the data are passed to its getTransferData
+ * method. If the DataHandler fails to locate a DataContentHandler
+ * and the flavor specifies this object's MIME type and its class,
+ * this DataHandler's referenced object is returned.
+ * Otherwise it throws an IOException.
+ *
+ * @param flavor the requested flavor for the data
+ * @return the object
+ * @throws IOException if the data could not be
+ * converted to the requested flavor
+ * @throws IOException if an I/O error occurs
+ * @see ActivationDataFlavor
+ */
+ public Object getTransferData(ActivationDataFlavor flavor)
+ throws IOException {
+ return getDataContentHandler().getTransferData(flavor, dataSource);
+ }
+
+ /**
+ * Return the preferred commands for this type of data.
+ * This method calls the getPreferredCommands method
+ * in the CommandMap associated with this instance of DataHandler.
+ * This method returns an array that represents a subset of
+ * available commands. In cases where multiple commands for the
+ * MIME type represented by this DataHandler are present, the
+ * installed CommandMap chooses the appropriate commands.
+ *
+ * @return the CommandInfo objects representing the preferred commands
+ * @see CommandMap#getPreferredCommands
+ */
+ public CommandInfo[] getPreferredCommands() {
+ if (dataSource != null)
+ return getCommandMap().getPreferredCommands(getBaseType(),
+ dataSource);
+ else
+ return getCommandMap().getPreferredCommands(getBaseType());
+ }
+
+ /**
+ * Return all the commands for this type of data.
+ * This method returns an array containing all commands
+ * for the type of data represented by this DataHandler. The
+ * MIME type for the underlying data represented by this DataHandler
+ * is used to call through to the getAllCommands method
+ * of the CommandMap associated with this DataHandler.
+ *
+ * @return the CommandInfo objects representing all the commands
+ * @see CommandMap#getAllCommands
+ */
+ public CommandInfo[] getAllCommands() {
+ if (dataSource != null)
+ return getCommandMap().getAllCommands(getBaseType(), dataSource);
+ else
+ return getCommandMap().getAllCommands(getBaseType());
+ }
+
+ /**
+ * Get the command cmdName. Use the search semantics as
+ * defined by the CommandMap installed in this DataHandler. The
+ * MIME type for the underlying data represented by this DataHandler
+ * is used to call through to the getCommand method
+ * of the CommandMap associated with this DataHandler.
+ *
+ * @param cmdName the command name
+ * @return the CommandInfo corresponding to the command
+ * @see CommandMap#getCommand
+ */
+ public CommandInfo getCommand(String cmdName) {
+ if (dataSource != null)
+ return getCommandMap().getCommand(getBaseType(), cmdName,
+ dataSource);
+ else
+ return getCommandMap().getCommand(getBaseType(), cmdName);
+ }
+
+ /**
+ * Return the data in its preferred Object form.
+ *
+ * If the DataHandler was instantiated with an object, return
+ * the object.
+ *
+ * If the DataHandler was instantiated with a DataSource,
+ * this method uses a DataContentHandler to return the content
+ * object for the data represented by this DataHandler. If no
+ * DataContentHandler can be found for the
+ * the type of this data, the DataHandler returns an
+ * InputStream for the data.
+ *
+ * @return the content.
+ * @throws IOException if an IOException occurs during
+ * this operation.
+ */
+ public Object getContent() throws IOException {
+ if (object != null)
+ return object;
+ else
+ return getDataContentHandler().getContent(getDataSource());
+ }
+
+ /**
+ * A convenience method that takes a CommandInfo object
+ * and instantiates the corresponding command, usually
+ * a JavaBean component.
+ *
+ * This method calls the CommandInfo's getCommandObject
+ * method with the ClassLoader used to load
+ * the jakarta.activation.DataHandler class itself.
+ *
+ * @param cmdinfo the CommandInfo corresponding to a command
+ * @return the instantiated command object
+ */
+ public Object getBean(CommandInfo cmdinfo) {
+ Object bean = null;
+
+ try {
+ // make the bean
+ ClassLoader cld = null;
+ // First try the "application's" class loader.
+ cld = Util.getContextClassLoader();
+ if (cld == null)
+ cld = this.getClass().getClassLoader();
+ bean = cmdinfo.getCommandObject(this, cld);
+ } catch (IOException | ClassNotFoundException e) {
+ }
+
+ return bean;
+ }
+
+ /**
+ * Get the DataContentHandler for this DataHandler:
+ *
+ * If a DataContentHandlerFactory is set, use it.
+ * Otherwise look for an object to serve DCH in the
+ * following order:
+ *
+ * 1) if a factory is set, use it
+ * 2) if a CommandMap is set, use it
+ * 3) use the default CommandMap
+ *
+ * In any case, wrap the real DataContentHandler with one of our own
+ * to handle any missing cases, fill in defaults, and to ensure that
+ * we always have a non-null DataContentHandler.
+ *
+ * @return the requested DataContentHandler
+ */
+ private synchronized DataContentHandler getDataContentHandler() {
+
+ // make sure the factory didn't change
+ if (factory != oldFactory) {
+ oldFactory = factory;
+ factoryDCH = null;
+ dataContentHandler = null;
+ transferFlavors = emptyFlavors;
+ }
+
+ if (dataContentHandler != null)
+ return dataContentHandler;
+
+ String simpleMT = getBaseType();
+
+ if (factoryDCH == null && factory != null)
+ factoryDCH = factory.createDataContentHandler(simpleMT);
+
+ if (factoryDCH != null)
+ dataContentHandler = factoryDCH;
+
+ if (dataContentHandler == null) {
+ if (dataSource != null)
+ dataContentHandler = getCommandMap().
+ createDataContentHandler(simpleMT, dataSource);
+ else
+ dataContentHandler = getCommandMap().
+ createDataContentHandler(simpleMT);
+ }
+
+ // getDataContentHandler always uses these 'wrapper' handlers
+ // to make sure it returns SOMETHING meaningful...
+ if (dataSource != null)
+ dataContentHandler = new DataSourceDataContentHandler(
+ dataContentHandler,
+ dataSource);
+ else
+ dataContentHandler = new ObjectDataContentHandler(
+ dataContentHandler,
+ object,
+ objectMimeType);
+ return dataContentHandler;
+ }
+
+ /**
+ * Use the MimeType class to extract the MIME type/subtype,
+ * ignoring the parameters. The type is cached.
+ */
+ private synchronized String getBaseType() {
+ if (shortType == null) {
+ String ct = getContentType();
+ try {
+ MimeType mt = new MimeType(ct);
+ shortType = mt.getBaseType();
+ } catch (MimeTypeParseException e) {
+ shortType = ct;
+ }
+ }
+ return shortType;
+ }
+}
+
+/**
+ * The DataHanderDataSource class implements the
+ * DataSource interface when the DataHandler is constructed
+ * with an Object and a mimeType string.
+ */
+class DataHandlerDataSource implements DataSource {
+ DataHandler dataHandler = null;
+
+ /**
+ * The constructor.
+ */
+ public DataHandlerDataSource(DataHandler dh) {
+ this.dataHandler = dh;
+ }
+
+ /**
+ * Returns an InputStream representing this object.
+ *
+ * @return the InputStream
+ */
+ public InputStream getInputStream() throws IOException {
+ return dataHandler.getInputStream();
+ }
+
+ /**
+ * Returns the OutputStream for this object.
+ *
+ * @return the OutputStream
+ */
+ public OutputStream getOutputStream() throws IOException {
+ return dataHandler.getOutputStream();
+ }
+
+ /**
+ * Returns the MIME type of the data represented by this object.
+ *
+ * @return the MIME type
+ */
+ public String getContentType() {
+ return dataHandler.getContentType();
+ }
+
+ /**
+ * Returns the name of this object.
+ *
+ * @return the name of this object
+ */
+ public String getName() {
+ return dataHandler.getName(); // what else would it be?
+ }
+}
+
+/*
+ * DataSourceDataContentHandler
+ *
+ * This is a private DataContentHandler that wraps the real
+ * DataContentHandler in the case where the DataHandler was instantiated
+ * with a DataSource.
+ */
+class DataSourceDataContentHandler implements DataContentHandler {
+ private DataSource ds = null;
+ private ActivationDataFlavor[] transferFlavors = null;
+ private DataContentHandler dch = null;
+
+ /**
+ * The constructor.
+ */
+ public DataSourceDataContentHandler(DataContentHandler dch, DataSource ds) {
+ this.ds = ds;
+ this.dch = dch;
+ }
+
+ /**
+ * Return the ActivationDataFlavors for this
+ * DataContentHandler.
+ *
+ * @return the ActivationDataFlavors
+ */
+ public ActivationDataFlavor[] getTransferDataFlavors() {
+
+ if (transferFlavors == null) {
+ if (dch != null) { // is there a dch?
+ transferFlavors = dch.getTransferDataFlavors();
+ } else {
+ transferFlavors = new ActivationDataFlavor[1];
+ transferFlavors[0] =
+ new ActivationDataFlavor(ds.getContentType(),
+ ds.getContentType());
+ }
+ }
+ return transferFlavors;
+ }
+
+ /**
+ * Return the Transfer Data of type ActivationDataFlavor from InputStream.
+ *
+ * @param df the ActivationDataFlavor
+ * @param ds the DataSource
+ * @return the constructed Object
+ */
+ public Object getTransferData(ActivationDataFlavor df, DataSource ds)
+ throws IOException {
+
+ if (dch != null)
+ return dch.getTransferData(df, ds);
+ else if (df.equals(getTransferDataFlavors()[0])) // only have one now
+ return ds.getInputStream();
+ else
+ throw new IOException("Unsupported DataFlavor: " + df);
+ }
+
+ public Object getContent(DataSource ds) throws IOException {
+
+ if (dch != null)
+ return dch.getContent(ds);
+ else
+ return ds.getInputStream();
+ }
+
+ /**
+ * Write the object to the output stream.
+ */
+ public void writeTo(Object obj, String mimeType, OutputStream os)
+ throws IOException {
+ if (dch != null)
+ dch.writeTo(obj, mimeType, os);
+ else
+ throw new UnsupportedDataTypeException(
+ "no DCH for content type " + ds.getContentType());
+ }
+}
+
+/*
+ * ObjectDataContentHandler
+ *
+ * This is a private DataContentHandler that wraps the real
+ * DataContentHandler in the case where the DataHandler was instantiated
+ * with an object.
+ */
+class ObjectDataContentHandler implements DataContentHandler {
+ private ActivationDataFlavor[] transferFlavors = null;
+ private Object obj;
+ private String mimeType;
+ private DataContentHandler dch = null;
+
+ /**
+ * The constructor.
+ */
+ public ObjectDataContentHandler(DataContentHandler dch,
+ Object obj, String mimeType) {
+ this.obj = obj;
+ this.mimeType = mimeType;
+ this.dch = dch;
+ }
+
+ /**
+ * Return the DataContentHandler for this object.
+ * Used only by the DataHandler class.
+ */
+ public DataContentHandler getDCH() {
+ return dch;
+ }
+
+ /**
+ * Return the ActivationDataFlavors for this
+ * DataContentHandler.
+ *
+ * @return the ActivationDataFlavors
+ */
+ public synchronized ActivationDataFlavor[] getTransferDataFlavors() {
+ if (transferFlavors == null) {
+ if (dch != null) {
+ transferFlavors = dch.getTransferDataFlavors();
+ } else {
+ transferFlavors = new ActivationDataFlavor[1];
+ transferFlavors[0] = new ActivationDataFlavor(obj.getClass(),
+ mimeType, mimeType);
+ }
+ }
+ return transferFlavors;
+ }
+
+ /**
+ * Return the Transfer Data of type ActivationDataFlavor from InputStream.
+ *
+ * @param df the ActivationDataFlavor
+ * @param ds the DataSource
+ * @return the constructed Object
+ */
+ public Object getTransferData(ActivationDataFlavor df, DataSource ds)
+ throws IOException {
+
+ if (dch != null)
+ return dch.getTransferData(df, ds);
+ else if (df.equals(getTransferDataFlavors()[0])) // only have one now
+ return obj;
+ else
+ throw new IOException("Unsupported DataFlavor: " + df);
+
+ }
+
+ public Object getContent(DataSource ds) {
+ return obj;
+ }
+
+ /**
+ * Write the object to the output stream.
+ */
+ public void writeTo(Object obj, String mimeType, OutputStream os)
+ throws IOException {
+ if (dch != null)
+ dch.writeTo(obj, mimeType, os);
+ else if (obj instanceof byte[])
+ os.write((byte[]) obj);
+ else if (obj instanceof String) {
+ OutputStreamWriter osw = new OutputStreamWriter(os, Charset.defaultCharset());
+ osw.write((String) obj);
+ osw.flush();
+ } else
+ throw new UnsupportedDataTypeException(
+ "no object DCH for MIME type " + this.mimeType);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/DataSource.java b/net-mail/src/main/java/jakarta/activation/DataSource.java
new file mode 100644
index 0000000..1dd1c9a
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/DataSource.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * The DataSource interface provides Jakarta Activation
+ * with an abstraction of an arbitrary collection of data. It
+ * provides a type for that data as well as access
+ * to it in the form of InputStreams and
+ * OutputStreams where appropriate.
+ */
+
+public interface DataSource {
+
+ /**
+ * This method returns an InputStream representing
+ * the data and throws the appropriate exception if it can
+ * not do so. Note that a new InputStream object must be
+ * returned each time this method is called, and the stream must be
+ * positioned at the beginning of the data.
+ *
+ * @return an InputStream
+ * @throws IOException for failures creating the InputStream
+ */
+ InputStream getInputStream() throws IOException;
+
+ /**
+ * This method returns an OutputStream where the
+ * data can be written and throws the appropriate exception if it can
+ * not do so. Note that a new OutputStream object must
+ * be returned each time this method is called, and the stream must
+ * be positioned at the location the data is to be written.
+ *
+ * @return an OutputStream
+ * @throws IOException for failures creating the OutputStream
+ */
+ OutputStream getOutputStream() throws IOException;
+
+ /**
+ * This method returns the MIME type of the data in the form of a
+ * string. It should always return a valid type. It is suggested
+ * that getContentType return "application/octet-stream" if the
+ * DataSource implementation can not determine the data type.
+ *
+ * @return the MIME Type
+ */
+ String getContentType();
+
+ /**
+ * Return the name of this object where the name of the object
+ * is dependant on the nature of the underlying objects. DataSources
+ * encapsulating files may choose to return the filename of the object.
+ * (Typically this would be the last component of the filename, not an
+ * entire pathname.)
+ *
+ * @return the name of the object.
+ */
+ String getName();
+}
diff --git a/net-mail/src/main/java/jakarta/activation/FactoryFinder.java b/net-mail/src/main/java/jakarta/activation/FactoryFinder.java
new file mode 100644
index 0000000..e2f5075
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/FactoryFinder.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2021, 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.util.Arrays;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+class FactoryFinder {
+
+ private static final Logger logger = Logger.getLogger("jakarta.activation");
+
+ private static final ServiceLoaderUtil.ExceptionHandler EXCEPTION_HANDLER =
+ new ServiceLoaderUtil.ExceptionHandler<>() {
+ @Override
+ public RuntimeException createException(Throwable throwable, String message) {
+ return new IllegalStateException(message, throwable);
+ }
+ };
+
+ /**
+ * Finds the implementation {@code Class} object for the given
+ * factory type.
+ *
+ * This method is package private so that this code can be shared.
+ *
+ * @param factoryClass factory abstract class or interface to be found
+ * @return the {@code Class} object of the specified message factory;
+ * may not be {@code null}
+ * @throws IllegalStateException if there is no factory found
+ */
+ static T find(Class factoryClass) throws RuntimeException {
+ for (ClassLoader l : getClassLoaders(Thread.class, FactoryFinder.class, System.class)) {
+ T f = find(factoryClass, l);
+ if (f != null) {
+ return f;
+ }
+ }
+ throw EXCEPTION_HANDLER.createException(null, "Provider for " + factoryClass.getName() + " cannot be found");
+ }
+
+ static T find(Class factoryClass, ClassLoader loader) throws RuntimeException {
+ String className = fromSystemProperty(factoryClass.getName());
+ if (className != null) {
+ T result = newInstance(className, factoryClass, loader);
+ if (result != null) {
+ return result;
+ }
+ }
+ return ServiceLoaderUtil.firstByServiceLoader(factoryClass, loader, logger, EXCEPTION_HANDLER);
+ }
+
+ private static T newInstance(String className,
+ Class extends T> service, ClassLoader loader)
+ throws RuntimeException {
+ return ServiceLoaderUtil.newInstance(className, service, loader, EXCEPTION_HANDLER);
+ }
+
+ private static String fromSystemProperty(String factoryId) {
+ return getSystemProperty(factoryId);
+ }
+
+ private static String getSystemProperty(final String property) {
+ logger.log(Level.FINE, "Checking system property {0}", property);
+ String value = System.getProperty(property);
+ logFound(value);
+ return value;
+ }
+
+ private static void logFound(String value) {
+ if (value != null) {
+ logger.log(Level.FINE, " found {0}", value);
+ } else {
+ logger.log(Level.FINE, " not found");
+ }
+ }
+
+ private static ClassLoader[] getClassLoaders(final Class>... classes) {
+ ClassLoader[] loaders = new ClassLoader[classes.length];
+ int w = 0;
+ for (Class> k : classes) {
+ ClassLoader cl = null;
+ if (k == Thread.class) {
+ cl = Thread.currentThread().getContextClassLoader();
+ } else if (k == System.class) {
+ cl = ClassLoader.getSystemClassLoader();
+ } else {
+ cl = k.getClassLoader();
+ }
+ if (cl != null) {
+ loaders[w++] = cl;
+ }
+ }
+ if (loaders.length != w) {
+ loaders = Arrays.copyOf(loaders, w);
+ }
+ return loaders;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/FileDataSource.java b/net-mail/src/main/java/jakarta/activation/FileDataSource.java
new file mode 100644
index 0000000..2882188
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/FileDataSource.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 1997, 2023, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * The FileDataSource class implements a simple DataSource object
+ * that encapsulates a file. It provides data typing services via
+ * a FileTypeMap object.
+ *
+ * FileDataSource Typing Semantics
+ *
+ * The FileDataSource class delegates data typing of files
+ * to an object subclassed from the FileTypeMap class.
+ * The setFileTypeMap method can be used to explicitly
+ * set the FileTypeMap for an instance of FileDataSource. If no
+ * FileTypeMap is set, the FileDataSource will call the FileTypeMap's
+ * getDefaultFileTypeMap method to get the System's default FileTypeMap.
+ *
+ * API Note:
+ * It is recommended to construct a {@code FileDataSource} using a {@code Path}
+ * instead of using a {@code File} since {@code Path} contains enhanced functionality.
+ *
+ * @see DataSource
+ * @see FileTypeMap
+ * @see MimetypesFileTypeMap
+ */
+public class FileDataSource implements DataSource {
+
+ // keep track of original 'ref' passed in, non-null
+ // one indicated which was passed in:
+ private Path _path = null;
+ private FileTypeMap typeMap = null;
+
+ /**
+ * Creates a FileDataSource from a File object. Note:
+ * The file will not actually be opened until a method is
+ * called that requires the file to be opened.
+ *
+ * API Note:
+ * {@code FileDataSource(Path)} constructor should be preferred over this one.
+ *
+ * @param file the file
+ */
+ public FileDataSource(File file) {
+ _path = file.toPath(); // save the file Object...
+ }
+
+ /**
+ * Creates a FileDataSource from a Path object. Note: The file will not
+ * actually be opened until a method is called that requires the file to be
+ * opened.
+ *
+ * @param path the file
+ */
+ public FileDataSource(Path path) {
+ _path = path;
+ }
+
+ /**
+ * Creates a FileDataSource from
+ * the specified path name. Note:
+ * The file will not actually be opened until a method is
+ * called that requires the file to be opened.
+ *
+ * @param name the system-dependent file name.
+ */
+ public FileDataSource(String name) {
+ this(Paths.get(name)); // use the file constructor
+ }
+
+ /**
+ * This method will return an InputStream representing the
+ * the data and will throw an IOException if it can
+ * not do so. This method will return a new
+ * instance of InputStream with each invocation.
+ *
+ * @return an InputStream
+ */
+ public InputStream getInputStream() throws IOException {
+ return Files.newInputStream(_path);
+ }
+
+ /**
+ * This method will return an OutputStream representing the
+ * the data and will throw an IOException if it can
+ * not do so. This method will return a new instance of
+ * OutputStream with each invocation.
+ *
+ * @return an OutputStream
+ */
+ public OutputStream getOutputStream() throws IOException {
+ return Files.newOutputStream(_path);
+ }
+
+ /**
+ * This method returns the MIME type of the data in the form of a
+ * string. This method uses the currently installed FileTypeMap. If
+ * there is no FileTypeMap explicitly set, the FileDataSource will
+ * call the getDefaultFileTypeMap method on
+ * FileTypeMap to acquire a default FileTypeMap. Note: By
+ * default, the FileTypeMap used will be a MimetypesFileTypeMap.
+ *
+ * @return the MIME Type
+ * @see FileTypeMap#getDefaultFileTypeMap
+ */
+ public String getContentType() {
+ // check to see if the type map is null?
+ if (typeMap == null)
+ return FileTypeMap.getDefaultFileTypeMap().getContentType(_path);
+ else
+ return typeMap.getContentType(_path);
+ }
+
+ /**
+ * Return the name of this object. The FileDataSource
+ * will return the file name of the object.
+ *
+ * @return the name of the object.
+ * @see DataSource
+ */
+ public String getName() {
+ return _path.getFileName().toString();
+ }
+
+ /**
+ * Return the File object that corresponds to this FileDataSource.
+ *
+ * @return the File object for the file represented by this object.
+ */
+ public File getFile() {
+ return _path.toFile();
+ }
+
+ /**
+ * Return the Path object that corresponds to this FileDataSource.
+ *
+ * @return the Path object for the file represented by this object.
+ */
+ public Path getPath() {
+ return _path;
+ }
+
+ /**
+ * Set the FileTypeMap to use with this FileDataSource
+ *
+ * @param map The FileTypeMap for this object.
+ */
+ public void setFileTypeMap(FileTypeMap map) {
+ typeMap = map;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/FileTypeMap.java b/net-mail/src/main/java/jakarta/activation/FileTypeMap.java
new file mode 100644
index 0000000..1359f77
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/FileTypeMap.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 1997, 2023, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * The FileTypeMap is an abstract class that provides a data typing
+ * interface for files. Implementations of this class will
+ * implement the getContentType methods which will derive a content
+ * type from a file name or a File object. FileTypeMaps could use any
+ * scheme to determine the data type, from examining the file extension
+ * of a file (like the MimetypesFileTypeMap) to opening the file and
+ * trying to derive its type from the contents of the file. The
+ * FileDataSource class uses the default FileTypeMap (a MimetypesFileTypeMap
+ * unless changed) to determine the content type of files.
+ *
+ * @see FileTypeMap
+ * @see FileDataSource
+ * @see MimetypesFileTypeMap
+ */
+
+public abstract class FileTypeMap {
+
+ private static FileTypeMap defaultMap = null;
+
+ private static Map map = new WeakHashMap<>();
+
+ /**
+ * The default constructor.
+ */
+ public FileTypeMap() {
+ super();
+ }
+
+ /**
+ * Return the default FileTypeMap for the system.
+ * If setDefaultFileTypeMap was called, return
+ * that instance, otherwise return an instance of
+ * MimetypesFileTypeMap.
+ *
+ * @return The default FileTypeMap
+ * @see FileTypeMap#setDefaultFileTypeMap
+ */
+ public static synchronized FileTypeMap getDefaultFileTypeMap() {
+ if (defaultMap != null) {
+ return defaultMap;
+ }
+ ClassLoader tccl = Util.getContextClassLoader();
+ FileTypeMap def = map.get(tccl);
+ if (def == null) {
+ def = new MimetypesFileTypeMap();
+ map.put(tccl, def);
+ }
+ return def;
+ }
+
+ /**
+ * Sets the default FileTypeMap for the system. This instance
+ * will be returned to callers of getDefaultFileTypeMap.
+ *
+ * @param fileTypeMap The FileTypeMap.
+ */
+ public static synchronized void setDefaultFileTypeMap(FileTypeMap fileTypeMap) {
+ map.remove(Util.getContextClassLoader());
+ defaultMap = fileTypeMap;
+ }
+
+ /**
+ * Return the type of the file object. This method should
+ * always return a valid MIME type.
+ *
+ * @param file A file to be typed.
+ * @return The content type.
+ */
+ abstract public String getContentType(File file);
+
+ /**
+ * Return the type of the file Path object. This method should
+ * always return a valid MIME type.
+ *
+ * @param path A file Path to be typed.
+ * @return The content type.
+ */
+ abstract public String getContentType(Path path);
+
+ /**
+ * Return the type of the file passed in. This method should
+ * always return a valid MIME type.
+ *
+ * @param filename the pathname of the file.
+ * @return The content type.
+ */
+ abstract public String getContentType(String filename);
+}
diff --git a/net-mail/src/main/java/jakarta/activation/MailcapCommandMap.java b/net-mail/src/main/java/jakarta/activation/MailcapCommandMap.java
new file mode 100644
index 0000000..75715e2
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/MailcapCommandMap.java
@@ -0,0 +1,667 @@
+/*
+ * Copyright (c) 1997, 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import jakarta.activation.spi.MailcapRegistryProvider;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.ServiceConfigurationError;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * MailcapCommandMap extends the CommandMap
+ * abstract class. It implements a CommandMap whose configuration
+ * is based on mailcap files
+ * (RFC 1524).
+ * The MailcapCommandMap can be configured both programmatically
+ * and via configuration files.
+ *
+ * Mailcap file search order:
+ * The MailcapCommandMap looks in various places in the user's
+ * system for mailcap file entries. When requests are made
+ * to search for commands in the MailcapCommandMap, it searches
+ * mailcap files in the following order:
+ *
+ *
Programatically added entries to the MailcapCommandMap instance.
+ *
The file .mailcap in the user's home directory.
+ *
The file mailcap in the Java runtime.
+ *
The file or resources named META-INF/mailcap.
+ *
The file or resource named META-INF/mailcap.default
+ * (usually found only in the activation.jar file).
+ *
+ *
+ * (The current implementation looks for the mailcap file
+ * in the Java runtime in the directory java.home/conf
+ * if it exists, and otherwise in the directory
+ * java.home/lib, where java.home is the value
+ * of the "java.home" System property. Note that the "conf" directory was
+ * introduced in JDK 9.)
+ *
+ * Mailcap file format:
+ *
+ * Mailcap files must conform to the mailcap
+ * file specification (RFC 1524, A User Agent Configuration Mechanism
+ * For Multimedia Mail Format Information).
+ * The file format consists of entries corresponding to
+ * particular MIME types. In general, the specification
+ * specifies applications for clients to use when they
+ * themselves cannot operate on the specified MIME type. The
+ * MailcapCommandMap extends this specification by using a parameter mechanism
+ * in mailcap files that allows JavaBeans(tm) components to be specified as
+ * corresponding to particular commands for a MIME type.
+ *
+ * When a mailcap file is
+ * parsed, the MailcapCommandMap recognizes certain parameter signatures,
+ * specifically those parameter names that begin with x-java-.
+ * The MailcapCommandMap uses this signature to find
+ * command entries for inclusion into its registries.
+ * Parameter names with the form x-java-<name>
+ * are read by the MailcapCommandMap as identifying a command
+ * with the name name. When the name is
+ * content-handler the MailcapCommandMap recognizes the class
+ * signified by this parameter as a DataContentHandler.
+ * All other commands are handled generically regardless of command
+ * name. The command implementation is specified by a fully qualified
+ * class name of a JavaBean(tm) component. For example; a command for viewing
+ * some data can be specified as: x-java-view=com.foo.ViewBean.
+ *
+ * When the command name is fallback-entry, the value of
+ * the command may be true or false. An
+ * entry for a MIME type that includes a parameter of
+ * x-java-fallback-entry=true defines fallback commands
+ * for that MIME type that will only be used if no non-fallback entry
+ * can be found. For example, an entry of the form text/*; ;
+ * x-java-fallback-entry=true; x-java-view=com.sun.TextViewer
+ * specifies a view command to be used for any text MIME type. This
+ * view command would only be used if a non-fallback view command for
+ * the MIME type could not be found.
+ *
+ * MailcapCommandMap aware mailcap files have the
+ * following general form:
+ *
+ * # Comments begin with a '#' and continue to the end of the line.
+ * <mime type>; ; <parameter list>
+ * # Where a parameter list consists of one or more parameters,
+ * # where parameters look like: x-java-view=com.sun.TextViewer
+ * # and a parameter list looks like:
+ * text/plain; ; x-java-view=com.sun.TextViewer; x-java-edit=com.sun.TextEdit
+ *
+ * # Note that mailcap entries that do not contain 'x-java' parameters
+ * # and comply to RFC 1524 are simply ignored:
+ * image/gif; /usr/dt/bin/sdtimage %s
+ *
+ *
+ *
+ * @author Bart Calder
+ * @author Bill Shannon
+ */
+
+public class MailcapCommandMap extends CommandMap {
+
+ private static final Logger logger = Logger.getLogger(MailcapCommandMap.class.getName());
+
+ private static final int PROG = 0; // programmatically added entries
+ private static final String confDir;
+
+ static {
+ String dir = null;
+ String home = System.getProperty("java.home");
+ String newdir = home + File.separator + "conf";
+ File conf = new File(newdir);
+ if (conf.exists())
+ dir = newdir + File.separator;
+ else
+ dir = home + File.separator + "lib" + File.separator;
+ confDir = dir;
+ }
+
+ /*
+ * We manage a collection of databases, searched in order.
+ */
+ private MailcapRegistry[] DB;
+
+ /**
+ * The default Constructor.
+ */
+ public MailcapCommandMap() {
+ super();
+ List dbv = new ArrayList<>(5); // usually 5 or less databases
+ MailcapRegistry mf = null;
+ dbv.add(null); // place holder for PROG entry
+
+ logger.log(Level.FINE, "MailcapCommandMap: load HOME");
+ String user_home = System.getProperty("user.home");
+
+ if (user_home != null) {
+ String path = user_home + File.separator + ".mailcap";
+ mf = loadFile(path);
+ if (mf != null)
+ dbv.add(mf);
+ }
+
+ logger.log(Level.FINE, "MailcapCommandMap: load SYS");
+ // check system's home
+ if (confDir != null) {
+ mf = loadFile(confDir + "mailcap");
+ if (mf != null)
+ dbv.add(mf);
+ }
+
+ logger.log(Level.FINE, "MailcapCommandMap: load JAR");
+ // load from the app's jar file
+ loadAllResources(dbv, "META-INF/mailcap");
+
+ logger.log(Level.FINE, "MailcapCommandMap: load DEF");
+ mf = loadResource("/META-INF/mailcap.default");
+
+ if (mf != null)
+ dbv.add(mf);
+
+ DB = new MailcapRegistry[dbv.size()];
+ DB = dbv.toArray(DB);
+ }
+
+ /**
+ * Constructor that allows the caller to specify the path
+ * of a mailcap file.
+ *
+ * @param fileName The name of the mailcap file to open
+ * @throws IOException if the file can't be accessed
+ */
+ public MailcapCommandMap(String fileName) throws IOException {
+ this();
+ if (DB[PROG] == null) {
+ try {
+ DB[PROG] = getImplementation().getByFileName(fileName);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ String message = "Cannot find or load an implementation for MailcapRegistryProvider. " +
+ "MailcapRegistry: can't load " + fileName;
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, message, e);
+ }
+ throw new IOException(message, e);
+ }
+ }
+ if (DB[PROG] != null && logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "MailcapCommandMap: load PROG from " + fileName);
+ }
+ }
+
+ /**
+ * Constructor that allows the caller to specify an InputStream
+ * containing a mailcap file.
+ *
+ * @param is InputStream of the mailcap file to open
+ */
+ public MailcapCommandMap(InputStream is) {
+ this();
+
+ if (DB[PROG] == null) {
+ try {
+ DB[PROG] = getImplementation().getByInputStream(is);
+ } catch (IOException ex) {
+ // XXX - should throw it
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ if (logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider." +
+ "MailcapRegistry: can't load InputStream", e);
+ }
+ }
+ }
+ if (DB[PROG] != null && logger.isLoggable(Level.FINE)) {
+ logger.log(Level.FINE, "MailcapCommandMap: load PROG");
+ }
+ }
+
+ /**
+ * Load from the named resource.
+ */
+ private MailcapRegistry loadResource(String name) {
+ try (InputStream clis = Util.getResourceAsStream(this.getClass(), name)) {
+ if (clis != null) {
+ MailcapRegistry mf = getImplementation().getByInputStream(clis);
+ logger.log(Level.FINE, "MailcapCommandMap: successfully loaded " +
+ "mailcap file: " + name);
+ return mf;
+ } else {
+ logger.log(Level.FINE, "MailcapCommandMap: not loading " +
+ "mailcap file: " + name);
+ }
+ } catch (IOException e) {
+ logger.log(Level.FINE, "MailcapCommandMap: can't load " + name, e);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider. " +
+ "MailcapRegistry: can't load " + name, e);
+ }
+ return null;
+ }
+
+ /**
+ * Load all of the named resource.
+ */
+ private void loadAllResources(List v, String name) {
+ boolean anyLoaded = false;
+ try {
+ URL[] urls;
+ ClassLoader cld = null;
+ // First try the "application's" class loader.
+ cld = Util.getContextClassLoader();
+ if (cld == null)
+ cld = this.getClass().getClassLoader();
+ if (cld != null)
+ urls = Util.getResources(cld, name);
+ else
+ urls = Util.getSystemResources(name);
+ if (urls != null) {
+ logger.log(Level.FINE, "MailcapCommandMap: getResources");
+ for (int i = 0; i < urls.length; i++) {
+ URL url = urls[i];
+ logger.log(Level.FINE, "MailcapCommandMap: URL " + url);
+ try (InputStream clis = Util.openStream(url)) {
+ if (clis != null) {
+ v.add(getImplementation().getByInputStream(clis));
+ anyLoaded = true;
+ logger.log(Level.FINE, "MailcapCommandMap: " +
+ "successfully loaded " +
+ "mailcap file from URL: " +
+ url);
+ } else {
+ logger.log(Level.FINE, "MailcapCommandMap: " +
+ "not loading mailcap " +
+ "file from URL: " + url);
+ }
+ } catch (IOException ioex) {
+ logger.log(Level.FINE, "MailcapCommandMap: can't load " +
+ url, ioex);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider. " +
+ "MailcapRegistry: can't load " + name, e);
+ }
+ }
+ }
+ } catch (Exception ex) {
+ logger.log(Level.FINE, "MailcapCommandMap: can't load " + name, ex);
+ }
+
+ // if failed to load anything, fall back to old technique, just in case
+ if (!anyLoaded) {
+ logger.log(Level.FINE, "MailcapCommandMap: !anyLoaded");
+ MailcapRegistry mf = loadResource("/" + name);
+ if (mf != null)
+ v.add(mf);
+ }
+ }
+
+ /**
+ * Load from the named file.
+ */
+ private MailcapRegistry loadFile(String name) {
+ MailcapRegistry mtf = null;
+
+ try {
+ mtf = getImplementation().getByFileName(name);
+ } catch (IOException e) {
+ logger.log(Level.FINE, "MailcapRegistry: can't load from file - " + name, e);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider. " +
+ "MailcapRegistry: can't load " + name, e);
+ }
+ return mtf;
+ }
+
+ /**
+ * Get the preferred command list for a MIME Type. The MailcapCommandMap
+ * searches the mailcap files as described above under
+ * Mailcap file search order.
+ *
+ * The result of the search is a proper subset of available
+ * commands in all mailcap files known to this instance of
+ * MailcapCommandMap. The first entry for a particular command
+ * is considered the preferred command.
+ *
+ * @param mimeType the MIME type
+ * @return the CommandInfo objects representing the preferred commands.
+ */
+ public synchronized CommandInfo[] getPreferredCommands(String mimeType) {
+ List cmdList = new ArrayList<>();
+ if (mimeType != null)
+ mimeType = mimeType.toLowerCase(Locale.ENGLISH);
+
+ for (int i = 0; i < DB.length; i++) {
+ if (DB[i] == null)
+ continue;
+ Map> cmdMap = DB[i].getMailcapList(mimeType);
+ if (cmdMap != null)
+ appendPrefCmdsToList(cmdMap, cmdList);
+ }
+
+ // now add the fallback commands
+ for (int i = 0; i < DB.length; i++) {
+ if (DB[i] == null)
+ continue;
+ Map> cmdMap = DB[i].getMailcapFallbackList(mimeType);
+ if (cmdMap != null)
+ appendPrefCmdsToList(cmdMap, cmdList);
+ }
+
+ CommandInfo[] cmdInfos = new CommandInfo[cmdList.size()];
+ cmdInfos = cmdList.toArray(cmdInfos);
+
+ return cmdInfos;
+ }
+
+ /**
+ * Put the commands that are in the hash table, into the list.
+ */
+ private void appendPrefCmdsToList(Map> cmdHash, List cmdList) {
+ Iterator verb_enum = cmdHash.keySet().iterator();
+
+ while (verb_enum.hasNext()) {
+ String verb = verb_enum.next();
+ if (!checkForVerb(cmdList, verb)) {
+ List cmdList2 = cmdHash.get(verb); // get the list
+ String className = cmdList2.get(0);
+ cmdList.add(new CommandInfo(verb, className));
+ }
+ }
+ }
+
+ /**
+ * Check the cmdList to see if this command exists, return
+ * true if the verb is there.
+ */
+ private boolean checkForVerb(List cmdList, String verb) {
+ Iterator ee = cmdList.iterator();
+ while (ee.hasNext()) {
+ String enum_verb = (ee.next()).getCommandName();
+ if (enum_verb.equals(verb))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Get all the available commands in all mailcap files known to
+ * this instance of MailcapCommandMap for this MIME type.
+ *
+ * @param mimeType the MIME type
+ * @return the CommandInfo objects representing all the commands.
+ */
+ public synchronized CommandInfo[] getAllCommands(String mimeType) {
+ List cmdList = new ArrayList<>();
+ if (mimeType != null)
+ mimeType = mimeType.toLowerCase(Locale.ENGLISH);
+
+ for (int i = 0; i < DB.length; i++) {
+ if (DB[i] == null)
+ continue;
+ Map> cmdMap = DB[i].getMailcapList(mimeType);
+ if (cmdMap != null)
+ appendCmdsToList(cmdMap, cmdList);
+ }
+
+ // now add the fallback commands
+ for (int i = 0; i < DB.length; i++) {
+ if (DB[i] == null)
+ continue;
+ Map> cmdMap = DB[i].getMailcapFallbackList(mimeType);
+ if (cmdMap != null)
+ appendCmdsToList(cmdMap, cmdList);
+ }
+
+ CommandInfo[] cmdInfos = new CommandInfo[cmdList.size()];
+ cmdInfos = cmdList.toArray(cmdInfos);
+
+ return cmdInfos;
+ }
+
+ /**
+ * Put the commands that are in the hash table, into the list.
+ */
+ private void appendCmdsToList(Map> typeHash, List cmdList) {
+ Iterator verb_enum = typeHash.keySet().iterator();
+
+ while (verb_enum.hasNext()) {
+ String verb = verb_enum.next();
+ List cmdList2 = typeHash.get(verb);
+ Iterator cmd_enum = cmdList2.iterator();
+
+ while (cmd_enum.hasNext()) {
+ String cmd = cmd_enum.next();
+ cmdList.add(new CommandInfo(verb, cmd));
+ // cmdList.add(0, new CommandInfo(verb, cmd));
+ }
+ }
+ }
+
+ /**
+ * Get the command corresponding to cmdName for the MIME type.
+ *
+ * @param mimeType the MIME type
+ * @param cmdName the command name
+ * @return the CommandInfo object corresponding to the command.
+ */
+ public synchronized CommandInfo getCommand(String mimeType,
+ String cmdName) {
+ if (mimeType != null)
+ mimeType = mimeType.toLowerCase(Locale.ENGLISH);
+
+ for (int i = 0; i < DB.length; i++) {
+ if (DB[i] == null)
+ continue;
+ Map> cmdMap = DB[i].getMailcapList(mimeType);
+ if (cmdMap != null) {
+ // get the cmd list for the cmd
+ List v = cmdMap.get(cmdName);
+ if (v != null) {
+ String cmdClassName = v.get(0);
+
+ if (cmdClassName != null)
+ return new CommandInfo(cmdName, cmdClassName);
+ }
+ }
+ }
+
+ // now try the fallback list
+ for (int i = 0; i < DB.length; i++) {
+ if (DB[i] == null)
+ continue;
+ Map> cmdMap = DB[i].getMailcapFallbackList(mimeType);
+ if (cmdMap != null) {
+ // get the cmd list for the cmd
+ List v = cmdMap.get(cmdName);
+ if (v != null) {
+ String cmdClassName = v.get(0);
+
+ if (cmdClassName != null)
+ return new CommandInfo(cmdName, cmdClassName);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Add entries to the registry. Programmatically
+ * added entries are searched before other entries.
+ *
+ * The string that is passed in should be in mailcap
+ * format.
+ *
+ * @param mail_cap a correctly formatted mailcap string
+ */
+ public synchronized void addMailcap(String mail_cap) {
+ // check to see if one exists
+ logger.log(Level.FINE, "MailcapCommandMap: add to PROG");
+ try {
+ if (DB[PROG] == null) {
+ DB[PROG] = getImplementation().getInMemory();
+ }
+ DB[PROG].appendToMailcap(mail_cap);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider. " +
+ "MailcapRegistry: can't load", e);
+ throw e;
+ }
+ }
+
+ /**
+ * Return the DataContentHandler for the specified MIME type.
+ *
+ * @param mimeType the MIME type
+ * @return the DataContentHandler
+ */
+ public synchronized DataContentHandler createDataContentHandler(
+ String mimeType) {
+ logger.log(Level.FINE, "MailcapCommandMap: createDataContentHandler for " + mimeType);
+ if (mimeType != null)
+ mimeType = mimeType.toLowerCase(Locale.ENGLISH);
+
+ for (int i = 0; i < DB.length; i++) {
+ if (DB[i] == null)
+ continue;
+ logger.log(Level.FINE, " search DB #" + i);
+ Map> cmdMap = DB[i].getMailcapList(mimeType);
+ if (cmdMap != null) {
+ List v = cmdMap.get("content-handler");
+ if (v != null) {
+ String name = v.get(0);
+ DataContentHandler dch = getDataContentHandler(name);
+ if (dch != null)
+ return dch;
+ }
+ }
+ }
+
+ // now try the fallback entries
+ for (int i = 0; i < DB.length; i++) {
+ if (DB[i] == null)
+ continue;
+ logger.log(Level.FINE, " search fallback DB #" + i);
+ Map> cmdMap = DB[i].getMailcapFallbackList(mimeType);
+ if (cmdMap != null) {
+ List v = cmdMap.get("content-handler");
+ if (v != null) {
+ String name = v.get(0);
+ DataContentHandler dch = getDataContentHandler(name);
+ if (dch != null)
+ return dch;
+ }
+ }
+ }
+ return null;
+ }
+
+ private DataContentHandler getDataContentHandler(String name) {
+ logger.log(Level.FINE, " got content-handler");
+ logger.log(Level.FINE, " class " + name);
+ try {
+ ClassLoader cld = null;
+ // First try the "application's" class loader.
+ cld = Util.getContextClassLoader();
+ if (cld == null)
+ cld = this.getClass().getClassLoader();
+ Class> cl = null;
+ try {
+ cl = cld.loadClass(name);
+ } catch (Exception ex) {
+ // if anything goes wrong, do it the old way
+ cl = Class.forName(name);
+ }
+ if (cl != null) // XXX - always true?
+ return (DataContentHandler)
+ cl.getConstructor().newInstance();
+ } catch (ReflectiveOperationException e) {
+ logger.log(Level.FINE, "Can't load DCH " + name, e);
+ }
+ return null;
+ }
+
+ /**
+ * Get all the MIME types known to this command map.
+ *
+ * @return array of MIME types as strings
+ * @since JAF 1.1
+ */
+ public synchronized String[] getMimeTypes() {
+ List mtList = new ArrayList<>();
+
+ for (int i = 0; i < DB.length; i++) {
+ if (DB[i] == null)
+ continue;
+ String[] ts = DB[i].getMimeTypes();
+ if (ts != null) {
+ for (int j = 0; j < ts.length; j++) {
+ // eliminate duplicates
+ if (!mtList.contains(ts[j]))
+ mtList.add(ts[j]);
+ }
+ }
+ }
+
+ String[] mts = new String[mtList.size()];
+ mts = mtList.toArray(mts);
+
+ return mts;
+ }
+
+ /**
+ * Get the native commands for the given MIME type.
+ * Returns an array of strings where each string is
+ * an entire mailcap file entry. The application
+ * will need to parse the entry to extract the actual
+ * command as well as any attributes it needs. See
+ * RFC 1524
+ * for details of the mailcap entry syntax. Only mailcap
+ * entries that specify a view command for the specified
+ * MIME type are returned.
+ *
+ * @param mimeType the MIME type
+ * @return array of native command entries
+ * @since JAF 1.1
+ */
+ public synchronized String[] getNativeCommands(String mimeType) {
+ List cmdList = new ArrayList<>();
+ if (mimeType != null) {
+ mimeType = mimeType.toLowerCase(Locale.ENGLISH);
+ }
+ for (MailcapRegistry mailcapRegistry : DB) {
+ if (mailcapRegistry == null) {
+ continue;
+ }
+ String[] cmds = mailcapRegistry.getNativeCommands(mimeType);
+ if (cmds != null) {
+ for (String cmd : cmds) {
+ if (!cmdList.contains(cmd)) {
+ cmdList.add(cmd);
+ }
+ }
+ }
+ }
+ String[] cmds = new String[cmdList.size()];
+ cmds = cmdList.toArray(cmds);
+
+ return cmds;
+ }
+
+ private MailcapRegistryProvider getImplementation() {
+ return FactoryFinder.find(MailcapRegistryProvider.class);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/MailcapRegistry.java b/net-mail/src/main/java/jakarta/activation/MailcapRegistry.java
new file mode 100644
index 0000000..e22f7fb
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/MailcapRegistry.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The MailcapRegistry interface is implemented by objects that can
+ * be used to store and retrieve MailcapEntries.
+ *
+ * Application must implement {@link jakarta.activation.spi.MailcapRegistryProvider}
+ * to create new instances of the MailcapRegistry. Implementation of the MailcapRegistry
+ * can store MailcapEntries in different ways and that storage must be accessible through the
+ * {@link jakarta.activation.spi.MailcapRegistryProvider} methods.
+ * Implementation of the MailcapRegistry must contain in-memory storage for MailcapEntries.
+ */
+public interface MailcapRegistry {
+
+ /**
+ * Get the Map of MailcapEntries based on the MIME type.
+ *
+ *
+ * Semantics: First check for the literal mime type,
+ * if that fails looks for wildcard <type>/\* and return that.
+ * Return the list of all that hit.
+ *
+ * @param mime_type the MIME type
+ * @return the map of MailcapEntries
+ */
+ Map> getMailcapList(String mime_type);
+
+ /**
+ * Get the Map of fallback MailcapEntries based on the MIME type.
+ *
+ *
+ * Semantics: First check for the literal mime type,
+ * if that fails looks for wildcard <type>/\* and return that.
+ * Return the list of all that hit.
+ *
+ * @param mime_type the MIME type
+ * @return the map of fallback MailcapEntries
+ */
+ Map> getMailcapFallbackList(String mime_type);
+
+ /**
+ * Return all the MIME types known to this mailcap file.
+ *
+ * @return a String array of the MIME types
+ */
+ String[] getMimeTypes();
+
+ /**
+ * Return all the native comands for the given MIME type.
+ *
+ * @param mime_type the MIME type
+ * @return a String array of the commands
+ */
+ String[] getNativeCommands(String mime_type);
+
+ /**
+ * appendToMailcap: Append to this Mailcap DB, use the mailcap
+ * format:
+ * Comment == "# comment string"
+ * Entry == "mimetype; javabeanclass"
+ *
+ * Example:
+ * # this is a comment
+ * image/gif jaf.viewers.ImageViewer
+ *
+ * @param mail_cap the mailcap string
+ */
+ void appendToMailcap(String mail_cap);
+}
diff --git a/net-mail/src/main/java/jakarta/activation/MimeType.java b/net-mail/src/main/java/jakarta/activation/MimeType.java
new file mode 100644
index 0000000..fa65104
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/MimeType.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.util.Locale;
+
+/**
+ * A Multipurpose Internet Mail Extension (MIME) type, as defined
+ * in RFC 2045 and 2046.
+ */
+public class MimeType {
+
+ /**
+ * A string that holds all the special chars.
+ */
+ private static final String TSPECIALS = "()<>@,;:/[]?=\\\"";
+ private String primaryType;
+ private String subType;
+ private MimeTypeParameterList parameters;
+
+ /**
+ * Default constructor.
+ */
+ public MimeType() {
+ primaryType = "application";
+ subType = "*";
+ parameters = new MimeTypeParameterList();
+ }
+
+ /**
+ * Constructor that builds a MimeType from a String.
+ *
+ * @param rawdata the MIME type string
+ * @throws MimeTypeParseException if the MIME type can't be parsed
+ */
+ public MimeType(String rawdata) throws MimeTypeParseException {
+ parse(rawdata);
+ }
+
+ /**
+ * Constructor that builds a MimeType with the given primary and sub type
+ * but has an empty parameter list.
+ *
+ * @param primary the primary MIME type
+ * @param sub the MIME sub-type
+ * @throws MimeTypeParseException if the primary type or subtype
+ * is not a valid token
+ */
+ public MimeType(String primary, String sub) throws MimeTypeParseException {
+ // check to see if primary is valid
+ if (isValidToken(primary)) {
+ primaryType = primary.toLowerCase(Locale.ENGLISH);
+ } else {
+ throw new MimeTypeParseException("Primary type is invalid.");
+ }
+
+ // check to see if sub is valid
+ if (isValidToken(sub)) {
+ subType = sub.toLowerCase(Locale.ENGLISH);
+ } else {
+ throw new MimeTypeParseException("Sub type is invalid.");
+ }
+
+ parameters = new MimeTypeParameterList();
+ }
+
+ /**
+ * Determine whether or not a given character belongs to a legal token.
+ */
+ private static boolean isTokenChar(char c) {
+ return ((c > 040) && (c < 0177)) && (TSPECIALS.indexOf(c) < 0);
+ }
+
+ /**
+ * A routine for parsing the MIME type out of a String.
+ */
+ private void parse(String rawdata) throws MimeTypeParseException {
+ int slashIndex = rawdata.indexOf('/');
+ int semIndex = rawdata.indexOf(';');
+ if ((slashIndex < 0) && (semIndex < 0)) {
+ // neither character is present, so treat it
+ // as an error
+ throw new MimeTypeParseException("Unable to find a sub type.");
+ } else if ((slashIndex < 0) && (semIndex >= 0)) {
+ // we have a ';' (and therefore a parameter list),
+ // but no '/' indicating a sub type is present
+ throw new MimeTypeParseException("Unable to find a sub type.");
+ } else if ((slashIndex >= 0) && (semIndex < 0)) {
+ // we have a primary and sub type but no parameter list
+ primaryType = rawdata.substring(0, slashIndex).trim().
+ toLowerCase(Locale.ENGLISH);
+ subType = rawdata.substring(slashIndex + 1).trim().
+ toLowerCase(Locale.ENGLISH);
+ parameters = new MimeTypeParameterList();
+ } else if (slashIndex < semIndex) {
+ // we have all three items in the proper sequence
+ primaryType = rawdata.substring(0, slashIndex).trim().
+ toLowerCase(Locale.ENGLISH);
+ subType = rawdata.substring(slashIndex + 1, semIndex).trim().
+ toLowerCase(Locale.ENGLISH);
+ parameters = new MimeTypeParameterList(rawdata.substring(semIndex));
+ } else {
+ // we have a ';' lexically before a '/' which means we
+ // have a primary type and a parameter list but no sub type
+ throw new MimeTypeParseException("Unable to find a sub type.");
+ }
+
+ // now validate the primary and sub types
+
+ // check to see if primary is valid
+ if (!isValidToken(primaryType))
+ throw new MimeTypeParseException("Primary type is invalid.");
+
+ // check to see if sub is valid
+ if (!isValidToken(subType))
+ throw new MimeTypeParseException("Sub type is invalid.");
+ }
+
+ /**
+ * Retrieve the primary type of this object.
+ *
+ * @return the primary MIME type
+ */
+ public String getPrimaryType() {
+ return primaryType;
+ }
+
+ /**
+ * Set the primary type for this object to the given String.
+ *
+ * @param primary the primary MIME type
+ * @throws MimeTypeParseException if the primary type
+ * is not a valid token
+ */
+ public void setPrimaryType(String primary) throws MimeTypeParseException {
+ // check to see if primary is valid
+ if (!isValidToken(primaryType))
+ throw new MimeTypeParseException("Primary type is invalid.");
+ primaryType = primary.toLowerCase(Locale.ENGLISH);
+ }
+
+ /**
+ * Retrieve the subtype of this object.
+ *
+ * @return the MIME subtype
+ */
+ public String getSubType() {
+ return subType;
+ }
+
+ /**
+ * Set the subtype for this object to the given String.
+ *
+ * @param sub the MIME subtype
+ * @throws MimeTypeParseException if the subtype
+ * is not a valid token
+ */
+ public void setSubType(String sub) throws MimeTypeParseException {
+ // check to see if sub is valid
+ if (!isValidToken(subType))
+ throw new MimeTypeParseException("Sub type is invalid.");
+ subType = sub.toLowerCase(Locale.ENGLISH);
+ }
+
+ /**
+ * Retrieve this object's parameter list.
+ *
+ * @return a MimeTypeParameterList object representing the parameters
+ */
+ public MimeTypeParameterList getParameters() {
+ return parameters;
+ }
+
+ /**
+ * Retrieve the value associated with the given name, or null if there
+ * is no current association.
+ *
+ * @param name the parameter name
+ * @return the paramter's value
+ */
+ public String getParameter(String name) {
+ return parameters.get(name);
+ }
+
+ /**
+ * Set the value to be associated with the given name, replacing
+ * any previous association.
+ *
+ * @param name the parameter name
+ * @param value the paramter's value
+ */
+ public void setParameter(String name, String value) {
+ parameters.set(name, value);
+ }
+
+ /**
+ * Remove any value associated with the given name.
+ *
+ * @param name the parameter name
+ */
+ public void removeParameter(String name) {
+ parameters.remove(name);
+ }
+
+ /**
+ * Return the String representation of this object.
+ */
+ public String toString() {
+ return getBaseType() + parameters.toString();
+ }
+
+ /**
+ * Return a String representation of this object
+ * without the parameter list.
+ *
+ * @return the MIME type and sub-type
+ */
+ public String getBaseType() {
+ return primaryType + "/" + subType;
+ }
+
+ /**
+ * Determine if the primary and sub type of this object is
+ * the same as what is in the given type.
+ *
+ * @param type the MimeType object to compare with
+ * @return true if they match
+ */
+ public boolean match(MimeType type) {
+ return primaryType.equals(type.getPrimaryType())
+ && (subType.equals("*")
+ || type.getSubType().equals("*")
+ || (subType.equals(type.getSubType())));
+ }
+
+ // below here be scary parsing related things
+
+ /**
+ * Determine if the primary and sub type of this object is
+ * the same as the content type described in rawdata.
+ *
+ * @param rawdata the MIME type string to compare with
+ * @return true if they match
+ * @throws MimeTypeParseException if the MIME type can't be parsed
+ */
+ public boolean match(String rawdata) throws MimeTypeParseException {
+ return match(new MimeType(rawdata));
+ }
+
+ /**
+ * Determine whether or not a given string is a legal token.
+ */
+ private boolean isValidToken(String s) {
+ int len = s.length();
+ if (len > 0) {
+ for (int i = 0; i < len; ++i) {
+ char c = s.charAt(i);
+ if (!isTokenChar(c)) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/MimeTypeEntry.java b/net-mail/src/main/java/jakarta/activation/MimeTypeEntry.java
new file mode 100644
index 0000000..6f4be61
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/MimeTypeEntry.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+/**
+ * Represents mapping between the file extension and the MIME type string.
+ */
+public class MimeTypeEntry {
+ private String type;
+ private String extension;
+
+ /**
+ * Create new {@code MimeTypeEntry}
+ *
+ * @param mime_type the MIME type string
+ * @param file_ext the file extension
+ */
+ public MimeTypeEntry(String mime_type, String file_ext) {
+ type = mime_type;
+ extension = file_ext;
+ }
+
+ /**
+ * Get MIME type string
+ *
+ * @return the MIME type string
+ */
+ public String getMIMEType() {
+ return type;
+ }
+
+ /**
+ * Get the file extension
+ *
+ * @return the file extension
+ */
+ public String getFileExtension() {
+ return extension;
+ }
+
+ @Override
+ public String toString() {
+ return "MIMETypeEntry: " + type + ", " + extension;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/MimeTypeParameterList.java b/net-mail/src/main/java/jakarta/activation/MimeTypeParameterList.java
new file mode 100644
index 0000000..f37eb1e
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/MimeTypeParameterList.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Locale;
+
+/**
+ * A parameter list of a MimeType
+ * as defined in RFC 2045 and 2046. The Primary type of the
+ * object must already be stripped off.
+ *
+ * @see MimeType
+ */
+public class MimeTypeParameterList {
+ /**
+ * A string that holds all the special chars.
+ */
+ private static final String TSPECIALS = "()<>@,;:/[]?=\\\"";
+ private Hashtable parameters;
+
+
+ /**
+ * Default constructor.
+ */
+ public MimeTypeParameterList() {
+ parameters = new Hashtable<>();
+ }
+
+ /**
+ * Constructs a new MimeTypeParameterList with the passed in data.
+ *
+ * @param parameterList an RFC 2045, 2046 compliant parameter list.
+ * @throws MimeTypeParseException if the MIME type can't be parsed
+ */
+ public MimeTypeParameterList(String parameterList)
+ throws MimeTypeParseException {
+ parameters = new Hashtable<>();
+
+ // now parse rawdata
+ parse(parameterList);
+ }
+
+ /**
+ * Determine whether or not a given character belongs to a legal token.
+ */
+ private static boolean isTokenChar(char c) {
+ return ((c > 040) && (c < 0177)) && (TSPECIALS.indexOf(c) < 0);
+ }
+
+ /**
+ * return the index of the first non white space character in
+ * rawdata at or after index i.
+ */
+ private static int skipWhiteSpace(String rawdata, int i) {
+ int length = rawdata.length();
+ while ((i < length) && Character.isWhitespace(rawdata.charAt(i)))
+ i++;
+ return i;
+ }
+
+ /**
+ * A routine that knows how and when to quote and escape the given value.
+ */
+ private static String quote(String value) {
+ boolean needsQuotes = false;
+
+ // check to see if we actually have to quote this thing
+ int length = value.length();
+ for (int i = 0; (i < length) && !needsQuotes; i++) {
+ needsQuotes = !isTokenChar(value.charAt(i));
+ }
+
+ if (needsQuotes) {
+ StringBuilder buffer = new StringBuilder();
+ buffer.ensureCapacity((int) (length * 1.5));
+
+ // add the initial quote
+ buffer.append('"');
+
+ // add the properly escaped text
+ for (int i = 0; i < length; ++i) {
+ char c = value.charAt(i);
+ if ((c == '\\') || (c == '"'))
+ buffer.append('\\');
+ buffer.append(c);
+ }
+
+ // add the closing quote
+ buffer.append('"');
+
+ return buffer.toString();
+ } else {
+ return value;
+ }
+ }
+
+ /**
+ * A routine that knows how to strip the quotes and
+ * escape sequences from the given value.
+ */
+ private static String unquote(String value) {
+ int valueLength = value.length();
+ StringBuilder buffer = new StringBuilder();
+ buffer.ensureCapacity(valueLength);
+
+ boolean escaped = false;
+ for (int i = 0; i < valueLength; ++i) {
+ char currentChar = value.charAt(i);
+ if (!escaped && (currentChar != '\\')) {
+ buffer.append(currentChar);
+ } else if (escaped) {
+ buffer.append(currentChar);
+ escaped = false;
+ } else {
+ escaped = true;
+ }
+ }
+
+ return buffer.toString();
+ }
+
+ /**
+ * A routine for parsing the parameter list out of a String.
+ *
+ * @param parameterList an RFC 2045, 2046 compliant parameter list.
+ * @throws MimeTypeParseException if the MIME type can't be parsed
+ */
+ private void parse(String parameterList) throws MimeTypeParseException {
+ if (parameterList == null)
+ return;
+
+ int length = parameterList.length();
+ if (length == 0)
+ return;
+
+ int i;
+ char c;
+ for (i = skipWhiteSpace(parameterList, 0);
+ i < length && (c = parameterList.charAt(i)) == ';';
+ i = skipWhiteSpace(parameterList, i)) {
+ int lastIndex;
+ String name;
+ String value;
+
+ // eat the ';'
+ i++;
+
+ // now parse the parameter name
+
+ // skip whitespace
+ i = skipWhiteSpace(parameterList, i);
+
+ // tolerate trailing semicolon, even though it violates the spec
+ if (i >= length)
+ return;
+
+ // find the end of the token char run
+ lastIndex = i;
+ while ((i < length) && isTokenChar(parameterList.charAt(i)))
+ i++;
+
+ name = parameterList.substring(lastIndex, i).
+ toLowerCase(Locale.ENGLISH);
+
+ // now parse the '=' that separates the name from the value
+ i = skipWhiteSpace(parameterList, i);
+
+ if (i >= length || parameterList.charAt(i) != '=')
+ throw new MimeTypeParseException(
+ "Couldn't find the '=' that separates a " +
+ "parameter name from its value.");
+
+ // eat it and parse the parameter value
+ i++;
+ i = skipWhiteSpace(parameterList, i);
+
+ if (i >= length)
+ throw new MimeTypeParseException(
+ "Couldn't find a value for parameter named " + name);
+
+ // now find out whether or not we have a quoted value
+ c = parameterList.charAt(i);
+ if (c == '"') {
+ // yup it's quoted so eat it and capture the quoted string
+ i++;
+ if (i >= length)
+ throw new MimeTypeParseException(
+ "Encountered unterminated quoted parameter value.");
+
+ lastIndex = i;
+
+ // find the next unescaped quote
+ while (i < length) {
+ c = parameterList.charAt(i);
+ if (c == '"')
+ break;
+ if (c == '\\') {
+ // found an escape sequence
+ // so skip this and the
+ // next character
+ i++;
+ }
+ i++;
+ }
+ if (c != '"')
+ throw new MimeTypeParseException(
+ "Encountered unterminated quoted parameter value.");
+
+ value = unquote(parameterList.substring(lastIndex, i));
+ // eat the quote
+ i++;
+ } else if (isTokenChar(c)) {
+ // nope it's an ordinary token so it
+ // ends with a non-token char
+ lastIndex = i;
+ while (i < length && isTokenChar(parameterList.charAt(i)))
+ i++;
+ value = parameterList.substring(lastIndex, i);
+ } else {
+ // it ain't a value
+ throw new MimeTypeParseException(
+ "Unexpected character encountered at index " + i);
+ }
+
+ // now put the data into the hashtable
+ parameters.put(name, value);
+ }
+ if (i < length) {
+ throw new MimeTypeParseException(
+ "More characters encountered in input than expected.");
+ }
+ }
+
+ /**
+ * Return the number of name-value pairs in this list.
+ *
+ * @return the number of parameters
+ */
+ public int size() {
+ return parameters.size();
+ }
+
+ /**
+ * Determine whether or not this list is empty.
+ *
+ * @return true if there are no parameters
+ */
+ public boolean isEmpty() {
+ return parameters.isEmpty();
+ }
+
+ /**
+ * Retrieve the value associated with the given name, or null if there
+ * is no current association.
+ *
+ * @param name the parameter name
+ * @return the parameter's value
+ */
+ public String get(String name) {
+ return parameters.get(name.trim().toLowerCase(Locale.ENGLISH));
+ }
+
+ // below here be scary parsing related things
+
+ /**
+ * Set the value to be associated with the given name, replacing
+ * any previous association.
+ *
+ * @param name the parameter name
+ * @param value the parameter's value
+ */
+ public void set(String name, String value) {
+ parameters.put(name.trim().toLowerCase(Locale.ENGLISH), value);
+ }
+
+ /**
+ * Remove any value associated with the given name.
+ *
+ * @param name the parameter name
+ */
+ public void remove(String name) {
+ parameters.remove(name.trim().toLowerCase(Locale.ENGLISH));
+ }
+
+ /**
+ * Retrieve an enumeration of all the names in this list.
+ *
+ * @return an enumeration of all parameter names
+ */
+ public Enumeration getNames() {
+ return parameters.keys();
+ }
+
+ /**
+ * Return a string representation of this object.
+ */
+ public String toString() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.ensureCapacity(parameters.size() * 16);
+ // heuristic: 8 characters per field
+
+ Enumeration keys = parameters.keys();
+ while (keys.hasMoreElements()) {
+ String key = keys.nextElement();
+ buffer.append("; ");
+ buffer.append(key);
+ buffer.append('=');
+ buffer.append(quote(parameters.get(key)));
+ }
+
+ return buffer.toString();
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/MimeTypeParseException.java b/net-mail/src/main/java/jakarta/activation/MimeTypeParseException.java
new file mode 100644
index 0000000..8384242
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/MimeTypeParseException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+/**
+ * A class to encapsulate MimeType parsing related exceptions.
+ */
+@SuppressWarnings("serial")
+public class MimeTypeParseException extends Exception {
+
+ /**
+ * Constructs a MimeTypeParseException with no specified detail message.
+ */
+ public MimeTypeParseException() {
+ super();
+ }
+
+ /**
+ * Constructs a MimeTypeParseException with the specified detail message.
+ *
+ * @param s the detail message.
+ */
+ public MimeTypeParseException(String s) {
+ super(s);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/MimeTypeRegistry.java b/net-mail/src/main/java/jakarta/activation/MimeTypeRegistry.java
new file mode 100644
index 0000000..4ae3c65
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/MimeTypeRegistry.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+/**
+ * The MimeTypeRegistry interface is implemented by objects that can
+ * be used to store and retrieve MimeTypeEntries.
+ *
+ * Application must implement {@link jakarta.activation.spi.MimeTypeRegistryProvider}
+ * to create new instances of the MimeTypeRegistry. Implementation of the MimeTypeRegistry
+ * can store MimeTypeEntries in different ways and that storage must be accessible through the
+ * {@link jakarta.activation.spi.MimeTypeRegistryProvider} methods.
+ * Implementation of the MimeTypeRegistry must contain in-memory storage for MimeTypeEntries.
+ */
+public interface MimeTypeRegistry {
+
+ /**
+ * get the MimeTypeEntry based on the file extension
+ *
+ * @param file_ext the file extension
+ * @return the MimeTypeEntry
+ */
+ MimeTypeEntry getMimeTypeEntry(String file_ext);
+
+ /**
+ * Get the MIME type string corresponding to the file extension.
+ *
+ * @param file_ext the file extension
+ * @return the MIME type string
+ */
+ default String getMIMETypeString(String file_ext) {
+ MimeTypeEntry entry = this.getMimeTypeEntry(file_ext);
+
+ if (entry != null) {
+ return entry.getMIMEType();
+ }
+ return null;
+ }
+
+ /**
+ * Appends string of entries to the types registry
+ *
+ * @param mime_types the mime.types string
+ */
+ void appendToRegistry(String mime_types);
+}
\ No newline at end of file
diff --git a/net-mail/src/main/java/jakarta/activation/MimetypesFileTypeMap.java b/net-mail/src/main/java/jakarta/activation/MimetypesFileTypeMap.java
new file mode 100644
index 0000000..410f6a2
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/MimetypesFileTypeMap.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (c) 1997, 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import jakarta.activation.spi.MimeTypeRegistryProvider;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.NoSuchElementException;
+import java.util.ServiceConfigurationError;
+import java.util.Vector;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class extends FileTypeMap and provides data typing of files
+ * via their file extension. It uses the .mime.types format.
+ *
+ * MIME types file search order:
+ * The MimetypesFileTypeMap looks in various places in the user's
+ * system for MIME types file entries. When requests are made
+ * to search for MIME types in the MimetypesFileTypeMap, it searches
+ * MIME types files in the following order:
+ *
+ *
Programmatically added entries to the MimetypesFileTypeMap instance.
+ *
The file .mime.types in the user's home directory.
+ *
The file mime.types in the Java runtime.
+ *
The file or resources named META-INF/mime.types.
+ *
The file or resource named META-INF/mimetypes.default
+ * (usually found only in the activation.jar file).
+ *
+ *
+ * (The current implementation looks for the mime.types file
+ * in the Java runtime in the directory java.home/conf
+ * if it exists, and otherwise in the directory
+ * java.home/lib, where java.home is the value
+ * of the "java.home" System property. Note that the "conf" directory was
+ * introduced in JDK 9.)
+ *
+ * MIME types file format:
+ *
+ *
+ * # comments begin with a '#'
+ * # the format is <mime type> <space separated file extensions>
+ * # for example:
+ * text/plain txt text TXT
+ * # this would map file.txt, file.text, and file.TXT to
+ * # the mime type "text/plain"
+ *
+ *
+ * @author Bart Calder
+ * @author Bill Shannon
+ */
+public class MimetypesFileTypeMap extends FileTypeMap {
+
+ private static final Logger logger = Logger.getLogger(MimetypesFileTypeMap.class.getName());
+
+ private static final int PROG = 0;
+ private static final String defaultType = "application/octet-stream";
+ private static final String confDir;
+
+ static {
+ String dir;
+ String home = System.getProperty("java.home");
+ String newdir = home + File.separator + "conf";
+ File conf = new File(newdir);
+ if (conf.exists()) {
+ dir = newdir + File.separator;
+ } else {
+ dir = home + File.separator + "lib" + File.separator;
+ }
+ confDir = dir;
+ }
+
+ private MimeTypeRegistry[] DB;
+
+ /**
+ * The default constructor.
+ */
+ public MimetypesFileTypeMap() {
+ Vector dbv = new Vector<>(5);
+ MimeTypeRegistry mf;
+ dbv.addElement(null);
+ logger.log(Level.FINE, "MimetypesFileTypeMap: load HOME");
+ String user_home = System.getProperty("user.home");
+ if (user_home != null) {
+ String path = user_home + File.separator + ".mime.types";
+ mf = loadFile(path);
+ if (mf != null)
+ dbv.addElement(mf);
+ }
+ logger.log(Level.FINE, "MimetypesFileTypeMap: load SYS");
+ // check system's home
+ if (confDir != null) {
+ mf = loadFile(confDir + "mime.types");
+ if (mf != null)
+ dbv.addElement(mf);
+ }
+ logger.log(Level.FINE, "MimetypesFileTypeMap: load JAR");
+ // load from the app's jar file
+ loadAllResources(dbv, "META-INF/mime.types");
+
+ logger.log(Level.FINE, "MimetypesFileTypeMap: load DEF");
+ mf = loadResource("/META-INF/mimetypes.default");
+
+ if (mf != null)
+ dbv.addElement(mf);
+
+ DB = new MimeTypeRegistry[dbv.size()];
+ dbv.copyInto(DB);
+ }
+
+ /**
+ * Construct a MimetypesFileTypeMap with programmatic entries
+ * added from the named file.
+ *
+ * @param mimeTypeFileName the file name
+ * @throws IOException for errors reading the file
+ */
+ public MimetypesFileTypeMap(String mimeTypeFileName) throws IOException {
+ this();
+ try {
+ DB[PROG] = getImplementation().getByFileName(mimeTypeFileName);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ String errorMessage = "Cannot find or load an implementation for MimeTypeRegistryProvider." +
+ "MimeTypeRegistry: can't load " + mimeTypeFileName;
+ logger.log(Level.FINE, errorMessage, e);
+ throw new IOException(errorMessage, e);
+ }
+ }
+
+ /**
+ * Construct a MimetypesFileTypeMap with programmatic entries
+ * added from the InputStream.
+ *
+ * @param is the input stream to read from
+ */
+ public MimetypesFileTypeMap(InputStream is) {
+ this();
+ try {
+ DB[PROG] = getImplementation().getByInputStream(is);
+ } catch (IOException ex) {
+ // XXX - really should throw it
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." +
+ "MimeTypeRegistry: can't load InputStream", e);
+ }
+ }
+
+ /**
+ * Load from the named resource.
+ */
+ private MimeTypeRegistry loadResource(String name) {
+ InputStream clis = null;
+ try {
+ clis = Util.getResourceAsStream(this.getClass(), name);
+ if (clis != null) {
+ MimeTypeRegistry mf = getImplementation().getByInputStream(clis);
+ logger.log(Level.FINE, "MimetypesFileTypeMap: successfully " +
+ "loaded mime types file: " + name);
+ return mf;
+ } else {
+ logger.log(Level.FINE, "MimetypesFileTypeMap: not loading " +
+ "mime types file: " + name);
+ }
+ } catch (IOException e) {
+ logger.log(Level.FINE, "MimetypesFileTypeMap: can't load " + name, e);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." +
+ "MimeTypeRegistry: can't load " + name, e);
+ } finally {
+ try {
+ if (clis != null)
+ clis.close();
+ } catch (IOException ex) {
+ logger.log(Level.FINE, "InputStream cannot be close for " + name, ex);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Load all of the named resource.
+ */
+ private void loadAllResources(Vector v, String name) {
+ boolean anyLoaded = false;
+ try {
+ URL[] urls;
+ ClassLoader cld = null;
+ // First try the "application's" class loader.
+ cld = Util.getContextClassLoader();
+ if (cld == null)
+ cld = this.getClass().getClassLoader();
+ if (cld != null)
+ urls = Util.getResources(cld, name);
+ else
+ urls = Util.getSystemResources(name);
+ if (urls != null) {
+ logger.log(Level.FINE, "MimetypesFileTypeMap: getResources");
+ for (int i = 0; i < urls.length; i++) {
+ URL url = urls[i];
+ InputStream clis = null;
+ logger.log(Level.FINE, "MimetypesFileTypeMap: URL " + url);
+ try {
+ clis = Util.openStream(url);
+ if (clis != null) {
+ v.addElement(
+ getImplementation().getByInputStream(clis)
+ );
+ anyLoaded = true;
+ logger.log(Level.FINE, "MimetypesFileTypeMap: " +
+ "successfully loaded " +
+ "mime types from URL: " + url);
+ } else {
+ logger.log(Level.FINE, "MimetypesFileTypeMap: " +
+ "not loading " +
+ "mime types from URL: " + url);
+ }
+ } catch (IOException ioex) {
+ logger.log(Level.FINE, "MimetypesFileTypeMap: can't load " +
+ url, ioex);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." +
+ "MimeTypeRegistry: can't load " + url, e);
+ } finally {
+ try {
+ if (clis != null)
+ clis.close();
+ } catch (IOException cex) {
+ logger.log(Level.FINE, "InputStream cannot be close for " + name, cex);
+ }
+ }
+ }
+ }
+ } catch (Exception ex) {
+ logger.log(Level.FINE, "MimetypesFileTypeMap: can't load " + name, ex);
+ }
+
+ // if failed to load anything, fall back to old technique, just in case
+ if (!anyLoaded) {
+ logger.log(Level.FINE, "MimetypesFileTypeMap: !anyLoaded");
+ MimeTypeRegistry mf = loadResource("/" + name);
+ if (mf != null)
+ v.addElement(mf);
+ }
+ }
+
+ /**
+ * Load the named file.
+ */
+ private MimeTypeRegistry loadFile(String name) {
+ MimeTypeRegistry mtf = null;
+
+ try {
+ mtf = getImplementation().getByFileName(name);
+ } catch (IOException e) {
+ logger.log(Level.FINE, "MimeTypeRegistry: can't load from file - " + name, e);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." +
+ "MimeTypeRegistry: can't load " + name, e);
+ }
+ return mtf;
+ }
+
+ /**
+ * Prepend the MIME type values to the registry.
+ *
+ * @param mime_types A .mime.types formatted string of entries.
+ */
+ public synchronized void addMimeTypes(String mime_types) {
+ try {
+ if (DB[PROG] == null) {
+ DB[PROG] = getImplementation().getInMemory();
+ }
+ DB[PROG].appendToRegistry(mime_types);
+ } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) {
+ logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." +
+ "MimeTypeRegistry: can't add " + mime_types, e);
+ throw e;
+ }
+ }
+
+ /**
+ * Return the MIME type of the File object.
+ * The implementation in this class calls
+ * getContentType(f.getName()).
+ *
+ * @param f the file
+ * @return the file's MIME type
+ */
+ public String getContentType(File f) {
+ return this.getContentType(f.getName());
+ }
+
+ /**
+ * Return the MIME type of the Path object.
+ * The implementation in this class calls
+ * getContentType(p.getFileName().toString()).
+ *
+ * @param p the file Path
+ * @return the file's MIME type
+ */
+ public String getContentType(Path p) {
+ return this.getContentType(p.getFileName().toString());
+ }
+
+ /**
+ * Return the MIME type based on the specified file name.
+ * The MIME type entries are searched as described above under
+ * MIME types file search order.
+ * If no entry is found, the type "application/octet-stream" is returned.
+ *
+ * @param filename the file name
+ * @return the file's MIME type
+ */
+ public synchronized String getContentType(String filename) {
+ int dot_pos = filename.lastIndexOf("."); // period index
+ if (dot_pos < 0) {
+ return defaultType;
+ }
+ String file_ext = filename.substring(dot_pos + 1);
+ if (file_ext.isEmpty()) {
+ return defaultType;
+ }
+ for (MimeTypeRegistry mimeTypeRegistry : DB) {
+ if (mimeTypeRegistry == null) {
+ continue;
+ }
+ String result = mimeTypeRegistry.getMIMETypeString(file_ext);
+ if (result != null) {
+ return result;
+ }
+ }
+ return defaultType;
+ }
+
+ private MimeTypeRegistryProvider getImplementation() {
+ return FactoryFinder.find(MimeTypeRegistryProvider.class);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/ServiceLoaderUtil.java b/net-mail/src/main/java/jakarta/activation/ServiceLoaderUtil.java
new file mode 100644
index 0000000..1c0f38d
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/ServiceLoaderUtil.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2021, 2024 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.util.ServiceLoader;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Shared ServiceLoader/FactoryFinder Utils. JAF and MAIL use the same loading
+ * logic of thread context class loader, calling class loader, and finally the
+ * system class loader.
+ *
+ * @author Miroslav.Kos@oracle.com
+ */
+class ServiceLoaderUtil {
+
+ static
safeLoadClass(String className,
+ ClassLoader classLoader) throws ClassNotFoundException {
+ return nullSafeLoadClass(className, classLoader);
+ }
+
+ static abstract class ExceptionHandler {
+
+ public abstract T createException(Throwable throwable, String message);
+
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/URLDataSource.java b/net-mail/src/main/java/jakarta/activation/URLDataSource.java
new file mode 100644
index 0000000..e2ce8c5
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/URLDataSource.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * The URLDataSource class provides an object that wraps a URL
+ * object in a DataSource interface. URLDataSource simplifies the handling
+ * of data described by URLs within Jakarta Activation
+ * because this class can be used to create new DataHandlers. NOTE: The
+ * DataHandler object creates a URLDataSource internally,
+ * when it is constructed with a URL.
+ *
+ * @see DataSource
+ * @see DataHandler
+ */
+public class URLDataSource implements DataSource {
+ private URL url = null;
+ private URLConnection url_conn = null;
+
+ /**
+ * URLDataSource constructor. The URLDataSource class will
+ * not open a connection to the URL until a method requiring it
+ * to do so is called.
+ *
+ * @param url The URL to be encapsulated in this object.
+ */
+ public URLDataSource(URL url) {
+ this.url = url;
+ }
+
+ /**
+ * Returns the value of the URL content-type header field.
+ * It calls the URL's URLConnection.getContentType method
+ * after retrieving a URLConnection object.
+ * Note: this method attempts to call the openConnection
+ * method on the URL. If this method fails, or if a content type is not
+ * returned from the URLConnection, getContentType returns
+ * "application/octet-stream" as the content type.
+ *
+ * @return the content type.
+ */
+ public String getContentType() {
+ String type = null;
+
+ try {
+ if (url_conn == null)
+ url_conn = url.openConnection();
+ } catch (IOException e) {
+ }
+
+ if (url_conn != null)
+ type = url_conn.getContentType();
+
+ if (type == null)
+ type = "application/octet-stream";
+
+ return type;
+ }
+
+ /**
+ * Calls the getFile method on the URL used to
+ * instantiate the object.
+ *
+ * @return the result of calling the URL's getFile method.
+ */
+ public String getName() {
+ return url.getFile();
+ }
+
+ /**
+ * The getInputStream method from the URL. Calls the
+ * openStream method on the URL.
+ *
+ * @return the InputStream.
+ */
+ public InputStream getInputStream() throws IOException {
+ return url.openStream();
+ }
+
+ /**
+ * The getOutputStream method from the URL. First an attempt is
+ * made to get the URLConnection object for the URL. If that
+ * succeeds, the getOutputStream method on the URLConnection
+ * is returned.
+ *
+ * @return the OutputStream.
+ */
+ public OutputStream getOutputStream() throws IOException {
+ // get the url connection if it is available
+ url_conn = url.openConnection();
+
+ if (url_conn != null) {
+ url_conn.setDoOutput(true);
+ return url_conn.getOutputStream();
+ } else
+ return null;
+ }
+
+ /**
+ * Return the URL used to create this DataSource.
+ *
+ * @return The URL.
+ */
+ public URL getURL() {
+ return url;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/UnsupportedDataTypeException.java b/net-mail/src/main/java/jakarta/activation/UnsupportedDataTypeException.java
new file mode 100644
index 0000000..bd0ea0d
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/UnsupportedDataTypeException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.IOException;
+
+/**
+ * Signals that the requested operation does not support the
+ * requested data type.
+ *
+ * @see DataHandler
+ */
+@SuppressWarnings("serial")
+public class UnsupportedDataTypeException extends IOException {
+
+ /**
+ * Constructs an UnsupportedDataTypeException with no detail
+ * message.
+ */
+ public UnsupportedDataTypeException() {
+ super();
+ }
+
+ /**
+ * Constructs an UnsupportedDataTypeException with the specified
+ * message.
+ *
+ * @param s The detail message.
+ */
+ public UnsupportedDataTypeException(String s) {
+ super(s);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/Util.java b/net-mail/src/main/java/jakarta/activation/Util.java
new file mode 100644
index 0000000..79b7d39
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/Util.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+
+class Util {
+
+ private Util() {
+ // private constructor, can't create an instance
+ }
+
+ public static ClassLoader getContextClassLoader() {
+ return Thread.currentThread().getContextClassLoader();
+ }
+
+ public static InputStream getResourceAsStream(final Class> c,
+ final String name) throws IOException {
+ return c.getResourceAsStream(name);
+ }
+
+ public static URL[] getResources(final ClassLoader cl, final String name) {
+ URL[] ret = null;
+ try {
+ List v = new ArrayList<>();
+ Enumeration e = cl.getResources(name);
+ while (e != null && e.hasMoreElements()) {
+ URL url = e.nextElement();
+ if (url != null)
+ v.add(url);
+ }
+ if (!v.isEmpty()) {
+ ret = new URL[v.size()];
+ ret = v.toArray(ret);
+ }
+ } catch (IOException ioex) {
+ //
+ }
+ return ret;
+ }
+
+ public static URL[] getSystemResources(final String name) {
+ URL[] ret = null;
+ try {
+ List v = new ArrayList<>();
+ Enumeration e = ClassLoader.getSystemResources(name);
+ while (e != null && e.hasMoreElements()) {
+ URL url = e.nextElement();
+ if (url != null)
+ v.add(url);
+ }
+ if (!v.isEmpty()) {
+ ret = new URL[v.size()];
+ ret = v.toArray(ret);
+ }
+ } catch (IOException ioex) {
+ //
+ }
+ return ret;
+ }
+
+ public static InputStream openStream(final URL url) throws IOException {
+ return url.openStream();
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/activation/package-info.java b/net-mail/src/main/java/jakarta/activation/package-info.java
new file mode 100644
index 0000000..2e26440
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/package-info.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * Jakarta Activation is used by Jakarta Mail to manage MIME data.
+ */
+package jakarta.activation;
\ No newline at end of file
diff --git a/net-mail/src/main/java/jakarta/activation/spi/MailcapRegistryProvider.java b/net-mail/src/main/java/jakarta/activation/spi/MailcapRegistryProvider.java
new file mode 100644
index 0000000..4dd02d7
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/spi/MailcapRegistryProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation.spi;
+
+import jakarta.activation.MailcapRegistry;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.NoSuchElementException;
+import java.util.ServiceConfigurationError;
+
+/**
+ * This interface defines a factory for MailcapRegistry. An
+ * implementation of this interface should provide instances of the MailcapRegistry
+ * based on the way how to access the storage for MailcapEntries.
+ *
+ * Jakarta Activation uses Service Provider Interface and ServiceLoader
+ * to obtain an instance of the implementation of the MailcapRegistryProvider.
+ */
+public interface MailcapRegistryProvider {
+
+ /**
+ * Retrieve an instance of the MailcapRegistry based on the name of the file where the MailcapEntries are stored.
+ *
+ * @param name The name of the file that stores MailcapEntries.
+ * @return The instance of the MailcapRegistry, or null if none are found.
+ * @throws IOException If an instance of the MailcapRegistry class cannot be found or loaded.
+ */
+ MailcapRegistry getByFileName(String name) throws IOException;
+
+ /**
+ * Retrieve an instance of the MailcapRegistry based on the InputStream
+ * that is used to read data from some named resource.
+ *
+ * @param inputStream InputStream for some resource that contains MailcapEntries.
+ * @return The instance of the MailcapRegistry, or null if none are found.
+ * @throws IOException If an instance of the MailcapRegistry class cannot be found or loaded.
+ */
+ MailcapRegistry getByInputStream(InputStream inputStream) throws IOException;
+
+ /**
+ * Retrieve an instance of the in-memory implementation of the MailcapRegistry.
+ *
+ * @return In-memory implementation of the MailcapRegistry.
+ * @throws NoSuchElementException If no implementations were found.
+ * @throws ServiceConfigurationError If no implementations were loaded.
+ */
+ MailcapRegistry getInMemory();
+}
diff --git a/net-mail/src/main/java/jakarta/activation/spi/MimeTypeRegistryProvider.java b/net-mail/src/main/java/jakarta/activation/spi/MimeTypeRegistryProvider.java
new file mode 100644
index 0000000..700e818
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/spi/MimeTypeRegistryProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package jakarta.activation.spi;
+
+import jakarta.activation.MimeTypeRegistry;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.NoSuchElementException;
+import java.util.ServiceConfigurationError;
+
+/**
+ * This interface defines a factory for MimeTypeRegistry. An
+ * implementation of this interface should provide instances of the MimeTypeRegistry
+ * based on the way how to access the storage for MimeTypeEntries.
+ *
+ * Jakarta Activation uses Service Provider Interface and ServiceLoader
+ * to obtain an instance of the implementation of the MimeTypeRegistryProvider.
+ */
+public interface MimeTypeRegistryProvider {
+
+ /**
+ * Retrieve an instance of the MimeTypeRegistry based on the name of the file where the MimeTypeEntries are stored.
+ *
+ * @param name The name of the file that stores MimeTypeEntries.
+ * @return The instance of the MimeTypeRegistry, or null if none are found.
+ * @throws IOException If an instance of the MailcapRegistry class cannot be found or loaded.
+ */
+ MimeTypeRegistry getByFileName(String name) throws IOException;
+
+ /**
+ * Retrieve an instance of the MimeTypeRegistry based on the InputStream
+ * that is used to read data from some named resource.
+ *
+ * @param inputStream InputStream for some resource that contains MimeTypeEntries.
+ * @return The instance of the MimeTypeRegistry, or null if none are found.
+ * @throws IOException If an instance of the MailcapRegistry class cannot be found or loaded.
+ */
+ MimeTypeRegistry getByInputStream(InputStream inputStream) throws IOException;
+
+ /**
+ * Retrieve an instance of the in-memory implementation of the MimeTypeRegistry.
+ * Jakarta Activation can throw NoSuchElementException or ServiceConfigurationError
+ * if no implementations were found.
+ *
+ * @return In-memory implementation of the MimeTypeRegistry.
+ * @throws NoSuchElementException If no implementations were found.
+ * @throws ServiceConfigurationError If no implementations were loaded.
+ */
+ MimeTypeRegistry getInMemory();
+}
\ No newline at end of file
diff --git a/net-mail/src/main/java/jakarta/activation/spi/package-info.java b/net-mail/src/main/java/jakarta/activation/spi/package-info.java
new file mode 100644
index 0000000..59488ad
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/activation/spi/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0, which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ *
Provides interfaces which implementations will be used as service providers for other services
+ * that used by Jakarta Activation.
+ *
Implementation of Jakarta Activation must implement interfaces declared in this package.
+ * Jakarta Activation uses {@link java.util.ServiceLoader} class to discover
+ * and load implementations of the interfaces from this package using standard Java SPI mechanism.
+ */
+package jakarta.activation.spi;
\ No newline at end of file
diff --git a/net-mail/src/main/java/jakarta/mail/Address.java b/net-mail/src/main/java/jakarta/mail/Address.java
new file mode 100644
index 0000000..048beb1
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Address.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * This abstract class models the addresses in a message.
+ * Subclasses provide specific implementations. Subclasses
+ * will typically be serializable so that (for example) the
+ * use of Address objects in search terms can be serialized
+ * along with the search terms.
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ */
+
+public abstract class Address {
+
+ /**
+ * Creates a default {@code Address}.
+ */
+ public Address() {
+ }
+
+ /**
+ * Return a type string that identifies this address type.
+ *
+ * @return address type
+ * @see jakarta.mail.internet.InternetAddress
+ */
+ public abstract String getType();
+
+ /**
+ * Return a String representation of this address object.
+ *
+ * @return string representation of this address
+ */
+ @Override
+ public abstract String toString();
+
+ /**
+ * The equality operator. Subclasses should provide an
+ * implementation of this method that supports value equality
+ * (do the two Address objects represent the same destination?),
+ * not object reference equality. A subclass must also provide
+ * a corresponding implementation of the hashCode
+ * method that preserves the general contract of
+ * equals and hashCode - objects that
+ * compare as equal must have the same hashCode.
+ *
+ * @param address Address object
+ */
+ @Override
+ public abstract boolean equals(Object address);
+}
diff --git a/net-mail/src/main/java/jakarta/mail/AuthenticationFailedException.java b/net-mail/src/main/java/jakarta/mail/AuthenticationFailedException.java
new file mode 100644
index 0000000..6527694
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/AuthenticationFailedException.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * This exception is thrown when the connect method on a Store or
+ * Transport object fails due to an authentication failure (e.g.,
+ * bad user name or password).
+ *
+ * @author Bill Shannon
+ */
+@SuppressWarnings("serial")
+public class AuthenticationFailedException extends MessagingException {
+
+ /**
+ * Constructs an AuthenticationFailedException.
+ */
+ public AuthenticationFailedException() {
+ super();
+ }
+
+ /**
+ * Constructs an AuthenticationFailedException with the specified
+ * detail message.
+ *
+ * @param message The detailed error message
+ */
+ public AuthenticationFailedException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs an AuthenticationFailedException with the specified
+ * detail message and embedded exception. The exception is chained
+ * to this exception.
+ *
+ * @param message The detailed error message
+ * @param e The embedded exception
+ * @since JavaMail 1.5
+ */
+ public AuthenticationFailedException(String message, Exception e) {
+ super(message, e);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Authenticator.java b/net-mail/src/main/java/jakarta/mail/Authenticator.java
new file mode 100644
index 0000000..8a7e8a1
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Authenticator.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import java.net.InetAddress;
+
+/**
+ * The class Authenticator represents an object that knows how to obtain
+ * authentication for a network connection. Usually, it will do this
+ * by prompting the user for information.
+ *
+ * Applications use this class by creating a subclass, and registering
+ * an instance of that subclass with the session when it is created.
+ * When authentication is required, the system will invoke a method
+ * on the subclass (like getPasswordAuthentication). The subclass's
+ * method can query about the authentication being requested with a
+ * number of inherited methods (getRequestingXXX()), and form an
+ * appropriate message for the user.
+ *
+ * All methods that request authentication have a default implementation
+ * that fails.
+ *
+ * @author Bill Foote
+ * @author Bill Shannon
+ * @see java.net.Authenticator
+ * @see Session#getInstance(java.util.Properties,
+ * Authenticator)
+ * @see Session#getDefaultInstance(java.util.Properties,
+ * Authenticator)
+ * @see Session#requestPasswordAuthentication
+ * @see PasswordAuthentication
+ */
+public abstract class Authenticator {
+
+ private InetAddress requestingSite;
+ private int requestingPort;
+ private String requestingProtocol;
+ private String requestingPrompt;
+ private String requestingUserName;
+
+ /**
+ * Creates a default {@code Authenticator}.
+ * There are no abstract methods, but to be useful the user must subclass.
+ *
+ * @see #getPasswordAuthentication()
+ */
+ public Authenticator() {
+ }
+
+ /**
+ * Ask the authenticator for a password.
+ *
+ *
+ * @param addr The InetAddress of the site requesting authorization,
+ * or null if not known.
+ * @param port the port for the requested connection
+ * @param protocol The protocol that's requesting the connection
+ * (@see java.net.Authenticator.getProtocol())
+ * @param prompt A prompt string for the user
+ * @return The username/password, or null if one can't be gotten.
+ */
+ final synchronized PasswordAuthentication requestPasswordAuthentication(
+ InetAddress addr, int port, String protocol,
+ String prompt, String defaultUserName) {
+ requestingSite = addr;
+ requestingPort = port;
+ requestingProtocol = protocol;
+ requestingPrompt = prompt;
+ requestingUserName = defaultUserName;
+ return getPasswordAuthentication();
+ }
+
+ /**
+ * @return the InetAddress of the site requesting authorization, or null
+ * if it's not available.
+ */
+ protected final InetAddress getRequestingSite() {
+ return requestingSite;
+ }
+
+ /**
+ * @return the port for the requested connection
+ */
+ protected final int getRequestingPort() {
+ return requestingPort;
+ }
+
+ /**
+ * Give the protocol that's requesting the connection. Often this
+ * will be based on a URLName.
+ *
+ * @return the protcol
+ * @see URLName#getProtocol
+ */
+ protected final String getRequestingProtocol() {
+ return requestingProtocol;
+ }
+
+ /**
+ * @return the prompt string given by the requestor
+ */
+ protected final String getRequestingPrompt() {
+ return requestingPrompt;
+ }
+
+ /**
+ * @return the default user name given by the requestor
+ */
+ protected final String getDefaultUserName() {
+ return requestingUserName;
+ }
+
+ /**
+ * Called when password authentication is needed. Subclasses should
+ * override the default implementation, which returns null.
+ *
+ * Note that if this method uses a dialog to prompt the user for this
+ * information, the dialog needs to block until the user supplies the
+ * information. This method can not simply return after showing the
+ * dialog.
+ *
+ * @return The PasswordAuthentication collected from the
+ * user, or null if none is provided.
+ */
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return null;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/BodyPart.java b/net-mail/src/main/java/jakarta/mail/BodyPart.java
new file mode 100644
index 0000000..9cbd2e0
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/BodyPart.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.mail.util.StreamProvider;
+
+/**
+ * This class models a Part that is contained within a Multipart.
+ * This is an abstract class. Subclasses provide actual implementations.
+ *
+ * BodyPart implements the Part interface. Thus, it contains a set of
+ * attributes and a "content".
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ */
+
+public abstract class BodyPart implements Part {
+
+ /**
+ * Instance of stream provider.
+ *
+ * @since JavaMail 2.1
+ */
+ protected final StreamProvider streamProvider = StreamProvider.provider();
+ /**
+ * The Multipart object containing this BodyPart,
+ * if known.
+ *
+ * @since JavaMail 1.1
+ */
+ protected Multipart parent;
+
+ /**
+ * Creates a default {@code BodyPart}.
+ */
+ public BodyPart() {
+ }
+
+ /**
+ * Return the containing Multipart object,
+ * or null if not known.
+ *
+ * @return the parent Multipart
+ */
+ public Multipart getParent() {
+ return parent;
+ }
+
+ /**
+ * Set the parent of this BodyPart to be the specified
+ * Multipart. Normally called by Multipart's
+ * addBodyPart method. parent may be
+ * null if the BodyPart is being removed
+ * from its containing Multipart.
+ *
+ * @since JavaMail 1.1
+ */
+ void setParent(Multipart parent) {
+ this.parent = parent;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/EncodingAware.java b/net-mail/src/main/java/jakarta/mail/EncodingAware.java
new file mode 100644
index 0000000..2d3697c
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/EncodingAware.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * A {@link jakarta.activation.DataSource DataSource} that also implements
+ * EncodingAware may specify the Content-Transfer-Encoding
+ * to use for its data. Valid Content-Transfer-Encoding values specified
+ * by RFC 2045 are "7bit", "8bit", "quoted-printable", "base64", and "binary".
+ *
+ * For example, a {@link jakarta.activation.FileDataSource FileDataSource}
+ * could be created that forces all files to be base64 encoded:
+ *
+ *
+ * @author Bill Shannon
+ * @since JavaMail 1.5
+ */
+
+public interface EncodingAware {
+
+ /**
+ * Return the MIME Content-Transfer-Encoding to use for this data,
+ * or null to indicate that an appropriate value should be chosen
+ * by the caller.
+ *
+ * @return the Content-Transfer-Encoding value, or null
+ */
+ String getEncoding();
+}
diff --git a/net-mail/src/main/java/jakarta/mail/EventQueue.java b/net-mail/src/main/java/jakarta/mail/EventQueue.java
new file mode 100644
index 0000000..30bbbe4
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/EventQueue.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.mail.event.MailEvent;
+import java.util.EventListener;
+import java.util.Vector;
+import java.util.WeakHashMap;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * Package private class used by Store & Folder to dispatch events.
+ * This class implements an event queue, and a dispatcher thread that
+ * dequeues and dispatches events from the queue.
+ *
+ * @author Bill Shannon
+ */
+class EventQueue implements Runnable {
+
+ private static WeakHashMap appq;
+ private volatile BlockingQueue q;
+ private Executor executor;
+
+ /**
+ * Construct an EventQueue using the specified Executor.
+ * If the Executor is null, threads will be created as needed.
+ */
+ EventQueue(Executor ex) {
+ this.executor = ex;
+ }
+
+ /**
+ * Create (if necessary) an application-scoped event queue.
+ * Application scoping is based on the thread's context class loader.
+ */
+ static synchronized EventQueue getApplicationEventQueue(Executor ex) {
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ if (appq == null)
+ appq = new WeakHashMap<>();
+ EventQueue q = appq.get(cl);
+ if (q == null) {
+ q = new EventQueue(ex);
+ appq.put(cl, q);
+ }
+ return q;
+ }
+
+ /**
+ * Enqueue an event.
+ */
+ synchronized void enqueue(MailEvent event,
+ Vector extends EventListener> vector) {
+ // if this is the first event, create the queue and start the event task
+ if (q == null) {
+ q = new LinkedBlockingQueue<>();
+ if (executor != null) {
+ executor.execute(this);
+ } else {
+ Thread qThread = new Thread(this, "Jakarta-Mail-EventQueue");
+ qThread.setDaemon(true); // not a user thread
+ qThread.start();
+ }
+ }
+ q.add(new QueueElement(event, vector));
+ }
+
+ /**
+ * Terminate the task running the queue, but only if there is a queue.
+ */
+ synchronized void terminateQueue() {
+ if (q != null) {
+ Vector dummyListeners = new Vector<>();
+ dummyListeners.setSize(1); // need atleast one listener
+ q.add(new QueueElement(new TerminatorEvent(), dummyListeners));
+ q = null;
+ }
+ }
+
+ /**
+ * Pull events off the queue and dispatch them.
+ */
+ @Override
+ public void run() {
+
+ BlockingQueue bq = q;
+ if (bq == null)
+ return;
+ try {
+ loop:
+ for (; ; ) {
+ // block until an item is available
+ QueueElement qe = bq.take();
+ MailEvent e = qe.event;
+ Vector extends EventListener> v = qe.vector;
+
+ for (int i = 0; i < v.size(); i++)
+ try {
+ e.dispatch(v.elementAt(i));
+ } catch (Throwable t) {
+ if (t instanceof InterruptedException)
+ break loop;
+ // ignore anything else thrown by the listener
+ }
+
+ qe = null;
+ e = null;
+ v = null;
+ }
+ } catch (InterruptedException e) {
+ // just die
+ }
+ }
+
+ /**
+ * A special event that causes the queue processing task to terminate.
+ */
+ @SuppressWarnings("serial")
+ static class TerminatorEvent extends MailEvent {
+
+ TerminatorEvent() {
+ super(new Object());
+ }
+
+ @Override
+ public void dispatch(Object listener) {
+ // Kill the event dispatching thread.
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * A "struct" to put on the queue.
+ */
+ static class QueueElement {
+ MailEvent event;
+ Vector extends EventListener> vector;
+
+ QueueElement(MailEvent event, Vector extends EventListener> vector) {
+ this.event = event;
+ this.vector = vector;
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/FetchProfile.java b/net-mail/src/main/java/jakarta/mail/FetchProfile.java
new file mode 100644
index 0000000..10d98c0
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/FetchProfile.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import java.util.Vector;
+
+/**
+ * Clients use a FetchProfile to list the Message attributes that
+ * it wishes to prefetch from the server for a range of messages.
+ *
+ * Messages obtained from a Folder are light-weight objects that
+ * typically start off as empty references to the actual messages.
+ * Such a Message object is filled in "on-demand" when the appropriate
+ * get*() methods are invoked on that particular Message. Certain
+ * server-based message access protocols (Ex: IMAP) allow batch
+ * fetching of message attributes for a range of messages in a single
+ * request. Clients that want to use message attributes for a range of
+ * Messages (Example: to display the top-level headers in a headerlist)
+ * might want to use the optimization provided by such servers. The
+ * FetchProfile allows the client to indicate this desire
+ * to the server.
+ *
+ * Note that implementations are not obligated to support
+ * FetchProfiles, since there might be cases where the backend service
+ * does not allow easy, efficient fetching of such profiles.
+ *
+ * Sample code that illustrates the use of a FetchProfile is given
+ * below:
+ *
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ * @see Folder#fetch
+ */
+
+public class FetchProfile {
+
+ private Vector specials; // specials
+ private Vector headers; // vector of header names
+
+ /**
+ * Create an empty FetchProfile.
+ */
+ public FetchProfile() {
+ specials = null;
+ headers = null;
+ }
+
+ /**
+ * Add the given special item as one of the attributes to
+ * be prefetched.
+ *
+ * @param item the special item to be fetched
+ * @see Item#ENVELOPE
+ * @see Item#CONTENT_INFO
+ * @see Item#FLAGS
+ */
+ public void add(Item item) {
+ if (specials == null)
+ specials = new Vector<>();
+ specials.addElement(item);
+ }
+
+ /**
+ * Add the specified header-field to the list of attributes
+ * to be prefetched.
+ *
+ * @param headerName header to be prefetched
+ */
+ public void add(String headerName) {
+ if (headers == null)
+ headers = new Vector<>();
+ headers.addElement(headerName);
+ }
+
+ /**
+ * Returns true if the fetch profile contains the given special item.
+ *
+ * @param item the Item to test
+ * @return true if the fetch profile contains the given special item
+ */
+ public boolean contains(Item item) {
+ return specials != null && specials.contains(item);
+ }
+
+ /**
+ * Returns true if the fetch profile contains the given header name.
+ *
+ * @param headerName the header to test
+ * @return true if the fetch profile contains the given header name
+ */
+ public boolean contains(String headerName) {
+ return headers != null && headers.contains(headerName);
+ }
+
+ /**
+ * Get the items set in this profile.
+ *
+ * @return items set in this profile
+ */
+ public Item[] getItems() {
+ if (specials == null)
+ return new Item[0];
+
+ Item[] s = new Item[specials.size()];
+ specials.copyInto(s);
+ return s;
+ }
+
+ /**
+ * Get the names of the header-fields set in this profile.
+ *
+ * @return headers set in this profile
+ */
+ public String[] getHeaderNames() {
+ if (headers == null)
+ return new String[0];
+
+ String[] s = new String[headers.size()];
+ headers.copyInto(s);
+ return s;
+ }
+
+ /**
+ * This inner class is the base class of all items that
+ * can be requested in a FetchProfile. The items currently
+ * defined here are ENVELOPE, CONTENT_INFO
+ * and FLAGS. The UIDFolder interface
+ * defines the UID Item as well.
+ *
+ * Note that this class only has a protected constructor, therby
+ * restricting new Item types to either this class or subclasses.
+ * This effectively implements a enumeration of allowed Item types.
+ *
+ * @see UIDFolder
+ */
+
+ public static class Item {
+ /**
+ * This is the Envelope item.
+ *
+ * The Envelope is an aggregration of the common attributes
+ * of a Message. Implementations should include the following
+ * attributes: From, To, Cc, Bcc, ReplyTo, Subject and Date.
+ * More items may be included as well.
+ *
+ * For implementations of the IMAP4 protocol (RFC 2060), the
+ * Envelope should include the ENVELOPE data item. More items
+ * may be included too.
+ */
+ public static final Item ENVELOPE = new Item("ENVELOPE");
+
+ /**
+ * This item is for fetching information about the
+ * content of the message.
+ *
+ * This includes all the attributes that describe the content
+ * of the message. Implementations should include the following
+ * attributes: ContentType, ContentDisposition,
+ * ContentDescription, Size and LineCount. Other items may be
+ * included as well.
+ */
+ public static final Item CONTENT_INFO = new Item("CONTENT_INFO");
+
+ /**
+ * SIZE is a fetch profile item that can be included in a
+ * FetchProfile during a fetch request to a Folder.
+ * This item indicates that the sizes of the messages in the specified
+ * range should be prefetched.
+ *
+ * @since JavaMail 1.5
+ */
+ public static final Item SIZE = new Item("SIZE");
+
+ /**
+ * This is the Flags item.
+ */
+ public static final Item FLAGS = new Item("FLAGS");
+
+ private String name;
+
+ /**
+ * Constructor for an item. The name is used only for debugging.
+ *
+ * @param name the item name
+ */
+ protected Item(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Include the name in the toString return value for debugging.
+ */
+ @Override
+ public String toString() {
+ return getClass().getName() + "[" + name + "]";
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Flags.java b/net-mail/src/main/java/jakarta/mail/Flags.java
new file mode 100644
index 0000000..5786d91
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Flags.java
@@ -0,0 +1,667 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Locale;
+import java.util.Vector;
+
+/**
+ * The Flags class represents the set of flags on a Message. Flags
+ * are composed of predefined system flags, and user defined flags.
+ *
+ * A System flag is represented by the Flags.Flag
+ * inner class. A User defined flag is represented as a String.
+ * User flags are case-independent.
+ *
+ * A set of standard system flags are predefined. Most folder
+ * implementations are expected to support these flags. Some
+ * implementations may also support arbitrary user-defined flags. The
+ * getPermanentFlags method on a Folder returns a Flags
+ * object that holds all the flags that are supported by that folder
+ * implementation.
+ *
+ * A Flags object is serializable so that (for example) the
+ * use of Flags objects in search terms can be serialized
+ * along with the search terms.
+ *
+ * Warning:
+ * Serialized objects of this class may not be compatible with future
+ * Jakarta Mail API releases. The current serialization support is
+ * appropriate for short term storage.
+ *
+ * The below code sample illustrates how to set, examine, and get the
+ * flags for a message.
+ *
+ *
+ * Message m = folder.getMessage(1);
+ * m.setFlag(Flags.Flag.DELETED, true); // set the DELETED flag
+ *
+ * // Check if DELETED flag is set on this message
+ * if (m.isSet(Flags.Flag.DELETED))
+ * System.out.println("DELETED message");
+ *
+ * // Examine ALL system flags for this message
+ * Flags flags = m.getFlags();
+ * Flags.Flag[] sf = flags.getSystemFlags();
+ * for (int i = 0; i < sf.length; i++) {
+ * if (sf[i] == Flags.Flag.DELETED)
+ * System.out.println("DELETED message");
+ * else if (sf[i] == Flags.Flag.SEEN)
+ * System.out.println("SEEN message");
+ * ......
+ * ......
+ * }
+ *
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ * @see Folder#getPermanentFlags
+ */
+
+public class Flags {
+
+ private final static int ANSWERED_BIT = 0x01;
+ private final static int DELETED_BIT = 0x02;
+ private final static int DRAFT_BIT = 0x04;
+ private final static int FLAGGED_BIT = 0x08;
+ private final static int RECENT_BIT = 0x10;
+ private final static int SEEN_BIT = 0x20;
+ private final static int USER_BIT = 0x80000000;
+ private int system_flags = 0;
+ // used as a case-independent Set that preserves the original case,
+ // the key is the lowercase flag name and the value is the original
+ private Hashtable user_flags = null;
+
+ /**
+ * Construct an empty Flags object.
+ */
+ public Flags() {
+ }
+
+
+ /**
+ * Construct a Flags object initialized with the given flags.
+ *
+ * @param flags the flags for initialization
+ */
+ @SuppressWarnings("unchecked")
+ public Flags(Flags flags) {
+ this.system_flags = flags.system_flags;
+ if (flags.user_flags != null)
+ this.user_flags = (Hashtable) flags.user_flags.clone();
+ }
+
+ /**
+ * Construct a Flags object initialized with the given system flag.
+ *
+ * @param flag the flag for initialization
+ */
+ public Flags(Flag flag) {
+ this.system_flags |= flag.bit;
+ }
+
+ /**
+ * Construct a Flags object initialized with the given user flag.
+ *
+ * @param flag the flag for initialization
+ */
+ public Flags(String flag) {
+ user_flags = new Hashtable<>(1);
+ user_flags.put(flag.toLowerCase(Locale.ENGLISH), flag);
+ }
+
+ /**
+ * Add the specified system flag to this Flags object.
+ *
+ * @param flag the flag to add
+ */
+ public void add(Flag flag) {
+ system_flags |= flag.bit;
+ }
+
+ /**
+ * Add the specified user flag to this Flags object.
+ *
+ * @param flag the flag to add
+ */
+ public void add(String flag) {
+ if (user_flags == null)
+ user_flags = new Hashtable<>(1);
+ user_flags.put(flag.toLowerCase(Locale.ENGLISH), flag);
+ }
+
+ /**
+ * Add all the flags in the given Flags object to this
+ * Flags object.
+ *
+ * @param f Flags object
+ */
+ public void add(Flags f) {
+ system_flags |= f.system_flags; // add system flags
+
+ if (f.user_flags != null) { // add user-defined flags
+ if (user_flags == null)
+ user_flags = new Hashtable<>(1);
+
+ Enumeration e = f.user_flags.keys();
+
+ while (e.hasMoreElements()) {
+ String s = e.nextElement();
+ user_flags.put(s, f.user_flags.get(s));
+ }
+ }
+ }
+
+ /**
+ * Remove the specified system flag from this Flags object.
+ *
+ * @param flag the flag to be removed
+ */
+ public void remove(Flag flag) {
+ system_flags &= ~flag.bit;
+ }
+
+ /**
+ * Remove the specified user flag from this Flags object.
+ *
+ * @param flag the flag to be removed
+ */
+ public void remove(String flag) {
+ if (user_flags != null)
+ user_flags.remove(flag.toLowerCase(Locale.ENGLISH));
+ }
+
+ /**
+ * Remove all flags in the given Flags object from this
+ * Flags object.
+ *
+ * @param f the flag to be removed
+ */
+ public void remove(Flags f) {
+ system_flags &= ~f.system_flags; // remove system flags
+
+ if (f.user_flags != null) {
+ if (user_flags == null)
+ return;
+
+ Enumeration e = f.user_flags.keys();
+ while (e.hasMoreElements())
+ user_flags.remove(e.nextElement());
+ }
+ }
+
+ /**
+ * Remove any flags not in the given Flags object.
+ * Useful for clearing flags not supported by a server. If the
+ * given Flags object includes the Flags.Flag.USER flag, all user
+ * flags in this Flags object are retained.
+ *
+ * @param f the flags to keep
+ * @return true if this Flags object changed
+ * @since JavaMail 1.6
+ */
+ public boolean retainAll(Flags f) {
+ boolean changed = false;
+ int sf = system_flags & f.system_flags;
+ if (system_flags != sf) {
+ system_flags = sf;
+ changed = true;
+ }
+
+ // if we have user flags, and the USER flag is not set in "f",
+ // determine which user flags to clear
+ if (user_flags != null && (f.system_flags & USER_BIT) == 0) {
+ if (f.user_flags != null) {
+ Enumeration e = user_flags.keys();
+ while (e.hasMoreElements()) {
+ String key = e.nextElement();
+ if (!f.user_flags.containsKey(key)) {
+ user_flags.remove(key);
+ changed = true;
+ }
+ }
+ } else {
+ // if anything in user_flags, throw them away
+ changed = user_flags.size() > 0;
+ user_flags = null;
+ }
+ }
+ return changed;
+ }
+
+ /**
+ * Check whether the specified system flag is present in this Flags object.
+ *
+ * @param flag the flag to test
+ * @return true of the given flag is present, otherwise false.
+ */
+ public boolean contains(Flag flag) {
+ return (system_flags & flag.bit) != 0;
+ }
+
+ /**
+ * Check whether the specified user flag is present in this Flags object.
+ *
+ * @param flag the flag to test
+ * @return true of the given flag is present, otherwise false.
+ */
+ public boolean contains(String flag) {
+ if (user_flags == null)
+ return false;
+ else
+ return user_flags.containsKey(flag.toLowerCase(Locale.ENGLISH));
+ }
+
+ /**
+ * Check whether all the flags in the specified Flags object are
+ * present in this Flags object.
+ *
+ * @param f the flags to test
+ * @return true if all flags in the given Flags object are present,
+ * otherwise false.
+ */
+ public boolean contains(Flags f) {
+ // Check system flags
+ if ((f.system_flags & system_flags) != f.system_flags)
+ return false;
+
+ // Check user flags
+ if (f.user_flags != null) {
+ if (user_flags == null)
+ return false;
+ Enumeration e = f.user_flags.keys();
+
+ while (e.hasMoreElements()) {
+ if (!user_flags.containsKey(e.nextElement()))
+ return false;
+ }
+ }
+
+ // If we've made it till here, return true
+ return true;
+ }
+
+ /**
+ * Check whether the two Flags objects are equal.
+ *
+ * @return true if they're equal
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Flags))
+ return false;
+
+ Flags f = (Flags) obj;
+
+ // Check system flags
+ if (f.system_flags != this.system_flags)
+ return false;
+
+ // Check user flags
+ int size = this.user_flags == null ? 0 : this.user_flags.size();
+ int fsize = f.user_flags == null ? 0 : f.user_flags.size();
+ if (size == 0 && fsize == 0)
+ return true;
+ if (f.user_flags != null && this.user_flags != null && fsize == size)
+ return user_flags.keySet().equals(f.user_flags.keySet());
+
+ return false;
+ }
+
+ /**
+ * Compute a hash code for this Flags object.
+ *
+ * @return the hash code
+ */
+ @Override
+ public int hashCode() {
+ int hash = system_flags;
+ if (user_flags != null) {
+ Enumeration e = user_flags.keys();
+ while (e.hasMoreElements())
+ hash += e.nextElement().hashCode();
+ }
+ return hash;
+ }
+
+ /**
+ * Return all the system flags in this Flags object. Returns
+ * an array of size zero if no flags are set.
+ *
+ * @return array of Flags.Flag objects representing system flags
+ */
+ public Flag[] getSystemFlags() {
+ Vector v = new Vector<>();
+ if ((system_flags & ANSWERED_BIT) != 0)
+ v.addElement(Flag.ANSWERED);
+ if ((system_flags & DELETED_BIT) != 0)
+ v.addElement(Flag.DELETED);
+ if ((system_flags & DRAFT_BIT) != 0)
+ v.addElement(Flag.DRAFT);
+ if ((system_flags & FLAGGED_BIT) != 0)
+ v.addElement(Flag.FLAGGED);
+ if ((system_flags & RECENT_BIT) != 0)
+ v.addElement(Flag.RECENT);
+ if ((system_flags & SEEN_BIT) != 0)
+ v.addElement(Flag.SEEN);
+ if ((system_flags & USER_BIT) != 0)
+ v.addElement(Flag.USER);
+
+ Flag[] f = new Flag[v.size()];
+ v.copyInto(f);
+ return f;
+ }
+
+ /**
+ * Return all the user flags in this Flags object. Returns
+ * an array of size zero if no flags are set.
+ *
+ * @return array of Strings, each String represents a flag.
+ */
+ public String[] getUserFlags() {
+ Vector v = new Vector<>();
+ if (user_flags != null) {
+ Enumeration e = user_flags.elements();
+
+ while (e.hasMoreElements())
+ v.addElement(e.nextElement());
+ }
+
+ String[] f = new String[v.size()];
+ v.copyInto(f);
+ return f;
+ }
+
+ /**
+ * Clear all of the system flags.
+ *
+ * @since JavaMail 1.6
+ */
+ public void clearSystemFlags() {
+ system_flags = 0;
+ }
+
+ /**
+ * Clear all of the user flags.
+ *
+ * @since JavaMail 1.6
+ */
+ public void clearUserFlags() {
+ user_flags = null;
+ }
+
+ /**
+ * Returns a clone of this Flags object.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object clone() {
+ Flags f = null;
+ try {
+ f = (Flags) super.clone();
+ } catch (CloneNotSupportedException cex) {
+ // ignore, can't happen
+ }
+ if (this.user_flags != null)
+ f.user_flags = (Hashtable) this.user_flags.clone();
+ return f;
+ }
+
+ /**
+ * Return a string representation of this Flags object.
+ * Note that the exact format of the string is subject to change.
+ */
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+
+ if ((system_flags & ANSWERED_BIT) != 0)
+ sb.append("\\Answered ");
+ if ((system_flags & DELETED_BIT) != 0)
+ sb.append("\\Deleted ");
+ if ((system_flags & DRAFT_BIT) != 0)
+ sb.append("\\Draft ");
+ if ((system_flags & FLAGGED_BIT) != 0)
+ sb.append("\\Flagged ");
+ if ((system_flags & RECENT_BIT) != 0)
+ sb.append("\\Recent ");
+ if ((system_flags & SEEN_BIT) != 0)
+ sb.append("\\Seen ");
+ if ((system_flags & USER_BIT) != 0)
+ sb.append("\\* ");
+
+ boolean first = true;
+ if (user_flags != null) {
+ Enumeration e = user_flags.elements();
+
+ while (e.hasMoreElements()) {
+ if (first)
+ first = false;
+ else
+ sb.append(' ');
+ sb.append(e.nextElement());
+ }
+ }
+
+ if (first && sb.length() > 0)
+ sb.setLength(sb.length() - 1); // smash trailing space
+
+ return sb.toString();
+ }
+
+ /**
+ * This inner class represents an individual system flag. A set
+ * of standard system flag objects are predefined here.
+ */
+ public static final class Flag {
+ /**
+ * This message has been answered. This flag is set by clients
+ * to indicate that this message has been answered to.
+ */
+ public static final Flag ANSWERED = new Flag(ANSWERED_BIT);
+
+ /**
+ * This message is marked deleted. Clients set this flag to
+ * mark a message as deleted. The expunge operation on a folder
+ * removes all messages in that folder that are marked for deletion.
+ */
+ public static final Flag DELETED = new Flag(DELETED_BIT);
+
+ /**
+ * This message is a draft. This flag is set by clients
+ * to indicate that the message is a draft message.
+ */
+ public static final Flag DRAFT = new Flag(DRAFT_BIT);
+
+ /**
+ * This message is flagged. No semantic is defined for this flag.
+ * Clients alter this flag.
+ */
+ public static final Flag FLAGGED = new Flag(FLAGGED_BIT);
+
+ /**
+ * This message is recent. Folder implementations set this flag
+ * to indicate that this message is new to this folder, that is,
+ * it has arrived since the last time this folder was opened.
+ *
+ * Clients cannot alter this flag.
+ */
+ public static final Flag RECENT = new Flag(RECENT_BIT);
+
+ /**
+ * This message is seen. This flag is implicitly set by the
+ * implementation when this Message's content is returned
+ * to the client in some form. The getInputStream
+ * and getContent methods on Message cause this
+ * flag to be set.
+ *
+ * Clients can alter this flag.
+ */
+ public static final Flag SEEN = new Flag(SEEN_BIT);
+
+ /**
+ * A special flag that indicates that this folder supports
+ * user defined flags.
+ *
+ * The implementation sets this flag. Clients cannot alter
+ * this flag but can use it to determine if a folder supports
+ * user defined flags by using
+ * folder.getPermanentFlags().contains(Flags.Flag.USER).
+ */
+ public static final Flag USER = new Flag(USER_BIT);
+
+ // flags are stored as bits for efficiency
+ private int bit;
+
+ private Flag(int bit) {
+ this.bit = bit;
+ }
+ }
+
+ /*
+ public static void main(String argv[]) throws Exception {
+ // a new flags object
+ Flags f1 = new Flags();
+ f1.add(Flags.Flag.DELETED);
+ f1.add(Flags.Flag.SEEN);
+ f1.add(Flags.Flag.RECENT);
+ f1.add(Flags.Flag.ANSWERED);
+
+ // check copy constructor with only system flags
+ Flags fc = new Flags(f1);
+ if (f1.equals(fc) && fc.equals(f1))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ // check clone with only system flags
+ fc = (Flags)f1.clone();
+ if (f1.equals(fc) && fc.equals(f1))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ // add a user flag and make sure it still works right
+ f1.add("MyFlag");
+
+ // shouldn't be equal here
+ if (!f1.equals(fc) && !fc.equals(f1))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ // check clone
+ fc = (Flags)f1.clone();
+ if (f1.equals(fc) && fc.equals(f1))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ // make sure user flag hash tables are separate
+ fc.add("AnotherFlag");
+ if (!f1.equals(fc) && !fc.equals(f1))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ // check copy constructor
+ fc = new Flags(f1);
+ if (f1.equals(fc) && fc.equals(f1))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ // another new flags object
+ Flags f2 = new Flags(Flags.Flag.ANSWERED);
+ f2.add("MyFlag");
+
+ if (f1.contains(Flags.Flag.DELETED))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ if (f1.contains(Flags.Flag.SEEN))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ if (f1.contains(Flags.Flag.RECENT))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ if (f1.contains("MyFlag"))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ if (f2.contains(Flags.Flag.ANSWERED))
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+
+ System.out.println("----------------");
+
+ String[] s = f1.getUserFlags();
+ for (int i = 0; i < s.length; i++)
+ System.out.println(s[i]);
+ System.out.println("----------------");
+ s = f2.getUserFlags();
+ for (int i = 0; i < s.length; i++)
+ System.out.println(s[i]);
+
+ System.out.println("----------------");
+
+ if (f1.contains(f2)) // this should be true
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ if (!f2.contains(f1)) // this should be false
+ System.out.println("success");
+ else
+ System.out.println("fail");
+
+ Flags f3 = new Flags();
+ f3.add(Flags.Flag.DELETED);
+ f3.add(Flags.Flag.SEEN);
+ f3.add(Flags.Flag.RECENT);
+ f3.add(Flags.Flag.ANSWERED);
+ f3.add("ANOTHERFLAG");
+ f3.add("MYFLAG");
+
+ f1.add("AnotherFlag");
+
+ if (f1.equals(f3))
+ System.out.println("equals success");
+ else
+ System.out.println("fail");
+ if (f3.equals(f1))
+ System.out.println("equals success");
+ else
+ System.out.println("fail");
+ System.out.println("f1 hash code " + f1.hashCode());
+ System.out.println("f3 hash code " + f3.hashCode());
+ if (f1.hashCode() == f3.hashCode())
+ System.out.println("success");
+ else
+ System.out.println("fail");
+ }
+ ****/
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Folder.java b/net-mail/src/main/java/jakarta/mail/Folder.java
new file mode 100644
index 0000000..64a5d72
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Folder.java
@@ -0,0 +1,1645 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.mail.event.ConnectionEvent;
+import jakarta.mail.event.ConnectionListener;
+import jakarta.mail.event.FolderEvent;
+import jakarta.mail.event.FolderListener;
+import jakarta.mail.event.MailEvent;
+import jakarta.mail.event.MessageChangedEvent;
+import jakarta.mail.event.MessageChangedListener;
+import jakarta.mail.event.MessageCountEvent;
+import jakarta.mail.event.MessageCountListener;
+import jakarta.mail.search.SearchTerm;
+import java.util.ArrayList;
+import java.util.EventListener;
+import java.util.List;
+import java.util.Vector;
+import java.util.concurrent.Executor;
+
+/**
+ * Folder is an abstract class that represents a folder for mail
+ * messages. Subclasses implement protocol specific Folders.
+ *
+ * Folders can contain Messages, other Folders or both, thus providing
+ * a tree-like hierarchy rooted at the Store's default folder. (Note
+ * that some Folder implementations may not allow both Messages and
+ * other Folders in the same Folder).
+ *
+ * The interpretation of folder names is implementation dependent.
+ * The different levels of hierarchy in a folder's full name
+ * are separated from each other by the hierarchy delimiter
+ * character.
+ *
+ * The case-insensitive full folder name (that is, the full name
+ * relative to the default folder for a Store) INBOX
+ * is reserved to mean the "primary folder for this user on this
+ * server". Not all Stores will provide an INBOX folder, and not
+ * all users will have an INBOX folder at all times. The name
+ * INBOX is reserved to refer to this folder,
+ * when it exists, in Stores that provide it.
+ *
+ * A Folder object obtained from a Store need not actually exist
+ * in the backend store. The exists method tests whether
+ * the folder exists or not. The create method
+ * creates a Folder.
+ *
+ * A Folder is initially in the closed state. Certain methods are valid
+ * in this state; the documentation for those methods note this. A
+ * Folder is opened by calling its 'open' method. All Folder methods,
+ * except open, delete and
+ * renameTo, are valid in this state.
+ *
+ * The only way to get a Folder is by invoking the
+ * getFolder method on Store, Folder, or Session, or by invoking
+ * the list or listSubscribed methods
+ * on Folder. Folder objects returned by the above methods are not
+ * cached by the Store. Thus, invoking the getFolder method
+ * with the same folder name multiple times will return distinct Folder
+ * objects. Likewise for the list and listSubscribed
+ * methods.
+ *
+ * The Message objects within the Folder are cached by the Folder.
+ * Thus, invoking getMessage(msgno) on the same message number
+ * multiple times will return the same Message object, until an
+ * expunge is done on this Folder.
+ *
+ * Message objects from a Folder are only valid while a Folder is open
+ * and should not be accessed after the Folder is closed, even if the
+ * Folder is subsequently reopened. Instead, new Message objects must
+ * be fetched from the Folder after the Folder is reopened.
+ *
+ * Note that a Message's message number can change within a
+ * session if the containing Folder is expunged using the expunge
+ * method. Clients that use message numbers as references to messages
+ * should be aware of this and should be prepared to deal with this
+ * situation (probably by flushing out existing message number references
+ * and reloading them). Because of this complexity, it is better for
+ * clients to use Message objects as references to messages, rather than
+ * message numbers. Expunged Message objects still have to be
+ * pruned, but other Message objects in that folder are not affected by the
+ * expunge.
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ */
+
+public abstract class Folder implements AutoCloseable {
+
+ /**
+ * This folder can contain messages
+ */
+ public final static int HOLDS_MESSAGES = 0x01;
+ /**
+ * This folder can contain other folders
+ */
+ public final static int HOLDS_FOLDERS = 0x02;
+ /**
+ * The Folder is read only. The state and contents of this
+ * folder cannot be modified.
+ */
+ public static final int READ_ONLY = 1;
+ /**
+ * The state and contents of this folder can be modified.
+ */
+ public static final int READ_WRITE = 2;
+ /*
+ * The queue of events to be delivered.
+ */
+ private final EventQueue q;
+ /**
+ * The parent store.
+ */
+ protected Store store;
+ /**
+ * The open mode of this folder. The open mode is
+ * Folder.READ_ONLY, Folder.READ_WRITE,
+ * or -1 if not known.
+ *
+ * @since JavaMail 1.1
+ */
+ protected int mode = -1;
+ // Vector of connection listeners.
+ private volatile Vector connectionListeners = null;
+ // Vector of folder listeners
+ private volatile Vector folderListeners = null;
+ // Vector of MessageCount listeners
+ private volatile Vector messageCountListeners = null;
+ // Vector of MessageChanged listeners.
+ private volatile Vector messageChangedListeners
+ = null;
+
+ /**
+ * Constructor that takes a Store object.
+ *
+ * @param store the Store that holds this folder
+ */
+ protected Folder(Store store) {
+ this.store = store;
+
+ // create or choose the appropriate event queue
+ Session session = store.getSession();
+ String scope =
+ session.getProperties().getProperty("mail.event.scope", "folder");
+ Executor executor =
+ (Executor) session.getProperties().get("mail.event.executor");
+ if (scope.equalsIgnoreCase("application"))
+ q = EventQueue.getApplicationEventQueue(executor);
+ else if (scope.equalsIgnoreCase("session"))
+ q = session.getEventQueue();
+ else if (scope.equalsIgnoreCase("store"))
+ q = store.getEventQueue();
+ else // if (scope.equalsIgnoreCase("folder"))
+ q = new EventQueue(executor);
+ }
+
+ /**
+ * Returns the name of this Folder.
+ *
+ * This method can be invoked on a closed Folder.
+ *
+ * @return name of the Folder
+ */
+ public abstract String getName();
+
+ /**
+ * Returns the full name of this Folder. If the folder resides under
+ * the root hierarchy of this Store, the returned name is relative
+ * to the root. Otherwise an absolute name, starting with the
+ * hierarchy delimiter, is returned.
+ *
+ * This method can be invoked on a closed Folder.
+ *
+ * @return full name of the Folder
+ */
+ public abstract String getFullName();
+
+ /**
+ * Return a URLName representing this folder. The returned URLName
+ * does not include the password used to access the store.
+ *
+ * @return the URLName representing this folder
+ * @throws MessagingException for failures
+ * @see URLName
+ * @since JavaMail 1.1
+ */
+ public URLName getURLName() throws MessagingException {
+ URLName storeURL = getStore().getURLName();
+ String fullname = getFullName();
+ StringBuilder encodedName = new StringBuilder();
+
+ if (fullname != null) {
+ /*
+ // We need to encode each of the folder's names.
+ char separator = getSeparator();
+ StringTokenizer tok = new StringTokenizer(
+ fullname, new Character(separator).toString(), true);
+
+ while (tok.hasMoreTokens()) {
+ String s = tok.nextToken();
+ if (s.charAt(0) == separator)
+ encodedName.append(separator);
+ else
+ // XXX - should encode, but since there's no decoder...
+ //encodedName.append(java.net.URLEncoder.encode(s));
+ encodedName.append(s);
+ }
+ */
+ // append the whole thing, until we can encode
+ encodedName.append(fullname);
+ }
+
+ /*
+ * Sure would be convenient if URLName had a
+ * constructor that took a base URLName.
+ */
+ return new URLName(storeURL.getProtocol(), storeURL.getHost(),
+ storeURL.getPort(), encodedName.toString(),
+ storeURL.getUsername(),
+ null /* no password */);
+ }
+
+ /**
+ * Returns the Store that owns this Folder object.
+ * This method can be invoked on a closed Folder.
+ *
+ * @return the Store
+ */
+ public Store getStore() {
+ return store;
+ }
+
+ /**
+ * Returns the parent folder of this folder.
+ * This method can be invoked on a closed Folder. If this folder
+ * is the top of a folder hierarchy, this method returns null.
+ *
+ * Note that since Folder objects are not cached, invoking this method
+ * returns a new distinct Folder object.
+ *
+ * @return Parent folder
+ * @throws MessagingException for failures
+ */
+ public abstract Folder getParent() throws MessagingException;
+
+ /**
+ * Tests if this folder physically exists on the Store.
+ * This method can be invoked on a closed Folder.
+ *
+ * @return true if the folder exists, otherwise false
+ * @throws MessagingException typically if the connection
+ * to the server is lost.
+ * @see #create
+ */
+ public abstract boolean exists() throws MessagingException;
+
+ /**
+ * Returns a list of Folders belonging to this Folder's namespace
+ * that match the specified pattern. Patterns may contain the wildcard
+ * characters "%", which matches any character except hierarchy
+ * delimiters, and "*", which matches any character.
+ * list("*") on "Personal" will return the whole
+ * hierarchy.
+ * list("%") on "Personal" will return "Finance" and
+ * "Jokes".
+ * list("Jokes") on "Personal" will return "Jokes".
+ * list("Stock*") on "Finance" will return "Stocks"
+ * and "StockOptions".
+ *
+ * Folder objects are not cached by the Store, so invoking this
+ * method on the same pattern multiple times will return that many
+ * distinct Folder objects.
+ *
+ * This method can be invoked on a closed Folder.
+ *
+ * @param pattern the match pattern
+ * @return array of matching Folder objects. An empty
+ * array is returned if no matching Folders exist.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MessagingException for other failures
+ * @see #listSubscribed
+ */
+ public abstract Folder[] list(String pattern) throws MessagingException;
+
+ /**
+ * Returns a list of subscribed Folders belonging to this Folder's
+ * namespace that match the specified pattern. If the folder does
+ * not support subscription, this method should resolve to
+ * list.
+ * (The default implementation provided here, does just this).
+ * The pattern can contain wildcards as for list.
+ *
+ * Note that, at a given level of the folder hierarchy, a particular
+ * folder may not be subscribed, but folders underneath that folder
+ * in the folder hierarchy may be subscribed. In order to allow
+ * walking the folder hierarchy, such unsubscribed folders may be
+ * returned, indicating that a folder lower in the hierarchy is
+ * subscribed. The isSubscribed method on a folder will
+ * tell whether any particular folder is actually subscribed.
+ *
+ * Folder objects are not cached by the Store, so invoking this
+ * method on the same pattern multiple times will return that many
+ * distinct Folder objects.
+ *
+ * This method can be invoked on a closed Folder.
+ *
+ * @param pattern the match pattern
+ * @return array of matching subscribed Folder objects. An
+ * empty array is returned if no matching
+ * subscribed folders exist.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MessagingException for other failures
+ * @see #list
+ */
+ public Folder[] listSubscribed(String pattern) throws MessagingException {
+ return list(pattern);
+ }
+
+ /**
+ * Convenience method that returns the list of folders under this
+ * Folder. This method just calls the list(String pattern)
+ * method with "%" as the match pattern. This method can
+ * be invoked on a closed Folder.
+ *
+ * @return array of Folder objects under this Folder. An
+ * empty array is returned if no subfolders exist.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MessagingException for other failures
+ * @see #list
+ */
+
+ public Folder[] list() throws MessagingException {
+ return list("%");
+ }
+
+ /**
+ * Convenience method that returns the list of subscribed folders
+ * under this Folder. This method just calls the
+ * listSubscribed(String pattern) method with "%"
+ * as the match pattern. This method can be invoked on a closed Folder.
+ *
+ * @return array of subscribed Folder objects under this
+ * Folder. An empty array is returned if no subscribed
+ * subfolders exist.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MessagingException for other failures
+ * @see #listSubscribed
+ */
+ public Folder[] listSubscribed() throws MessagingException {
+ return listSubscribed("%");
+ }
+
+ /**
+ * Return the delimiter character that separates this Folder's pathname
+ * from the names of immediate subfolders. This method can be invoked
+ * on a closed Folder.
+ *
+ * @return Hierarchy separator character
+ * @throws FolderNotFoundException if the implementation
+ * requires the folder to exist, but it does not
+ */
+ public abstract char getSeparator() throws MessagingException;
+
+ /**
+ * Returns the type of this Folder, that is, whether this folder can hold
+ * messages or subfolders or both. The returned value is an integer
+ * bitfield with the appropriate bits set. This method can be invoked
+ * on a closed folder.
+ *
+ * @return integer with appropriate bits set
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @see #HOLDS_MESSAGES
+ * @see #HOLDS_FOLDERS
+ */
+ public abstract int getType() throws MessagingException;
+
+ /**
+ * Create this folder on the Store. When this folder is created, any
+ * folders in its path that do not exist are also created.
+ *
+ * If the creation is successful, a CREATED FolderEvent is delivered
+ * to any FolderListeners registered on this Folder and this Store.
+ *
+ * @param type The type of this folder.
+ * @return true if the creation succeeds, else false.
+ * @throws MessagingException for failures
+ * @see #HOLDS_MESSAGES
+ * @see FolderEvent
+ * @see #HOLDS_FOLDERS
+ */
+ public abstract boolean create(int type) throws MessagingException;
+
+ /**
+ * Returns true if this Folder is subscribed.
+ *
+ * This method can be invoked on a closed Folder.
+ *
+ * The default implementation provided here just returns true.
+ *
+ * @return true if this Folder is subscribed
+ */
+ public boolean isSubscribed() {
+ return true;
+ }
+
+ /**
+ * Subscribe or unsubscribe this Folder. Not all Stores support
+ * subscription.
+ *
+ * This method can be invoked on a closed Folder.
+ *
+ * The implementation provided here just throws the
+ * MethodNotSupportedException.
+ *
+ * @param subscribe true to subscribe, false to unsubscribe
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MethodNotSupportedException if this store
+ * does not support subscription
+ * @throws MessagingException for other failures
+ */
+ public void setSubscribed(boolean subscribe)
+ throws MessagingException {
+ throw new MethodNotSupportedException();
+ }
+
+ /**
+ * Returns true if this Folder has new messages since the last time
+ * this indication was reset. When this indication is set or reset
+ * depends on the Folder implementation (and in the case of IMAP,
+ * depends on the server). This method can be used to implement
+ * a lightweight "check for new mail" operation on a Folder without
+ * opening it. (For example, a thread that monitors a mailbox and
+ * flags when it has new mail.) This method should indicate whether
+ * any messages in the Folder have the RECENT flag set.
+ *
+ * Note that this is not an incremental check for new mail, i.e.,
+ * it cannot be used to determine whether any new messages have
+ * arrived since the last time this method was invoked. To
+ * implement incremental checks, the Folder needs to be opened.
+ *
+ * This method can be invoked on a closed Folder that can contain
+ * Messages.
+ *
+ * @return true if the Store has new Messages
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MessagingException for other failures
+ */
+ public abstract boolean hasNewMessages() throws MessagingException;
+
+ /**
+ * Return the Folder object corresponding to the given name. Note that
+ * this folder does not physically have to exist in the Store. The
+ * exists() method on a Folder indicates whether it really
+ * exists on the Store.
+ *
+ * In some Stores, name can be an absolute path if it starts with the
+ * hierarchy delimiter. Otherwise, it is interpreted relative to
+ * this Folder.
+ *
+ * Folder objects are not cached by the Store, so invoking this
+ * method on the same name multiple times will return that many
+ * distinct Folder objects.
+ *
+ * This method can be invoked on a closed Folder.
+ *
+ * @param name name of the Folder
+ * @return Folder object
+ * @throws MessagingException for failures
+ */
+ public abstract Folder getFolder(String name)
+ throws MessagingException;
+
+ /**
+ * Delete this Folder. This method will succeed only on a closed
+ * Folder.
+ *
+ * The recurse flag controls whether the deletion affects
+ * subfolders or not. If true, all subfolders are deleted, then this
+ * folder itself is deleted. If false, the behaviour is dependent on
+ * the folder type and is elaborated below:
+ *
+ *
+ *
+ * The folder can contain only messages: (type == HOLDS_MESSAGES).
+ *
+ * All messages within the folder are removed. The folder
+ * itself is then removed. An appropriate FolderEvent is generated by
+ * the Store and this folder.
+ *
+ *
+ * The folder can contain only subfolders: (type == HOLDS_FOLDERS).
+ *
+ * If this folder is empty (does not contain any
+ * subfolders at all), it is removed. An appropriate FolderEvent is
+ * generated by the Store and this folder.
+ * If this folder contains any subfolders, the delete fails
+ * and returns false.
+ *
+ *
+ * The folder can contain subfolders as well as messages:
+ * If the folder is empty (no messages or subfolders), it
+ * is removed. If the folder contains no subfolders, but only messages,
+ * then all messages are removed. The folder itself is then removed.
+ * In both the above cases, an appropriate FolderEvent is
+ * generated by the Store and this folder.
+ *
+ * If the folder contains subfolders there are 3 possible
+ * choices an implementation is free to do:
+ *
+ *
+ *
The operation fails, irrespective of whether this folder
+ * contains messages or not. Some implementations might elect to go
+ * with this simple approach. The delete() method returns false.
+ *
+ *
Any messages within the folder are removed. Subfolders
+ * are not removed. The folder itself is not removed or affected
+ * in any manner. The delete() method returns true. And the
+ * exists() method on this folder will return true indicating that
+ * this folder still exists.
+ * An appropriate FolderEvent is generated by the Store and this folder.
+ *
+ *
Any messages within the folder are removed. Subfolders are
+ * not removed. The folder itself changes its type from
+ * HOLDS_FOLDERS | HOLDS_MESSAGES to HOLDS_FOLDERS. Thus new
+ * messages cannot be added to this folder, but new subfolders can
+ * be created underneath. The delete() method returns true indicating
+ * success. The exists() method on this folder will return true
+ * indicating that this folder still exists.
+ * An appropriate FolderEvent is generated by the Store and this folder.
+ *
+ *
+ *
+ * @param recurse also delete subfolders?
+ * @return true if the Folder is deleted successfully
+ * @throws FolderNotFoundException if this folder does
+ * not exist
+ * @throws IllegalStateException if this folder is not in
+ * the closed state.
+ * @throws MessagingException for other failures
+ * @see FolderEvent
+ */
+ public abstract boolean delete(boolean recurse)
+ throws MessagingException;
+
+ /**
+ * Rename this Folder. This method will succeed only on a closed
+ * Folder.
+ *
+ * If the rename is successful, a RENAMED FolderEvent is delivered
+ * to FolderListeners registered on this folder and its containing
+ * Store.
+ *
+ * @param f a folder representing the new name for this Folder
+ * @return true if the Folder is renamed successfully
+ * @throws FolderNotFoundException if this folder does
+ * not exist
+ * @throws IllegalStateException if this folder is not in
+ * the closed state.
+ * @throws MessagingException for other failures
+ * @see FolderEvent
+ */
+ public abstract boolean renameTo(Folder f) throws MessagingException;
+
+ /**
+ * Open this Folder. This method is valid only on Folders that
+ * can contain Messages and that are closed.
+ *
+ * If this folder is opened successfully, an OPENED ConnectionEvent
+ * is delivered to any ConnectionListeners registered on this
+ * Folder.
+ *
+ * The effect of opening multiple connections to the same folder
+ * on a specifc Store is implementation dependent. Some implementations
+ * allow multiple readers, but only one writer. Others allow
+ * multiple writers as well as readers.
+ *
+ * @param mode open the Folder READ_ONLY or READ_WRITE
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws IllegalStateException if this folder is not in
+ * the closed state.
+ * @throws MessagingException for other failures
+ * @see #READ_ONLY
+ * @see #READ_WRITE
+ * @see #getType()
+ * @see ConnectionEvent
+ */
+ public abstract void open(int mode) throws MessagingException;
+
+ /**
+ * Close this Folder. This method is valid only on open Folders.
+ *
+ * A CLOSED ConnectionEvent is delivered to any ConnectionListeners
+ * registered on this Folder. Note that the folder is closed even
+ * if this method terminates abnormally by throwing a
+ * MessagingException.
+ *
+ * @param expunge expunges all deleted messages if this flag is true
+ * @throws IllegalStateException if this folder is not opened
+ * @throws MessagingException for other failures
+ * @see ConnectionEvent
+ */
+ public abstract void close(boolean expunge) throws MessagingException;
+
+ /**
+ * Close this Folder and expunge deleted messages.
+ *
+ * A CLOSED ConnectionEvent is delivered to any ConnectionListeners
+ * registered on this Folder. Note that the folder is closed even
+ * if this method terminates abnormally by throwing a
+ * MessagingException.
+ *
+ * This method supports the {@link AutoCloseable AutoCloseable}
+ * interface.
+ *
+ * This implementation calls close(true).
+ *
+ * @throws IllegalStateException if this folder is not opened
+ * @throws MessagingException for other failures
+ * @see ConnectionEvent
+ * @since JavaMail 1.6
+ */
+ @Override
+ public void close() throws MessagingException {
+ close(true);
+ }
+
+ /**
+ * Indicates whether this Folder is in the 'open' state.
+ *
+ * @return true if this Folder is in the 'open' state.
+ */
+ public abstract boolean isOpen();
+
+ /**
+ * Return the open mode of this folder. Returns
+ * Folder.READ_ONLY, Folder.READ_WRITE,
+ * or -1 if the open mode is not known (usually only because an older
+ * Folder provider has not been updated to use this new
+ * method).
+ *
+ * @return the open mode of this folder
+ * @throws IllegalStateException if this folder is not opened
+ * @since JavaMail 1.1
+ */
+ public synchronized int getMode() {
+ if (!isOpen())
+ throw new IllegalStateException("Folder not open");
+ return mode;
+ }
+
+ /**
+ * Get the permanent flags supported by this Folder. Returns a Flags
+ * object that contains all the flags supported.
+ *
+ * The special flag Flags.Flag.USER indicates that this Folder
+ * supports arbitrary user-defined flags.
+ *
+ * The supported permanent flags for a folder may not be available
+ * until the folder is opened.
+ *
+ * @return permanent flags, or null if not known
+ */
+ public abstract Flags getPermanentFlags();
+
+ /**
+ * Get total number of messages in this Folder.
+ *
+ * This method can be invoked on a closed folder. However, note
+ * that for some folder implementations, getting the total message
+ * count can be an expensive operation involving actually opening
+ * the folder. In such cases, a provider can choose not to support
+ * this functionality in the closed state, in which case this method
+ * must return -1.
+ *
+ * Clients invoking this method on a closed folder must be aware
+ * that this is a potentially expensive operation. Clients must
+ * also be prepared to handle a return value of -1 in this case.
+ *
+ * @return total number of messages. -1 may be returned
+ * by certain implementations if this method is
+ * invoked on a closed folder.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MessagingException for other failures
+ */
+ public abstract int getMessageCount() throws MessagingException;
+
+ /**
+ * Get the number of new messages in this Folder.
+ *
+ * This method can be invoked on a closed folder. However, note
+ * that for some folder implementations, getting the new message
+ * count can be an expensive operation involving actually opening
+ * the folder. In such cases, a provider can choose not to support
+ * this functionality in the closed state, in which case this method
+ * must return -1.
+ *
+ * Clients invoking this method on a closed folder must be aware
+ * that this is a potentially expensive operation. Clients must
+ * also be prepared to handle a return value of -1 in this case.
+ *
+ * This implementation returns -1 if this folder is closed. Else
+ * this implementation gets each Message in the folder using
+ * getMessage(int) and checks whether its
+ * RECENT flag is set. The total number of messages
+ * that have this flag set is returned.
+ *
+ * @return number of new messages. -1 may be returned
+ * by certain implementations if this method is
+ * invoked on a closed folder.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MessagingException for other failures
+ */
+ public synchronized int getNewMessageCount()
+ throws MessagingException {
+ if (!isOpen())
+ return -1;
+
+ int newmsgs = 0;
+ int total = getMessageCount();
+ for (int i = 1; i <= total; i++) {
+ try {
+ if (getMessage(i).isSet(Flags.Flag.RECENT))
+ newmsgs++;
+ } catch (MessageRemovedException me) {
+ // This is an expunged message, ignore it.
+ continue;
+ }
+ }
+ return newmsgs;
+ }
+
+ /**
+ * Get the total number of unread messages in this Folder.
+ *
+ * This method can be invoked on a closed folder. However, note
+ * that for some folder implementations, getting the unread message
+ * count can be an expensive operation involving actually opening
+ * the folder. In such cases, a provider can choose not to support
+ * this functionality in the closed state, in which case this method
+ * must return -1.
+ *
+ * Clients invoking this method on a closed folder must be aware
+ * that this is a potentially expensive operation. Clients must
+ * also be prepared to handle a return value of -1 in this case.
+ *
+ * This implementation returns -1 if this folder is closed. Else
+ * this implementation gets each Message in the folder using
+ * getMessage(int) and checks whether its
+ * SEEN flag is set. The total number of messages
+ * that do not have this flag set is returned.
+ *
+ * @return total number of unread messages. -1 may be returned
+ * by certain implementations if this method is
+ * invoked on a closed folder.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MessagingException for other failures
+ */
+ public synchronized int getUnreadMessageCount()
+ throws MessagingException {
+ if (!isOpen())
+ return -1;
+
+ int unread = 0;
+ int total = getMessageCount();
+ for (int i = 1; i <= total; i++) {
+ try {
+ if (!getMessage(i).isSet(Flags.Flag.SEEN))
+ unread++;
+ } catch (MessageRemovedException me) {
+ // This is an expunged message, ignore it.
+ continue;
+ }
+ }
+ return unread;
+ }
+
+ /**
+ * Get the number of deleted messages in this Folder.
+ *
+ * This method can be invoked on a closed folder. However, note
+ * that for some folder implementations, getting the deleted message
+ * count can be an expensive operation involving actually opening
+ * the folder. In such cases, a provider can choose not to support
+ * this functionality in the closed state, in which case this method
+ * must return -1.
+ *
+ * Clients invoking this method on a closed folder must be aware
+ * that this is a potentially expensive operation. Clients must
+ * also be prepared to handle a return value of -1 in this case.
+ *
+ * This implementation returns -1 if this folder is closed. Else
+ * this implementation gets each Message in the folder using
+ * getMessage(int) and checks whether its
+ * DELETED flag is set. The total number of messages
+ * that have this flag set is returned.
+ *
+ * @return number of deleted messages. -1 may be returned
+ * by certain implementations if this method is
+ * invoked on a closed folder.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws MessagingException for other failures
+ * @since JavaMail 1.3
+ */
+ public synchronized int getDeletedMessageCount() throws MessagingException {
+ if (!isOpen())
+ return -1;
+
+ int deleted = 0;
+ int total = getMessageCount();
+ for (int i = 1; i <= total; i++) {
+ try {
+ if (getMessage(i).isSet(Flags.Flag.DELETED))
+ deleted++;
+ } catch (MessageRemovedException me) {
+ // This is an expunged message, ignore it.
+ continue;
+ }
+ }
+ return deleted;
+ }
+
+ /**
+ * Get the Message object corresponding to the given message
+ * number. A Message object's message number is the relative
+ * position of this Message in its Folder. Messages are numbered
+ * starting at 1 through the total number of message in the folder.
+ * Note that the message number for a particular Message can change
+ * during a session if other messages in the Folder are deleted and
+ * the Folder is expunged.
+ *
+ * Message objects are light-weight references to the actual message
+ * that get filled up on demand. Hence Folder implementations are
+ * expected to provide light-weight Message objects.
+ *
+ * Unlike Folder objects, repeated calls to getMessage with the
+ * same message number will return the same Message object, as
+ * long as no messages in this folder have been expunged.
+ *
+ * Since message numbers can change within a session if the folder
+ * is expunged , clients are advised not to use message numbers as
+ * references to messages. Use Message objects instead.
+ *
+ * @param msgnum the message number
+ * @return the Message object
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws IllegalStateException if this folder is not opened
+ * @throws IndexOutOfBoundsException if the message number
+ * is out of range.
+ * @throws MessagingException for other failures
+ * @see #getMessageCount
+ * @see #fetch
+ */
+ public abstract Message getMessage(int msgnum)
+ throws MessagingException;
+
+ /**
+ * Get the Message objects for message numbers ranging from start
+ * through end, both start and end inclusive. Note that message
+ * numbers start at 1, not 0.
+ *
+ * Message objects are light-weight references to the actual message
+ * that get filled up on demand. Hence Folder implementations are
+ * expected to provide light-weight Message objects.
+ *
+ * This implementation uses getMessage(index) to obtain the required
+ * Message objects. Note that the returned array must contain
+ * (end-start+1) Message objects.
+ *
+ * @param start the number of the first message
+ * @param end the number of the last message
+ * @return the Message objects
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws IllegalStateException if this folder is not opened.
+ * @throws IndexOutOfBoundsException if the start or end
+ * message numbers are out of range.
+ * @throws MessagingException for other failures
+ * @see #fetch
+ */
+ public synchronized Message[] getMessages(int start, int end)
+ throws MessagingException {
+ Message[] msgs = new Message[end - start + 1];
+ for (int i = start; i <= end; i++)
+ msgs[i - start] = getMessage(i);
+ return msgs;
+ }
+
+ /**
+ * Get the Message objects for message numbers specified in
+ * the array.
+ *
+ * Message objects are light-weight references to the actual message
+ * that get filled up on demand. Hence Folder implementations are
+ * expected to provide light-weight Message objects.
+ *
+ * This implementation uses getMessage(index) to obtain the required
+ * Message objects. Note that the returned array must contain
+ * msgnums.length Message objects
+ *
+ * @param msgnums the array of message numbers
+ * @return the array of Message objects.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws IllegalStateException if this folder is not opened.
+ * @throws IndexOutOfBoundsException if any message number
+ * in the given array is out of range.
+ * @throws MessagingException for other failures
+ * @see #fetch
+ */
+ public synchronized Message[] getMessages(int[] msgnums)
+ throws MessagingException {
+ int len = msgnums.length;
+ Message[] msgs = new Message[len];
+ for (int i = 0; i < len; i++)
+ msgs[i] = getMessage(msgnums[i]);
+ return msgs;
+ }
+
+ /**
+ * Get all Message objects from this Folder. Returns an empty array
+ * if the folder is empty.
+ *
+ * Clients can use Message objects (instead of sequence numbers)
+ * as references to the messages within a folder; this method supplies
+ * the Message objects to the client. Folder implementations are
+ * expected to provide light-weight Message objects, which get
+ * filled on demand.
+ *
+ * This implementation invokes getMessageCount() to get
+ * the current message count and then uses getMessage()
+ * to get Message objects from 1 till the message count.
+ *
+ * @return array of Message objects, empty array if folder
+ * is empty.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws IllegalStateException if this folder is not opened.
+ * @throws MessagingException for other failures
+ * @see #fetch
+ */
+ public synchronized Message[] getMessages() throws MessagingException {
+ if (!isOpen()) // otherwise getMessageCount might return -1
+ throw new IllegalStateException("Folder not open");
+ int total = getMessageCount();
+ Message[] msgs = new Message[total];
+ for (int i = 1; i <= total; i++)
+ msgs[i - 1] = getMessage(i);
+ return msgs;
+ }
+
+ /**
+ * Append given Messages to this folder. This method can be
+ * invoked on a closed Folder. An appropriate MessageCountEvent
+ * is delivered to any MessageCountListener registered on this
+ * folder when the messages arrive in the folder.
+ *
+ * Folder implementations must not abort this operation if a
+ * Message in the given message array turns out to be an
+ * expunged Message.
+ *
+ * @param msgs array of Messages to be appended
+ * @throws MessagingException if the append failed.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ */
+ public abstract void appendMessages(Message[] msgs)
+ throws MessagingException;
+
+ /**
+ * Prefetch the items specified in the FetchProfile for the
+ * given Messages.
+ *
+ * Clients use this method to indicate that the specified items are
+ * needed en-masse for the given message range. Implementations are
+ * expected to retrieve these items for the given message range in
+ * a efficient manner. Note that this method is just a hint to the
+ * implementation to prefetch the desired items.
+ *
+ * An example is a client filling its header-view window with
+ * the Subject, From and X-mailer headers for all messages in the
+ * folder.
+ *
+ * The implementation provided here just returns without
+ * doing anything useful. Providers wanting to provide a real
+ * implementation for this method should override this method.
+ *
+ * @param msgs fetch items for these messages
+ * @param fp the FetchProfile
+ * @throws IllegalStateException if this folder is not opened
+ * @throws MessagingException for other failures
+ */
+ public void fetch(Message[] msgs, FetchProfile fp)
+ throws MessagingException {
+ return;
+ }
+
+ /**
+ * Set the specified flags on the messages specified in the array.
+ * This will result in appropriate MessageChangedEvents being
+ * delivered to any MessageChangedListener registered on this
+ * Message's containing folder.
+ *
+ * Note that the specified Message objects must
+ * belong to this folder. Certain Folder implementations can
+ * optimize the operation of setting Flags for a group of messages,
+ * so clients might want to use this method, rather than invoking
+ * Message.setFlags for each Message.
+ *
+ * This implementation degenerates to invoking setFlags()
+ * on each Message object. Specific Folder implementations that can
+ * optimize this case should do so.
+ * Also, an implementation must not abort the operation if a Message
+ * in the array turns out to be an expunged Message.
+ *
+ * @param msgs the array of message objects
+ * @param flag Flags object containing the flags to be set
+ * @param value set the flags to this boolean value
+ * @throws IllegalStateException if this folder is not opened
+ * or if it has been opened READ_ONLY.
+ * @throws MessagingException for other failures
+ * @see Message#setFlags
+ * @see MessageChangedEvent
+ */
+ public synchronized void setFlags(Message[] msgs,
+ Flags flag, boolean value) throws MessagingException {
+ for (int i = 0; i < msgs.length; i++) {
+ try {
+ msgs[i].setFlags(flag, value);
+ } catch (MessageRemovedException me) {
+ // This message is expunged, skip
+ }
+ }
+ }
+
+ /**
+ * Set the specified flags on the messages numbered from start
+ * through end, both start and end inclusive. Note that message
+ * numbers start at 1, not 0.
+ * This will result in appropriate MessageChangedEvents being
+ * delivered to any MessageChangedListener registered on this
+ * Message's containing folder.
+ *
+ * Certain Folder implementations can
+ * optimize the operation of setting Flags for a group of messages,
+ * so clients might want to use this method, rather than invoking
+ * Message.setFlags for each Message.
+ *
+ * The default implementation uses getMessage(int) to
+ * get each Message object and then invokes
+ * setFlags on that object to set the flags.
+ * Specific Folder implementations that can optimize this case should do so.
+ * Also, an implementation must not abort the operation if a message
+ * number refers to an expunged message.
+ *
+ * @param start the number of the first message
+ * @param end the number of the last message
+ * @param flag Flags object containing the flags to be set
+ * @param value set the flags to this boolean value
+ * @throws IllegalStateException if this folder is not opened
+ * or if it has been opened READ_ONLY.
+ * @throws IndexOutOfBoundsException if the start or end
+ * message numbers are out of range.
+ * @throws MessagingException for other failures
+ * @see Message#setFlags
+ * @see MessageChangedEvent
+ */
+ public synchronized void setFlags(int start, int end,
+ Flags flag, boolean value) throws MessagingException {
+ for (int i = start; i <= end; i++) {
+ try {
+ Message msg = getMessage(i);
+ msg.setFlags(flag, value);
+ } catch (MessageRemovedException me) {
+ // This message is expunged, skip
+ }
+ }
+ }
+
+ /**
+ * Set the specified flags on the messages whose message numbers
+ * are in the array.
+ * This will result in appropriate MessageChangedEvents being
+ * delivered to any MessageChangedListener registered on this
+ * Message's containing folder.
+ *
+ * Certain Folder implementations can
+ * optimize the operation of setting Flags for a group of messages,
+ * so clients might want to use this method, rather than invoking
+ * Message.setFlags for each Message.
+ *
+ * The default implementation uses getMessage(int) to
+ * get each Message object and then invokes
+ * setFlags on that object to set the flags.
+ * Specific Folder implementations that can optimize this case should do so.
+ * Also, an implementation must not abort the operation if a message
+ * number refers to an expunged message.
+ *
+ * @param msgnums the array of message numbers
+ * @param flag Flags object containing the flags to be set
+ * @param value set the flags to this boolean value
+ * @throws IllegalStateException if this folder is not opened
+ * or if it has been opened READ_ONLY.
+ * @throws IndexOutOfBoundsException if any message number
+ * in the given array is out of range.
+ * @throws MessagingException for other failures
+ * @see Message#setFlags
+ * @see MessageChangedEvent
+ */
+ public synchronized void setFlags(int[] msgnums,
+ Flags flag, boolean value) throws MessagingException {
+ for (int i = 0; i < msgnums.length; i++) {
+ try {
+ Message msg = getMessage(msgnums[i]);
+ msg.setFlags(flag, value);
+ } catch (MessageRemovedException me) {
+ // This message is expunged, skip
+ }
+ }
+ }
+
+ /*
+ * The set of listeners are stored in Vectors appropriate to their
+ * type. We mark all listener Vectors as "volatile" because, while
+ * we initialize them inside this folder's synchronization lock,
+ * they are accessed (checked for null) in the "notify" methods,
+ * which can't be synchronized due to lock ordering constraints.
+ * Since the listener fields (the handles on the Vector objects)
+ * are only ever set, and are never cleared, we believe this is
+ * safe. The code that dispatches the notifications will either
+ * see the null and assume there are no listeners or will see the
+ * Vector and will process the listeners. There's an inherent race
+ * between adding a listener and notifying the listeners; the lack
+ * of synchronization during notification does not make the race
+ * condition significantly worse. If one thread is setting a
+ * listener at the "same" time an event is being dispatched, the
+ * dispatch code might not see the listener right away. The
+ * dispatch code doesn't have to worry about the Vector handle
+ * being set to null, and thus using an out-of-date set of
+ * listeners, because we never set the field to null.
+ */
+
+ /**
+ * Copy the specified Messages from this Folder into another
+ * Folder. This operation appends these Messages to the
+ * destination Folder. The destination Folder does not have to
+ * be opened. An appropriate MessageCountEvent
+ * is delivered to any MessageCountListener registered on the
+ * destination folder when the messages arrive in the folder.
+ *
+ * Note that the specified Message objects must
+ * belong to this folder. Folder implementations might be able
+ * to optimize this method by doing server-side copies.
+ *
+ * This implementation just invokes appendMessages()
+ * on the destination folder to append the given Messages. Specific
+ * folder implementations that support server-side copies should
+ * do so, if the destination folder's Store is the same as this
+ * folder's Store.
+ * Also, an implementation must not abort the operation if a
+ * Message in the array turns out to be an expunged Message.
+ *
+ * @param msgs the array of message objects
+ * @param folder the folder to copy the messages to
+ * @throws FolderNotFoundException if the destination
+ * folder does not exist.
+ * @throws IllegalStateException if this folder is not opened.
+ * @throws MessagingException for other failures
+ * @see #appendMessages
+ */
+ public void copyMessages(Message[] msgs, Folder folder)
+ throws MessagingException {
+ if (!folder.exists())
+ throw new FolderNotFoundException(
+ folder.getFullName() + " does not exist",
+ folder);
+
+ folder.appendMessages(msgs);
+ }
+
+ /**
+ * Expunge (permanently remove) messages marked DELETED. Returns an
+ * array containing the expunged message objects. The
+ * getMessageNumber method
+ * on each of these message objects returns that Message's original
+ * (that is, prior to the expunge) sequence number. A MessageCountEvent
+ * containing the expunged messages is delivered to any
+ * MessageCountListeners registered on the folder.
+ *
+ * Expunge causes the renumbering of Message objects subsequent to
+ * the expunged messages. Clients that use message numbers as
+ * references to messages should be aware of this and should be
+ * prepared to deal with the situation (probably by flushing out
+ * existing message number caches and reloading them). Because of
+ * this complexity, it is better for clients to use Message objects
+ * as references to messages, rather than message numbers. Any
+ * expunged Messages objects still have to be pruned, but other
+ * Messages in that folder are not affected by the expunge.
+ *
+ * After a message is expunged, only the isExpunged and
+ * getMessageNumber methods are still valid on the
+ * corresponding Message object; other methods may throw
+ * MessageRemovedException
+ *
+ * @return array of expunged Message objects
+ * @throws FolderNotFoundException if this folder does not
+ * exist
+ * @throws IllegalStateException if this folder is not opened.
+ * @throws MessagingException for other failures
+ * @see Message#isExpunged
+ * @see MessageCountEvent
+ */
+ public abstract Message[] expunge() throws MessagingException;
+
+ /**
+ * Search this Folder for messages matching the specified
+ * search criterion. Returns an array containing the matching
+ * messages . Returns an empty array if no matches were found.
+ *
+ * This implementation invokes
+ * search(term, getMessages()), to apply the search
+ * over all the messages in this folder. Providers that can implement
+ * server-side searching might want to override this method to provide
+ * a more efficient implementation.
+ *
+ * @param term the search criterion
+ * @return array of matching messages
+ * @throws jakarta.mail.search.SearchException if the search
+ * term is too complex for the implementation to handle.
+ * @throws FolderNotFoundException if this folder does
+ * not exist.
+ * @throws IllegalStateException if this folder is not opened.
+ * @throws MessagingException for other failures
+ * @see SearchTerm
+ */
+ public Message[] search(SearchTerm term) throws MessagingException {
+ return search(term, getMessages());
+ }
+
+ /**
+ * Search the given array of messages for those that match the
+ * specified search criterion. Returns an array containing the
+ * matching messages. Returns an empty array if no matches were
+ * found.
+ *
+ * Note that the specified Message objects must
+ * belong to this folder.
+ *
+ * This implementation iterates through the given array of messages,
+ * and applies the search criterion on each message by calling
+ * its match() method with the given term. The
+ * messages that succeed in the match are returned. Providers
+ * that can implement server-side searching might want to override
+ * this method to provide a more efficient implementation. If the
+ * search term is too complex or contains user-defined terms that
+ * cannot be executed on the server, providers may elect to either
+ * throw a SearchException or degenerate to client-side searching by
+ * calling super.search() to invoke this implementation.
+ *
+ * @param term the search criterion
+ * @param msgs the messages to be searched
+ * @return array of matching messages
+ * @throws jakarta.mail.search.SearchException if the search
+ * term is too complex for the implementation to handle.
+ * @throws IllegalStateException if this folder is not opened
+ * @throws MessagingException for other failures
+ * @see SearchTerm
+ */
+ public Message[] search(SearchTerm term, Message[] msgs)
+ throws MessagingException {
+ List matchedMsgs = new ArrayList<>();
+
+ // Run thru the given messages
+ for (Message msg : msgs) {
+ try {
+ if (msg.match(term)) // matched
+ matchedMsgs.add(msg); // add it
+ } catch (MessageRemovedException mrex) {
+ }
+ }
+
+ return matchedMsgs.toArray(new Message[0]);
+ }
+
+ /**
+ * Add a listener for Connection events on this Folder.
+ *
+ * The implementation provided here adds this listener
+ * to an internal list of ConnectionListeners.
+ *
+ * @param l the Listener for Connection events
+ * @see ConnectionEvent
+ */
+ public synchronized void
+ addConnectionListener(ConnectionListener l) {
+ if (connectionListeners == null)
+ connectionListeners = new Vector<>();
+ connectionListeners.addElement(l);
+ }
+
+ /**
+ * Remove a Connection event listener.
+ *
+ * The implementation provided here removes this listener
+ * from the internal list of ConnectionListeners.
+ *
+ * @param l the listener
+ * @see #addConnectionListener
+ */
+ public synchronized void
+ removeConnectionListener(ConnectionListener l) {
+ if (connectionListeners != null)
+ connectionListeners.removeElement(l);
+ }
+
+ /**
+ * Notify all ConnectionListeners. Folder implementations are
+ * expected to use this method to broadcast connection events.
+ *
+ * The provided implementation queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the registered
+ * ConnectionListeners. Note that the event dispatching occurs
+ * in a separate thread, thus avoiding potential deadlock problems.
+ *
+ * @param type the ConnectionEvent type
+ * @see ConnectionEvent
+ */
+ protected void notifyConnectionListeners(int type) {
+ if (connectionListeners != null) {
+ ConnectionEvent e = new ConnectionEvent(this, type);
+ queueEvent(e, connectionListeners);
+ }
+
+ /* Fix for broken JDK1.1.x Garbage collector :
+ * The 'conservative' GC in JDK1.1.x occasionally fails to
+ * garbage-collect Threads which are in the wait state.
+ * This would result in thread (and consequently memory) leaks.
+ *
+ * We attempt to fix this by sending a 'terminator' event
+ * to the queue, after we've sent the CLOSED event. The
+ * terminator event causes the event-dispatching thread to
+ * self destruct.
+ */
+ if (type == ConnectionEvent.CLOSED)
+ q.terminateQueue();
+ }
+
+ /**
+ * Add a listener for Folder events on this Folder.
+ *
+ * The implementation provided here adds this listener
+ * to an internal list of FolderListeners.
+ *
+ * @param l the Listener for Folder events
+ * @see FolderEvent
+ */
+ public synchronized void addFolderListener(FolderListener l) {
+ if (folderListeners == null)
+ folderListeners = new Vector<>();
+ folderListeners.addElement(l);
+ }
+
+ /**
+ * Remove a Folder event listener.
+ *
+ * The implementation provided here removes this listener
+ * from the internal list of FolderListeners.
+ *
+ * @param l the listener
+ * @see #addFolderListener
+ */
+ public synchronized void removeFolderListener(FolderListener l) {
+ if (folderListeners != null)
+ folderListeners.removeElement(l);
+ }
+
+ /**
+ * Notify all FolderListeners registered on this Folder and
+ * this folder's Store. Folder implementations are expected
+ * to use this method to broadcast Folder events.
+ *
+ * The implementation provided here queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the
+ * FolderListeners registered on this folder. The implementation
+ * also invokes notifyFolderListeners on this folder's
+ * Store to notify any FolderListeners registered on the store.
+ *
+ * @param type type of FolderEvent
+ * @see #notifyFolderRenamedListeners
+ */
+ protected void notifyFolderListeners(int type) {
+ if (folderListeners != null) {
+ FolderEvent e = new FolderEvent(this, this, type);
+ queueEvent(e, folderListeners);
+ }
+ store.notifyFolderListeners(type, this);
+ }
+
+ /**
+ * Notify all FolderListeners registered on this Folder and
+ * this folder's Store about the renaming of this folder.
+ * Folder implementations are expected to use this method to
+ * broadcast Folder events indicating the renaming of folders.
+ *
+ * The implementation provided here queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the
+ * FolderListeners registered on this folder. The implementation
+ * also invokes notifyFolderRenamedListeners on this
+ * folder's Store to notify any FolderListeners registered on the store.
+ *
+ * @param folder Folder representing the new name.
+ * @see #notifyFolderListeners
+ * @since JavaMail 1.1
+ */
+ protected void notifyFolderRenamedListeners(Folder folder) {
+ if (folderListeners != null) {
+ FolderEvent e = new FolderEvent(this, this, folder,
+ FolderEvent.RENAMED);
+ queueEvent(e, folderListeners);
+ }
+ store.notifyFolderRenamedListeners(this, folder);
+ }
+
+ /**
+ * Add a listener for MessageCount events on this Folder.
+ *
+ * The implementation provided here adds this listener
+ * to an internal list of MessageCountListeners.
+ *
+ * @param l the Listener for MessageCount events
+ * @see MessageCountEvent
+ */
+ public synchronized void addMessageCountListener(MessageCountListener l) {
+ if (messageCountListeners == null)
+ messageCountListeners = new Vector<>();
+ messageCountListeners.addElement(l);
+ }
+
+ /**
+ * Remove a MessageCount listener.
+ *
+ * The implementation provided here removes this listener
+ * from the internal list of MessageCountListeners.
+ *
+ * @param l the listener
+ * @see #addMessageCountListener
+ */
+ public synchronized void
+ removeMessageCountListener(MessageCountListener l) {
+ if (messageCountListeners != null)
+ messageCountListeners.removeElement(l);
+ }
+
+ /**
+ * Notify all MessageCountListeners about the addition of messages
+ * into this folder. Folder implementations are expected to use this
+ * method to broadcast MessageCount events for indicating arrival of
+ * new messages.
+ *
+ * The provided implementation queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the registered
+ * MessageCountListeners. Note that the event dispatching occurs
+ * in a separate thread, thus avoiding potential deadlock problems.
+ *
+ * @param msgs the messages that were added
+ */
+ protected void notifyMessageAddedListeners(Message[] msgs) {
+ if (messageCountListeners == null)
+ return;
+
+ MessageCountEvent e = new MessageCountEvent(
+ this,
+ MessageCountEvent.ADDED,
+ false,
+ msgs);
+
+ queueEvent(e, messageCountListeners);
+ }
+
+ /**
+ * Notify all MessageCountListeners about the removal of messages
+ * from this Folder. Folder implementations are expected to use this
+ * method to broadcast MessageCount events indicating removal of
+ * messages.
+ *
+ * The provided implementation queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the registered
+ * MessageCountListeners. Note that the event dispatching occurs
+ * in a separate thread, thus avoiding potential deadlock problems.
+ *
+ * @param removed was the message removed by this client?
+ * @param msgs the messages that were removed
+ */
+ protected void notifyMessageRemovedListeners(boolean removed,
+ Message[] msgs) {
+ if (messageCountListeners == null)
+ return;
+
+ MessageCountEvent e = new MessageCountEvent(
+ this,
+ MessageCountEvent.REMOVED,
+ removed,
+ msgs);
+ queueEvent(e, messageCountListeners);
+ }
+
+ /**
+ * Add a listener for MessageChanged events on this Folder.
+ *
+ * The implementation provided here adds this listener
+ * to an internal list of MessageChangedListeners.
+ *
+ * @param l the Listener for MessageChanged events
+ * @see MessageChangedEvent
+ */
+ public synchronized void addMessageChangedListener(MessageChangedListener l) {
+ if (messageChangedListeners == null)
+ messageChangedListeners = new Vector<>();
+ messageChangedListeners.addElement(l);
+ }
+
+ /**
+ * Remove a MessageChanged listener.
+ *
+ * The implementation provided here removes this listener
+ * from the internal list of MessageChangedListeners.
+ *
+ * @param l the listener
+ * @see #addMessageChangedListener
+ */
+ public synchronized void removeMessageChangedListener(MessageChangedListener l) {
+ if (messageChangedListeners != null)
+ messageChangedListeners.removeElement(l);
+ }
+
+ /**
+ * Notify all MessageChangedListeners. Folder implementations are
+ * expected to use this method to broadcast MessageChanged events.
+ *
+ * The provided implementation queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to registered
+ * MessageChangedListeners. Note that the event dispatching occurs
+ * in a separate thread, thus avoiding potential deadlock problems.
+ *
+ * @param type the MessageChangedEvent type
+ * @param msg the message that changed
+ */
+ protected void notifyMessageChangedListeners(int type, Message msg) {
+ if (messageChangedListeners == null)
+ return;
+
+ MessageChangedEvent e = new MessageChangedEvent(this, type, msg);
+ queueEvent(e, messageChangedListeners);
+ }
+
+ /*
+ * Add the event and vector of listeners to the queue to be delivered.
+ */
+ private void queueEvent(MailEvent event,
+ Vector extends EventListener> vector) {
+ /*
+ * Copy the vector in order to freeze the state of the set
+ * of EventListeners the event should be delivered to prior
+ * to delivery. This ensures that any changes made to the
+ * Vector from a target listener's method during the delivery
+ * of this event will not take effect until after the event is
+ * delivered.
+ */
+ @SuppressWarnings("unchecked")
+ Vector extends EventListener> v = (Vector extends EventListener>) vector.clone();
+ q.enqueue(event, v);
+ }
+
+ /**
+ * override the default toString(), it will return the String
+ * from Folder.getFullName() or if that is null, it will use
+ * the default toString() behavior.
+ */
+
+ @Override
+ public String toString() {
+ String s = getFullName();
+ if (s != null)
+ return s;
+ else
+ return super.toString();
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/FolderClosedException.java b/net-mail/src/main/java/jakarta/mail/FolderClosedException.java
new file mode 100644
index 0000000..89c458f
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/FolderClosedException.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * This exception is thrown when a method is invoked on a Messaging object
+ * and the Folder that owns that object has died due to some reason.
+ *
+ * Following the exception, the Folder is reset to the "closed" state.
+ * All messaging objects owned by the Folder should be considered invalid.
+ * The Folder can be reopened using the "open" method to reestablish the
+ * lost connection.
+ *
+ * The getMessage() method returns more detailed information about the
+ * error that caused this exception.
+ *
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class FolderClosedException extends MessagingException {
+ transient private Folder folder;
+
+ /**
+ * Constructs a FolderClosedException.
+ *
+ * @param folder The Folder
+ */
+ public FolderClosedException(Folder folder) {
+ this(folder, null);
+ }
+
+ /**
+ * Constructs a FolderClosedException with the specified
+ * detail message.
+ *
+ * @param folder The Folder
+ * @param message The detailed error message
+ */
+ public FolderClosedException(Folder folder, String message) {
+ super(message);
+ this.folder = folder;
+ }
+
+ /**
+ * Constructs a FolderClosedException with the specified
+ * detail message and embedded exception. The exception is chained
+ * to this exception.
+ *
+ * @param folder The Folder
+ * @param message The detailed error message
+ * @param e The embedded exception
+ * @since JavaMail 1.5
+ */
+ public FolderClosedException(Folder folder, String message, Exception e) {
+ super(message, e);
+ this.folder = folder;
+ }
+
+ /**
+ * Returns the dead Folder object
+ *
+ * @return the dead Folder object
+ */
+ public Folder getFolder() {
+ return folder;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/FolderNotFoundException.java b/net-mail/src/main/java/jakarta/mail/FolderNotFoundException.java
new file mode 100644
index 0000000..a2880e4
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/FolderNotFoundException.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * This exception is thrown by Folder methods, when those
+ * methods are invoked on a non existent folder.
+ *
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class FolderNotFoundException extends MessagingException {
+ transient private Folder folder;
+
+ /**
+ * Constructs a FolderNotFoundException with no detail message.
+ */
+ public FolderNotFoundException() {
+ super();
+ }
+
+ /**
+ * Constructs a FolderNotFoundException.
+ *
+ * @param folder The Folder
+ * @since JavaMail 1.2
+ */
+ public FolderNotFoundException(Folder folder) {
+ super();
+ this.folder = folder;
+ }
+
+ /**
+ * Constructs a FolderNotFoundException with the specified
+ * detail message.
+ *
+ * @param folder The Folder
+ * @param s The detailed error message
+ * @since JavaMail 1.2
+ */
+ public FolderNotFoundException(Folder folder, String s) {
+ super(s);
+ this.folder = folder;
+ }
+
+ /**
+ * Constructs a FolderNotFoundException with the specified
+ * detail message and embedded exception. The exception is chained
+ * to this exception.
+ *
+ * @param folder The Folder
+ * @param s The detailed error message
+ * @param e The embedded exception
+ * @since JavaMail 1.5
+ */
+ public FolderNotFoundException(Folder folder, String s, Exception e) {
+ super(s, e);
+ this.folder = folder;
+ }
+
+ /**
+ * Constructs a FolderNotFoundException with the specified detail message
+ * and the specified folder.
+ *
+ * @param s The detail message
+ * @param folder The Folder
+ */
+ public FolderNotFoundException(String s, Folder folder) {
+ super(s);
+ this.folder = folder;
+ }
+
+ /**
+ * Returns the offending Folder object.
+ *
+ * @return the Folder object. Note that the returned value can be
+ * null.
+ */
+ public Folder getFolder() {
+ return folder;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Header.java b/net-mail/src/main/java/jakarta/mail/Header.java
new file mode 100644
index 0000000..cddca3c
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Header.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import java.util.Objects;
+
+/**
+ * The Header class stores a name/value pair to represent headers.
+ *
+ * @author John Mani
+ */
+
+public class Header {
+
+ /**
+ * The name of the header.
+ *
+ * @since JavaMail 1.4
+ */
+ protected String name;
+
+ /**
+ * The value of the header.
+ *
+ * @since JavaMail 1.4
+ */
+ protected String value;
+
+ /**
+ * Construct a Header object.
+ *
+ * @param name name of the header
+ * @param value value of the header
+ */
+ public Header(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ /**
+ * Returns the name of this header.
+ *
+ * @return name of the header
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the value of this header.
+ *
+ * @return value of the header
+ */
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (obj == null) {
+ return false;
+ } else if (getClass() != obj.getClass()) {
+ return false;
+ } else {
+ Header other = (Header) obj;
+ return Objects.equals(name, other.getName()) && Objects.equals(value, other.getValue());
+ }
+ }
+
+}
diff --git a/net-mail/src/main/java/jakarta/mail/IllegalWriteException.java b/net-mail/src/main/java/jakarta/mail/IllegalWriteException.java
new file mode 100644
index 0000000..ff452ec
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/IllegalWriteException.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+
+/**
+ * The exception thrown when a write is attempted on a read-only attribute
+ * of any Messaging object.
+ *
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class IllegalWriteException extends MessagingException {
+
+ /**
+ * Constructs an IllegalWriteException with no detail message.
+ */
+ public IllegalWriteException() {
+ super();
+ }
+
+ /**
+ * Constructs an IllegalWriteException with the specified
+ * detail message.
+ *
+ * @param s The detailed error message
+ */
+ public IllegalWriteException(String s) {
+ super(s);
+ }
+
+ /**
+ * Constructs an IllegalWriteException with the specified
+ * detail message and embedded exception. The exception is chained
+ * to this exception.
+ *
+ * @param s The detailed error message
+ * @param e The embedded exception
+ * @since JavaMail 1.5
+ */
+ public IllegalWriteException(String s, Exception e) {
+ super(s, e);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/MailLogger.java b/net-mail/src/main/java/jakarta/mail/MailLogger.java
new file mode 100644
index 0000000..07b20d5
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/MailLogger.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import java.io.PrintStream;
+import java.text.MessageFormat;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A simplified logger used by Jakarta Mail to handle logging to a
+ * PrintStream and logging through a java.util.logging.Logger.
+ * If debug is set, messages are written to the PrintStream and
+ * prefixed by the specified prefix (which is not included in
+ * Logger messages).
+ * Messages are logged by the Logger based on the configuration
+ * of the logging system.
+ */
+
+/*
+ * It would be so much simpler to just subclass Logger and override
+ * the log(LogRecord) method, as the javadocs suggest, but that doesn't
+ * work because Logger makes the decision about whether to log the message
+ * or not before calling the log(LogRecord) method. Instead, we just
+ * provide the few log methods we need here.
+ */
+
+final class MailLogger {
+ /**
+ * For log messages.
+ */
+ private final Logger logger;
+ /**
+ * For debug output.
+ */
+ private final String prefix;
+ /**
+ * Produce debug output?
+ */
+ private final boolean debug;
+ /**
+ * Stream for debug output.
+ */
+ private final PrintStream out;
+
+ /**
+ * Construct a new MailLogger using the specified Logger name,
+ * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream.
+ *
+ * @param name the Logger name
+ * @param prefix the prefix for debug output, or null for none
+ * @param debug if true, write to PrintStream
+ * @param out the PrintStream to write to
+ */
+ public MailLogger(String name, String prefix, boolean debug,
+ PrintStream out) {
+ logger = Logger.getLogger(name);
+ this.prefix = prefix;
+ this.debug = debug;
+ this.out = out != null ? out : System.out;
+ }
+
+ /**
+ * Construct a new MailLogger using the specified class' package
+ * name as the Logger name,
+ * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream.
+ *
+ * @param clazz the Logger name is the package name of this class
+ * @param prefix the prefix for debug output, or null for none
+ * @param debug if true, write to PrintStream
+ * @param out the PrintStream to write to
+ */
+ public MailLogger(Class> clazz, String prefix, boolean debug,
+ PrintStream out) {
+ String name = packageOf(clazz);
+ logger = Logger.getLogger(name);
+ this.prefix = prefix;
+ this.debug = debug;
+ this.out = out != null ? out : System.out;
+ }
+
+ /**
+ * Construct a new MailLogger using the specified class' package
+ * name combined with the specified subname as the Logger name,
+ * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream.
+ *
+ * @param clazz the Logger name is the package name of this class
+ * @param subname the Logger name relative to this Logger name
+ * @param prefix the prefix for debug output, or null for none
+ * @param debug if true, write to PrintStream
+ * @param out the PrintStream to write to
+ */
+ public MailLogger(Class> clazz, String subname, String prefix, boolean debug,
+ PrintStream out) {
+ String name = packageOf(clazz) + "." + subname;
+ logger = Logger.getLogger(name);
+ this.prefix = prefix;
+ this.debug = debug;
+ this.out = out != null ? out : System.out;
+ }
+
+ /**
+ * Construct a new MailLogger using the specified Logger name and
+ * debug prefix (e.g., "DEBUG"). Get the debug flag and PrintStream
+ * from the Session.
+ *
+ * @param name the Logger name
+ * @param prefix the prefix for debug output, or null for none
+ * @param session where to get the debug flag and PrintStream
+ */
+ @Deprecated
+ public MailLogger(String name, String prefix, Session session) {
+ this(name, prefix, session.getDebug(), session.getDebugOut());
+ }
+
+ /**
+ * Construct a new MailLogger using the specified class' package
+ * name as the Logger name and the specified
+ * debug prefix (e.g., "DEBUG"). Get the debug flag and PrintStream
+ * from the Session.
+ *
+ * @param clazz the Logger name is the package name of this class
+ * @param prefix the prefix for debug output, or null for none
+ * @param session where to get the debug flag and PrintStream
+ */
+ @Deprecated
+ public MailLogger(Class> clazz, String prefix, Session session) {
+ this(clazz, prefix, session.getDebug(), session.getDebugOut());
+ }
+
+ /**
+ * Create a MailLogger that uses a Logger with the specified name
+ * and prefix. The new MailLogger uses the same debug flag and
+ * PrintStream as this MailLogger.
+ *
+ * @param name the Logger name
+ * @param prefix the prefix for debug output, or null for none
+ * @return a MailLogger for the given name and prefix.
+ */
+ public MailLogger getLogger(String name, String prefix) {
+ return new MailLogger(name, prefix, debug, out);
+ }
+
+ /**
+ * Create a MailLogger using the specified class' package
+ * name as the Logger name and the specified prefix.
+ * The new MailLogger uses the same debug flag and
+ * PrintStream as this MailLogger.
+ *
+ * @param clazz the Logger name is the package name of this class
+ * @param prefix the prefix for debug output, or null for none
+ * @return a MailLogger for the given name and prefix.
+ */
+ public MailLogger getLogger(Class> clazz, String prefix) {
+ return new MailLogger(clazz, prefix, debug, out);
+ }
+
+ /**
+ * Create a MailLogger that uses a Logger whose name is composed
+ * of this MailLogger's name plus the specified sub-name, separated
+ * by a dot. The new MailLogger uses the new prefix for debug output.
+ * This is used primarily by the protocol trace code that wants a
+ * different prefix (none).
+ *
+ * @param subname the Logger name relative to this Logger name
+ * @param prefix the prefix for debug output, or null for none
+ * @return a MailLogger for the given name and prefix.
+ */
+ public MailLogger getSubLogger(String subname, String prefix) {
+ return new MailLogger(logger.getName() + "." + subname, prefix,
+ debug, out);
+ }
+
+ /**
+ * Create a MailLogger that uses a Logger whose name is composed
+ * of this MailLogger's name plus the specified sub-name, separated
+ * by a dot. The new MailLogger uses the new prefix for debug output.
+ * This is used primarily by the protocol trace code that wants a
+ * different prefix (none).
+ *
+ * @param subname the Logger name relative to this Logger name
+ * @param prefix the prefix for debug output, or null for none
+ * @param debug the debug flag for the sub-logger
+ * @return a MailLogger for the given name and prefix.
+ */
+ public MailLogger getSubLogger(String subname, String prefix,
+ boolean debug) {
+ return new MailLogger(logger.getName() + "." + subname, prefix,
+ debug, out);
+ }
+
+ /**
+ * Log the message at the specified level.
+ *
+ * @param level the log level.
+ * @param msg the message.
+ */
+ public void log(Level level, String msg) {
+ ifDebugOut(msg);
+ if (logger.isLoggable(level)) {
+ final StackTraceElement frame = inferCaller();
+ logger.logp(level, frame.getClassName(), frame.getMethodName(), msg);
+ }
+ }
+
+ /**
+ * Log the message at the specified level.
+ *
+ * @param level the log level.
+ * @param msg the message.
+ * @param param1 the additional parameter.
+ */
+ public void log(Level level, String msg, Object param1) {
+ if (debug) {
+ msg = MessageFormat.format(msg, param1);
+ debugOut(msg);
+ }
+
+ if (logger.isLoggable(level)) {
+ final StackTraceElement frame = inferCaller();
+ logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, param1);
+ }
+ }
+
+ /**
+ * Log the message at the specified level.
+ *
+ * @param level the log level.
+ * @param msg the message.
+ * @param params the message parameters.
+ */
+ public void log(Level level, String msg, Object... params) {
+ if (debug) {
+ msg = MessageFormat.format(msg, params);
+ debugOut(msg);
+ }
+
+ if (logger.isLoggable(level)) {
+ final StackTraceElement frame = inferCaller();
+ logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, params);
+ }
+ }
+
+ /**
+ * Log the message at the specified level using a format string.
+ *
+ * @param level the log level.
+ * @param msg the message format string.
+ * @param params the message parameters.
+ * @since JavaMail 1.5.4
+ */
+ public void logf(Level level, String msg, Object... params) {
+ msg = String.format(msg, params);
+ ifDebugOut(msg);
+ logger.log(level, msg);
+ }
+
+ /**
+ * Log the message at the specified level.
+ *
+ * @param level the log level.
+ * @param msg the message.
+ * @param thrown the throwable to log.
+ */
+ public void log(Level level, String msg, Throwable thrown) {
+ if (debug) {
+ if (thrown != null) {
+ debugOut(msg + ", THROW: ");
+ thrown.printStackTrace(out);
+ } else {
+ debugOut(msg);
+ }
+ }
+
+ if (logger.isLoggable(level)) {
+ final StackTraceElement frame = inferCaller();
+ logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, thrown);
+ }
+ }
+
+ /**
+ * Log a message at the CONFIG level.
+ *
+ * @param msg the message.
+ */
+ public void config(String msg) {
+ log(Level.CONFIG, msg);
+ }
+
+ /**
+ * Log a message at the FINE level.
+ *
+ * @param msg the message.
+ */
+ public void fine(String msg) {
+ log(Level.FINE, msg);
+ }
+
+ /**
+ * Log a message at the FINER level.
+ *
+ * @param msg the message.
+ */
+ public void finer(String msg) {
+ log(Level.FINER, msg);
+ }
+
+ /**
+ * Log a message at the FINEST level.
+ *
+ * @param msg the message.
+ */
+ public void finest(String msg) {
+ log(Level.FINEST, msg);
+ }
+
+ /**
+ * If "debug" is set, or our embedded Logger is loggable at the
+ * given level, return true.
+ *
+ * @param level the log level.
+ * @return true if loggable.
+ */
+ public boolean isLoggable(Level level) {
+ return debug || logger.isLoggable(level);
+ }
+
+ /**
+ * Common code to conditionally log debug statements.
+ *
+ * @param msg the message to log.
+ */
+ private void ifDebugOut(String msg) {
+ if (debug)
+ debugOut(msg);
+ }
+
+ /**
+ * Common formatting for debug output.
+ *
+ * @param msg the message to log.
+ */
+ private void debugOut(String msg) {
+ if (prefix != null)
+ out.println(prefix + ": " + msg);
+ else
+ out.println(msg);
+ }
+
+ /**
+ * Return the package name of the class.
+ * Sometimes there will be no Package object for the class,
+ * e.g., if the class loader hasn't created one (see Class.getPackage()).
+ *
+ * @param clazz the class source.
+ * @return the package name or an empty string.
+ */
+ private String packageOf(Class> clazz) {
+ Package p = clazz.getPackage();
+ if (p != null)
+ return p.getName(); // hopefully the common case
+ String cname = clazz.getName();
+ int i = cname.lastIndexOf('.');
+ if (i > 0)
+ return cname.substring(0, i);
+ // no package name, now what?
+ return "";
+ }
+
+ /**
+ * A disadvantage of not being able to use Logger directly in Jakarta Mail
+ * code is that the "source class" information that Logger guesses will
+ * always refer to this class instead of our caller. This method
+ * duplicates what Logger does to try to find *our* caller, so that
+ * Logger doesn't have to do it (and get the wrong answer), and because
+ * our caller is what's wanted.
+ *
+ * @return StackTraceElement that logged the message. Treat as read-only.
+ */
+ private StackTraceElement inferCaller() {
+ // Get the stack trace.
+ StackTraceElement[] stack = (new Throwable()).getStackTrace();
+ // First, search back to a method in the Logger class.
+ int ix = 0;
+ while (ix < stack.length) {
+ StackTraceElement frame = stack[ix];
+ String cname = frame.getClassName();
+ if (isLoggerImplFrame(cname)) {
+ break;
+ }
+ ix++;
+ }
+ // Now search for the first frame before the "Logger" class.
+ while (ix < stack.length) {
+ StackTraceElement frame = stack[ix];
+ String cname = frame.getClassName();
+ if (!isLoggerImplFrame(cname)) {
+ // We've found the relevant frame.
+ return frame;
+ }
+ ix++;
+ }
+ // We haven't found a suitable frame, so just punt. This is
+ // OK as we are only committed to making a "best effort" here.
+ return new StackTraceElement(MailLogger.class.getName(), "log",
+ MailLogger.class.getName(), -1);
+ }
+
+ /**
+ * Frames to ignore as part of the MailLogger to JUL bridge.
+ *
+ * @param cname the class name.
+ * @return true if the class name is part of the MailLogger bridge.
+ */
+ private boolean isLoggerImplFrame(String cname) {
+ return MailLogger.class.getName().equals(cname);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/MailSessionDefinition.java b/net-mail/src/main/java/jakarta/mail/MailSessionDefinition.java
new file mode 100644
index 0000000..2ead5ec
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/MailSessionDefinition.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used by Jakarta EE applications to define a MailSession
+ * to be registered with JNDI. The MailSession may be configured
+ * by setting the annotation elements for commonly used Session
+ * properties. Additional standard and vendor-specific properties may be
+ * specified using the properties element.
+ *
+ * The session will be registered under the name specified in the
+ * name element. It may be defined to be in any valid
+ * Jakarta EE namespace, and will determine the accessibility of
+ * the session from other components.
+ *
+ * @since JavaMail 1.5
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Repeatable(MailSessionDefinitions.class)
+public @interface MailSessionDefinition {
+
+ /**
+ * Description of this mail session.
+ *
+ * @return the description
+ */
+ String description() default "";
+
+ /**
+ * JNDI name by which the mail session will be registered.
+ *
+ * @return the JNDI name
+ */
+ String name();
+
+ /**
+ * Store protocol name.
+ *
+ * @return the store protocol name
+ */
+ String storeProtocol() default "";
+
+ /**
+ * Transport protocol name.
+ *
+ * @return the transport protocol name
+ */
+ String transportProtocol() default "";
+
+ /**
+ * Host name for the mail server.
+ *
+ * @return the host name
+ */
+ String host() default "";
+
+ /**
+ * User name to use for authentication.
+ *
+ * @return the user name
+ */
+ String user() default "";
+
+ /**
+ * Password to use for authentication.
+ *
+ * @return the password
+ */
+ String password() default "";
+
+ /**
+ * From address for the user.
+ *
+ * @return the from address
+ */
+ String from() default "";
+
+ /**
+ * Properties to include in the Session.
+ * Properties are specified using the format:
+ * propertyName=propertyValue with one property per array element.
+ *
+ * @return the properties
+ */
+ String[] properties() default {};
+}
diff --git a/net-mail/src/main/java/jakarta/mail/MailSessionDefinitions.java b/net-mail/src/main/java/jakarta/mail/MailSessionDefinitions.java
new file mode 100644
index 0000000..cae5653
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/MailSessionDefinitions.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares one or more MailSessionDefinition annotations.
+ *
+ * @see MailSessionDefinition
+ * @since JavaMail 1.5
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MailSessionDefinitions {
+ MailSessionDefinition[] value();
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Message.java b/net-mail/src/main/java/jakarta/mail/Message.java
new file mode 100644
index 0000000..13d1ebb
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Message.java
@@ -0,0 +1,678 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.mail.search.SearchTerm;
+import java.util.Date;
+
+/**
+ * This class models an email message. This is an abstract class.
+ * Subclasses provide actual implementations.
+ *
+ * Message implements the Part interface. Message contains a set of
+ * attributes and a "content". Messages within a folder also have a
+ * set of flags that describe its state within the folder.
+ *
+ * Message defines some new attributes in addition to those defined
+ * in the Part interface. These attributes specify meta-data
+ * for the message - i.e., addressing and descriptive information about
+ * the message.
+ *
+ * Message objects are obtained either from a Folder or by constructing
+ * a new Message object of the appropriate subclass. Messages that have
+ * been received are normally retrieved from a folder named "INBOX".
+ *
+ * A Message object obtained from a folder is just a lightweight
+ * reference to the actual message. The Message is 'lazily' filled
+ * up (on demand) when each item is requested from the message. Note
+ * that certain folder implementations may return Message objects that
+ * are pre-filled with certain user-specified items.
+ *
+ * To send a message, an appropriate subclass of Message (e.g.,
+ * MimeMessage) is instantiated, the attributes and content are
+ * filled in, and the message is sent using the Transport.send
+ * method.
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ * @author Max Spivak
+ * @see Part
+ */
+
+public abstract class Message implements Part {
+
+ /**
+ * The number of this message within its folder, or zero if
+ * the message was not retrieved from a folder.
+ */
+ protected int msgnum = 0;
+
+ /**
+ * True if this message has been expunged.
+ */
+ protected boolean expunged = false;
+
+ /**
+ * The containing folder, if this message is obtained from a folder
+ */
+ protected Folder folder = null;
+
+ /**
+ * The Session object for this Message
+ */
+ protected Session session = null;
+
+ /**
+ * No-arg version of the constructor.
+ */
+ protected Message() {
+ }
+
+ /**
+ * Constructor that takes a Folder and a message number.
+ * Used by Folder implementations.
+ *
+ * @param folder containing folder
+ * @param msgnum this message's sequence number within this folder
+ */
+ protected Message(Folder folder, int msgnum) {
+ this.folder = folder;
+ this.msgnum = msgnum;
+ session = folder.store.session;
+ }
+
+ /**
+ * Constructor that takes a Session. Used for client created
+ * Message objects.
+ *
+ * @param session A Session object
+ */
+ protected Message(Session session) {
+ this.session = session;
+ }
+
+ /**
+ * Return the Session used when this message was created.
+ *
+ * @return the message's Session
+ * @since JavaMail 1.5
+ */
+ public Session getSession() {
+ return session;
+ }
+
+ /**
+ * Returns the "From" attribute. The "From" attribute contains
+ * the identity of the person(s) who wished this message to
+ * be sent.
+ *
+ * In certain implementations, this may be different
+ * from the entity that actually sent the message.
+ *
+ * This method returns null if this attribute
+ * is not present in this message. Returns an empty array if
+ * this attribute is present, but contains no addresses.
+ *
+ * @return array of Address objects
+ * @throws MessagingException for failures
+ */
+ public abstract Address[] getFrom() throws MessagingException;
+
+ /**
+ * Set the "From" attribute in this Message.
+ *
+ * @param address the sender
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MessagingException for other failures
+ */
+ public abstract void setFrom(Address address)
+ throws MessagingException;
+
+ /**
+ * Set the "From" attribute in this Message. The value of this
+ * attribute is obtained from the property "mail.user". If this
+ * property is absent, the system property "user.name" is used.
+ *
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MessagingException for other failures
+ */
+ public abstract void setFrom() throws MessagingException;
+
+ /**
+ * Add these addresses to the existing "From" attribute
+ *
+ * @param addresses the senders
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MessagingException for other failures
+ */
+ public abstract void addFrom(Address[] addresses)
+ throws MessagingException;
+
+ /**
+ * Get all the recipient addresses of the given type.
+ *
+ * This method returns null if no recipients of
+ * the given type are present in this message. It may return an
+ * empty array if the header is present, but contains no addresses.
+ *
+ * @param type the recipient type
+ * @return array of Address objects
+ * @throws MessagingException for failures
+ * @see RecipientType#TO
+ * @see RecipientType#CC
+ * @see RecipientType#BCC
+ */
+ public abstract Address[] getRecipients(RecipientType type)
+ throws MessagingException;
+
+ /**
+ * Get all the recipient addresses for the message.
+ * The default implementation extracts the TO, CC, and BCC
+ * recipients using the getRecipients method.
+ *
+ * This method returns null if none of the recipient
+ * headers are present in this message. It may Return an empty array
+ * if any recipient header is present, but contains no addresses.
+ *
+ * @return array of Address objects
+ * @throws MessagingException for failures
+ * @see RecipientType#TO
+ * @see RecipientType#CC
+ * @see RecipientType#BCC
+ * @see #getRecipients
+ */
+ public Address[] getAllRecipients() throws MessagingException {
+ Address[] to = getRecipients(RecipientType.TO);
+ Address[] cc = getRecipients(RecipientType.CC);
+ Address[] bcc = getRecipients(RecipientType.BCC);
+
+ if (cc == null && bcc == null)
+ return to; // a common case
+
+ int numRecip =
+ (to != null ? to.length : 0) +
+ (cc != null ? cc.length : 0) +
+ (bcc != null ? bcc.length : 0);
+ Address[] addresses = new Address[numRecip];
+ int pos = 0;
+ if (to != null) {
+ System.arraycopy(to, 0, addresses, pos, to.length);
+ pos += to.length;
+ }
+ if (cc != null) {
+ System.arraycopy(cc, 0, addresses, pos, cc.length);
+ pos += cc.length;
+ }
+ if (bcc != null) {
+ System.arraycopy(bcc, 0, addresses, pos, bcc.length);
+ // pos += bcc.length;
+ }
+ return addresses;
+ }
+
+ /**
+ * Set the recipient addresses. All addresses of the specified
+ * type are replaced by the addresses parameter.
+ *
+ * @param type the recipient type
+ * @param addresses the addresses
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MessagingException for other failures
+ */
+ public abstract void setRecipients(RecipientType type, Address[] addresses)
+ throws MessagingException;
+
+ /**
+ * Set the recipient address. All addresses of the specified
+ * type are replaced by the address parameter.
+ *
+ * The default implementation uses the setRecipients method.
+ *
+ * @param type the recipient type
+ * @param address the address
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws MessagingException for other failures
+ */
+ public void setRecipient(RecipientType type, Address address)
+ throws MessagingException {
+ if (address == null)
+ setRecipients(type, null);
+ else {
+ Address[] a = new Address[1];
+ a[0] = address;
+ setRecipients(type, a);
+ }
+ }
+
+ /**
+ * Add these recipient addresses to the existing ones of the given type.
+ *
+ * @param type the recipient type
+ * @param addresses the addresses
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MessagingException for other failures
+ */
+ public abstract void addRecipients(RecipientType type, Address[] addresses)
+ throws MessagingException;
+
+ /**
+ * Add this recipient address to the existing ones of the given type.
+ *
+ * The default implementation uses the addRecipients method.
+ *
+ * @param type the recipient type
+ * @param address the address
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws MessagingException for other failures
+ */
+ public void addRecipient(RecipientType type, Address address)
+ throws MessagingException {
+ Address[] a = new Address[1];
+ a[0] = address;
+ addRecipients(type, a);
+ }
+
+ /**
+ * Get the addresses to which replies should be directed.
+ * This will usually be the sender of the message, but
+ * some messages may direct replies to a different address.
+ *
+ * The default implementation simply calls the getFrom
+ * method.
+ *
+ * This method returns null if the corresponding
+ * header is not present. Returns an empty array if the header
+ * is present, but contains no addresses.
+ *
+ * @return addresses to which replies should be directed
+ * @throws MessagingException for failures
+ * @see #getFrom
+ */
+ public Address[] getReplyTo() throws MessagingException {
+ return getFrom();
+ }
+
+ /**
+ * Set the addresses to which replies should be directed.
+ * (Normally only a single address will be specified.)
+ * Not all message types allow this to be specified separately
+ * from the sender of the message.
+ *
+ * The default implementation provided here just throws the
+ * MethodNotSupportedException.
+ *
+ * @param addresses addresses to which replies should be directed
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MethodNotSupportedException if the underlying
+ * implementation does not support setting this
+ * attribute
+ * @throws MessagingException for other failures
+ */
+ public void setReplyTo(Address[] addresses) throws MessagingException {
+ throw new MethodNotSupportedException("setReplyTo not supported");
+ }
+
+ /**
+ * Get the subject of this message.
+ *
+ * @return the subject
+ * @throws MessagingException for failures
+ */
+ public abstract String getSubject() throws MessagingException;
+
+ /**
+ * Set the subject of this message.
+ *
+ * @param subject the subject
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MessagingException for other failures
+ */
+ public abstract void setSubject(String subject)
+ throws MessagingException;
+
+ /**
+ * Get the date this message was sent.
+ *
+ * @return the date this message was sent
+ * @throws MessagingException for failures
+ */
+ public abstract Date getSentDate() throws MessagingException;
+
+ /**
+ * Set the sent date of this message.
+ *
+ * @param date the sent date of this message
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MessagingException for other failures
+ */
+ public abstract void setSentDate(Date date) throws MessagingException;
+
+ /**
+ * Get the date this message was received.
+ *
+ * @return the date this message was received
+ * @throws MessagingException for failures
+ */
+ public abstract Date getReceivedDate() throws MessagingException;
+
+ /**
+ * Returns a Flags object containing the flags for
+ * this message.
+ *
+ * Modifying any of the flags in this returned Flags object will
+ * not affect the flags of this message. Use setFlags()
+ * to do that.
+ *
+ * @return Flags object containing the flags for this message
+ * @throws MessagingException for failures
+ * @see Flags
+ * @see #setFlags
+ */
+ public abstract Flags getFlags() throws MessagingException;
+
+ /**
+ * Check whether the flag specified in the flag
+ * argument is set in this message.
+ *
+ * The default implementation uses getFlags.
+ *
+ * @param flag the flag
+ * @return value of the specified flag for this message
+ * @throws MessagingException for failures
+ * @see Flags.Flag#ANSWERED
+ * @see Flags.Flag#DELETED
+ * @see Flags.Flag#DRAFT
+ * @see Flags.Flag#FLAGGED
+ * @see Flags.Flag#RECENT
+ * @see Flags.Flag#SEEN
+ * @see Flags.Flag
+ */
+ public boolean isSet(Flags.Flag flag) throws MessagingException {
+ return getFlags().contains(flag);
+ }
+
+ /**
+ * Set the specified flags on this message to the specified value.
+ * Note that any flags in this message that are not specified in
+ * the given Flags object are unaffected.
+ *
+ * This will result in a MessageChangedEvent being
+ * delivered to any MessageChangedListener registered on this
+ * Message's containing folder.
+ *
+ * @param flag Flags object containing the flags to be set
+ * @param set the value to be set
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values.
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MessagingException for other failures
+ * @see jakarta.mail.event.MessageChangedEvent
+ */
+ public abstract void setFlags(Flags flag, boolean set)
+ throws MessagingException;
+
+ /**
+ * Set the specified flag on this message to the specified value.
+ *
+ * This will result in a MessageChangedEvent being
+ * delivered to any MessageChangedListener registered on this
+ * Message's containing folder.
+ *
+ * The default implementation uses the setFlags method.
+ *
+ * @param flag Flags.Flag object containing the flag to be set
+ * @param set the value to be set
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values.
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws MessagingException for other failures
+ * @see jakarta.mail.event.MessageChangedEvent
+ */
+ public void setFlag(Flags.Flag flag, boolean set)
+ throws MessagingException {
+ Flags f = new Flags(flag);
+ setFlags(f, set);
+ }
+
+ /**
+ * Get the Message number for this Message.
+ * A Message object's message number is the relative
+ * position of this Message in its Folder. Note that the message
+ * number for a particular Message can change during a session
+ * if other messages in the Folder are deleted and expunged.
+ *
+ * Valid message numbers start at 1. Messages that do not belong
+ * to any folder (like newly composed or derived messages) have 0
+ * as their message number.
+ *
+ * @return the message number
+ */
+ public int getMessageNumber() {
+ return msgnum;
+ }
+
+ /**
+ * Set the Message number for this Message. This method is
+ * invoked only by the implementation classes.
+ *
+ * @param msgnum the message number
+ */
+ protected void setMessageNumber(int msgnum) {
+ this.msgnum = msgnum;
+ }
+
+ /**
+ * Get the folder from which this message was obtained. If
+ * this is a new message or nested message, this method returns
+ * null.
+ *
+ * @return the containing folder
+ */
+ public Folder getFolder() {
+ return folder;
+ }
+
+ /**
+ * Checks whether this message is expunged. All other methods except
+ * getMessageNumber() are invalid on an expunged
+ * Message object.
+ *
+ * Messages that are expunged due to an explict expunge()
+ * request on the containing Folder are removed from the Folder
+ * immediately. Messages that are externally expunged by another source
+ * are marked "expunged" and return true for the isExpunged() method,
+ * but they are not removed from the Folder until an explicit
+ * expunge() is done on the Folder.
+ *
+ * See the description of expunge() for more details on
+ * expunge handling.
+ *
+ * @return true if the message is expunged
+ * @see Folder#expunge
+ */
+ public boolean isExpunged() {
+ return expunged;
+ }
+
+ /**
+ * Sets the expunged flag for this Message. This method is to
+ * be used only by the implementation classes.
+ *
+ * @param expunged the expunged flag
+ */
+ protected void setExpunged(boolean expunged) {
+ this.expunged = expunged;
+ }
+
+ /**
+ * Get a new Message suitable for a reply to this message.
+ * The new Message will have its attributes and headers
+ * set up appropriately. Note that this new message object
+ * will be empty, that is, it will not have a "content".
+ * These will have to be suitably filled in by the client.
+ *
+ * If replyToAll is set, the new Message will be addressed
+ * to all recipients of this message. Otherwise, the reply will be
+ * addressed to only the sender of this message (using the value
+ * of the getReplyTo method).
+ *
+ * The "Subject" field is filled in with the original subject
+ * prefixed with "Re:" (unless it already starts with "Re:").
+ *
+ * The reply message will use the same session as this message.
+ *
+ * @param replyToAll reply should be sent to all recipients
+ * of this message
+ * @return the reply Message
+ * @throws MessagingException for failures
+ */
+ public abstract Message reply(boolean replyToAll) throws MessagingException;
+
+ /**
+ * Save any changes made to this message into the message-store
+ * when the containing folder is closed, if the message is contained
+ * in a folder. (Some implementations may save the changes
+ * immediately.) Update any header fields to be consistent with the
+ * changed message contents. If any part of a message's headers or
+ * contents are changed, saveChanges must be called to ensure that
+ * those changes are permanent. If saveChanges is not called, any
+ * such modifications may or may not be saved, depending on the
+ * message store and folder implementation.
+ *
+ * Messages obtained from folders opened READ_ONLY should not be
+ * modified and saveChanges should not be called on such messages.
+ *
+ * @throws IllegalStateException if this message is
+ * obtained from a READ_ONLY folder.
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values.
+ * @throws MessagingException for other failures
+ */
+ public abstract void saveChanges() throws MessagingException;
+
+ /**
+ * Apply the specified Search criterion to this message.
+ *
+ * @param term the Search criterion
+ * @return true if the Message matches this search
+ * criterion, false otherwise.
+ * @throws MessagingException for failures
+ * @see SearchTerm
+ */
+ public boolean match(SearchTerm term) throws MessagingException {
+ return term.match(this);
+ }
+
+ /**
+ * This inner class defines the types of recipients allowed by
+ * the Message class. The currently defined types are TO,
+ * CC and BCC.
+ *
+ * Note that this class only has a protected constructor, thereby
+ * restricting new Recipient types to either this class or subclasses.
+ * This effectively implements an enumeration of the allowed Recipient
+ * types.
+ *
+ * The following code sample shows how to use this class to obtain
+ * the "TO" recipients from a message.
+ *
+ *
+ * @see Message#getRecipients
+ * @see Message#setRecipients
+ * @see Message#addRecipients
+ */
+ public static class RecipientType {
+ /**
+ * The "To" (primary) recipients.
+ */
+ public static final RecipientType TO = new RecipientType("To");
+ /**
+ * The "Cc" (carbon copy) recipients.
+ */
+ public static final RecipientType CC = new RecipientType("Cc");
+ /**
+ * The "Bcc" (blind carbon copy) recipients.
+ */
+ public static final RecipientType BCC = new RecipientType("Bcc");
+
+ /**
+ * The type of recipient, usually the name of a corresponding
+ * Internet standard header.
+ */
+ protected String type;
+
+ /**
+ * Constructor for use by subclasses.
+ *
+ * @param type the recipient type
+ */
+ protected RecipientType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return type;
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/MessageAware.java b/net-mail/src/main/java/jakarta/mail/MessageAware.java
new file mode 100644
index 0000000..f3b5a3a
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/MessageAware.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * An interface optionally implemented by DataSources to
+ * supply information to a DataContentHandler about the
+ * message context in which the data content object is operating.
+ *
+ * @see MessageContext
+ * @see jakarta.activation.DataSource
+ * @see jakarta.activation.DataContentHandler
+ * @since JavaMail 1.1
+ */
+public interface MessageAware {
+ /**
+ * Return the message context.
+ *
+ * @return the message context
+ */
+ MessageContext getMessageContext();
+}
diff --git a/net-mail/src/main/java/jakarta/mail/MessageContext.java b/net-mail/src/main/java/jakarta/mail/MessageContext.java
new file mode 100644
index 0000000..c8d1555
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/MessageContext.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * The context in which a piece of Message content is contained. A
+ * MessageContext object is returned by the
+ * getMessageContext method of the
+ * MessageAware interface. MessageAware is
+ * typically implemented by DataSources to allow a
+ * DataContentHandler to pass on information about the
+ * context in which a data content object is operating.
+ *
+ * @see MessageAware
+ * @see jakarta.activation.DataSource
+ * @see jakarta.activation.DataContentHandler
+ * @since JavaMail 1.1
+ */
+public class MessageContext {
+ private Part part;
+
+ /**
+ * Create a MessageContext object describing the context of the given Part.
+ *
+ * @param part the Part
+ */
+ public MessageContext(Part part) {
+ this.part = part;
+ }
+
+ /**
+ * Return the Message containing an arbitrary Part.
+ * Follows the parent chain up through containing Multipart
+ * objects until it comes to a Message object, or null.
+ *
+ * @return the containing Message, or null if none
+ * @see BodyPart#getParent
+ * @see Multipart#getParent
+ */
+ private static Message getMessage(Part p) throws MessagingException {
+ while (p != null) {
+ if (p instanceof Message)
+ return (Message) p;
+ BodyPart bp = (BodyPart) p;
+ Multipart mp = bp.getParent();
+ if (mp == null) // MimeBodyPart might not be in a MimeMultipart
+ return null;
+ p = mp.getParent();
+ }
+ return null;
+ }
+
+ /**
+ * Return the Part that contains the content.
+ *
+ * @return the containing Part, or null if not known
+ */
+ public Part getPart() {
+ return part;
+ }
+
+ /**
+ * Return the Message that contains the content.
+ * Follows the parent chain up through containing Multipart
+ * objects until it comes to a Message object, or null.
+ *
+ * @return the containing Message, or null if not known
+ */
+ public Message getMessage() {
+ try {
+ return getMessage(part);
+ } catch (MessagingException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Return the Session we're operating in.
+ *
+ * @return the Session, or null if not known
+ */
+ public Session getSession() {
+ Message msg = getMessage();
+ return msg != null ? msg.getSession() : null;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/MessageRemovedException.java b/net-mail/src/main/java/jakarta/mail/MessageRemovedException.java
new file mode 100644
index 0000000..acc394b
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/MessageRemovedException.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * The exception thrown when an invalid method is invoked on an expunged
+ * Message. The only valid methods on an expunged Message are
+ * isExpunged() and getMessageNumber().
+ *
+ * @author John Mani
+ * @see Message#isExpunged()
+ * @see Message#getMessageNumber()
+ */
+@SuppressWarnings("serial")
+public class MessageRemovedException extends MessagingException {
+
+ /**
+ * Constructs a MessageRemovedException with no detail message.
+ */
+ public MessageRemovedException() {
+ super();
+ }
+
+ /**
+ * Constructs a MessageRemovedException with the specified
+ * detail message.
+ *
+ * @param s The detailed error message
+ */
+ public MessageRemovedException(String s) {
+ super(s);
+ }
+
+ /**
+ * Constructs a MessageRemovedException with the specified
+ * detail message and embedded exception. The exception is chained
+ * to this exception.
+ *
+ * @param s The detailed error message
+ * @param e The embedded exception
+ * @since JavaMail 1.5
+ */
+ public MessageRemovedException(String s, Exception e) {
+ super(s, e);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/MessagingException.java b/net-mail/src/main/java/jakarta/mail/MessagingException.java
new file mode 100644
index 0000000..5a438a5
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/MessagingException.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * The base class for all exceptions thrown by the Messaging classes
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ */
+@SuppressWarnings("serial")
+public class MessagingException extends Exception {
+
+ /**
+ * The next exception in the chain.
+ *
+ * @serial
+ */
+ private Exception next;
+
+ /**
+ * Constructs a MessagingException with no detail message.
+ */
+ public MessagingException() {
+ super();
+ }
+
+ /**
+ * Constructs a MessagingException with the specified detail message.
+ *
+ * @param s the detail message
+ */
+ public MessagingException(String s) {
+ super(s);
+ }
+
+ /**
+ * Constructs a MessagingException with the specified
+ * Exception and detail message. The specified exception is chained
+ * to this exception.
+ *
+ * @param s the detail message
+ * @param e the embedded exception
+ * @see #getNextException
+ * @see #setNextException
+ * @see #getCause
+ */
+ public MessagingException(String s, Exception e) {
+ super(s);
+ next = e;
+ }
+
+ /**
+ * Get the next exception chained to this one. If the
+ * next exception is a MessagingException, the chain
+ * may extend further.
+ *
+ * @return next Exception, null if none.
+ */
+ public synchronized Exception getNextException() {
+ return next;
+ }
+
+ /**
+ * Overrides the getCause method of Throwable
+ * to return the next exception in the chain of nested exceptions.
+ *
+ * @return next Exception, null if none.
+ */
+ @Override
+ public synchronized Throwable getCause() {
+ return next;
+ }
+
+ /**
+ * Add an exception to the end of the chain. If the end
+ * is not a MessagingException, this
+ * exception cannot be added to the end.
+ *
+ * @param ex the new end of the Exception chain
+ * @return true if this Exception
+ * was added, false otherwise.
+ */
+ public synchronized boolean setNextException(Exception ex) {
+ Exception theEnd = this;
+ while (theEnd instanceof MessagingException &&
+ ((MessagingException) theEnd).next != null) {
+ theEnd = ((MessagingException) theEnd).next;
+ }
+ // If the end is a MessagingException, we can add this
+ // exception to the chain.
+ if (theEnd instanceof MessagingException) {
+ ((MessagingException) theEnd).next = ex;
+ return true;
+ } else
+ return false;
+ }
+
+ /**
+ * Override toString method to provide information on
+ * nested exceptions.
+ */
+ @Override
+ public synchronized String toString() {
+ String s = super.toString();
+ Exception n = next;
+ if (n == null)
+ return s;
+ StringBuilder sb = new StringBuilder(s == null ? "" : s);
+ while (n != null) {
+ sb.append(";\n nested exception is:\n\t");
+ if (n instanceof MessagingException) {
+ MessagingException mex = (MessagingException) n;
+ sb.append(mex.superToString());
+ n = mex.next;
+ } else {
+ sb.append(n);
+ n = null;
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Return the "toString" information for this exception,
+ * without any information on nested exceptions.
+ */
+ private final String superToString() {
+ return super.toString();
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/MethodNotSupportedException.java b/net-mail/src/main/java/jakarta/mail/MethodNotSupportedException.java
new file mode 100644
index 0000000..3ae8456
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/MethodNotSupportedException.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+
+/**
+ * The exception thrown when a method is not supported by the
+ * implementation
+ *
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class MethodNotSupportedException extends MessagingException {
+
+ /**
+ * Constructs a MethodNotSupportedException with no detail message.
+ */
+ public MethodNotSupportedException() {
+ super();
+ }
+
+ /**
+ * Constructs a MethodNotSupportedException with the specified
+ * detail message.
+ *
+ * @param s The detailed error message
+ */
+ public MethodNotSupportedException(String s) {
+ super(s);
+ }
+
+ /**
+ * Constructs a MethodNotSupportedException with the specified
+ * detail message and embedded exception. The exception is chained
+ * to this exception.
+ *
+ * @param s The detailed error message
+ * @param e The embedded exception
+ * @since JavaMail 1.5
+ */
+ public MethodNotSupportedException(String s, Exception e) {
+ super(s, e);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Multipart.java b/net-mail/src/main/java/jakarta/mail/Multipart.java
new file mode 100644
index 0000000..fa799ca
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Multipart.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.mail.util.StreamProvider;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Vector;
+
+/**
+ * Multipart is a container that holds multiple body parts. Multipart
+ * provides methods to retrieve and set its subparts.
+ *
+ * Multipart also acts as the base class for the content object returned
+ * by most Multipart DataContentHandlers. For example, invoking getContent()
+ * on a DataHandler whose source is a "multipart/signed" data source may
+ * return an appropriate subclass of Multipart.
+ *
+ * Some messaging systems provide different subtypes of Multiparts. For
+ * example, MIME specifies a set of subtypes that include "alternative",
+ * "mixed", "related", "parallel", "signed", etc.
+ *
+ * Multipart is an abstract class. Subclasses provide actual implementations.
+ *
+ * @author John Mani
+ */
+
+public abstract class Multipart {
+
+ /**
+ * Instance of stream provider.
+ *
+ * @since JavaMail 2.1
+ */
+ protected final StreamProvider streamProvider = StreamProvider.provider();
+ /**
+ * Vector of BodyPart objects.
+ */
+ protected Vector parts = new Vector<>(); // Holds BodyParts
+ /**
+ * This field specifies the content-type of this multipart
+ * object. It defaults to "multipart/mixed".
+ */
+ protected String contentType = "multipart/mixed"; // Content-Type
+ /**
+ * The Part containing this Multipart,
+ * if known.
+ *
+ * @since JavaMail 1.1
+ */
+ protected Part parent;
+
+ /**
+ * Default constructor. An empty Multipart object is created.
+ */
+ protected Multipart() {
+ }
+
+ /**
+ * Setup this Multipart object from the given MultipartDataSource.
+ *
+ * The method adds the MultipartDataSource's BodyPart
+ * objects into this Multipart. This Multipart's contentType is
+ * set to that of the MultipartDataSource.
+ *
+ * This method is typically used in those cases where one
+ * has a multipart data source that has already been pre-parsed into
+ * the individual body parts (for example, an IMAP datasource), but
+ * needs to create an appropriate Multipart subclass that represents
+ * a specific multipart subtype.
+ *
+ * @param mp Multipart datasource
+ * @throws MessagingException for failures
+ */
+ protected synchronized void setMultipartDataSource(MultipartDataSource mp)
+ throws MessagingException {
+ contentType = mp.getContentType();
+
+ int count = mp.getCount();
+ for (int i = 0; i < count; i++)
+ addBodyPart(mp.getBodyPart(i));
+ }
+
+ /**
+ * Return the content-type of this Multipart.
+ *
+ * This implementation just returns the value of the
+ * contentType field.
+ *
+ * @return content-type
+ * @see #contentType
+ */
+ public synchronized String getContentType() {
+ return contentType;
+ }
+
+ /**
+ * Return the number of enclosed BodyPart objects.
+ *
+ * @return number of parts
+ * @throws MessagingException for failures
+ * @see #parts
+ */
+ public synchronized int getCount() throws MessagingException {
+ if (parts == null)
+ return 0;
+
+ return parts.size();
+ }
+
+ /**
+ * Get the specified Part. Parts are numbered starting at 0.
+ *
+ * @param index the index of the desired Part
+ * @return the Part
+ * @throws IndexOutOfBoundsException if the given index
+ * is out of range.
+ * @throws MessagingException for other failures
+ */
+ public synchronized BodyPart getBodyPart(int index)
+ throws MessagingException {
+ if (parts == null)
+ throw new IndexOutOfBoundsException("No such BodyPart");
+
+ return parts.elementAt(index);
+ }
+
+ /**
+ * Remove the specified part from the multipart message.
+ * Shifts all the parts after the removed part down one.
+ *
+ * @param part The part to remove
+ * @return true if part removed, false otherwise
+ * @throws MessagingException if no such Part exists
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ */
+ public synchronized boolean removeBodyPart(BodyPart part)
+ throws MessagingException {
+ if (parts == null)
+ throw new MessagingException("No such body part");
+
+ boolean ret = parts.removeElement(part);
+ part.setParent(null);
+ return ret;
+ }
+
+ /**
+ * Remove the part at specified location (starting from 0).
+ * Shifts all the parts after the removed part down one.
+ *
+ * @param index Index of the part to remove
+ * @throws IndexOutOfBoundsException if the given index
+ * is out of range.
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws MessagingException for other failures
+ */
+ public synchronized void removeBodyPart(int index)
+ throws MessagingException {
+ if (parts == null)
+ throw new IndexOutOfBoundsException("No such BodyPart");
+
+ BodyPart part = parts.elementAt(index);
+ parts.removeElementAt(index);
+ part.setParent(null);
+ }
+
+ /**
+ * Adds a Part to the multipart. The BodyPart is appended to
+ * the list of existing Parts.
+ *
+ * @param part The Part to be appended
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws MessagingException for other failures
+ */
+ public synchronized void addBodyPart(BodyPart part)
+ throws MessagingException {
+ if (parts == null)
+ parts = new Vector<>();
+
+ parts.addElement(part);
+ part.setParent(this);
+ }
+
+ /**
+ * Adds a BodyPart at position index.
+ * If index is not the last one in the list,
+ * the subsequent parts are shifted up. If index
+ * is larger than the number of parts present, the
+ * BodyPart is appended to the end.
+ *
+ * @param part The BodyPart to be inserted
+ * @param index Location where to insert the part
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws MessagingException for other failures
+ */
+ public synchronized void addBodyPart(BodyPart part, int index)
+ throws MessagingException {
+ if (parts == null)
+ parts = new Vector<>();
+
+ parts.insertElementAt(part, index);
+ part.setParent(this);
+ }
+
+ /**
+ * Output an appropriately encoded bytestream to the given
+ * OutputStream. The implementation subclass decides the
+ * appropriate encoding algorithm to be used. The bytestream
+ * is typically used for sending.
+ *
+ * @param os the stream to write to
+ * @throws IOException if an IO related exception occurs
+ * @throws MessagingException for other failures
+ */
+ public abstract void writeTo(OutputStream os)
+ throws IOException, MessagingException;
+
+ /**
+ * Return the Part that contains this Multipart
+ * object, or null if not known.
+ *
+ * @return the parent Part
+ * @since JavaMail 1.1
+ */
+ public synchronized Part getParent() {
+ return parent;
+ }
+
+ /**
+ * Set the parent of this Multipart to be the specified
+ * Part. Normally called by the Message
+ * or BodyPartsetContent(Multipart) method.
+ * parent may be null if the
+ * Multipart is being removed from its containing
+ * Part.
+ *
+ * @param parent the parent Part
+ * @since JavaMail 1.1
+ */
+ public synchronized void setParent(Part parent) {
+ this.parent = parent;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/MultipartDataSource.java b/net-mail/src/main/java/jakarta/mail/MultipartDataSource.java
new file mode 100644
index 0000000..02a9355
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/MultipartDataSource.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.activation.DataSource;
+
+/**
+ * MultipartDataSource is a DataSource that contains body
+ * parts. This allows "mail aware" DataContentHandlers to
+ * be implemented more efficiently by being aware of such
+ * DataSources and using the appropriate methods to access
+ * BodyParts.
+ *
+ * Note that the data of a MultipartDataSource is also available as
+ * an input stream.
+ *
+ * This interface will typically be implemented by providers that
+ * preparse multipart bodies, for example an IMAP provider.
+ *
+ * @author John Mani
+ * @see jakarta.activation.DataSource
+ */
+
+public interface MultipartDataSource extends DataSource {
+
+ /**
+ * Return the number of enclosed BodyPart objects.
+ *
+ * @return number of parts
+ */
+ int getCount();
+
+ /**
+ * Get the specified Part. Parts are numbered starting at 0.
+ *
+ * @param index the index of the desired Part
+ * @return the Part
+ * @throws IndexOutOfBoundsException if the given index
+ * is out of range.
+ * @throws MessagingException for other failures
+ */
+ BodyPart getBodyPart(int index) throws MessagingException;
+
+}
diff --git a/net-mail/src/main/java/jakarta/mail/NoSuchProviderException.java b/net-mail/src/main/java/jakarta/mail/NoSuchProviderException.java
new file mode 100644
index 0000000..a5a1013
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/NoSuchProviderException.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * This exception is thrown when Session attempts to instantiate a
+ * Provider that doesn't exist.
+ *
+ * @author Max Spivak
+ */
+@SuppressWarnings("serial")
+public class NoSuchProviderException extends MessagingException {
+
+ /**
+ * Constructs a NoSuchProviderException with no detail message.
+ */
+ public NoSuchProviderException() {
+ super();
+ }
+
+ /**
+ * Constructs a NoSuchProviderException with the specified
+ * detail message.
+ *
+ * @param message The detailed error message
+ */
+ public NoSuchProviderException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a NoSuchProviderException with the specified
+ * detail message and embedded exception. The exception is chained
+ * to this exception.
+ *
+ * @param message The detailed error message
+ * @param e The embedded exception
+ * @since JavaMail 1.5
+ */
+ public NoSuchProviderException(String message, Exception e) {
+ super(message, e);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Part.java b/net-mail/src/main/java/jakarta/mail/Part.java
new file mode 100644
index 0000000..aeadd21
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Part.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.activation.DataHandler;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Enumeration;
+
+/**
+ * The Part interface is the common base interface for
+ * Messages and BodyParts.
+ *
+ * Part consists of a set of attributes and a "Content".
+ *
+ * Attributes:
+ *
+ * The Jakarta Mail API defines a set of standard Part attributes that are
+ * considered to be common to most existing Mail systems. These
+ * attributes have their own settor and gettor methods. Mail systems
+ * may support other Part attributes as well, these are represented as
+ * name-value pairs where both the name and value are Strings.
+ *
+ * Content:
+ *
+ * The data type of the "content" is returned by
+ * the getContentType() method. The MIME typing system
+ * is used to name data types.
+ *
+ * The "content" of a Part is available in various formats:
+ *
+ *
As a DataHandler - using the getDataHandler() method.
+ * The "content" of a Part is also available through a
+ * jakarta.activation.DataHandler object. The DataHandler
+ * object allows clients to discover the operations available on the
+ * content, and to instantiate the appropriate component to perform
+ * those operations.
+ *
+ *
As an input stream - using the getInputStream() method.
+ * Any mail-specific encodings are decoded before this stream is returned.
+ *
+ *
As a Java object - using the getContent() method.
+ * This method returns the "content" as a Java object.
+ * The returned object is of course dependent on the content
+ * itself. In particular, a "multipart" Part's content is always a
+ * Multipart or subclass thereof. That is, getContent() on a
+ * "multipart" type Part will always return a Multipart (or subclass) object.
+ *
+ *
+ * Part provides the writeTo() method that streams
+ * out its bytestream in mail-safe form suitable for transmission.
+ * This bytestream is typically an aggregation of the Part attributes
+ * and its content's bytestream.
+ *
+ * Message and BodyPart implement the Part interface. Note that in
+ * MIME parlance, Part models an Entity (RFC 2045, Section 2.4).
+ *
+ * @author John Mani
+ */
+
+public interface Part {
+
+ /**
+ * This part should be presented as an attachment.
+ *
+ * @see #getDisposition
+ * @see #setDisposition
+ */
+ String ATTACHMENT = "attachment";
+ /**
+ * This part should be presented inline.
+ *
+ * @see #getDisposition
+ * @see #setDisposition
+ */
+ String INLINE = "inline";
+
+ /**
+ * Return the size of the content of this part in bytes.
+ * Return -1 if the size cannot be determined.
+ *
+ * Note that the size may not be an exact measure of the content
+ * size and may or may not account for any transfer encoding
+ * of the content. The size is appropriate for display in a
+ * user interface to give the user a rough idea of the size
+ * of this part.
+ *
+ * @return size of content in bytes
+ * @throws MessagingException for failures
+ */
+ int getSize() throws MessagingException;
+
+ /**
+ * Return the number of lines in the content of this part.
+ * Return -1 if the number cannot be determined.
+ *
+ * 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.
+ *
+ * @return number of lines in the content.
+ * @throws MessagingException for failures
+ */
+ int getLineCount() throws MessagingException;
+
+ /**
+ * Returns the Content-Type of the content of this part.
+ * Returns null if the Content-Type could not be determined.
+ *
+ * The MIME typing system is used to name Content-types.
+ *
+ * @return The ContentType of this part
+ * @throws MessagingException for failures
+ * @see jakarta.activation.DataHandler
+ */
+ String getContentType() throws MessagingException;
+
+ /**
+ * Is this Part of the specified MIME type? This method
+ * compares only the primaryType and
+ * subType.
+ * The parameters of the content types are ignored.
+ *
+ * For example, this method will return true when
+ * comparing a Part of content type "text/plain"
+ * with "text/plain; charset=foobar".
+ *
+ * If the subType of mimeType is the
+ * special character '*', then the subtype is ignored during the
+ * comparison.
+ *
+ * @param mimeType the MIME type to test
+ * @return true if this part is of the specified type
+ * @throws MessagingException for failures
+ */
+ boolean isMimeType(String mimeType) throws MessagingException;
+
+ /**
+ * Return the disposition of this part. The disposition
+ * describes how the part should be presented to the user.
+ * (See RFC 2183.) The return value should be considered
+ * without regard to case. For example:
+ *
+ * String disp = part.getDisposition();
+ * if (disp == null || disp.equalsIgnoreCase(Part.ATTACHMENT))
+ * // treat as attachment if not first part
+ *
+ *
+ * @return disposition of this part, or null if unknown
+ * @throws MessagingException for failures
+ * @see #ATTACHMENT
+ * @see #INLINE
+ * @see #getFileName
+ */
+ String getDisposition() throws MessagingException;
+
+ /**
+ * Set the disposition of this part.
+ *
+ * @param disposition disposition of this part
+ * @throws IllegalWriteException if the underlying implementation
+ * does not support modification of this header
+ * @throws IllegalStateException if this Part is obtained
+ * from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ * @see #ATTACHMENT
+ * @see #INLINE
+ * @see #setFileName
+ */
+ void setDisposition(String disposition) throws MessagingException;
+
+ /**
+ * Return a description String for this part. This typically
+ * associates some descriptive information with this part.
+ * Returns null if none is available.
+ *
+ * @return description of this part
+ * @throws MessagingException for failures
+ */
+ String getDescription() throws MessagingException;
+
+ /**
+ * Set a description String for this part. This typically
+ * associates some descriptive information with this part.
+ *
+ * @param description description of this part
+ * @throws IllegalWriteException if the underlying implementation
+ * does not support modification of this header
+ * @throws IllegalStateException if this Part is obtained
+ * from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ */
+ void setDescription(String description) throws MessagingException;
+
+ /**
+ * Get the filename associated with this part, if possible.
+ * Useful if this part represents an "attachment" that was
+ * loaded from a file. The filename will usually be a simple
+ * name, not including directory components.
+ *
+ * @return Filename to associate with this part
+ * @throws MessagingException for failures
+ */
+ String getFileName() throws MessagingException;
+
+ /**
+ * Set the filename associated with this part, if possible.
+ * Useful if this part represents an "attachment" that was
+ * loaded from a file. The filename will usually be a simple
+ * name, not including directory components.
+ *
+ * @param filename Filename to associate with this part
+ * @throws IllegalWriteException if the underlying implementation
+ * does not support modification of this header
+ * @throws IllegalStateException if this Part is obtained
+ * from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ */
+ void setFileName(String filename) throws MessagingException;
+
+ /**
+ * Return an input stream for this part's "content". Any
+ * mail-specific transfer encodings will be decoded before the
+ * input stream is provided.
+ *
+ * This is typically a convenience method that just invokes
+ * the DataHandler's getInputStream() method.
+ *
+ * @return an InputStream
+ * @throws IOException this is typically thrown by the
+ * DataHandler. Refer to the documentation for
+ * jakarta.activation.DataHandler for more details.
+ * @throws MessagingException for other failures
+ * @see #getDataHandler
+ * @see jakarta.activation.DataHandler#getInputStream
+ */
+ InputStream getInputStream()
+ throws IOException, MessagingException;
+
+ /**
+ * Return a DataHandler for the content within this part. The
+ * DataHandler allows clients to operate on as well as retrieve
+ * the content.
+ *
+ * @return DataHandler for the content
+ * @throws MessagingException for failures
+ */
+ DataHandler getDataHandler() throws MessagingException;
+
+ /**
+ * This method provides the mechanism to set this part's content.
+ * The DataHandler wraps around the actual content.
+ *
+ * @param dh The DataHandler for the content.
+ * @throws IllegalWriteException if the underlying implementation
+ * does not support modification of existing values
+ * @throws IllegalStateException if this Part is obtained
+ * from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ */
+ void setDataHandler(DataHandler dh) throws MessagingException;
+
+ /**
+ * Return the content as a Java object. The type of the returned
+ * object is of course dependent on the content itself. For example,
+ * the object returned for "text/plain" content is usually a String
+ * object. The object returned for a "multipart" content is always a
+ * Multipart subclass. For content-types that are unknown to the
+ * DataHandler system, an input stream is returned as the content
+ *
+ * This is a convenience method that just invokes the DataHandler's
+ * getContent() method
+ *
+ * @return Object
+ * @throws IOException this is typically thrown by the
+ * DataHandler. Refer to the documentation for
+ * jakarta.activation.DataHandler for more details.
+ * @throws MessagingException for other failures
+ * @see jakarta.activation.DataHandler#getContent
+ */
+ Object getContent() throws IOException, MessagingException;
+
+ /**
+ * This method sets the given Multipart object as this message's
+ * content.
+ *
+ * @param mp The multipart object that is the Message's content
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification of
+ * existing values
+ * @throws IllegalStateException if this Part is obtained
+ * from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ */
+ void setContent(Multipart mp) throws MessagingException;
+
+ /**
+ * A convenience method for setting this part's content. The part
+ * internally wraps the content in a DataHandler.
+ *
+ * Note that a DataContentHandler class for the specified type should
+ * be available to the Jakarta Mail implementation for this to work right.
+ * i.e., to do setContent(foobar, "application/x-foobar"),
+ * a DataContentHandler for "application/x-foobar" should be installed.
+ * Refer to the Java Activation Framework for more information.
+ *
+ * @param obj A java object.
+ * @param type MIME type of this object.
+ * @throws IllegalWriteException if the underlying implementation
+ * does not support modification of existing values
+ * @throws IllegalStateException if this Part is obtained
+ * from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ */
+ void setContent(Object obj, String type)
+ throws MessagingException;
+
+ /**
+ * A convenience method that sets the given String as this
+ * part's content with a MIME type of "text/plain".
+ *
+ * @param text The text that is the Message's content.
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification of
+ * existing values
+ * @throws IllegalStateException if this Part is obtained
+ * from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ */
+ void setText(String text) throws MessagingException;
+
+ /**
+ * Output a bytestream for this Part. This bytestream is
+ * typically an aggregration of the Part attributes and
+ * an appropriately encoded bytestream from its 'content'.
+ *
+ * Classes that implement the Part interface decide on
+ * the appropriate encoding algorithm to be used.
+ *
+ * The bytestream is typically used for sending.
+ *
+ * @param os the stream to write to
+ * @throws IOException if an error occurs writing to the
+ * stream or if an error is generated
+ * by the jakarta.activation layer.
+ * @throws MessagingException if an error occurs fetching the
+ * data to be written
+ * @see jakarta.activation.DataHandler#writeTo
+ */
+ void writeTo(OutputStream os) throws IOException, MessagingException;
+
+ /**
+ * Get all the headers for this header name. Returns null
+ * if no headers for this header name are available.
+ *
+ * @param header_name the name of this header
+ * @return the value fields for all headers with
+ * this name
+ * @throws MessagingException for failures
+ */
+ String[] getHeader(String header_name)
+ throws MessagingException;
+
+ /**
+ * Set the value for this header_name. Replaces all existing
+ * header values with this new value.
+ *
+ * @param header_name the name of this header
+ * @param header_value the value for this header
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this Part is
+ * obtained from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ */
+ void setHeader(String header_name, String header_value)
+ throws MessagingException;
+
+ /**
+ * Add this value to the existing values for this header_name.
+ *
+ * @param header_name the name of this header
+ * @param header_value the value for this header
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this Part is
+ * obtained from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ */
+ void addHeader(String header_name, String header_value)
+ throws MessagingException;
+
+ /**
+ * Remove all headers with this name.
+ *
+ * @param header_name the name of this header
+ * @throws IllegalWriteException if the underlying
+ * implementation does not support modification
+ * of existing values
+ * @throws IllegalStateException if this Part is
+ * obtained from a READ_ONLY folder
+ * @throws MessagingException for other failures
+ */
+ void removeHeader(String header_name)
+ throws MessagingException;
+
+ /**
+ * Return all the headers from this part as an Enumeration of
+ * Header objects.
+ *
+ * @return enumeration of Header objects
+ * @throws MessagingException for failures
+ */
+ Enumeration getAllHeaders() throws MessagingException;
+
+ /**
+ * Return matching headers from this part as an Enumeration of
+ * Header objects.
+ *
+ * @param header_names the headers to match
+ * @return enumeration of Header objects
+ * @throws MessagingException for failures
+ */
+ Enumeration getMatchingHeaders(String[] header_names)
+ throws MessagingException;
+
+ /**
+ * Return non-matching headers from this envelope as an Enumeration
+ * of Header objects.
+ *
+ * @param header_names the headers to not match
+ * @return enumeration of Header objects
+ * @throws MessagingException for failures
+ */
+ Enumeration getNonMatchingHeaders(String[] header_names)
+ throws MessagingException;
+}
diff --git a/net-mail/src/main/java/jakarta/mail/PasswordAuthentication.java b/net-mail/src/main/java/jakarta/mail/PasswordAuthentication.java
new file mode 100644
index 0000000..e354e63
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/PasswordAuthentication.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+
+/**
+ * The class PasswordAuthentication is a data holder that is used by
+ * Authenticator. It is simply a repository for a user name and a password.
+ *
+ * @author Bill Foote
+ * @see java.net.PasswordAuthentication
+ * @see Authenticator
+ * @see Authenticator#getPasswordAuthentication()
+ */
+
+public final class PasswordAuthentication {
+
+ private final String userName;
+ private final String password;
+
+ /**
+ * Initialize a new PasswordAuthentication
+ *
+ * @param userName the user name
+ * @param password The user's password
+ */
+ public PasswordAuthentication(String userName, String password) {
+ this.userName = userName;
+ this.password = password;
+ }
+
+ /**
+ * @return the user name
+ */
+ public String getUserName() {
+ return userName;
+ }
+
+ /**
+ * @return the password
+ */
+ public String getPassword() {
+ return password;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Provider.java b/net-mail/src/main/java/jakarta/mail/Provider.java
new file mode 100644
index 0000000..2b58856
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Provider.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * The Provider is a class that describes a protocol
+ * implementation. The values typically come from the
+ * javamail.providers and javamail.default.providers
+ * resource files. An application may also create and
+ * register a Provider object to dynamically add support
+ * for a new provider.
+ *
+ * @author Max Spivak
+ * @author Bill Shannon
+ */
+public class Provider {
+
+ private Type type;
+ private String protocol, className, vendor, version;
+ /**
+ * Create a new provider of the specified type for the specified
+ * protocol. The specified class implements the provider.
+ *
+ * @param type Type.STORE or Type.TRANSPORT
+ * @param protocol valid protocol for the type
+ * @param classname class name that implements this protocol
+ * @param vendor optional string identifying the vendor (may be null)
+ * @param version optional implementation version string (may be null)
+ * @since JavaMail 1.4
+ */
+ public Provider(Type type, String protocol, String classname,
+ String vendor, String version) {
+ this.type = type;
+ this.protocol = protocol;
+ this.className = classname;
+ this.vendor = vendor;
+ this.version = version;
+ }
+
+ /**
+ * Returns the type of this Provider.
+ *
+ * @return the provider type
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Returns the protocol supported by this Provider.
+ *
+ * @return the protocol
+ */
+ public String getProtocol() {
+ return protocol;
+ }
+
+ /**
+ * Returns the name of the class that implements the protocol.
+ *
+ * @return the class name
+ */
+ public String getClassName() {
+ return className;
+ }
+
+ /**
+ * Returns the name of the vendor associated with this implementation
+ * or null.
+ *
+ * @return the vendor
+ */
+ public String getVendor() {
+ return vendor;
+ }
+
+ /**
+ * Returns the version of this implementation or null if no version.
+ *
+ * @return the version
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * Overrides Object.toString()
+ */
+ @Override
+ public String toString() {
+ String s = "jakarta.mail.Provider[" + type + "," +
+ protocol + "," + className;
+
+ if (vendor != null)
+ s += "," + vendor;
+
+ if (version != null)
+ s += "," + version;
+
+ s += "]";
+ return s;
+ }
+
+ /**
+ * This inner class defines the Provider type.
+ * Currently, STORE and TRANSPORT are the only two provider types
+ * supported.
+ */
+
+ public static class Type {
+ /**
+ * The Provider of type {@code STORE}.
+ */
+ public static final Type STORE = new Type("STORE");
+ /**
+ * The Provider of type {@code TRANSPORT}.
+ */
+ public static final Type TRANSPORT = new Type("TRANSPORT");
+
+ private String type;
+
+ private Type(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ return type;
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Quota.java b/net-mail/src/main/java/jakarta/mail/Quota.java
new file mode 100644
index 0000000..49f482e
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Quota.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * This class represents a set of quotas for a given quota root.
+ * Each quota root has a set of resources, represented by the
+ * Quota.Resource class. Each resource has a name
+ * (for example, "STORAGE"), a current usage, and a usage limit.
+ * See RFC 2087.
+ *
+ * @author Bill Shannon
+ * @since JavaMail 1.4
+ */
+
+public class Quota {
+
+ /**
+ * The name of the quota root.
+ */
+ public String quotaRoot;
+ /**
+ * The set of resources associated with this quota root.
+ */
+ public Resource[] resources;
+
+ /**
+ * Create a Quota object for the named quotaroot with no associated
+ * resources.
+ *
+ * @param quotaRoot the name of the quota root
+ */
+ public Quota(String quotaRoot) {
+ this.quotaRoot = quotaRoot;
+ }
+
+ /**
+ * Set a resource limit for this quota root.
+ *
+ * @param name the name of the resource
+ * @param limit the resource limit
+ */
+ public void setResourceLimit(String name, long limit) {
+ if (resources == null) {
+ resources = new Resource[1];
+ resources[0] = new Resource(name, 0, limit);
+ return;
+ }
+ for (int i = 0; i < resources.length; i++) {
+ if (resources[i].name.equalsIgnoreCase(name)) {
+ resources[i].limit = limit;
+ return;
+ }
+ }
+ Resource[] ra = new Resource[resources.length + 1];
+ System.arraycopy(resources, 0, ra, 0, resources.length);
+ ra[ra.length - 1] = new Resource(name, 0, limit);
+ resources = ra;
+ }
+
+ /**
+ * An individual resource in a quota root.
+ *
+ * @since JavaMail 1.4
+ */
+ public static class Resource {
+ /**
+ * The name of the resource.
+ */
+ public String name;
+ /**
+ * The current usage of the resource.
+ */
+ public long usage;
+ /**
+ * The usage limit for the resource.
+ */
+ public long limit;
+
+ /**
+ * Construct a Resource object with the given name,
+ * usage, and limit.
+ *
+ * @param name the resource name
+ * @param usage the current usage of the resource
+ * @param limit the usage limit for the resource
+ */
+ public Resource(String name, long usage, long limit) {
+ this.name = name;
+ this.usage = usage;
+ this.limit = limit;
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/QuotaAwareStore.java b/net-mail/src/main/java/jakarta/mail/QuotaAwareStore.java
new file mode 100644
index 0000000..cc188c0
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/QuotaAwareStore.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * An interface implemented by Stores that support quotas.
+ * The {@link #getQuota getQuota} and {@link #setQuota setQuota} methods
+ * support the quota model defined by the IMAP QUOTA extension.
+ * Refer to RFC 2087
+ * for more information.
+ *
+ * @since JavaMail 1.4
+ */
+public interface QuotaAwareStore {
+ /**
+ * Get the quotas for the named folder.
+ * Quotas are controlled on the basis of a quota root, not
+ * (necessarily) a folder. The relationship between folders
+ * and quota roots depends on the server. Some servers
+ * might implement a single quota root for all folders owned by
+ * a user. Other servers might implement a separate quota root
+ * for each folder. A single folder can even have multiple
+ * quota roots, perhaps controlling quotas for different
+ * resources.
+ *
+ * @param folder the name of the folder
+ * @return array of Quota objects
+ * @throws MessagingException if the server doesn't support the
+ * QUOTA extension
+ */
+ Quota[] getQuota(String folder) throws MessagingException;
+
+ /**
+ * Set the quotas for the quota root specified in the quota argument.
+ * Typically this will be one of the quota roots obtained from the
+ * getQuota method, but it need not be.
+ *
+ * @param quota the quota to set
+ * @throws MessagingException if the server doesn't support the
+ * QUOTA extension
+ */
+ void setQuota(Quota quota) throws MessagingException;
+}
diff --git a/net-mail/src/main/java/jakarta/mail/ReadOnlyFolderException.java b/net-mail/src/main/java/jakarta/mail/ReadOnlyFolderException.java
new file mode 100644
index 0000000..0821d67
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/ReadOnlyFolderException.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * This exception is thrown when an attempt is made to open a folder
+ * read-write access when the folder is marked read-only.
+ *
+ * The getMessage() method returns more detailed information about the
+ * error that caused this exception.
+ *
+ * @author Jim Glennon
+ */
+@SuppressWarnings("serial")
+public class ReadOnlyFolderException extends MessagingException {
+ transient private Folder folder;
+
+ /**
+ * Constructs a ReadOnlyFolderException with the specified
+ * folder and no detail message.
+ *
+ * @param folder the Folder
+ * @since JavaMail 1.2
+ */
+ public ReadOnlyFolderException(Folder folder) {
+ this(folder, null);
+ }
+
+ /**
+ * Constructs a ReadOnlyFolderException with the specified
+ * detail message.
+ *
+ * @param folder The Folder
+ * @param message The detailed error message
+ * @since JavaMail 1.2
+ */
+ public ReadOnlyFolderException(Folder folder, String message) {
+ super(message);
+ this.folder = folder;
+ }
+
+ /**
+ * Constructs a ReadOnlyFolderException with the specified
+ * detail message and embedded exception. The exception is chained
+ * to this exception.
+ *
+ * @param folder The Folder
+ * @param message The detailed error message
+ * @param e The embedded exception
+ * @since JavaMail 1.5
+ */
+ public ReadOnlyFolderException(Folder folder, String message, Exception e) {
+ super(message, e);
+ this.folder = folder;
+ }
+
+ /**
+ * Returns the Folder object.
+ *
+ * @return the Folder
+ * @since JavaMail 1.2
+ */
+ public Folder getFolder() {
+ return folder;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/SendFailedException.java b/net-mail/src/main/java/jakarta/mail/SendFailedException.java
new file mode 100644
index 0000000..f6dc9d3
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/SendFailedException.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * This exception is thrown when the message cannot be sent.
+ *
+ * The exception includes those addresses to which the message could not be
+ * sent as well as the valid addresses to which the message was sent and
+ * valid addresses to which the message was not sent.
+ *
+ * @author John Mani
+ * @author Max Spivak
+ * @see Transport#send
+ * @see Transport#sendMessage
+ * @see jakarta.mail.event.TransportEvent
+ */
+@SuppressWarnings("serial")
+public class SendFailedException extends MessagingException {
+
+ /**
+ * The invalid addresses.
+ */
+ transient protected Address[] invalid;
+ /**
+ * Valid addresses to which message was sent.
+ */
+ transient protected Address[] validSent;
+ /**
+ * Valid addresses to which message was not sent.
+ */
+ transient protected Address[] validUnsent;
+
+ /**
+ * Constructs a SendFailedException with no detail message.
+ */
+ public SendFailedException() {
+ super();
+ }
+
+ /**
+ * Constructs a SendFailedException with the specified detail message.
+ *
+ * @param s the detail message
+ */
+ public SendFailedException(String s) {
+ super(s);
+ }
+
+ /**
+ * Constructs a SendFailedException with the specified
+ * Exception and detail message. The specified exception is chained
+ * to this exception.
+ *
+ * @param s the detail message
+ * @param e the embedded exception
+ * @see #getNextException
+ * @see #setNextException
+ */
+ public SendFailedException(String s, Exception e) {
+ super(s, e);
+ }
+
+
+ /**
+ * Constructs a SendFailedException with the specified string
+ * and the specified address objects.
+ *
+ * @param msg the detail message
+ * @param ex the embedded exception
+ * @param validSent valid addresses to which message was sent
+ * @param validUnsent valid addresses to which message was not sent
+ * @param invalid the invalid addresses
+ * @see #getNextException
+ * @see #setNextException
+ */
+ public SendFailedException(String msg, Exception ex, Address[] validSent,
+ Address[] validUnsent, Address[] invalid) {
+ super(msg, ex);
+ this.validSent = validSent;
+ this.validUnsent = validUnsent;
+ this.invalid = invalid;
+ }
+
+ /**
+ * Return the addresses to which this message was sent succesfully.
+ *
+ * @return Addresses to which the message was sent successfully or null
+ */
+ public Address[] getValidSentAddresses() {
+ return validSent;
+ }
+
+ /**
+ * Return the addresses that are valid but to which this message
+ * was not sent.
+ *
+ * @return Addresses that are valid but to which the message was
+ * not sent successfully or null
+ */
+ public Address[] getValidUnsentAddresses() {
+ return validUnsent;
+ }
+
+ /**
+ * Return the addresses to which this message could not be sent.
+ *
+ * @return Addresses to which the message sending failed or null;
+ */
+ public Address[] getInvalidAddresses() {
+ return invalid;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Service.java b/net-mail/src/main/java/jakarta/mail/Service.java
new file mode 100644
index 0000000..0cce025
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Service.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.mail.event.ConnectionEvent;
+import jakarta.mail.event.ConnectionListener;
+import jakarta.mail.event.MailEvent;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.EventListener;
+import java.util.Vector;
+import java.util.concurrent.Executor;
+
+/**
+ * An abstract class that contains the functionality
+ * common to messaging services, such as stores and transports.
+ * A messaging service is created from a Session and is
+ * named using a URLName. A service must be connected
+ * before it can be used. Connection events are sent to reflect
+ * its connection status.
+ *
+ * @author Christopher Cotton
+ * @author Bill Shannon
+ * @author Kanwar Oberoi
+ */
+
+public abstract class Service implements AutoCloseable {
+
+ /*
+ * connectionListeners is a Vector, initialized here,
+ * because we depend on it always existing and depend
+ * on the synchronization that Vector provides.
+ * (Sychronizing on the Service object itself can cause
+ * deadlocks when notifying listeners.)
+ */
+ private final Vector connectionListeners
+ = new Vector<>();
+ /**
+ * The queue of events to be delivered.
+ */
+ private final EventQueue q;
+ /**
+ * The session from which this service was created.
+ */
+ protected Session session;
+ /**
+ * The URLName of this service.
+ */
+ protected volatile URLName url = null;
+ /**
+ * Debug flag for this service. Set from the session's debug
+ * flag when this service is created.
+ */
+ protected boolean debug;
+ private boolean connected = false;
+
+ /**
+ * Constructor.
+ *
+ * @param session Session object for this service
+ * @param urlname URLName object to be used for this service
+ */
+ protected Service(Session session, URLName urlname) {
+ this.session = session;
+ debug = session.getDebug();
+ url = urlname;
+
+ /*
+ * Initialize the URLName with default values.
+ * The URLName will be updated when connect is called.
+ */
+ String protocol = null;
+ String host = null;
+ int port = -1;
+ String user = null;
+ String password = null;
+ String file = null;
+
+ // get whatever information we can from the URL
+ // XXX - url should always be non-null here, Session
+ // passes it into the constructor
+ if (url != null) {
+ protocol = url.getProtocol();
+ host = url.getHost();
+ port = url.getPort();
+ user = url.getUsername();
+ password = url.getPassword();
+ file = url.getFile();
+ }
+
+ // try to get protocol-specific default properties
+ if (protocol != null) {
+ if (host == null)
+ host = session.getProperty("mail." + protocol + ".host");
+ if (user == null)
+ user = session.getProperty("mail." + protocol + ".user");
+ }
+
+ // try to get mail-wide default properties
+ if (host == null)
+ host = session.getProperty("mail.host");
+
+ if (user == null)
+ user = session.getProperty("mail.user");
+
+ // try using the system username
+ if (user == null) {
+ user = System.getProperty("user.name");
+ }
+
+ url = new URLName(protocol, host, port, file, user, password);
+
+ // create or choose the appropriate event queue
+ String scope =
+ session.getProperties().getProperty("mail.event.scope", "folder");
+ Executor executor =
+ (Executor) session.getProperties().get("mail.event.executor");
+ if (scope.equalsIgnoreCase("application"))
+ q = EventQueue.getApplicationEventQueue(executor);
+ else if (scope.equalsIgnoreCase("session"))
+ q = session.getEventQueue();
+ else // if (scope.equalsIgnoreCase("store") ||
+ // scope.equalsIgnoreCase("folder"))
+ q = new EventQueue(executor);
+ }
+
+ /**
+ * A generic connect method that takes no parameters. Subclasses
+ * can implement the appropriate authentication schemes. Subclasses
+ * that need additional information might want to use some properties
+ * or might get it interactively using a popup window.
+ *
+ * If the connection is successful, an "open" ConnectionEvent
+ * is delivered to any ConnectionListeners on this service.
+ *
+ * Most clients should just call this method to connect to the service.
+ *
+ * It is an error to connect to an already connected service.
+ *
+ * The implementation provided here simply calls the following
+ * connect(String, String, String) method with nulls.
+ *
+ * @throws AuthenticationFailedException for authentication failures
+ * @throws IllegalStateException if the service is already connected
+ * @throws MessagingException for other failures
+ * @see ConnectionEvent
+ */
+ public void connect() throws MessagingException {
+ connect(null, null, null);
+ }
+
+ /**
+ * Connect to the specified address. This method provides a simple
+ * authentication scheme that requires a username and password.
+ *
+ * If the connection is successful, an "open" ConnectionEvent
+ * is delivered to any ConnectionListeners on this service.
+ *
+ * It is an error to connect to an already connected service.
+ *
+ * The implementation in the Service class will collect defaults
+ * for the host, user, and password from the session, from the
+ * URLName for this service, and from the supplied
+ * parameters and then call the protocolConnect method.
+ * If the protocolConnect method returns false,
+ * the user will be prompted for any missing information and the
+ * protocolConnect method will be called again. The
+ * subclass should override the protocolConnect method.
+ * The subclass should also implement the getURLName
+ * method, or use the implementation in this class.
+ *
+ * On a successful connection, the setURLName method is
+ * called with a URLName that includes the information used to make
+ * the connection, including the password.
+ *
+ * If the username passed in is null, a default value will be chosen
+ * as described above.
+ *
+ * If the password passed in is null and this is the first successful
+ * connection to this service, the user name and the password
+ * collected from the user will be saved as defaults for subsequent
+ * connection attempts to this same service when using other Service object
+ * instances (the connection information is typically always saved within
+ * a particular Service object instance). The password is saved using the
+ * Session method setPasswordAuthentication. If the
+ * password passed in is not null, it is not saved, on the assumption
+ * that the application is managing passwords explicitly.
+ *
+ * @param host the host to connect to
+ * @param user the user name
+ * @param password this user's password
+ * @throws AuthenticationFailedException for authentication failures
+ * @throws IllegalStateException if the service is already connected
+ * @throws MessagingException for other failures
+ * @see ConnectionEvent
+ * @see Session#setPasswordAuthentication
+ */
+ public void connect(String host, String user, String password)
+ throws MessagingException {
+ connect(host, -1, user, password);
+ }
+
+ /**
+ * Connect to the current host using the specified username
+ * and password. This method is equivalent to calling the
+ * connect(host, user, password) method with null
+ * for the host name.
+ *
+ * @param user the user name
+ * @param password this user's password
+ * @throws AuthenticationFailedException for authentication failures
+ * @throws IllegalStateException if the service is already connected
+ * @throws MessagingException for other failures
+ * @see ConnectionEvent
+ * @see Session#setPasswordAuthentication
+ * @see #connect(String, String, String)
+ * @since JavaMail 1.4
+ */
+ public void connect(String user, String password)
+ throws MessagingException {
+ connect(null, user, password);
+ }
+
+ /**
+ * Similar to connect(host, user, password) except a specific port
+ * can be specified.
+ *
+ * @param host the host to connect to
+ * @param port the port to connect to (-1 means the default port)
+ * @param user the user name
+ * @param password this user's password
+ * @throws AuthenticationFailedException for authentication failures
+ * @throws IllegalStateException if the service is already connected
+ * @throws MessagingException for other failures
+ * @see #connect(String, String, String)
+ * @see ConnectionEvent
+ */
+ public synchronized void connect(String host, int port,
+ String user, String password) throws MessagingException {
+
+ // see if the service is already connected
+ if (isConnected())
+ throw new IllegalStateException("already connected");
+
+ PasswordAuthentication pw;
+ boolean connected = false;
+ boolean save = false;
+ String protocol = null;
+ String file = null;
+
+ // get whatever information we can from the URL
+ // XXX - url should always be non-null here, Session
+ // passes it into the constructor
+ if (url != null) {
+ protocol = url.getProtocol();
+ if (host == null)
+ host = url.getHost();
+ if (port == -1)
+ port = url.getPort();
+
+ if (user == null) {
+ user = url.getUsername();
+ if (password == null) // get password too if we need it
+ password = url.getPassword();
+ } else {
+ if (password == null && user.equals(url.getUsername()))
+ // only get the password if it matches the username
+ password = url.getPassword();
+ }
+
+ file = url.getFile();
+ }
+
+ // try to get protocol-specific default properties
+ if (protocol != null) {
+ if (host == null)
+ host = session.getProperty("mail." + protocol + ".host");
+ if (user == null)
+ user = session.getProperty("mail." + protocol + ".user");
+ }
+
+ // try to get mail-wide default properties
+ if (host == null)
+ host = session.getProperty("mail.host");
+
+ if (user == null)
+ user = session.getProperty("mail.user");
+
+ // try using the system username
+ if (user == null) {
+ user = System.getProperty("user.name");
+ }
+
+ // if we don't have a password, look for saved authentication info
+ if (password == null && url != null) {
+ // canonicalize the URLName
+ setURLName(new URLName(protocol, host, port, file, user, null));
+ pw = session.getPasswordAuthentication(getURLName());
+ if (pw != null) {
+ if (user == null) {
+ user = pw.getUserName();
+ password = pw.getPassword();
+ } else if (user.equals(pw.getUserName())) {
+ password = pw.getPassword();
+ }
+ } else
+ save = true;
+ }
+
+ // try connecting, if the protocol needs some missing
+ // information (user, password) it will not connect.
+ // if it tries to connect and fails, remember why for later.
+ AuthenticationFailedException authEx = null;
+ try {
+ connected = protocolConnect(host, port, user, password);
+ } catch (AuthenticationFailedException ex) {
+ authEx = ex;
+ }
+
+ // if not connected, ask the user and try again
+ if (!connected) {
+ InetAddress addr;
+ try {
+ addr = InetAddress.getByName(host);
+ } catch (UnknownHostException e) {
+ addr = null;
+ }
+ pw = session.requestPasswordAuthentication(
+ addr, port,
+ protocol,
+ null, user);
+ if (pw != null) {
+ user = pw.getUserName();
+ password = pw.getPassword();
+
+ // have the service connect again
+ connected = protocolConnect(host, port, user, password);
+ }
+ }
+
+ // if we're not connected by now, we give up
+ if (!connected) {
+ if (authEx != null)
+ throw authEx;
+ else if (user == null)
+ throw new AuthenticationFailedException(
+ "failed to connect, no user name specified?");
+ else if (password == null)
+ throw new AuthenticationFailedException(
+ "failed to connect, no password specified?");
+ else
+ throw new AuthenticationFailedException("failed to connect");
+ }
+
+ setURLName(new URLName(protocol, host, port, file, user, password));
+
+ if (save)
+ session.setPasswordAuthentication(getURLName(),
+ new PasswordAuthentication(user, password));
+
+ // set our connected state
+ setConnected(true);
+
+ // finally, deliver the connection event
+ notifyConnectionListeners(ConnectionEvent.OPENED);
+ }
+
+
+ /**
+ * The service implementation should override this method to
+ * perform the actual protocol-specific connection attempt.
+ * The default implementation of the connect method
+ * calls this method as needed.
+ *
+ * The protocolConnect method should return
+ * false if a user name or password is required
+ * for authentication but the corresponding parameter is null;
+ * the connect method will prompt the user when
+ * needed to supply missing information. This method may
+ * also return false if authentication fails for
+ * the supplied user name or password. Alternatively, this method
+ * may throw an AuthenticationFailedException when authentication
+ * fails. This exception may include a String message with more
+ * detail about the failure.
+ *
+ * The protocolConnect method should throw an
+ * exception to report failures not related to authentication,
+ * such as an invalid host name or port number, loss of a
+ * connection during the authentication process, unavailability
+ * of the server, etc.
+ *
+ * @param host the name of the host to connect to
+ * @param port the port to use (-1 means use default port)
+ * @param user the name of the user to login as
+ * @param password the user's password
+ * @return true if connection successful, false if authentication failed
+ * @throws AuthenticationFailedException for authentication failures
+ * @throws MessagingException for non-authentication failures
+ */
+ protected boolean protocolConnect(String host, int port, String user,
+ String password) throws MessagingException {
+ return false;
+ }
+
+ /**
+ * Is this service currently connected?
+ *
+ * This implementation uses a private boolean field to
+ * store the connection state. This method returns the value
+ * of that field.
+ *
+ * Subclasses may want to override this method to verify that any
+ * connection to the message store is still alive.
+ *
+ * @return true if the service is connected, false if it is not connected
+ */
+ public synchronized boolean isConnected() {
+ return connected;
+ }
+
+ /**
+ * Set the connection state of this service. The connection state
+ * will automatically be set by the service implementation during the
+ * connect and close methods.
+ * Subclasses will need to call this method to set the state
+ * if the service was automatically disconnected.
+ *
+ * The implementation in this class merely sets the private field
+ * returned by the isConnected method.
+ *
+ * @param connected true if the service is connected,
+ * false if it is not connected
+ */
+ protected synchronized void setConnected(boolean connected) {
+ this.connected = connected;
+ }
+
+ /**
+ * Close this service and terminate its connection. A close
+ * ConnectionEvent is delivered to any ConnectionListeners. Any
+ * Messaging components (Folders, Messages, etc.) belonging to this
+ * service are invalid after this service is closed. Note that the service
+ * is closed even if this method terminates abnormally by throwing
+ * a MessagingException.
+ *
+ * This implementation uses setConnected(false) to set
+ * this service's connected state to false. It will then
+ * send a close ConnectionEvent to any registered ConnectionListeners.
+ * Subclasses overriding this method to do implementation specific
+ * cleanup should call this method as a last step to insure event
+ * notification, probably by including a call to super.close()
+ * in a finally clause.
+ *
+ * @throws MessagingException for errors while closing
+ * @see ConnectionEvent
+ */
+ public synchronized void close() throws MessagingException {
+ setConnected(false);
+ notifyConnectionListeners(ConnectionEvent.CLOSED);
+ }
+
+ /**
+ * Return a URLName representing this service. The returned URLName
+ * does not include the password field.
+ *
+ * Subclasses should only override this method if their
+ * URLName does not follow the standard format.
+ *
+ * The implementation in the Service class returns (usually a copy of)
+ * the url field with the password and file information
+ * stripped out.
+ *
+ * @return the URLName representing this service
+ * @see URLName
+ */
+ public URLName getURLName() {
+ URLName url = this.url; // snapshot
+ if (url != null && (url.getPassword() != null || url.getFile() != null))
+ return new URLName(url.getProtocol(), url.getHost(),
+ url.getPort(), null /* no file */,
+ url.getUsername(), null /* no password */);
+ else
+ return url;
+ }
+
+ /**
+ * Set the URLName representing this service.
+ * Normally used to update the url field
+ * after a service has successfully connected.
+ *
+ * Subclasses should only override this method if their
+ * URL does not follow the standard format. In particular,
+ * subclasses should override this method if their URL
+ * does not require all the possible fields supported by
+ * URLName; a new URLName should
+ * be constructed with any unneeded fields removed.
+ *
+ * The implementation in the Service class simply sets the
+ * url field.
+ *
+ * @param url the URLName
+ * @see URLName
+ */
+ protected void setURLName(URLName url) {
+ this.url = url;
+ }
+
+ /**
+ * Add a listener for Connection events on this service.
+ *
+ * The default implementation provided here adds this listener
+ * to an internal list of ConnectionListeners.
+ *
+ * @param l the Listener for Connection events
+ * @see ConnectionEvent
+ */
+ public void addConnectionListener(ConnectionListener l) {
+ connectionListeners.addElement(l);
+ }
+
+ /**
+ * Remove a Connection event listener.
+ *
+ * The default implementation provided here removes this listener
+ * from the internal list of ConnectionListeners.
+ *
+ * @param l the listener
+ * @see #addConnectionListener
+ */
+ public void removeConnectionListener(ConnectionListener l) {
+ connectionListeners.removeElement(l);
+ }
+
+ /**
+ * Notify all ConnectionListeners. Service implementations are
+ * expected to use this method to broadcast connection events.
+ *
+ * The provided default implementation queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the registered
+ * ConnectionListeners. Note that the event dispatching occurs
+ * in a separate thread, thus avoiding potential deadlock problems.
+ *
+ * @param type the ConnectionEvent type
+ */
+ protected void notifyConnectionListeners(int type) {
+ /*
+ * Don't bother queuing an event if there's no listeners.
+ * Yes, listeners could be removed after checking, which
+ * just makes this an expensive no-op.
+ */
+ if (connectionListeners.size() > 0) {
+ ConnectionEvent e = new ConnectionEvent(this, type);
+ queueEvent(e, connectionListeners);
+ }
+
+ /* Fix for broken JDK1.1.x Garbage collector :
+ * The 'conservative' GC in JDK1.1.x occasionally fails to
+ * garbage-collect Threads which are in the wait state.
+ * This would result in thread (and consequently memory) leaks.
+ *
+ * We attempt to fix this by sending a 'terminator' event
+ * to the queue, after we've sent the CLOSED event. The
+ * terminator event causes the event-dispatching thread to
+ * self destruct.
+ */
+ if (type == ConnectionEvent.CLOSED)
+ q.terminateQueue();
+ }
+
+ /**
+ * Return getURLName.toString() if this service has a URLName,
+ * otherwise it will return the default toString.
+ */
+ @Override
+ public String toString() {
+ URLName url = getURLName();
+ if (url != null)
+ return url.toString();
+ else
+ return super.toString();
+ }
+
+ /**
+ * Add the event and vector of listeners to the queue to be delivered.
+ *
+ * @param event the event
+ * @param vector the vector of listeners
+ */
+ protected void queueEvent(MailEvent event,
+ Vector extends EventListener> vector) {
+ /*
+ * Copy the vector in order to freeze the state of the set
+ * of EventListeners the event should be delivered to prior
+ * to delivery. This ensures that any changes made to the
+ * Vector from a target listener's method during the delivery
+ * of this event will not take effect until after the event is
+ * delivered.
+ */
+ @SuppressWarnings("unchecked")
+ Vector extends EventListener> v = (Vector extends EventListener>) vector.clone();
+ q.enqueue(event, v);
+ }
+
+ /**
+ * Package private method to allow Folder to get the Session for a Store.
+ */
+ Session getSession() {
+ return session;
+ }
+
+ /**
+ * Package private method to allow Folder to get the EventQueue for a Store.
+ */
+ EventQueue getEventQueue() {
+ return q;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Session.java b/net-mail/src/main/java/jakarta/mail/Session.java
new file mode 100644
index 0000000..4d17b11
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Session.java
@@ -0,0 +1,1286 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.mail.util.LineInputStream;
+import jakarta.mail.util.StreamProvider;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.net.InetAddress;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+import java.util.StringTokenizer;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+
+/**
+ * Support interface to generalize
+ * code that loads resources from stream.
+ */
+interface StreamLoader {
+ void load(InputStream is) throws IOException;
+}
+
+/**
+ * The Session class represents a mail session and is not subclassed.
+ * It collects together properties and defaults used by the mail API's.
+ * A single default session can be shared by multiple applications on the
+ * desktop. Unshared sessions can also be created.
+ *
+ * The Session class provides access to the protocol providers that
+ * implement the Store, Transport, and related
+ * classes. The protocol providers are configured using the following files:
+ *
+ *
javamail.providers and
+ * javamail.default.providers
+ *
javamail.address.map and
+ * javamail.default.address.map
+ *
+ *
+ * Each javamail.X resource file is searched for using
+ * three methods in the following order:
+ *
+ *
java.home/conf/javamail.X
+ *
META-INF/javamail.X
+ *
META-INF/javamail.default.X
+ *
+ *
+ * (Where java.home is the value of the "java.home" System property
+ * and conf is the directory named "conf" if it exists,
+ * otherwise the directory named "lib"; the "conf" directory was
+ * introduced in JDK 1.9.)
+ *
+ * The first method allows the user to include their own version of the
+ * resource file by placing it in the conf directory where the
+ * java.home property points. The second method allows an
+ * application that uses the Jakarta Mail APIs to include their own resource
+ * files in their application's or jar file's META-INF
+ * directory. The javamail.default.X default files
+ * are part of the Jakarta Mail mail.jar file and should not be
+ * supplied by users.
+ *
+ * File location depends upon how the ClassLoader method
+ * getResource is implemented. Usually, the
+ * getResource method searches through CLASSPATH until it
+ * finds the requested file and then stops.
+ *
+ * The ordering of entries in the resource files matters. If multiple
+ * entries exist, the first entries take precedence over the later
+ * entries. For example, the first IMAP provider found will be set as the
+ * default IMAP implementation until explicitly changed by the
+ * application. The user- or system-supplied resource files augment, they
+ * do not override, the default files included with the Jakarta Mail APIs.
+ * This means that all entries in all files loaded will be available.
+ *
+ * javamail.providers and
+ * javamail.default.providers
+ *
+ * These resource files specify the stores and transports that are
+ * available on the system, allowing an application to "discover" what
+ * store and transport implementations are available. The protocol
+ * implementations are listed one per line. The file format defines four
+ * attributes that describe a protocol implementation. Each attribute is
+ * an "="-separated name-value pair with the name in lowercase. Each
+ * name-value pair is semi-colon (";") separated. The following names
+ * are defined.
+ *
+ *
+ *
+ * Attribute Names in Providers Files
+ *
+ *
+ *
Name
Description
+ *
+ *
+ *
protocol
+ *
Name assigned to protocol.
+ * For example, smtp for Transport.
+ *
+ *
+ *
type
+ *
Valid entries are store and transport.
+ *
+ *
+ *
class
+ *
Class name that implements this protocol.
+ *
+ *
+ *
vendor
+ *
Optional string identifying the vendor.
+ *
+ *
+ *
version
+ *
Optional string identifying the version.
+ *
+ *
+ *
+ * Here's an example of META-INF/javamail.default.providers
+ * file contents:
+ *
+ * The current implementation also supports configuring providers using
+ * the Java SE {@link ServiceLoader ServiceLoader} mechanism.
+ * When creating your own provider, create a {@link Provider} subclass,
+ * for example:
+ *
+ * Then include a file named META-INF/services/jakarta.mail.Provider
+ * in your jar file that lists the name of your Provider class:
+ *
+ * com.example.MyProvider
+ *
+ *
+ *
+ * javamail.address.map and
+ * javamail.default.address.map
+ *
+ * These resource files map transport address types to the transport
+ * protocol. The getType method of
+ * jakarta.mail.Address returns the address type. The
+ * javamail.address.map file maps the transport type to the
+ * protocol. The file format is a series of name-value pairs. Each key
+ * name should correspond to an address type that is currently installed
+ * on the system; there should also be an entry for each
+ * jakarta.mail.Address implementation that is present if it is
+ * to be used. For example, the
+ * jakarta.mail.internet.InternetAddress method
+ * getType returns "rfc822". Each referenced protocol should
+ * be installed on the system. For the case of news, below,
+ * the client should install a Transport provider supporting the nntp
+ * protocol.
+ *
+ * Here are the typical contents of a javamail.address.map file:
+ *
+ * rfc822=smtp
+ * news=nntp
+ *
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ * @author Max Spivak
+ */
+
+public final class Session {
+
+ // Support legacy @DefaultProvider
+ private static final String DEFAULT_PROVIDER = "org.xbib.net.mail.util.DefaultProvider";
+ private static final String confDir;
+ // The default session.
+ private static Session defaultSession = null;
+
+ static {
+ String dir = null;
+ String home = System.getProperty("java.home");
+ String newdir = home + File.separator + "conf";
+ File conf = new File(newdir);
+ if (conf.exists())
+ dir = newdir + File.separator;
+ else
+ dir = home + File.separator +
+ "lib" + File.separator;
+ confDir = dir;
+ }
+
+ private final StreamProvider streamProvider;
+ private final Properties props;
+ private final Authenticator authenticator;
+ private final Hashtable authTable
+ = new Hashtable<>();
+ private final List providers = new ArrayList<>();
+ private final Map providersByProtocol = new HashMap<>();
+ private final Map providersByClassName = new HashMap<>();
+ private final Properties addressMap = new Properties();
+ // maps type to protocol
+ // the queue of events to be delivered, if mail.event.scope===session
+ private final EventQueue q;
+ private boolean debug = false;
+ private PrintStream out; // debug output stream
+ private MailLogger logger;
+
+ // Constructor is not public
+ private Session(Properties props, Authenticator authenticator) {
+ this.props = props;
+ this.authenticator = authenticator;
+ this.streamProvider = StreamProvider.provider();
+
+ if (Boolean.parseBoolean(props.getProperty("mail.debug")))
+ debug = true;
+
+ initLogger();
+
+ // get the Class associated with the Authenticator
+ Class> cl;
+ if (authenticator != null) {
+ cl = authenticator.getClass();
+ } else {
+ // Use implementation class, because that class loader has access to jakarta.mail module and implementation resources
+ cl = streamProvider.getClass();
+ }
+ // load the resources
+ loadProviders(cl);
+ loadAddressMap(cl);
+ q = new EventQueue((Executor) props.get("mail.event.executor"));
+ }
+
+ /**
+ * Get a new Session object.
+ *
+ * @param props Properties object that hold relevant properties.
+ * It is expected that the client supplies values
+ * for the properties listed in Appendix A of the
+ * Jakarta Mail spec (particularly mail.store.protocol,
+ * mail.transport.protocol, mail.host, mail.user,
+ * and mail.from) as the defaults are unlikely to
+ * work in all cases.
+ * @param authenticator Authenticator object used to call back to
+ * the application when a user name and password is
+ * needed.
+ * @return a new Session object
+ * @see Authenticator
+ */
+ public static Session getInstance(Properties props, Authenticator authenticator) {
+ return new Session(props, authenticator);
+ }
+
+ /**
+ * Get a new Session object.
+ *
+ * @param props Properties object that hold relevant properties.
+ * It is expected that the client supplies values
+ * for the properties listed in Appendix A of the
+ * Jakarta Mail spec (particularly mail.store.protocol,
+ * mail.transport.protocol, mail.host, mail.user,
+ * and mail.from) as the defaults are unlikely to
+ * work in all cases.
+ * @return a new Session object
+ * @since JavaMail 1.2
+ */
+ public static Session getInstance(Properties props) {
+ return new Session(props, null);
+ }
+
+ /**
+ * Get the default Session object. If a default has not yet been
+ * setup, a new Session object is created and installed as the
+ * default.
+ *
+ * Since the default session is potentially available to all
+ * code executing in the same Java virtual machine, and the session
+ * can contain security sensitive information such as user names
+ * and passwords, access to the default session is restricted.
+ * The Authenticator object, which must be created by the caller,
+ * is used indirectly to check access permission. The Authenticator
+ * object passed in when the session is created is compared with
+ * the Authenticator object passed in to subsequent requests to
+ * get the default session. If both objects are the same, or are
+ * from the same ClassLoader, the request is allowed. Otherwise,
+ * it is denied.
+ *
+ * Note that if the Authenticator object used to create the session
+ * is null, anyone can get the default session by passing in null.
+ *
+ * Note also that the Properties object is used only the first time
+ * this method is called, when a new Session object is created.
+ * Subsequent calls return the Session object that was created by the
+ * first call, and ignore the passed Properties object. Use the
+ * getInstance method to get a new Session object every
+ * time the method is called.
+ *
+ * Additional security Permission objects may be used to
+ * control access to the default session.
+ *
+ * In the current implementation, if a SecurityManager is set, the
+ * caller must have the RuntimePermission("setFactory")
+ * permission.
+ *
+ * @param props Properties object. Used only if a new Session
+ * object is created.
+ * It is expected that the client supplies values
+ * for the properties listed in Appendix A of the
+ * Jakarta Mail spec (particularly mail.store.protocol,
+ * mail.transport.protocol, mail.host, mail.user,
+ * and mail.from) as the defaults are unlikely to
+ * work in all cases.
+ * @param authenticator Authenticator object. Used only if a
+ * new Session object is created. Otherwise,
+ * it must match the Authenticator used to create
+ * the Session.
+ * @return the default Session object
+ */
+ public static synchronized Session getDefaultInstance(Properties props, Authenticator authenticator) {
+ if (defaultSession == null) {
+ defaultSession = new Session(props, authenticator);
+ } else {
+ // have to check whether caller is allowed to see default session
+ if (defaultSession.authenticator == authenticator) {
+ ; // either same object or both null, either way OK
+ } else if (defaultSession.authenticator != null &&
+ authenticator != null &&
+ defaultSession.authenticator.getClass().getClassLoader() ==
+ authenticator.getClass().getClassLoader()) {
+ ; // both objects came from the same class loader, OK
+ } else
+ // anything else is not allowed
+ throw new IllegalStateException("Access to default session denied");
+ }
+
+ return defaultSession;
+ }
+
+ /**
+ * Get the default Session object. If a default has not yet been
+ * setup, a new Session object is created and installed as the
+ * default.
+ *
+ * Note that a default session created with no Authenticator is
+ * available to all code executing in the same Java virtual
+ * machine, and the session can contain security sensitive
+ * information such as user names and passwords.
+ *
+ * @param props Properties object. Used only if a new Session
+ * object is created.
+ * It is expected that the client supplies values
+ * for the properties listed in Appendix A of the
+ * Jakarta Mail spec (particularly mail.store.protocol,
+ * mail.transport.protocol, mail.host, mail.user,
+ * and mail.from) as the defaults are unlikely to
+ * work in all cases.
+ * @return the default Session object
+ * @since JavaMail 1.2
+ */
+ public static Session getDefaultInstance(Properties props) {
+ return getDefaultInstance(props, null);
+ }
+
+ static boolean containsDefaultProvider(Provider provider) {
+ Annotation[] annotations = provider.getClass().getDeclaredAnnotations();
+ for (Annotation annotation : annotations) {
+ if (DEFAULT_PROVIDER.equals(annotation.annotationType().getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static ClassLoader[] getClassLoaders(final Class>... classes) {
+ ClassLoader[] loaders = new ClassLoader[classes.length];
+ int w = 0;
+ for (Class> k : classes) {
+ ClassLoader cl;
+ if (k == Thread.class) {
+ cl = Thread.currentThread().getContextClassLoader();
+ } else if (k == System.class) {
+ cl = ClassLoader.getSystemClassLoader();
+ } else {
+ cl = k.getClassLoader();
+ }
+ if (cl != null) {
+ loaders[w++] = cl;
+ }
+ }
+ if (loaders.length != w) {
+ loaders = Arrays.copyOf(loaders, w);
+ }
+ return loaders;
+ }
+
+ private static InputStream getResourceAsStream(final Class> c, final String name) throws IOException {
+ try {
+ return c.getClassLoader().getResourceAsStream(name);
+ } catch (RuntimeException e) {
+ throw new IOException("ClassLoader.getResourceAsStream failed");
+ }
+ }
+
+ private static URL[] getResources(final ClassLoader cl, final String name) {
+ URL[] ret = null;
+ try {
+ List v = Collections.list(cl.getResources(name));
+ if (!v.isEmpty()) {
+ ret = new URL[v.size()];
+ v.toArray(ret);
+ }
+ } catch (IOException ioex) {
+ }
+ return ret;
+ }
+
+ private static URL[] getSystemResources(final String name) {
+ URL[] ret = null;
+ try {
+ List v = Collections.list(ClassLoader.getSystemResources(name));
+ if (!v.isEmpty()) {
+ ret = new URL[v.size()];
+ v.toArray(ret);
+ }
+ } catch (IOException ioex) {
+ }
+ return ret;
+ }
+
+ private static InputStream openStream(final URL url) throws IOException {
+ return url.openStream();
+ }
+
+ /**
+ * Get the stream provider instance of the session.
+ *
+ * @return the stream provider
+ * @since JavaMail 2.1
+ */
+ public StreamProvider getStreamProvider() {
+ return streamProvider;
+ }
+
+ private final synchronized void initLogger() {
+ logger = new MailLogger(this.getClass(), "DEBUG", debug, getDebugOut());
+ }
+
+ /**
+ * Get the debug setting for this Session.
+ *
+ * @return current debug setting
+ */
+ public synchronized boolean getDebug() {
+ return debug;
+ }
+
+ /**
+ * Set the debug setting for this Session.
+ *
+ * Since the debug setting can be turned on only after the Session
+ * has been created, to turn on debugging in the Session
+ * constructor, set the property mail.debug in the
+ * Properties object passed in to the constructor to true. The
+ * value of the mail.debug property is used to
+ * initialize the per-Session debugging flag. Subsequent calls to
+ * the setDebug method manipulate the per-Session
+ * debugging flag and have no effect on the mail.debug
+ * property.
+ *
+ * @param debug Debug setting
+ */
+ public synchronized void setDebug(boolean debug) {
+ this.debug = debug;
+ initLogger();
+ }
+
+ /**
+ * Returns the stream to be used for debugging output. If no stream
+ * has been set, System.out is returned.
+ *
+ * @return the PrintStream to use for debugging output
+ * @since JavaMail 1.3
+ */
+ public synchronized PrintStream getDebugOut() {
+ if (out == null)
+ return System.out;
+ else
+ return out;
+ }
+
+ /**
+ * Set the stream to be used for debugging output for this session.
+ * If out is null, System.out will be used.
+ * Note that debugging output that occurs before any session is created,
+ * as a result of setting the mail.debug system property,
+ * will always be sent to System.out.
+ *
+ * @param out the PrintStream to use for debugging output
+ * @since JavaMail 1.3
+ */
+ public synchronized void setDebugOut(PrintStream out) {
+ this.out = out;
+ initLogger();
+ }
+
+ /**
+ * This method returns an array of all the implementations installed
+ * via the javamail.[default.]providers files that can
+ * be loaded using the ClassLoader available to this application.
+ *
+ * @return Array of configured providers
+ */
+ public synchronized Provider[] getProviders() {
+ Provider[] _providers = new Provider[providers.size()];
+ providers.toArray(_providers);
+ return _providers;
+ }
+
+ /**
+ * Returns the default Provider for the protocol
+ * specified. Checks mail.<protocol>.class property
+ * first and if it exists, returns the Provider
+ * associated with this implementation. If it doesn't exist,
+ * returns the Provider that appeared first in the
+ * configuration files. If an implementation for the protocol
+ * isn't found, throws NoSuchProviderException
+ *
+ * @param protocol Configured protocol (i.e. smtp, imap, etc)
+ * @return Currently configured Provider for the specified protocol
+ * @throws NoSuchProviderException If a provider for the given
+ * protocol is not found.
+ */
+ public synchronized Provider getProvider(String protocol) throws NoSuchProviderException {
+
+ if (protocol == null || protocol.length() == 0) {
+ throw new NoSuchProviderException("Invalid protocol: null");
+ }
+
+ Provider _provider = null;
+
+ // check if the mail..class property exists
+ String _className = props.getProperty("mail." + protocol + ".class");
+ if (_className != null) {
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine("mail." + protocol +
+ ".class property exists and points to " +
+ _className);
+ }
+ _provider = providersByClassName.get(_className);
+ }
+
+ if (_provider != null) {
+ return _provider;
+ } else {
+ // returning currently default protocol in providersByProtocol
+ _provider = providersByProtocol.get(protocol);
+ }
+
+ if (_provider == null) {
+ throw new NoSuchProviderException("No provider for " + protocol);
+ } else {
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine("getProvider() returning " + _provider);
+ }
+ return _provider;
+ }
+ }
+
+ /**
+ * Set the passed Provider to be the default implementation
+ * for the protocol in Provider.protocol overriding any previous values.
+ *
+ * @param provider Currently configured Provider which will be
+ * set as the default for the protocol
+ * @throws NoSuchProviderException If the provider passed in
+ * is invalid.
+ */
+ public synchronized void setProvider(Provider provider)
+ throws NoSuchProviderException {
+ if (provider == null) {
+ throw new NoSuchProviderException("Can't set null provider");
+ }
+ providersByProtocol.put(provider.getProtocol(), provider);
+ providersByClassName.put(provider.getClassName(), provider);
+ props.put("mail." + provider.getProtocol() + ".class", provider.getClassName());
+ }
+
+ /**
+ * Get a Store object that implements this user's desired Store
+ * protocol. The mail.store.protocol property specifies the
+ * desired protocol. If an appropriate Store object is not obtained,
+ * NoSuchProviderException is thrown
+ *
+ * @return a Store object
+ * @throws NoSuchProviderException If a provider for the given
+ * protocol is not found.
+ */
+ public Store getStore() throws NoSuchProviderException {
+ return getStore(getProperty("mail.store.protocol"));
+ }
+
+ /**
+ * Get a Store object that implements the specified protocol. If an
+ * appropriate Store object cannot be obtained,
+ * NoSuchProviderException is thrown.
+ *
+ * @param protocol the Store protocol
+ * @return a Store object
+ * @throws NoSuchProviderException If a provider for the given
+ * protocol is not found.
+ */
+ public Store getStore(String protocol) throws NoSuchProviderException {
+ return getStore(new URLName(protocol, null, -1, null, null, null));
+ }
+
+ /**
+ * Get a Store object for the given URLName. If the requested Store
+ * object cannot be obtained, NoSuchProviderException is thrown.
+ *
+ * The "scheme" part of the URL string (Refer RFC 1738) is used
+ * to locate the Store protocol.
+ *
+ * @param url URLName that represents the desired Store
+ * @return a closed Store object
+ * @throws NoSuchProviderException If a provider for the given
+ * URLName is not found.
+ * @see #getFolder(URLName)
+ * @see URLName
+ */
+ public Store getStore(URLName url) throws NoSuchProviderException {
+ String protocol = url.getProtocol();
+ Provider p = getProvider(protocol);
+ return getStore(p, url);
+ }
+
+ /**
+ * Get an instance of the store specified by Provider. Instantiates
+ * the store and returns it.
+ *
+ * @param provider Store Provider that will be instantiated
+ * @return Instantiated Store
+ * @throws NoSuchProviderException If a provider for the given
+ * Provider is not found.
+ */
+ public Store getStore(Provider provider) throws NoSuchProviderException {
+ return getStore(provider, null);
+ }
+
+ /**
+ * Get an instance of the store specified by Provider. If the URLName
+ * is not null, uses it, otherwise creates a new one. Instantiates
+ * the store and returns it. This is a private method used by
+ * getStore(Provider) and getStore(URLName)
+ *
+ * @param provider Store Provider that will be instantiated
+ * @param url URLName used to instantiate the Store
+ * @return Instantiated Store
+ * @throws NoSuchProviderException If a provider for the given
+ * Provider/URLName is not found.
+ */
+ private Store getStore(Provider provider, URLName url) throws NoSuchProviderException {
+
+ // make sure we have the correct type of provider
+ if (provider == null || provider.getType() != Provider.Type.STORE) {
+ throw new NoSuchProviderException("invalid provider");
+ }
+
+ return getService(provider, url, Store.class);
+ }
+
+ /**
+ * Get a closed Folder object for the given URLName. If the requested
+ * Folder object cannot be obtained, null is returned.
+ *
+ * The "scheme" part of the URL string (Refer RFC 1738) is used
+ * to locate the Store protocol. The rest of the URL string (that is,
+ * the "schemepart", as per RFC 1738) is used by that Store
+ * in a protocol dependent manner to locate and instantiate the
+ * appropriate Folder object.
+ *
+ * Note that RFC 1738 also specifies the syntax for the
+ * "schemepart" for IP-based protocols (IMAP4, POP3, etc.).
+ * Providers of IP-based mail Stores should implement that
+ * syntax for referring to Folders.
+ *
+ * @param url URLName that represents the desired folder
+ * @return Folder
+ * @throws NoSuchProviderException If a provider for the given
+ * URLName is not found.
+ * @throws MessagingException if the Folder could not be
+ * located or created.
+ * @see #getStore(URLName)
+ * @see URLName
+ */
+ public Folder getFolder(URLName url) throws MessagingException {
+ // First get the Store
+ Store store = getStore(url);
+ store.connect();
+ return store.getFolder(url);
+ }
+
+ /**
+ * Get a Transport object that implements this user's desired
+ * Transport protcol. The mail.transport.protocol property
+ * specifies the desired protocol. If an appropriate Transport
+ * object cannot be obtained, MessagingException is thrown.
+ *
+ * @return a Transport object
+ * @throws NoSuchProviderException If the provider is not found.
+ */
+ public Transport getTransport() throws NoSuchProviderException {
+ String prot = getProperty("mail.transport.protocol");
+ if (prot != null)
+ return getTransport(prot);
+ // if the property isn't set, use the protocol for "rfc822"
+ prot = (String) addressMap.get("rfc822");
+ if (prot != null)
+ return getTransport(prot);
+ return getTransport("smtp"); // if all else fails
+ }
+
+ /**
+ * Get a Transport object that implements the specified protocol.
+ * If an appropriate Transport object cannot be obtained, null is
+ * returned.
+ *
+ * @param protocol the Transport protocol
+ * @return a Transport object
+ * @throws NoSuchProviderException If provider for the given
+ * protocol is not found.
+ */
+ public Transport getTransport(String protocol)
+ throws NoSuchProviderException {
+ return getTransport(new URLName(protocol, null, -1, null, null, null));
+ }
+
+ /**
+ * Get a Transport object for the given URLName. If the requested
+ * Transport object cannot be obtained, NoSuchProviderException is thrown.
+ *
+ * The "scheme" part of the URL string (Refer RFC 1738) is used
+ * to locate the Transport protocol.
+ *
+ * @param url URLName that represents the desired Transport
+ * @return a closed Transport object
+ * @throws NoSuchProviderException If a provider for the given
+ * URLName is not found.
+ * @see URLName
+ */
+ public Transport getTransport(URLName url) throws NoSuchProviderException {
+ String protocol = url.getProtocol();
+ Provider p = getProvider(protocol);
+ return getTransport(p, url);
+ }
+
+ /**
+ * Get an instance of the transport specified in the Provider. Instantiates
+ * the transport and returns it.
+ *
+ * @param provider Transport Provider that will be instantiated
+ * @return Instantiated Transport
+ * @throws NoSuchProviderException If provider for the given
+ * provider is not found.
+ */
+ public Transport getTransport(Provider provider) throws NoSuchProviderException {
+ return getTransport(provider, null);
+ }
+
+ /**
+ * Get a Transport object that can transport a Message of the
+ * specified address type.
+ *
+ * @param address an address for which a Transport is needed
+ * @return A Transport object
+ * @throws NoSuchProviderException If provider for the
+ * Address type is not found
+ * @see Address
+ */
+ public Transport getTransport(Address address) throws NoSuchProviderException {
+
+ String transportProtocol;
+ transportProtocol =
+ getProperty("mail.transport.protocol." + address.getType());
+ if (transportProtocol != null)
+ return getTransport(transportProtocol);
+ transportProtocol = (String) addressMap.get(address.getType());
+ if (transportProtocol != null)
+ return getTransport(transportProtocol);
+ throw new NoSuchProviderException("No provider for Address type: " + address.getType());
+ }
+
+ /**
+ * Get a Transport object using the given provider and urlname.
+ *
+ * @param provider the provider to use
+ * @param url urlname to use (can be null)
+ * @return A Transport object
+ * @throws NoSuchProviderException If no provider or the provider
+ * was the wrong class.
+ */
+
+ private Transport getTransport(Provider provider, URLName url) throws NoSuchProviderException {
+ // make sure we have the correct type of provider
+ if (provider == null || provider.getType() != Provider.Type.TRANSPORT) {
+ throw new NoSuchProviderException("invalid provider");
+ }
+
+ return getService(provider, url, Transport.class);
+ }
+
+ /**
+ * Get a Service object. Needs a provider object, but will
+ * create a URLName if needed. It attempts to instantiate
+ * the correct class.
+ *
+ * @param provider which provider to use
+ * @param url which URLName to use (can be null)
+ * @param type the service type (class)
+ * @throws NoSuchProviderException thrown when the class cannot be
+ * found or when it does not have the correct constructor
+ * (Session, URLName), or if it is not derived from
+ * Service.
+ */
+ private T getService(Provider provider, URLName url, Class type) throws NoSuchProviderException {
+ // need a provider and url
+ if (provider == null) {
+ throw new NoSuchProviderException("null");
+ }
+
+ // create a url if needed
+ if (url == null) {
+ url = new URLName(provider.getProtocol(), null, -1,
+ null, null, null);
+ }
+
+ // get the ClassLoader associated with the Authenticator
+ Class> acl;
+ if (authenticator != null)
+ acl = authenticator.getClass();
+ else
+ acl = streamProvider.getClass();
+
+ Class> serviceClass = null;
+ for (ClassLoader l : getClassLoaders(Thread.class,
+ provider.getClass(),
+ acl,
+ streamProvider.getClass(),
+ getClass(),
+ System.class)) {
+ try {
+ //load and verify provider is compatible in this classloader
+ serviceClass = Class.forName(provider.getClassName(),
+ false, l).asSubclass(type);
+ break;
+ } catch (ClassNotFoundException | ClassCastException ex) {
+ // ignore it
+ }
+ }
+
+ if (serviceClass == null) {
+ // That didn't work, now try the "system" class loader.
+ // (Need both of these because JDK 1.1 class loaders
+ // may not delegate to their parent class loader.)
+ try {
+ serviceClass = Class.forName(provider.getClassName())
+ .asSubclass(type);
+ } catch (Exception ex) {
+ // Nothing worked, give up.
+ logger.log(Level.FINE, "Exception loading provider", ex);
+ throw new NoSuchProviderException(provider.getProtocol());
+ }
+ }
+
+ // construct an instance of the class
+ try {
+ Class>[] c = {Session.class, URLName.class};
+ Constructor> cons = serviceClass.getConstructor(c);
+
+ Object[] o = {this, url};
+ return type.cast(cons.newInstance(o));
+ } catch (Exception ex) {
+ logger.log(Level.FINE, "Exception loading provider", ex);
+ throw new NoSuchProviderException(provider.getProtocol());
+ }
+ }
+
+ /**
+ * Save a PasswordAuthentication for this (store or transport) URLName.
+ * If pw is null the entry corresponding to the URLName is removed.
+ *
+ * This is normally used only by the store or transport implementations
+ * to allow authentication information to be shared among multiple
+ * uses of a session.
+ *
+ * @param url the URLName
+ * @param pw the PasswordAuthentication to save
+ */
+ public void setPasswordAuthentication(URLName url, PasswordAuthentication pw) {
+ if (pw == null)
+ authTable.remove(url);
+ else
+ authTable.put(url, pw);
+ }
+
+ /**
+ * Return any saved PasswordAuthentication for this (store or transport)
+ * URLName. Normally used only by store or transport implementations.
+ *
+ * @param url the URLName
+ * @return the PasswordAuthentication corresponding to the URLName
+ */
+ public PasswordAuthentication getPasswordAuthentication(URLName url) {
+ return authTable.get(url);
+ }
+
+ /**
+ * Call back to the application to get the needed user name and password.
+ * The application should put up a dialog something like:
+ *
+ * Connecting to <protocol> mail service on host <addr>, port <port>.
+ * <prompt>
+ *
+ * User Name: <defaultUserName>
+ * Password:
+ *
+ *
+ * @param addr InetAddress of the host. may be null.
+ * @param port the port on the host
+ * @param protocol protocol scheme (e.g. imap, pop3, etc.)
+ * @param prompt any additional String to show as part of
+ * the prompt; may be null.
+ * @param defaultUserName the default username. may be null.
+ * @return the authentication which was collected by the authenticator;
+ * may be null.
+ */
+ public PasswordAuthentication requestPasswordAuthentication(InetAddress addr, int port, String protocol, String prompt, String defaultUserName) {
+ if (authenticator != null) {
+ return authenticator.requestPasswordAuthentication(
+ addr, port, protocol, prompt, defaultUserName);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the Properties object associated with this Session
+ *
+ * @return Properties object
+ */
+ public Properties getProperties() {
+ return props;
+ }
+
+ /**
+ * Returns the value of the specified property. Returns null
+ * if this property does not exist.
+ *
+ * @param name the property name
+ * @return String that is the property value
+ */
+ public String getProperty(String name) {
+ return props.getProperty(name);
+ }
+
+ /**
+ * Load the protocol providers config files.
+ */
+ private void loadProviders(Class> cl) {
+ StreamLoader loader = new StreamLoader() {
+ @Override
+ public void load(InputStream is) throws IOException {
+ loadProvidersFromStream(is);
+ }
+ };
+
+ // load system-wide javamail.providers from the
+ // /{conf,lib} directory
+ if (confDir != null)
+ loadFile(confDir + "javamail.providers", loader);
+
+ //Fetch classloader of given class, falling back to others if needed.
+ ClassLoader gcl;
+ ClassLoader[] loaders = getClassLoaders(cl, Thread.class, System.class);
+ if (loaders.length != 0) {
+ gcl = loaders[0];
+ } else {
+ gcl = getClass().getClassLoader(); //getContextClassLoader(); //Fail safe
+ }
+
+ // next, add all the non-default services
+ ServiceLoader sl = ServiceLoader.load(Provider.class, gcl);
+ for (Provider p : sl) {
+ if (!containsDefaultProvider(p))
+ addProvider(p);
+ }
+
+ // load the META-INF/javamail.providers file supplied by an application
+ loadAllResources("META-INF/javamail.providers", cl, loader);
+
+ // load default META-INF/javamail.default.providers from mail.jar file
+ loadResource("META-INF/javamail.default.providers", cl, loader, false);
+
+ // finally, add all the default services
+ sl = ServiceLoader.load(Provider.class, gcl);
+ for (Provider p : sl) {
+ if (containsDefaultProvider(p))
+ addProvider(p);
+ }
+
+ /*
+ * If we haven't loaded any providers, fake it.
+ */
+ if (providers.isEmpty()) {
+ logger.config("failed to load any providers, using defaults");
+ // failed to load any providers, initialize with our defaults
+ addProvider(new Provider(Provider.Type.STORE,
+ "imap", "org.xbib.net.mail.imap.IMAPStore",
+ "xbib", ""));
+ addProvider(new Provider(Provider.Type.STORE,
+ "imaps", "org.xbib.net.mail.imap.IMAPSSLStore",
+ "xbib", ""));
+ addProvider(new Provider(Provider.Type.STORE,
+ "pop3", "org.xbib.net.mail.pop3.POP3Store",
+ "xbib", ""));
+ addProvider(new Provider(Provider.Type.STORE,
+ "pop3s", "org.xbib.net.mail.pop3.POP3SSLStore",
+ "xbib", ""));
+ addProvider(new Provider(Provider.Type.TRANSPORT,
+ "smtp", "org.xbib.net.mail.smtp.SMTPTransport",
+ "xbib", ""));
+ addProvider(new Provider(Provider.Type.TRANSPORT,
+ "smtps", "org.xbib.net.mail.smtp.SMTPSSLTransport",
+ "xbib", ""));
+ }
+
+ if (logger.isLoggable(Level.CONFIG)) {
+ // dump the output of the tables for debugging
+ logger.config("Tables of loaded providers");
+ logger.config("Providers Listed By Class Name: " +
+ providersByClassName);
+ logger.config("Providers Listed By Protocol: " +
+ providersByProtocol);
+ }
+ }
+
+ /*
+ * Following are security related methods that work on JDK 1.2 or newer.
+ */
+
+ private void loadProvidersFromStream(InputStream is) throws IOException {
+ if (is != null) {
+ LineInputStream lis = streamProvider.inputLineStream(is, false);
+ String currLine;
+
+ // load and process one line at a time using LineInputStream
+ while ((currLine = lis.readLine()) != null) {
+
+ if (currLine.startsWith("#"))
+ continue;
+ if (currLine.trim().isEmpty())
+ continue; // skip blank line
+ Provider.Type type = null;
+ String protocol = null, className = null;
+ String vendor = null, version = null;
+
+ // separate line into key-value tuples
+ StringTokenizer tuples = new StringTokenizer(currLine, ";");
+ while (tuples.hasMoreTokens()) {
+ String currTuple = tuples.nextToken().trim();
+
+ // set the value of each attribute based on its key
+ int sep = currTuple.indexOf("=");
+ if (currTuple.startsWith("protocol=")) {
+ protocol = currTuple.substring(sep + 1);
+ } else if (currTuple.startsWith("type=")) {
+ String strType = currTuple.substring(sep + 1);
+ if (strType.equalsIgnoreCase("store")) {
+ type = Provider.Type.STORE;
+ } else if (strType.equalsIgnoreCase("transport")) {
+ type = Provider.Type.TRANSPORT;
+ }
+ } else if (currTuple.startsWith("class=")) {
+ className = currTuple.substring(sep + 1);
+ } else if (currTuple.startsWith("vendor=")) {
+ vendor = currTuple.substring(sep + 1);
+ } else if (currTuple.startsWith("version=")) {
+ version = currTuple.substring(sep + 1);
+ }
+ }
+
+ // check if a valid Provider; else, continue
+ if (type == null || protocol == null || className == null
+ || protocol.length() == 0 || className.length() == 0) {
+
+ logger.log(Level.CONFIG, "Bad provider entry: {0}",
+ currLine);
+ continue;
+ }
+ Provider provider = new Provider(type, protocol, className,
+ vendor, version);
+
+ // add the newly-created Provider to the lookup tables
+ addProvider(provider);
+ }
+ }
+ }
+
+ /**
+ * Add a provider to the session.
+ *
+ * @param provider the provider to add
+ * @since JavaMail 1.4
+ */
+ public synchronized void addProvider(Provider provider) {
+ providers.add(provider);
+ providersByClassName.put(provider.getClassName(), provider);
+ if (!providersByProtocol.containsKey(provider.getProtocol()))
+ providersByProtocol.put(provider.getProtocol(), provider);
+ }
+
+ // load maps in reverse order of preference so that the preferred
+ // map is loaded last since its entries will override the previous ones
+ private void loadAddressMap(Class> cl) {
+ StreamLoader loader = addressMap::load;
+
+ // load default META-INF/javamail.default.address.map from mail.jar
+ loadResource("META-INF/javamail.default.address.map", cl, loader, true);
+
+ // load the META-INF/javamail.address.map file supplied by an app
+ loadAllResources("META-INF/javamail.address.map", cl, loader);
+
+ // load system-wide javamail.address.map from the
+ // /{conf,lib} directory
+ if (confDir != null)
+ loadFile(confDir + "javamail.address.map", loader);
+
+ if (addressMap.isEmpty()) {
+ logger.config("failed to load address map, using defaults");
+ addressMap.put("rfc822", "smtp");
+ }
+ }
+
+ /**
+ * Set the default transport protocol to use for addresses of
+ * the specified type. Normally the default is set by the
+ * javamail.default.address.map or
+ * javamail.address.map files or resources.
+ *
+ * @param addresstype type of address
+ * @param protocol name of protocol
+ * @see #getTransport(Address)
+ * @since JavaMail 1.4
+ */
+ public synchronized void setProtocolForAddress(String addresstype, String protocol) {
+ if (protocol == null)
+ addressMap.remove(addresstype);
+ else
+ addressMap.put(addresstype, protocol);
+ }
+
+ /**
+ * Load from the named file.
+ */
+ private void loadFile(String name, StreamLoader loader) {
+ InputStream clis = null;
+ try {
+ clis = new BufferedInputStream(new FileInputStream(name));
+ loader.load(clis);
+ logger.log(Level.CONFIG, "successfully loaded file: {0}", name);
+ } catch (FileNotFoundException fex) {
+ // ignore it
+ } catch (IOException e) {
+ if (logger.isLoggable(Level.CONFIG))
+ logger.log(Level.CONFIG, "not loading file: " + name, e);
+ } finally {
+ try {
+ if (clis != null)
+ clis.close();
+ } catch (IOException ex) {
+ } // ignore it
+ }
+ }
+
+ /**
+ * Load from the named resource.
+ */
+ private void loadResource(String name, Class> cl, StreamLoader loader, boolean expected) {
+ try (InputStream clis = getResourceAsStream(cl, name)) {
+ if (clis != null) {
+ loader.load(clis);
+ logger.log(Level.CONFIG, "successfully loaded resource: {0}",
+ name);
+ } else {
+ if (expected)
+ logger.log(Level.WARNING,
+ "expected resource not found: {0}", name);
+ }
+ } catch (IOException e) {
+ logger.log(Level.CONFIG, "Exception loading resource", e);
+ }
+ // ignore it
+ }
+
+ /**
+ * Load all of the named resource.
+ */
+ private void loadAllResources(String name, Class> cl, StreamLoader loader) {
+ boolean anyLoaded = false;
+ try {
+ URL[] urls;
+ ClassLoader cld = cl.getClassLoader();
+ if (cld != null)
+ urls = getResources(cld, name);
+ else
+ urls = getSystemResources(name);
+ if (urls != null) {
+ for (URL url : urls) {
+ logger.log(Level.CONFIG, "URL {0}", url);
+ try (InputStream clis = openStream(url)) {
+ if (clis != null) {
+ loader.load(clis);
+ anyLoaded = true;
+ logger.log(Level.CONFIG,
+ "successfully loaded resource: {0}", url);
+ } else {
+ logger.log(Level.CONFIG,
+ "not loading resource: {0}", url);
+ }
+ } catch (FileNotFoundException fex) {
+ // ignore it
+ } catch (IOException ioex) {
+ logger.log(Level.CONFIG, "Exception loading resource",
+ ioex);
+ }
+ }
+ }
+ } catch (Exception ex) {
+ logger.log(Level.CONFIG, "Exception loading resource", ex);
+ }
+
+ // if failed to load anything, fall back to old technique, just in case
+ if (!anyLoaded) {
+ /*
+ logger.config("!anyLoaded");
+ */
+ loadResource("/" + name, cl, loader, false);
+ }
+ }
+
+ EventQueue getEventQueue() {
+ return q;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Store.java b/net-mail/src/main/java/jakarta/mail/Store.java
new file mode 100644
index 0000000..b2b913a
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Store.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.mail.event.FolderEvent;
+import jakarta.mail.event.FolderListener;
+import jakarta.mail.event.StoreEvent;
+import jakarta.mail.event.StoreListener;
+import java.util.Vector;
+
+/**
+ * An abstract class that models a message store and its
+ * access protocol, for storing and retrieving messages.
+ * Subclasses provide actual implementations.
+ *
+ * Note that Store extends the Service
+ * class, which provides many common methods for naming stores,
+ * connecting to stores, and listening to connection events.
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ * @see Service
+ * @see jakarta.mail.event.ConnectionEvent
+ * @see StoreEvent
+ */
+
+public abstract class Store extends Service {
+
+ // Vector of Store listeners
+ private volatile Vector storeListeners = null;
+ // Vector of folder listeners
+ private volatile Vector folderListeners = null;
+
+ /**
+ * Constructor.
+ *
+ * @param session Session object for this Store.
+ * @param urlname URLName object to be used for this Store
+ */
+ protected Store(Session session, URLName urlname) {
+ super(session, urlname);
+ }
+
+ /**
+ * Returns a Folder object that represents the 'root' of
+ * the default namespace presented to the user by the Store.
+ *
+ * @return the root Folder
+ * @throws IllegalStateException if this Store is not connected.
+ * @throws MessagingException for other failures
+ */
+ public abstract Folder getDefaultFolder() throws MessagingException;
+
+ /**
+ * Return the Folder object corresponding to the given name. Note
+ * that a Folder object is returned even if the named folder does
+ * not physically exist on the Store. The exists()
+ * method on the folder object indicates whether this folder really
+ * exists.
+ *
+ * Folder objects are not cached by the Store, so invoking this
+ * method on the same name multiple times will return that many
+ * distinct Folder objects.
+ *
+ * @param name The name of the Folder. In some Stores, name can
+ * be an absolute path if it starts with the
+ * hierarchy delimiter. Else it is interpreted
+ * relative to the 'root' of this namespace.
+ * @return Folder object
+ * @throws IllegalStateException if this Store is not connected.
+ * @throws MessagingException for other failures
+ * @see Folder#create
+ * @see Folder#exists
+ */
+ public abstract Folder getFolder(String name)
+ throws MessagingException;
+
+ /**
+ * Return a closed Folder object, corresponding to the given
+ * URLName. The store specified in the given URLName should
+ * refer to this Store object.
+ *
+ * Implementations of this method may obtain the name of the
+ * actual folder using the getFile() method on
+ * URLName, and use that name to create the folder.
+ *
+ * @param url URLName that denotes a folder
+ * @return Folder object
+ * @throws IllegalStateException if this Store is not connected.
+ * @throws MessagingException for other failures
+ * @see URLName
+ */
+ public abstract Folder getFolder(URLName url)
+ throws MessagingException;
+
+ /**
+ * Return a set of folders representing the personal namespaces
+ * for the current user. A personal namespace is a set of names that
+ * is considered within the personal scope of the authenticated user.
+ * Typically, only the authenticated user has access to mail folders
+ * in their personal namespace. If an INBOX exists for a user, it
+ * must appear within the user's personal namespace. In the
+ * typical case, there should be only one personal namespace for each
+ * user in each Store.
+ *
+ * This implementation returns an array with a single entry containing
+ * the return value of the getDefaultFolder method.
+ * Subclasses should override this method to return appropriate information.
+ *
+ * @return array of Folder objects
+ * @throws IllegalStateException if this Store is not connected.
+ * @throws MessagingException for other failures
+ * @since JavaMail 1.2
+ */
+ public Folder[] getPersonalNamespaces() throws MessagingException {
+ return new Folder[]{getDefaultFolder()};
+ }
+
+ /**
+ * Return a set of folders representing the namespaces for
+ * user. The namespaces returned represent the
+ * personal namespaces for the user. To access mail folders in the
+ * other user's namespace, the currently authenticated user must be
+ * explicitly granted access rights. For example, it is common for
+ * a manager to grant to their secretary access rights to their
+ * mail folders.
+ *
+ * This implementation returns an empty array. Subclasses should
+ * override this method to return appropriate information.
+ *
+ * @param user the user name
+ * @return array of Folder objects
+ * @throws IllegalStateException if this Store is not connected.
+ * @throws MessagingException for other failures
+ * @since JavaMail 1.2
+ */
+ public Folder[] getUserNamespaces(String user)
+ throws MessagingException {
+ return new Folder[0];
+ }
+
+ /**
+ * Return a set of folders representing the shared namespaces.
+ * A shared namespace is a namespace that consists of mail folders
+ * that are intended to be shared amongst users and do not exist
+ * within a user's personal namespace.
+ *
+ * This implementation returns an empty array. Subclasses should
+ * override this method to return appropriate information.
+ *
+ * @return array of Folder objects
+ * @throws IllegalStateException if this Store is not connected.
+ * @throws MessagingException for other failures
+ * @since JavaMail 1.2
+ */
+ public Folder[] getSharedNamespaces() throws MessagingException {
+ return new Folder[0];
+ }
+
+ /**
+ * Add a listener for StoreEvents on this Store.
+ *
+ * The default implementation provided here adds this listener
+ * to an internal list of StoreListeners.
+ *
+ * @param l the Listener for Store events
+ * @see StoreEvent
+ */
+ public synchronized void addStoreListener(StoreListener l) {
+ if (storeListeners == null)
+ storeListeners = new Vector<>();
+ storeListeners.addElement(l);
+ }
+
+ /**
+ * Remove a listener for Store events.
+ *
+ * The default implementation provided here removes this listener
+ * from the internal list of StoreListeners.
+ *
+ * @param l the listener
+ * @see #addStoreListener
+ */
+ public synchronized void removeStoreListener(StoreListener l) {
+ if (storeListeners != null)
+ storeListeners.removeElement(l);
+ }
+
+ /**
+ * Notify all StoreListeners. Store implementations are
+ * expected to use this method to broadcast StoreEvents.
+ *
+ * The provided default implementation queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the registered
+ * StoreListeners. Note that the event dispatching occurs
+ * in a separate thread, thus avoiding potential deadlock problems.
+ *
+ * @param type the StoreEvent type
+ * @param message a message for the StoreEvent
+ */
+ protected void notifyStoreListeners(int type, String message) {
+ if (storeListeners == null)
+ return;
+
+ StoreEvent e = new StoreEvent(this, type, message);
+ queueEvent(e, storeListeners);
+ }
+
+ /**
+ * Add a listener for Folder events on any Folder object
+ * obtained from this Store. FolderEvents are delivered to
+ * FolderListeners on the affected Folder as well as to
+ * FolderListeners on the containing Store.
+ *
+ * The default implementation provided here adds this listener
+ * to an internal list of FolderListeners.
+ *
+ * @param l the Listener for Folder events
+ * @see FolderEvent
+ */
+ public synchronized void addFolderListener(FolderListener l) {
+ if (folderListeners == null)
+ folderListeners = new Vector<>();
+ folderListeners.addElement(l);
+ }
+
+ /**
+ * Remove a listener for Folder events.
+ *
+ * The default implementation provided here removes this listener
+ * from the internal list of FolderListeners.
+ *
+ * @param l the listener
+ * @see #addFolderListener
+ */
+ public synchronized void removeFolderListener(FolderListener l) {
+ if (folderListeners != null)
+ folderListeners.removeElement(l);
+ }
+
+ /**
+ * Notify all FolderListeners. Store implementations are
+ * expected to use this method to broadcast Folder events.
+ *
+ * The provided default implementation queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the registered
+ * FolderListeners. Note that the event dispatching occurs
+ * in a separate thread, thus avoiding potential deadlock problems.
+ *
+ * @param type type of FolderEvent
+ * @param folder affected Folder
+ * @see #notifyFolderRenamedListeners
+ */
+ protected void notifyFolderListeners(int type, Folder folder) {
+ if (folderListeners == null)
+ return;
+
+ FolderEvent e = new FolderEvent(this, folder, type);
+ queueEvent(e, folderListeners);
+ }
+
+ /**
+ * Notify all FolderListeners about the renaming of a folder.
+ * Store implementations are expected to use this method to broadcast
+ * Folder events indicating the renaming of folders.
+ *
+ * The provided default implementation queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the registered
+ * FolderListeners. Note that the event dispatching occurs
+ * in a separate thread, thus avoiding potential deadlock problems.
+ *
+ * @param oldF the folder being renamed
+ * @param newF the folder representing the new name.
+ * @since JavaMail 1.1
+ */
+ protected void notifyFolderRenamedListeners(Folder oldF, Folder newF) {
+ if (folderListeners == null)
+ return;
+
+ FolderEvent e = new FolderEvent(this, oldF, newF, FolderEvent.RENAMED);
+ queueEvent(e, folderListeners);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/StoreClosedException.java b/net-mail/src/main/java/jakarta/mail/StoreClosedException.java
new file mode 100644
index 0000000..031490d
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/StoreClosedException.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+/**
+ * This exception is thrown when a method is invoked on a Messaging object
+ * and the Store that owns that object has died due to some reason.
+ * This exception should be treated as a fatal error; in particular any
+ * messaging object belonging to that Store must be considered invalid.
+ *
+ * The connect method may be invoked on the dead Store object to
+ * revive it.
+ *
+ * The getMessage() method returns more detailed information about the
+ * error that caused this exception.
+ *
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class StoreClosedException extends MessagingException {
+ transient private Store store;
+
+ /**
+ * Constructs a StoreClosedException with no detail message.
+ *
+ * @param store The dead Store object
+ */
+ public StoreClosedException(Store store) {
+ this(store, null);
+ }
+
+ /**
+ * Constructs a StoreClosedException with the specified
+ * detail message.
+ *
+ * @param store The dead Store object
+ * @param message The detailed error message
+ */
+ public StoreClosedException(Store store, String message) {
+ super(message);
+ this.store = store;
+ }
+
+ /**
+ * Constructs a StoreClosedException with the specified
+ * detail message and embedded exception. The exception is chained
+ * to this exception.
+ *
+ * @param store The dead Store object
+ * @param message The detailed error message
+ * @param e The embedded exception
+ * @since JavaMail 1.5
+ */
+ public StoreClosedException(Store store, String message, Exception e) {
+ super(message, e);
+ this.store = store;
+ }
+
+ /**
+ * Returns the dead Store object.
+ *
+ * @return the dead Store object
+ */
+ public Store getStore() {
+ return store;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/Transport.java b/net-mail/src/main/java/jakarta/mail/Transport.java
new file mode 100644
index 0000000..645640d
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/Transport.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import jakarta.mail.event.TransportEvent;
+import jakarta.mail.event.TransportListener;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+ * An abstract class that models a message transport.
+ * Subclasses provide actual implementations.
+ *
+ * Note that Transport extends the Service
+ * class, which provides many common methods for naming transports,
+ * connecting to transports, and listening to connection events.
+ *
+ * @author John Mani
+ * @author Max Spivak
+ * @author Bill Shannon
+ * @see Service
+ * @see jakarta.mail.event.ConnectionEvent
+ * @see TransportEvent
+ */
+
+public abstract class Transport extends Service {
+
+ // Vector of Transport listeners
+ private volatile Vector transportListeners = null;
+
+ /**
+ * Constructor.
+ *
+ * @param session Session object for this Transport.
+ * @param urlname URLName object to be used for this Transport
+ */
+ public Transport(Session session, URLName urlname) {
+ super(session, urlname);
+ }
+
+ /**
+ * Send a message. The message will be sent to all recipient
+ * addresses specified in the message (as returned from the
+ * Message method getAllRecipients),
+ * using message transports appropriate to each address. The
+ * send method calls the saveChanges
+ * method on the message before sending it.
+ *
+ * If any of the recipient addresses is detected to be invalid by
+ * the Transport during message submission, a SendFailedException
+ * is thrown. Clients can get more detail about the failure by examining
+ * the exception. Whether or not the message is still sent successfully
+ * to any valid addresses depends on the Transport implementation. See
+ * SendFailedException for more details. Note also that success does
+ * not imply that the message was delivered to the ultimate recipient,
+ * as failures may occur in later stages of delivery. Once a Transport
+ * accepts a message for delivery to a recipient, failures that occur later
+ * should be reported to the user via another mechanism, such as
+ * returning the undeliverable message.
+ *
+ * In typical usage, a SendFailedException reflects an error detected
+ * by the server. The details of the SendFailedException will usually
+ * contain the error message from the server (such as an SMTP error
+ * message). An address may be detected as invalid for a variety of
+ * reasons - the address may not exist, the address may have invalid
+ * syntax, the address may have exceeded its quota, etc.
+ *
+ * Note that send is a static method that creates and
+ * manages its own connection. Any connection associated with any
+ * Transport instance used to invoke this method is ignored and not
+ * used. This method should only be invoked using the form
+ * Transport.send(msg);, and should never be invoked
+ * using an instance variable.
+ *
+ * @param msg the message to send
+ * @throws SendFailedException if the message could not
+ * be sent to some or any of the recipients.
+ * @throws MessagingException for other failures
+ * @see Message#saveChanges
+ * @see Message#getAllRecipients
+ * @see #send(Message, Address[])
+ * @see SendFailedException
+ */
+ public static void send(Message msg) throws MessagingException {
+ msg.saveChanges(); // do this first
+ send0(msg, msg.getAllRecipients(), null, null);
+ }
+
+ /**
+ * Send the message to the specified addresses, ignoring any
+ * recipients specified in the message itself. The
+ * send method calls the saveChanges
+ * method on the message before sending it.
+ *
+ * @param msg the message to send
+ * @param addresses the addresses to which to send the message
+ * @throws SendFailedException if the message could not
+ * be sent to some or any of the recipients.
+ * @throws MessagingException for other failures
+ * @see Message#saveChanges
+ * @see SendFailedException
+ * @see #send(Message)
+ */
+ public static void send(Message msg, Address[] addresses)
+ throws MessagingException {
+
+ msg.saveChanges();
+ send0(msg, addresses, null, null);
+ }
+
+ /**
+ * Send a message. The message will be sent to all recipient
+ * addresses specified in the message (as returned from the
+ * Message method getAllRecipients).
+ * The send method calls the saveChanges
+ * method on the message before sending it.
+ *
+ * Use the specified user name and password to authenticate to
+ * the mail server.
+ *
+ * @param msg the message to send
+ * @param user the user name
+ * @param password this user's password
+ * @throws SendFailedException if the message could not
+ * be sent to some or any of the recipients.
+ * @throws MessagingException for other failures
+ * @see Message#saveChanges
+ * @see SendFailedException
+ * @see #send(Message)
+ * @since JavaMail 1.5
+ */
+ public static void send(Message msg,
+ String user, String password) throws MessagingException {
+
+ msg.saveChanges();
+ send0(msg, msg.getAllRecipients(), user, password);
+ }
+
+ /**
+ * Send the message to the specified addresses, ignoring any
+ * recipients specified in the message itself. The
+ * send method calls the saveChanges
+ * method on the message before sending it.
+ *
+ * Use the specified user name and password to authenticate to
+ * the mail server.
+ *
+ * @param msg the message to send
+ * @param addresses the addresses to which to send the message
+ * @param user the user name
+ * @param password this user's password
+ * @throws SendFailedException if the message could not
+ * be sent to some or any of the recipients.
+ * @throws MessagingException for other failures
+ * @see Message#saveChanges
+ * @see SendFailedException
+ * @see #send(Message)
+ * @since JavaMail 1.5
+ */
+ public static void send(Message msg, Address[] addresses,
+ String user, String password) throws MessagingException {
+
+ msg.saveChanges();
+ send0(msg, addresses, user, password);
+ }
+
+ // send, but without the saveChanges
+ private static void send0(Message msg, Address[] addresses,
+ String user, String password) throws MessagingException {
+
+ if (addresses == null || addresses.length == 0)
+ throw new SendFailedException("No recipient addresses");
+
+ /*
+ * protocols is a map containing the addresses
+ * indexed by address type
+ */
+ Map> protocols
+ = new HashMap<>();
+
+ // Lists of addresses
+ List invalid = new ArrayList<>();
+ List validSent = new ArrayList<>();
+ List validUnsent = new ArrayList<>();
+
+ for (int i = 0; i < addresses.length; i++) {
+ // is this address type already in the map?
+ if (protocols.containsKey(addresses[i].getType())) {
+ List v = protocols.get(addresses[i].getType());
+ v.add(addresses[i]);
+ } else {
+ // need to add a new protocol
+ List w = new ArrayList<>();
+ w.add(addresses[i]);
+ protocols.put(addresses[i].getType(), w);
+ }
+ }
+
+ int dsize = protocols.size();
+ if (dsize == 0)
+ throw new SendFailedException("No recipient addresses");
+
+ Session s = (msg.session != null) ? msg.session :
+ Session.getDefaultInstance(System.getProperties(), null);
+ Transport transport;
+
+ /*
+ * Optimize the case of a single protocol.
+ */
+ if (dsize == 1) {
+ transport = s.getTransport(addresses[0]);
+ try {
+ if (user != null)
+ transport.connect(user, password);
+ else
+ transport.connect();
+ transport.sendMessage(msg, addresses);
+ } finally {
+ transport.close();
+ }
+ return;
+ }
+
+ /*
+ * More than one protocol. Have to do them one at a time
+ * and collect addresses and chain exceptions.
+ */
+ MessagingException chainedEx = null;
+ boolean sendFailed = false;
+
+ for (List v : protocols.values()) {
+ Address[] protaddresses = new Address[v.size()];
+ v.toArray(protaddresses);
+
+ // Get a Transport that can handle this address type.
+ if ((transport = s.getTransport(protaddresses[0])) == null) {
+ // Could not find an appropriate Transport ..
+ // Mark these addresses invalid.
+ Collections.addAll(invalid, protaddresses);
+ continue;
+ }
+ try {
+ transport.connect();
+ transport.sendMessage(msg, protaddresses);
+ } catch (SendFailedException sex) {
+ sendFailed = true;
+ // chain the exception we're catching to any previous ones
+ if (chainedEx == null)
+ chainedEx = sex;
+ else
+ chainedEx.setNextException(sex);
+
+ // retrieve invalid addresses
+ Address[] a = sex.getInvalidAddresses();
+ if (a != null)
+ Collections.addAll(invalid, a);
+
+ // retrieve validSent addresses
+ a = sex.getValidSentAddresses();
+ if (a != null)
+ Collections.addAll(validSent, a);
+
+ // retrieve validUnsent addresses
+ Address[] c = sex.getValidUnsentAddresses();
+ if (c != null)
+ Collections.addAll(validUnsent, c);
+ } catch (MessagingException mex) {
+ sendFailed = true;
+ // chain the exception we're catching to any previous ones
+ if (chainedEx == null)
+ chainedEx = mex;
+ else
+ chainedEx.setNextException(mex);
+ } finally {
+ transport.close();
+ }
+ }
+
+ // done with all protocols. throw exception if something failed
+ if (sendFailed || invalid.size() != 0 || validUnsent.size() != 0) {
+ Address[] a = null, b = null, c = null;
+
+ // copy address lists into arrays
+ if (validSent.size() > 0) {
+ a = new Address[validSent.size()];
+ validSent.toArray(a);
+ }
+ if (validUnsent.size() > 0) {
+ b = new Address[validUnsent.size()];
+ validUnsent.toArray(b);
+ }
+ if (invalid.size() > 0) {
+ c = new Address[invalid.size()];
+ invalid.toArray(c);
+ }
+ throw new SendFailedException("Sending failed", chainedEx,
+ a, b, c);
+ }
+ }
+
+ /**
+ * Send the Message to the specified list of addresses. An appropriate
+ * TransportEvent indicating the delivery status is delivered to any
+ * TransportListener registered on this Transport. Also, if any of
+ * the addresses is invalid, a SendFailedException is thrown.
+ * Whether or not the message is still sent succesfully to
+ * any valid addresses depends on the Transport implementation.
+ *
+ * Unlike the static send method, the sendMessage
+ * method does not call the saveChanges method on
+ * the message; the caller should do so.
+ *
+ * @param msg The Message to be sent
+ * @param addresses array of addresses to send this message to
+ * @throws SendFailedException if the send failed because of
+ * invalid addresses.
+ * @throws MessagingException if the connection is dead or not in the
+ * connected state
+ * @see TransportEvent
+ */
+ public abstract void sendMessage(Message msg, Address[] addresses)
+ throws MessagingException;
+
+ /**
+ * Add a listener for Transport events.
+ *
+ * The default implementation provided here adds this listener
+ * to an internal list of TransportListeners.
+ *
+ * @param l the Listener for Transport events
+ * @see TransportEvent
+ */
+ public synchronized void addTransportListener(TransportListener l) {
+ if (transportListeners == null)
+ transportListeners = new Vector<>();
+ transportListeners.addElement(l);
+ }
+
+ /**
+ * Remove a listener for Transport events.
+ *
+ * The default implementation provided here removes this listener
+ * from the internal list of TransportListeners.
+ *
+ * @param l the listener
+ * @see #addTransportListener
+ */
+ public synchronized void removeTransportListener(TransportListener l) {
+ if (transportListeners != null)
+ transportListeners.removeElement(l);
+ }
+
+ /**
+ * Notify all TransportListeners. Transport implementations are
+ * expected to use this method to broadcast TransportEvents.
+ *
+ * The provided default implementation queues the event into
+ * an internal event queue. An event dispatcher thread dequeues
+ * events from the queue and dispatches them to the registered
+ * TransportListeners. Note that the event dispatching occurs
+ * in a separate thread, thus avoiding potential deadlock problems.
+ *
+ * @param type the TransportEvent type
+ * @param validSent valid addresses to which message was sent
+ * @param validUnsent valid addresses to which message was not sent
+ * @param invalid the invalid addresses
+ * @param msg the message
+ */
+ protected void notifyTransportListeners(int type, Address[] validSent,
+ Address[] validUnsent,
+ Address[] invalid, Message msg) {
+ if (transportListeners == null)
+ return;
+
+ TransportEvent e = new TransportEvent(this, type, validSent,
+ validUnsent, invalid, msg);
+ queueEvent(e, transportListeners);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/UIDFolder.java b/net-mail/src/main/java/jakarta/mail/UIDFolder.java
new file mode 100644
index 0000000..474d96a
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/UIDFolder.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import java.util.NoSuchElementException;
+
+/**
+ * The UIDFolder interface is implemented by Folders
+ * that can support the "disconnected" mode of operation, by providing
+ * unique-ids for messages in the folder. This interface is based on
+ * the IMAP model for supporting disconnected operation.
+ *
+ * A Unique identifier (UID) is a positive long value, assigned to
+ * each message in a specific folder. Unique identifiers are assigned
+ * in a strictly ascending fashion in the mailbox.
+ * That is, as each message is added to the mailbox it is assigned a
+ * higher UID than the message(s) which were added previously. Unique
+ * identifiers persist across sessions. This permits a client to
+ * resynchronize its state from a previous session with the server.
+ *
+ * Associated with every mailbox is a unique identifier validity value.
+ * If unique identifiers from an earlier session fail to persist to
+ * this session, the unique identifier validity value
+ * must be greater than the one used in the earlier
+ * session.
+ *
+ * Refer to RFC 2060
+ * for more information.
+ *
+ * All the Folder objects returned by the default IMAP provider implement
+ * the UIDFolder interface. Use it as follows:
+ *
+ *
+ * @author Bill Shannon
+ * @author John Mani
+ */
+
+public interface UIDFolder {
+
+ /**
+ * This is a special value that can be used as the end
+ * parameter in getMessagesByUID(start, end), to denote the
+ * UID of the last message in the folder.
+ *
+ * @see #getMessagesByUID
+ */
+ long LASTUID = -1;
+ /**
+ * The largest value possible for a UID, a 32-bit unsigned integer.
+ * This can be used to fetch all new messages by keeping track of the
+ * last UID that was seen and using:
+ *
+ *
+ * @since JavaMail 1.6
+ */
+ long MAXUID = 0xffffffffL; // max 32-bit unsigned int
+
+ /**
+ * Returns the UIDValidity value associated with this folder.
+ *
+ * Clients typically compare this value against a UIDValidity
+ * value saved from a previous session to insure that any cached
+ * UIDs are not stale.
+ *
+ * @return UIDValidity
+ * @throws MessagingException for failures
+ */
+ long getUIDValidity() throws MessagingException;
+
+ /**
+ * Get the Message corresponding to the given UID. If no such
+ * message exists, null is returned.
+ *
+ * @param uid UID for the desired message
+ * @return the Message object. null is returned
+ * if no message corresponding to this UID is obtained.
+ * @throws MessagingException for failures
+ */
+ Message getMessageByUID(long uid) throws MessagingException;
+
+ /**
+ * Get the Messages specified by the given range. The special
+ * value LASTUID can be used for the end parameter
+ * to indicate the UID of the last message in the folder.
+ *
+ * Note that end need not be greater than start;
+ * the order of the range doesn't matter.
+ * Note also that, unless the folder is empty, use of LASTUID ensures
+ * that at least one message will be returned - the last message in the
+ * folder.
+ *
+ * @param start start UID
+ * @param end end UID
+ * @return array of Message objects
+ * @throws MessagingException for failures
+ * @see #LASTUID
+ */
+ Message[] getMessagesByUID(long start, long end)
+ throws MessagingException;
+
+ /**
+ * Get the Messages specified by the given array of UIDs. If any UID is
+ * invalid, null is returned for that entry.
+ *
+ * Note that the returned array will be of the same size as the specified
+ * array of UIDs, and null entries may be present in the
+ * array to indicate invalid UIDs.
+ *
+ * @param uids array of UIDs
+ * @return array of Message objects
+ * @throws MessagingException for failures
+ */
+ Message[] getMessagesByUID(long[] uids)
+ throws MessagingException;
+
+ /**
+ * Get the UID for the specified message. Note that the message
+ * must belong to this folder. Otherwise
+ * java.util.NoSuchElementException is thrown.
+ *
+ * @param message Message from this folder
+ * @return UID for this message
+ * @throws NoSuchElementException if the given Message
+ * is not in this Folder.
+ * @throws MessagingException for other failures
+ */
+ long getUID(Message message) throws MessagingException;
+
+ /**
+ * Returns the predicted UID that will be assigned to the
+ * next message that is appended to this folder.
+ * Messages might be appended to the folder after this value
+ * is retrieved, causing this value to be out of date.
+ * This value might only be updated when a folder is first opened.
+ * Note that messages may have been appended to the folder
+ * while it was open and thus this value may be out of
+ * date.
+ *
+ * If the value is unknown, -1 is returned.
+ *
+ * @return the UIDNEXT value, or -1 if unknown
+ * @throws MessagingException for failures
+ * @since JavaMail 1.6
+ */
+ long getUIDNext() throws MessagingException;
+
+ /**
+ * A fetch profile item for fetching UIDs.
+ * This inner class extends the FetchProfile.Item
+ * class to add new FetchProfile item types, specific to UIDFolders.
+ * The only item currently defined here is the UID item.
+ *
+ * @see FetchProfile
+ */
+ class FetchProfileItem extends FetchProfile.Item {
+ /**
+ * UID is a fetch profile item that can be included in a
+ * FetchProfile during a fetch request to a Folder.
+ * This item indicates that the UIDs for messages in the specified
+ * range are desired to be prefetched.
+ *
+ * An example of how a client uses this is below:
+ *
+ */
+ public static final FetchProfileItem UID =
+ new FetchProfileItem("UID");
+
+ /**
+ * Constructor for an item.
+ *
+ * @param name the item name
+ */
+ protected FetchProfileItem(String name) {
+ super(name);
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/URLName.java b/net-mail/src/main/java/jakarta/mail/URLName.java
new file mode 100644
index 0000000..1fb091a
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/URLName.java
@@ -0,0 +1,774 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
+import java.util.Locale;
+import java.util.Objects;
+
+
+/**
+ * The name of a URL. This class represents a URL name and also
+ * provides the basic parsing functionality to parse most internet
+ * standard URL schemes.
+ *
+ * Note that this class differs from java.net.URL
+ * in that this class just represents the name of a URL, it does
+ * not model the connection to a URL.
+ *
+ * @author Christopher Cotton
+ * @author Bill Shannon
+ */
+
+public class URLName {
+
+ static final int caseDiff = ('a' - 'A');
+ /**
+ * The class contains a utility method for converting a
+ * String into a MIME format called
+ * "x-www-form-urlencoded" format.
+ *
+ * To convert a String, each character is examined in turn:
+ *
+ *
The ASCII characters 'a' through 'z',
+ * 'A' through 'Z', '0'
+ * through '9', and ".", "-",
+ * "*", "_" remain the same.
+ *
The space character '' is converted into a
+ * plus sign '+'.
+ *
All other characters are converted into the 3-character string
+ * "%xy", where xy is the two-digit
+ * hexadecimal representation of the lower 8-bits of the character.
+ *
+ *
+ * @author Herb Jellinek
+ * @since JDK1.0
+ */
+ static BitSet dontNeedEncoding;
+ /**
+ * A way to turn off encoding, just in case...
+ */
+ private static boolean doEncode = true;
+
+ static {
+ try {
+ doEncode = !Boolean.getBoolean("mail.URLName.dontencode");
+ } catch (Exception ex) {
+ // ignore any errors
+ }
+ }
+
+ static {
+ dontNeedEncoding = new BitSet(256);
+ int i;
+ for (i = 'a'; i <= 'z'; i++) {
+ dontNeedEncoding.set(i);
+ }
+ for (i = 'A'; i <= 'Z'; i++) {
+ dontNeedEncoding.set(i);
+ }
+ for (i = '0'; i <= '9'; i++) {
+ dontNeedEncoding.set(i);
+ }
+ /* encoding a space to a + is done in the encode() method */
+ dontNeedEncoding.set(' ');
+ dontNeedEncoding.set('-');
+ dontNeedEncoding.set('_');
+ dontNeedEncoding.set('.');
+ dontNeedEncoding.set('*');
+ }
+
+ /**
+ * The full version of the URL
+ */
+ protected String fullURL;
+ /**
+ * The protocol to use (ftp, http, nntp, imap, pop3 ... etc.) .
+ */
+ private String protocol;
+ /**
+ * The username to use when connecting
+ */
+ private String username;
+ /**
+ * The password to use when connecting.
+ */
+ private String password;
+ /**
+ * The host name to which to connect.
+ */
+ private String host;
+ /**
+ * The host's IP address, used in equals and hashCode.
+ * Computed on demand.
+ */
+ private InetAddress hostAddress;
+ private boolean hostAddressKnown = false;
+ /**
+ * The protocol port to connect to.
+ */
+ private int port = -1;
+ /**
+ * The specified file name on that host.
+ */
+ private String file;
+ /**
+ * # reference.
+ */
+ private String ref;
+ /**
+ * Our hash code.
+ */
+ private int hashCode = 0;
+
+ /**
+ * Creates a URLName object from the specified protocol,
+ * host, port number, file, username, and password. Specifying a port
+ * number of -1 indicates that the URL should use the default port for
+ * the protocol.
+ *
+ * @param protocol the protocol
+ * @param host the host name
+ * @param port the port number
+ * @param file the file
+ * @param username the user name
+ * @param password the password
+ */
+ public URLName(
+ String protocol,
+ String host,
+ int port,
+ String file,
+ String username,
+ String password
+ ) {
+ this.protocol = protocol;
+ this.host = host;
+ this.port = port;
+ int refStart;
+ if (file != null && (refStart = file.indexOf('#')) != -1) {
+ this.file = file.substring(0, refStart);
+ this.ref = file.substring(refStart + 1);
+ } else {
+ this.file = file;
+ this.ref = null;
+ }
+ this.username = doEncode ? encode(username) : username;
+ this.password = doEncode ? encode(password) : password;
+ }
+
+ /**
+ * Construct a URLName from a java.net.URL object.
+ *
+ * @param url the URL
+ */
+ public URLName(URL url) {
+ this(url.toString());
+ }
+
+ /**
+ * Construct a URLName from the string. Parses out all the possible
+ * information (protocol, host, port, file, username, password).
+ *
+ * @param url the URL string
+ */
+ public URLName(String url) {
+ parseString(url);
+ }
+
+ /**
+ * Translates a string into x-www-form-urlencoded format.
+ *
+ * @param s String to be translated.
+ * @return the translated String.
+ */
+ static String encode(String s) {
+ if (s == null)
+ return null;
+ // the common case is no encoding is needed
+ for (int i = 0; i < s.length(); i++) {
+ int c = s.charAt(i);
+ if (c == ' ' || !dontNeedEncoding.get(c))
+ return _encode(s);
+ }
+ return s;
+ }
+
+ private static String _encode(String s) {
+ int maxBytesPerChar = 10;
+ StringBuilder out = new StringBuilder(s.length());
+ ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar);
+ OutputStreamWriter writer = new OutputStreamWriter(buf);
+
+ for (int i = 0; i < s.length(); i++) {
+ int c = s.charAt(i);
+ if (dontNeedEncoding.get(c)) {
+ if (c == ' ') {
+ c = '+';
+ }
+ out.append((char) c);
+ } else {
+ // convert to external encoding before hex conversion
+ try {
+ writer.write(c);
+ writer.flush();
+ } catch (IOException e) {
+ buf.reset();
+ continue;
+ }
+ byte[] ba = buf.toByteArray();
+ for (int j = 0; j < ba.length; j++) {
+ out.append('%');
+ char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16);
+ // converting to use uppercase letter as part of
+ // the hex value if ch is a letter.
+ if (Character.isLetter(ch)) {
+ ch -= caseDiff;
+ }
+ out.append(ch);
+ ch = Character.forDigit(ba[j] & 0xF, 16);
+ if (Character.isLetter(ch)) {
+ ch -= caseDiff;
+ }
+ out.append(ch);
+ }
+ buf.reset();
+ }
+ }
+
+ return out.toString();
+ }
+
+ /**
+ * Decodes a "x-www-form-urlencoded"
+ * to a String.
+ *
+ * @param s the String to decode
+ * @return the newly decoded String
+ */
+ static String decode(String s) {
+ if (s == null)
+ return null;
+ if (indexOfAny(s, "+%") == -1)
+ return s; // the common case
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case '+':
+ sb.append(' ');
+ break;
+ case '%':
+ try {
+ sb.append((char) Integer.parseInt(
+ s.substring(i + 1, i + 3), 16));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(
+ "Illegal URL encoded value: " +
+ s.substring(i, i + 3));
+ }
+ i += 2;
+ break;
+ default:
+ sb.append(c);
+ break;
+ }
+ }
+ // Undo conversion to external encoding
+ String result = sb.toString();
+ byte[] inputBytes = result.getBytes(StandardCharsets.ISO_8859_1);
+ result = new String(inputBytes);
+ return result;
+ }
+
+ /**
+ * 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) {
+ return indexOfAny(s, any, 0);
+ }
+
+ private static int indexOfAny(String s, String any, int start) {
+ try {
+ int len = s.length();
+ for (int i = start; i < len; i++) {
+ if (any.indexOf(s.charAt(i)) >= 0)
+ return i;
+ }
+ return -1;
+ } catch (StringIndexOutOfBoundsException e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Constructs a string representation of this URLName.
+ */
+ @Override
+ public String toString() {
+ if (fullURL == null) {
+ // add the "protocol:"
+ StringBuilder tempURL = new StringBuilder();
+ if (protocol != null) {
+ tempURL.append(protocol);
+ tempURL.append(":");
+ }
+
+ if (username != null || host != null) {
+ // add the "//"
+ tempURL.append("//");
+
+ // add the user:password@
+ // XXX - can you just have a password? without a username?
+ if (username != null) {
+ tempURL.append(username);
+
+ if (password != null) {
+ tempURL.append(":");
+ tempURL.append(password);
+ }
+
+ tempURL.append("@");
+ }
+
+ // add host
+ if (host != null) {
+ tempURL.append(host);
+ }
+
+ // add port (if needed)
+ if (port != -1) {
+ tempURL.append(":");
+ tempURL.append(port);
+ }
+ if (file != null)
+ tempURL.append("/");
+ }
+
+ // add the file
+ if (file != null) {
+ tempURL.append(file);
+ }
+
+ // add the ref
+ if (ref != null) {
+ tempURL.append("#");
+ tempURL.append(ref);
+ }
+
+ // create the fullURL now
+ fullURL = tempURL.toString();
+ }
+
+ return fullURL;
+ }
+
+ /**
+ * Method which does all of the work of parsing the string.
+ *
+ * @param url the URL string to parse
+ */
+ private void parseString(String url) {
+ // initialize everything in case called from subclass
+ // (URLName really should be a final class)
+ protocol = file = ref = host = username = password = null;
+ port = -1;
+
+ int len = url.length();
+
+ // find the protocol
+ // XXX - should check for only legal characters before the colon
+ // (legal: a-z, A-Z, 0-9, "+", ".", "-")
+ int protocolEnd = url.indexOf(':');
+ if (protocolEnd != -1)
+ protocol = url.substring(0, protocolEnd);
+
+ // is this an Internet standard URL that contains a host name?
+ if (url.regionMatches(protocolEnd + 1, "//", 0, 2)) {
+ // find where the file starts
+ String fullhost = null;
+ int fileStart = url.indexOf('/', protocolEnd + 3);
+ if (fileStart != -1) {
+ fullhost = url.substring(protocolEnd + 3, fileStart);
+ if (fileStart + 1 < len)
+ file = url.substring(fileStart + 1);
+ else
+ file = "";
+ } else
+ fullhost = url.substring(protocolEnd + 3);
+
+ // examine the fullhost, for username password etc.
+ int i = fullhost.indexOf('@');
+ if (i != -1) {
+ String fulluserpass = fullhost.substring(0, i);
+ fullhost = fullhost.substring(i + 1);
+
+ // get user and password
+ int passindex = fulluserpass.indexOf(':');
+ if (passindex != -1) {
+ username = fulluserpass.substring(0, passindex);
+ password = fulluserpass.substring(passindex + 1);
+ } else {
+ username = fulluserpass;
+ }
+ }
+
+ // get the port (if there)
+ int portindex;
+ if (fullhost.length() > 0 && fullhost.charAt(0) == '[') {
+ // an IPv6 address?
+ portindex = fullhost.indexOf(':', fullhost.indexOf(']'));
+ } else {
+ portindex = fullhost.indexOf(':');
+ }
+ if (portindex != -1) {
+ String portstring = fullhost.substring(portindex + 1);
+ if (portstring.length() > 0) {
+ try {
+ port = Integer.parseInt(portstring);
+ } catch (NumberFormatException nfex) {
+ port = -1;
+ }
+ }
+
+ host = fullhost.substring(0, portindex);
+ } else {
+ host = fullhost;
+ }
+ } else {
+ if (protocolEnd + 1 < len)
+ file = url.substring(protocolEnd + 1);
+ }
+
+ // extract the reference from the file name, if any
+ int refStart;
+ if (file != null && (refStart = file.indexOf('#')) != -1) {
+ ref = file.substring(refStart + 1);
+ file = file.substring(0, refStart);
+ }
+ }
+
+ /**
+ * Returns the port number of this URLName.
+ * Returns -1 if the port is not set.
+ *
+ * @return the port number
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Returns the protocol of this URLName.
+ * Returns null if this URLName has no protocol.
+ *
+ * @return the protocol
+ */
+ public String getProtocol() {
+ return protocol;
+ }
+
+ /**
+ * Returns the file name of this URLName.
+ * Returns null if this URLName has no file name.
+ *
+ * @return the file name of this URLName
+ */
+ public String getFile() {
+ return file;
+ }
+
+ /**
+ * Returns the reference of this URLName.
+ * Returns null if this URLName has no reference.
+ *
+ * @return the reference part of the URLName
+ */
+ public String getRef() {
+ return ref;
+ }
+
+ /**
+ * Returns the host of this URLName.
+ * Returns null if this URLName has no host.
+ *
+ * @return the host name
+ */
+ public String getHost() {
+ return host;
+ }
+
+ /* The list of characters that are not encoded have been determined by
+ referencing O'Reilly's "HTML: The Definitive Guide" (page 164). */
+
+ /**
+ * Returns the user name of this URLName.
+ * Returns null if this URLName has no user name.
+ *
+ * @return the user name
+ */
+ public String getUsername() {
+ return doEncode ? decode(username) : username;
+ }
+
+ /**
+ * Returns the password of this URLName.
+ * Returns null if this URLName has no password.
+ *
+ * @return the password
+ */
+ public String getPassword() {
+ return doEncode ? decode(password) : password;
+ }
+
+ /**
+ * Constructs a URL from the URLName.
+ *
+ * @return the URL
+ * @throws MalformedURLException if the URL is malformed
+ */
+ public URL getURL() throws MalformedURLException {
+ // URL expects the file to include the separating "/"
+ String f = getFile();
+ if (f == null)
+ f = "";
+ else
+ f = "/" + f;
+ return URI.create(getProtocol() + "://" + getHost() + ":" + getPort() + "/" + f).toURL();
+ }
+
+
+ /*
+ * The class contains a utility method for converting from
+ * a MIME format called "x-www-form-urlencoded"
+ * to a String
+ *
+ * To convert to a String, each character is examined in turn:
+ *
+ *
The ASCII characters 'a' through 'z',
+ * 'A' through 'Z', and '0'
+ * through '9' remain the same.
+ *
The plus sign '+'is converted into a
+ * space character ''.
+ *
The remaining characters are represented by 3-character
+ * strings which begin with the percent sign,
+ * "%xy", where xy is the two-digit
+ * hexadecimal representation of the lower 8-bits of the character.
+ *
+ *
+ * @author Mark Chamness
+ * @author Michael McCloskey
+ * @since 1.2
+ */
+
+ /**
+ * Compares two URLNames. The result is true if and only if the
+ * argument is not null and is a URLName object that represents the
+ * same URLName as this object. Two URLName objects are equal if
+ * they have the same protocol and the same host,
+ * the same port number on the host, the same username,
+ * and the same file on the host. The fields (host, username,
+ * file) are also considered the same if they are both
+ * null.
+ *
+ * Hosts are considered equal if the names are equal (case independent)
+ * or if host name lookups for them both succeed and they both reference
+ * the same IP address.
+ *
+ * Note that URLName has no knowledge of default port numbers for
+ * particular protocols, so "imap://host" and "imap://host:143"
+ * would not compare as equal.
+ *
+ * Note also that the password field is not included in the comparison,
+ * nor is any reference field appended to the filename.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof URLName))
+ return false;
+ URLName u2 = (URLName) obj;
+
+ // compare protocols
+ if (!(Objects.equals(protocol, u2.protocol)))
+ return false;
+
+ // compare hosts
+ InetAddress a1 = getHostAddress(), a2 = u2.getHostAddress();
+ // if we have internet address for both, and they're not the same, fail
+ if (a1 != null && a2 != null) {
+ if (!a1.equals(a2))
+ return false;
+ // else, if we have host names for both, and they're not the same, fail
+ } else if (host != null && u2.host != null) {
+ if (!host.equalsIgnoreCase(u2.host))
+ return false;
+ // else, if not both null
+ } else if (host != u2.host) {
+ return false;
+ }
+ // at this point, hosts match
+
+ // compare usernames
+ if (!(Objects.equals(username, u2.username)))
+ return false;
+
+ // Forget about password since it doesn't
+ // really denote a different store.
+
+ // compare files
+ String f1 = file == null ? "" : file;
+ String f2 = u2.file == null ? "" : u2.file;
+
+ if (!f1.equals(f2))
+ return false;
+
+ // compare ports
+ if (port != u2.port)
+ return false;
+
+ // all comparisons succeeded, they're equal
+ return true;
+ }
+
+ /**
+ * Compute the hash code for this URLName.
+ */
+ @Override
+ public int hashCode() {
+ if (hashCode != 0)
+ return hashCode;
+ if (protocol != null)
+ hashCode += protocol.hashCode();
+ InetAddress addr = getHostAddress();
+ if (addr != null)
+ hashCode += addr.hashCode();
+ else if (host != null)
+ hashCode += host.toLowerCase(Locale.ENGLISH).hashCode();
+ if (username != null)
+ hashCode += username.hashCode();
+ if (file != null)
+ hashCode += file.hashCode();
+ hashCode += port;
+ return hashCode;
+ }
+
+ /**
+ * Get the IP address of our host. Look up the
+ * name the first time and remember that we've done
+ * so, whether the lookup fails or not.
+ */
+ private synchronized InetAddress getHostAddress() {
+ if (hostAddressKnown)
+ return hostAddress;
+ if (host == null)
+ return null;
+ try {
+ hostAddress = InetAddress.getByName(host);
+ } catch (UnknownHostException ex) {
+ hostAddress = null;
+ }
+ hostAddressKnown = true;
+ return hostAddress;
+ }
+
+ /*
+ // Do not remove, this is needed when testing new URL cases
+ public static void main(String[] argv) {
+ String [] testURLNames = {
+ "protocol://userid:password@host:119/file",
+ "http://funny/folder/file.html",
+ "http://funny/folder/file.html#ref",
+ "http://funny/folder/file.html#",
+ "http://funny/#ref",
+ "imap://jmr:secret@labyrinth//var/mail/jmr",
+ "nntp://fred@labyrinth:143/save/it/now.mbox",
+ "imap://jmr@labyrinth/INBOX",
+ "imap://labryrinth",
+ "imap://labryrinth/",
+ "file:",
+ "file:INBOX",
+ "file:/home/shannon/mail/foo",
+ "/tmp/foo",
+ "//host/tmp/foo",
+ ":/tmp/foo",
+ "/really/weird:/tmp/foo#bar",
+ ""
+ };
+
+ URLName url =
+ new URLName("protocol", "host", 119, "file", "userid", "password");
+ System.out.println("Test URL: " + url.toString());
+ if (argv.length == 0) {
+ for (int i = 0; i < testURLNames.length; i++) {
+ print(testURLNames[i]);
+ System.out.println();
+ }
+ } else {
+ for (int i = 0; i < argv.length; i++) {
+ print(argv[i]);
+ System.out.println();
+ }
+ if (argv.length == 2) {
+ URLName u1 = new URLName(argv[0]);
+ URLName u2 = new URLName(argv[1]);
+ System.out.println("URL1 hash code: " + u1.hashCode());
+ System.out.println("URL2 hash code: " + u2.hashCode());
+ if (u1.equals(u2))
+ System.out.println("success, equal");
+ else
+ System.out.println("fail, not equal");
+ if (u2.equals(u1))
+ System.out.println("success, equal");
+ else
+ System.out.println("fail, not equal");
+ if (u1.hashCode() == u2.hashCode())
+ System.out.println("success, hashCodes equal");
+ else
+ System.out.println("fail, hashCodes not equal");
+ }
+ }
+ }
+
+ private static void print(String name) {
+ URLName url = new URLName(name);
+ System.out.println("Original URL: " + name);
+ System.out.println("The fullUrl : " + url.toString());
+ if (!name.equals(url.toString()))
+ System.out.println(" : NOT EQUAL!");
+ System.out.println("The protocol is: " + url.getProtocol());
+ System.out.println("The host is: " + url.getHost());
+ System.out.println("The port is: " + url.getPort());
+ System.out.println("The user is: " + url.getUsername());
+ System.out.println("The password is: " + url.getPassword());
+ System.out.println("The file is: " + url.getFile());
+ System.out.println("The ref is: " + url.getRef());
+ }
+ */
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/ConnectionAdapter.java b/net-mail/src/main/java/jakarta/mail/event/ConnectionAdapter.java
new file mode 100644
index 0000000..5521cd9
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/ConnectionAdapter.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * The adapter which receives connection events.
+ * The methods in this class are empty; this class is provided as a
+ * convenience for easily creating listeners by extending this class
+ * and overriding only the methods of interest.
+ *
+ * @author John Mani
+ */
+public abstract class ConnectionAdapter implements ConnectionListener {
+
+
+ /**
+ * Creates a default {@code ConnectionAdapter}.
+ */
+ public ConnectionAdapter() {
+ }
+
+ @Override
+ public void opened(ConnectionEvent e) {
+ }
+
+ @Override
+ public void disconnected(ConnectionEvent e) {
+ }
+
+ @Override
+ public void closed(ConnectionEvent e) {
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/ConnectionEvent.java b/net-mail/src/main/java/jakarta/mail/event/ConnectionEvent.java
new file mode 100644
index 0000000..513fdb3
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/ConnectionEvent.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * This class models Connection events.
+ *
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class ConnectionEvent extends MailEvent {
+
+ /**
+ * A connection was opened.
+ */
+ public static final int OPENED = 1;
+ /**
+ * A connection was disconnected (not currently used).
+ */
+ public static final int DISCONNECTED = 2;
+ /**
+ * A connection was closed.
+ */
+ public static final int CLOSED = 3;
+
+ /**
+ * The event type.
+ *
+ * @serial
+ */
+ protected int type;
+
+ /**
+ * Construct a ConnectionEvent.
+ *
+ * @param source The source object
+ * @param type the event type
+ */
+ public ConnectionEvent(Object source, int type) {
+ super(source);
+ this.type = type;
+ }
+
+ /**
+ * Return the type of this event
+ *
+ * @return type
+ */
+ public int getType() {
+ return type;
+ }
+
+ /**
+ * Invokes the appropriate ConnectionListener method
+ */
+ @Override
+ public void dispatch(Object listener) {
+ if (type == OPENED)
+ ((ConnectionListener) listener).opened(this);
+ else if (type == DISCONNECTED)
+ ((ConnectionListener) listener).disconnected(this);
+ else if (type == CLOSED)
+ ((ConnectionListener) listener).closed(this);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/ConnectionListener.java b/net-mail/src/main/java/jakarta/mail/event/ConnectionListener.java
new file mode 100644
index 0000000..2cdeb1d
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/ConnectionListener.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * This is the Listener interface for Connection events.
+ *
+ * @author John Mani
+ */
+
+public interface ConnectionListener extends java.util.EventListener {
+
+ /**
+ * Invoked when a Store/Folder/Transport is opened.
+ *
+ * @param e the ConnectionEvent
+ */
+ void opened(ConnectionEvent e);
+
+ /**
+ * Invoked when a Store is disconnected. Note that a folder
+ * cannot be disconnected, so a folder will not fire this event
+ *
+ * @param e the ConnectionEvent
+ */
+ void disconnected(ConnectionEvent e);
+
+ /**
+ * Invoked when a Store/Folder/Transport is closed.
+ *
+ * @param e the ConnectionEvent
+ */
+ void closed(ConnectionEvent e);
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/FolderAdapter.java b/net-mail/src/main/java/jakarta/mail/event/FolderAdapter.java
new file mode 100644
index 0000000..2e47de7
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/FolderAdapter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * The adapter which receives Folder events.
+ * The methods in this class are empty; this class is provided as a
+ * convenience for easily creating listeners by extending this class
+ * and overriding only the methods of interest.
+ *
+ * @author John Mani
+ */
+public abstract class FolderAdapter implements FolderListener {
+
+ /**
+ * Creates a default {@code FolderAdapter}.
+ */
+ public FolderAdapter() {
+ }
+
+ @Override
+ public void folderCreated(FolderEvent e) {
+ }
+
+ @Override
+ public void folderRenamed(FolderEvent e) {
+ }
+
+ @Override
+ public void folderDeleted(FolderEvent e) {
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/FolderEvent.java b/net-mail/src/main/java/jakarta/mail/event/FolderEvent.java
new file mode 100644
index 0000000..06ffc9b
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/FolderEvent.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+import jakarta.mail.Folder;
+
+/**
+ * This class models Folder existence events. FolderEvents are
+ * delivered to FolderListeners registered on the affected Folder as
+ * well as the containing Store.
+ *
+ * Service providers vary widely in their ability to notify clients of
+ * these events. At a minimum, service providers must notify listeners
+ * registered on the same Store or Folder object on which the operation
+ * occurs. Service providers may also notify listeners when changes
+ * are made through operations on other objects in the same virtual
+ * machine, or by other clients in the same or other hosts. Such
+ * notifications are not required and are typically not supported
+ * by mail protocols (including IMAP).
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ */
+@SuppressWarnings("serial")
+public class FolderEvent extends MailEvent {
+
+ /**
+ * The folder was created.
+ */
+ public static final int CREATED = 1;
+ /**
+ * The folder was deleted.
+ */
+ public static final int DELETED = 2;
+ /**
+ * The folder was renamed.
+ */
+ public static final int RENAMED = 3;
+
+ /**
+ * The event type.
+ *
+ * @serial
+ */
+ protected int type;
+
+ /**
+ * The folder the event occurred on.
+ */
+ transient protected Folder folder;
+
+ /**
+ * The folder that represents the new name, in case of a RENAMED event.
+ *
+ * @since JavaMail 1.1
+ */
+ transient protected Folder newFolder;
+
+ /**
+ * Constructor.
+ *
+ * @param source The source of the event
+ * @param folder The affected folder
+ * @param type The event type
+ */
+ public FolderEvent(Object source, Folder folder, int type) {
+ this(source, folder, folder, type);
+ }
+
+ /**
+ * Constructor. Use for RENAMED events.
+ *
+ * @param source The source of the event
+ * @param oldFolder The folder that is renamed
+ * @param newFolder The folder that represents the new name
+ * @param type The event type
+ * @since JavaMail 1.1
+ */
+ public FolderEvent(Object source, Folder oldFolder,
+ Folder newFolder, int type) {
+ super(source);
+ this.folder = oldFolder;
+ this.newFolder = newFolder;
+ this.type = type;
+ }
+
+ /**
+ * Return the type of this event.
+ *
+ * @return type
+ */
+ public int getType() {
+ return type;
+ }
+
+ /**
+ * Return the affected folder.
+ *
+ * @return the affected folder
+ * @see #getNewFolder
+ */
+ public Folder getFolder() {
+ return folder;
+ }
+
+ /**
+ * If this event indicates that a folder is renamed, (i.e, the event type
+ * is RENAMED), then this method returns the Folder object representing the
+ * new name.
+ *
+ * The getFolder() method returns the folder that is renamed.
+ *
+ * @return Folder representing the new name.
+ * @see #getFolder
+ * @since JavaMail 1.1
+ */
+ public Folder getNewFolder() {
+ return newFolder;
+ }
+
+ /**
+ * Invokes the appropriate FolderListener method
+ */
+ @Override
+ public void dispatch(Object listener) {
+ if (type == CREATED)
+ ((FolderListener) listener).folderCreated(this);
+ else if (type == DELETED)
+ ((FolderListener) listener).folderDeleted(this);
+ else if (type == RENAMED)
+ ((FolderListener) listener).folderRenamed(this);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/FolderListener.java b/net-mail/src/main/java/jakarta/mail/event/FolderListener.java
new file mode 100644
index 0000000..473dbfb
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/FolderListener.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * This is the Listener interface for Folder events.
+ *
+ * @author John Mani
+ */
+
+public interface FolderListener extends java.util.EventListener {
+ /**
+ * Invoked when a Folder is created.
+ *
+ * @param e the FolderEvent
+ */
+ void folderCreated(FolderEvent e);
+
+ /**
+ * Invoked when a folder is deleted.
+ *
+ * @param e the FolderEvent
+ */
+ void folderDeleted(FolderEvent e);
+
+ /**
+ * Invoked when a folder is renamed.
+ *
+ * @param e the FolderEvent
+ */
+ void folderRenamed(FolderEvent e);
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/MailEvent.java b/net-mail/src/main/java/jakarta/mail/event/MailEvent.java
new file mode 100644
index 0000000..e0a01b1
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/MailEvent.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+import java.util.EventObject;
+
+/**
+ * Common base class for mail events, defining the dispatch method.
+ *
+ * @author Bill Shannon
+ */
+@SuppressWarnings("serial")
+public abstract class MailEvent extends EventObject {
+
+ /**
+ * Construct a MailEvent referring to the given source.
+ *
+ * @param source the source of the event
+ */
+ public MailEvent(Object source) {
+ super(source);
+ }
+
+ /**
+ * This method invokes the appropriate method on a listener for
+ * this event. Subclasses provide the implementation.
+ *
+ * @param listener the listener to invoke on
+ */
+ public abstract void dispatch(Object listener);
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageChangedEvent.java b/net-mail/src/main/java/jakarta/mail/event/MessageChangedEvent.java
new file mode 100644
index 0000000..64775e9
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/MessageChangedEvent.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+import jakarta.mail.Message;
+
+/**
+ * This class models Message change events.
+ *
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class MessageChangedEvent extends MailEvent {
+
+ /**
+ * The message's flags changed.
+ */
+ public static final int FLAGS_CHANGED = 1;
+ /**
+ * The message's envelope (headers, but not body) changed.
+ */
+ public static final int ENVELOPE_CHANGED = 2;
+
+ /**
+ * The event type.
+ *
+ * @serial
+ */
+ protected int type;
+
+ /**
+ * The message that changed.
+ */
+ transient protected Message msg;
+
+ /**
+ * Constructor.
+ *
+ * @param source The folder that owns the message
+ * @param type The change type
+ * @param msg The changed message
+ */
+ public MessageChangedEvent(Object source, int type, Message msg) {
+ super(source);
+ this.msg = msg;
+ this.type = type;
+ }
+
+ /**
+ * Return the type of this event.
+ *
+ * @return type
+ */
+ public int getMessageChangeType() {
+ return type;
+ }
+
+ /**
+ * Return the changed Message.
+ *
+ * @return the message
+ */
+ public Message getMessage() {
+ return msg;
+ }
+
+ /**
+ * Invokes the appropriate MessageChangedListener method.
+ */
+ @Override
+ public void dispatch(Object listener) {
+ ((MessageChangedListener) listener).messageChanged(this);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageChangedListener.java b/net-mail/src/main/java/jakarta/mail/event/MessageChangedListener.java
new file mode 100644
index 0000000..4957049
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/MessageChangedListener.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * This is the Listener interface for MessageChanged events
+ *
+ * @author John Mani
+ */
+
+public interface MessageChangedListener extends java.util.EventListener {
+ /**
+ * Invoked when a message is changed. The change-type specifies
+ * what changed.
+ *
+ * @param e the MessageChangedEvent
+ * @see MessageChangedEvent#FLAGS_CHANGED
+ * @see MessageChangedEvent#ENVELOPE_CHANGED
+ */
+ void messageChanged(MessageChangedEvent e);
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageCountAdapter.java b/net-mail/src/main/java/jakarta/mail/event/MessageCountAdapter.java
new file mode 100644
index 0000000..c63ff0d
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/MessageCountAdapter.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * The adapter which receives MessageCount events.
+ * The methods in this class are empty; this class is provided as a
+ * convenience for easily creating listeners by extending this class
+ * and overriding only the methods of interest.
+ *
+ * @author John Mani
+ */
+public abstract class MessageCountAdapter implements MessageCountListener {
+
+ /**
+ * Creates a default {@code MessageCountAdapter}.
+ */
+ public MessageCountAdapter() {
+ }
+
+ @Override
+ public void messagesAdded(MessageCountEvent e) {
+ }
+
+ @Override
+ public void messagesRemoved(MessageCountEvent e) {
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageCountEvent.java b/net-mail/src/main/java/jakarta/mail/event/MessageCountEvent.java
new file mode 100644
index 0000000..fbba495
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/MessageCountEvent.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+import jakarta.mail.Folder;
+import jakarta.mail.Message;
+
+/**
+ * This class notifies changes in the number of messages in a folder.
+ *
+ * Note that some folder types may only deliver MessageCountEvents at
+ * certain times or after certain operations. IMAP in particular will
+ * only notify the client of MessageCountEvents when a client issues a
+ * new command. Refer to
+ * RFC 3501
+ * for details.
+ * A client may want to "poll" the folder by occasionally calling the
+ * {@link Folder#getMessageCount getMessageCount} or
+ * {@link Folder#isOpen isOpen} methods
+ * to solicit any such notifications.
+ *
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class MessageCountEvent extends MailEvent {
+
+ /**
+ * The messages were added to their folder
+ */
+ public static final int ADDED = 1;
+ /**
+ * The messages were removed from their folder
+ */
+ public static final int REMOVED = 2;
+
+ /**
+ * The event type.
+ *
+ * @serial
+ */
+ protected int type;
+
+ /**
+ * If true, this event is the result of an explicit
+ * expunge by this client, and the messages in this
+ * folder have been renumbered to account for this.
+ * If false, this event is the result of an expunge
+ * by external sources.
+ *
+ * @serial
+ */
+ protected boolean removed;
+
+ /**
+ * The messages.
+ */
+ transient protected Message[] msgs;
+
+ /**
+ * Constructor.
+ *
+ * @param folder The containing folder
+ * @param type The event type
+ * @param removed If true, this event is the result of an explicit
+ * expunge by this client, and the messages in this
+ * folder have been renumbered to account for this.
+ * If false, this event is the result of an expunge
+ * by external sources.
+ * @param msgs The messages added/removed
+ */
+ public MessageCountEvent(Folder folder, int type,
+ boolean removed, Message[] msgs) {
+ super(folder);
+ this.type = type;
+ this.removed = removed;
+ this.msgs = msgs;
+ }
+
+ /**
+ * Return the type of this event.
+ *
+ * @return type
+ */
+ public int getType() {
+ return type;
+ }
+
+ /**
+ * Indicates whether this event is the result of an explicit
+ * expunge by this client, or due to an expunge from external
+ * sources. If true, this event is due to an
+ * explicit expunge and hence all remaining messages in this
+ * folder have been renumbered. If false, this event
+ * is due to an external expunge.
+ *
+ * Note that this method is valid only if the type of this event
+ * is REMOVED
+ *
+ * @return true if the message has been removed
+ */
+ public boolean isRemoved() {
+ return removed;
+ }
+
+ /**
+ * Return the array of messages added or removed.
+ *
+ * @return array of messages
+ */
+ public Message[] getMessages() {
+ return msgs;
+ }
+
+ /**
+ * Invokes the appropriate MessageCountListener method.
+ */
+ @Override
+ public void dispatch(Object listener) {
+ if (type == ADDED)
+ ((MessageCountListener) listener).messagesAdded(this);
+ else // REMOVED
+ ((MessageCountListener) listener).messagesRemoved(this);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageCountListener.java b/net-mail/src/main/java/jakarta/mail/event/MessageCountListener.java
new file mode 100644
index 0000000..f6ee0b0
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/MessageCountListener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * This is the Listener interface for MessageCount events.
+ *
+ * @author John Mani
+ */
+
+public interface MessageCountListener extends java.util.EventListener {
+ /**
+ * Invoked when messages are added into a folder.
+ *
+ * @param e the MessageCountEvent
+ */
+ void messagesAdded(MessageCountEvent e);
+
+ /**
+ * Invoked when messages are removed (expunged) from a folder.
+ *
+ * @param e the MessageCountEvent
+ */
+ void messagesRemoved(MessageCountEvent e);
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/StoreEvent.java b/net-mail/src/main/java/jakarta/mail/event/StoreEvent.java
new file mode 100644
index 0000000..b4a9941
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/StoreEvent.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+import jakarta.mail.Store;
+
+/**
+ * This class models notifications from the Store connection. These
+ * notifications can be ALERTS or NOTICES. ALERTS must be presented
+ * to the user in a fashion that calls the user's attention to the
+ * message.
+ *
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class StoreEvent extends MailEvent {
+
+ /**
+ * Indicates that this message is an ALERT.
+ */
+ public static final int ALERT = 1;
+
+ /**
+ * Indicates that this message is a NOTICE.
+ */
+ public static final int NOTICE = 2;
+
+ /**
+ * The event type.
+ *
+ * @serial
+ */
+ protected int type;
+
+ /**
+ * The message text to be presented to the user.
+ *
+ * @serial
+ */
+ protected String message;
+
+ /**
+ * Construct a StoreEvent.
+ *
+ * @param store the source Store
+ * @param type the event type
+ * @param message a message assoicated with the event
+ */
+ public StoreEvent(Store store, int type, String message) {
+ super(store);
+ this.type = type;
+ this.message = message;
+ }
+
+ /**
+ * Return the type of this event.
+ *
+ * @return type
+ * @see #ALERT
+ * @see #NOTICE
+ */
+ public int getMessageType() {
+ return type;
+ }
+
+ /**
+ * Get the message from the Store.
+ *
+ * @return message from the Store
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Invokes the appropriate StoreListener method.
+ */
+ @Override
+ public void dispatch(Object listener) {
+ ((StoreListener) listener).notification(this);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/StoreListener.java b/net-mail/src/main/java/jakarta/mail/event/StoreListener.java
new file mode 100644
index 0000000..77c0692
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/StoreListener.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * This is the Listener interface for Store Notifications.
+ *
+ * @author John Mani
+ */
+
+public interface StoreListener extends java.util.EventListener {
+
+ /**
+ * Invoked when the Store generates a notification event.
+ *
+ * @param e the StoreEvent
+ * @see StoreEvent#ALERT
+ * @see StoreEvent#NOTICE
+ */
+ void notification(StoreEvent e);
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/TransportAdapter.java b/net-mail/src/main/java/jakarta/mail/event/TransportAdapter.java
new file mode 100644
index 0000000..e72817e
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/TransportAdapter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * The adapter which receives Transport events.
+ * The methods in this class are empty; this class is provided as a
+ * convenience for easily creating listeners by extending this class
+ * and overriding only the methods of interest.
+ *
+ * @author John Mani
+ */
+public abstract class TransportAdapter implements TransportListener {
+
+ /**
+ * Creates a default {@code TransportAdapter}.
+ */
+ public TransportAdapter() {
+ }
+
+ @Override
+ public void messageDelivered(TransportEvent e) {
+ }
+
+ @Override
+ public void messageNotDelivered(TransportEvent e) {
+ }
+
+ @Override
+ public void messagePartiallyDelivered(TransportEvent e) {
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/TransportEvent.java b/net-mail/src/main/java/jakarta/mail/event/TransportEvent.java
new file mode 100644
index 0000000..6e687c4
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/TransportEvent.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+import jakarta.mail.Address;
+import jakarta.mail.Message;
+import jakarta.mail.Transport;
+
+/**
+ * This class models Transport events.
+ *
+ * @author John Mani
+ * @author Max Spivak
+ * @see Transport
+ * @see TransportListener
+ */
+@SuppressWarnings("serial")
+public class TransportEvent extends MailEvent {
+
+ /**
+ * Message has been successfully delivered to all recipients by the
+ * transport firing this event. validSent[] contains all the addresses
+ * this transport sent to successfully. validUnsent[] and invalid[]
+ * should be null,
+ */
+ public static final int MESSAGE_DELIVERED = 1;
+
+ /**
+ * Message was not sent for some reason. validSent[] should be null.
+ * validUnsent[] may have addresses that are valid (but the message
+ * wasn't sent to them). invalid[] should likely contain invalid addresses.
+ */
+ public static final int MESSAGE_NOT_DELIVERED = 2;
+
+ /**
+ * Message was successfully sent to some recipients but not to all.
+ * validSent[] holds addresses of recipients to whom the message was sent.
+ * validUnsent[] holds valid addresses to which the message was not sent.
+ * invalid[] holds invalid addresses, if any.
+ */
+ public static final int MESSAGE_PARTIALLY_DELIVERED = 3;
+
+
+ /**
+ * The event type.
+ *
+ * @serial
+ */
+ protected int type;
+
+ /**
+ * The valid address to which the message was sent.
+ */
+ transient protected Address[] validSent;
+ /**
+ * The valid address to which the message was not sent.
+ */
+ transient protected Address[] validUnsent;
+ /**
+ * The invalid addresses.
+ */
+ transient protected Address[] invalid;
+ /**
+ * The Message to which this event applies.
+ */
+ transient protected Message msg;
+
+ /**
+ * Constructor.
+ *
+ * @param transport The Transport object
+ * @param type the event type (MESSAGE_DELIVERED, etc.)
+ * @param validSent the valid addresses to which the message was sent
+ * @param validUnsent the valid addresses to which the message was
+ * not sent
+ * @param invalid the invalid addresses
+ * @param msg the message being sent
+ */
+ public TransportEvent(Transport transport, int type, Address[] validSent,
+ Address[] validUnsent, Address[] invalid,
+ Message msg) {
+ super(transport);
+ this.type = type;
+ this.validSent = validSent;
+ this.validUnsent = validUnsent;
+ this.invalid = invalid;
+ this.msg = msg;
+ }
+
+ /**
+ * Return the type of this event.
+ *
+ * @return type
+ */
+ public int getType() {
+ return type;
+ }
+
+ /**
+ * Return the addresses to which this message was sent succesfully.
+ *
+ * @return Addresses to which the message was sent successfully or null
+ */
+ public Address[] getValidSentAddresses() {
+ return validSent;
+ }
+
+ /**
+ * Return the addresses that are valid but to which this message
+ * was not sent.
+ *
+ * @return Addresses that are valid but to which the message was
+ * not sent successfully or null
+ */
+ public Address[] getValidUnsentAddresses() {
+ return validUnsent;
+ }
+
+ /**
+ * Return the addresses to which this message could not be sent.
+ *
+ * @return Addresses to which the message sending failed or null
+ */
+ public Address[] getInvalidAddresses() {
+ return invalid;
+ }
+
+ /**
+ * Get the Message object associated with this Transport Event.
+ *
+ * @return the Message object
+ * @since JavaMail 1.2
+ */
+ public Message getMessage() {
+ return msg;
+ }
+
+ /**
+ * Invokes the appropriate TransportListener method.
+ */
+ @Override
+ public void dispatch(Object listener) {
+ if (type == MESSAGE_DELIVERED)
+ ((TransportListener) listener).messageDelivered(this);
+ else if (type == MESSAGE_NOT_DELIVERED)
+ ((TransportListener) listener).messageNotDelivered(this);
+ else // MESSAGE_PARTIALLY_DELIVERED
+ ((TransportListener) listener).messagePartiallyDelivered(this);
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/TransportListener.java b/net-mail/src/main/java/jakarta/mail/event/TransportListener.java
new file mode 100644
index 0000000..847d58e
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/TransportListener.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.event;
+
+/**
+ * This is the Listener interface for Transport events
+ *
+ * @author John Mani
+ * @author Max Spivak
+ * @see jakarta.mail.Transport
+ * @see TransportEvent
+ */
+
+public interface TransportListener extends java.util.EventListener {
+
+ /**
+ * Invoked when a Message is succesfully delivered.
+ *
+ * @param e TransportEvent
+ */
+ void messageDelivered(TransportEvent e);
+
+ /**
+ * Invoked when a Message is not delivered.
+ *
+ * @param e TransportEvent
+ * @see TransportEvent
+ */
+ void messageNotDelivered(TransportEvent e);
+
+ /**
+ * Invoked when a Message is partially delivered.
+ *
+ * @param e TransportEvent
+ * @see TransportEvent
+ */
+ void messagePartiallyDelivered(TransportEvent e);
+}
diff --git a/net-mail/src/main/java/jakarta/mail/event/package-info.java b/net-mail/src/main/java/jakarta/mail/event/package-info.java
new file mode 100644
index 0000000..2ca6ac8
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/event/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2021 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
+ */
+
+/**
+ * Listeners and events for the Jakarta Mail API.
+ * This package defines listener classes and event classes used by the classes
+ * defined in the jakarta.mail package.
+ */
+package jakarta.mail.event;
\ No newline at end of file
diff --git a/net-mail/src/main/java/jakarta/mail/internet/AddressException.java b/net-mail/src/main/java/jakarta/mail/internet/AddressException.java
new file mode 100644
index 0000000..599ef6e
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/internet/AddressException.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.internet;
+
+/**
+ * The exception thrown when a wrongly formatted address is encountered.
+ *
+ * @author Bill Shannon
+ * @author Max Spivak
+ */
+@SuppressWarnings("serial")
+public class AddressException extends ParseException {
+ /**
+ * The string being parsed.
+ *
+ * @serial
+ */
+ protected String ref = null;
+
+ /**
+ * The index in the string where the error occurred, or -1 if not known.
+ *
+ * @serial
+ */
+ protected int pos = -1;
+
+ /**
+ * Constructs an AddressException with no detail message.
+ */
+ public AddressException() {
+ super();
+ }
+
+ /**
+ * Constructs an AddressException with the specified detail message.
+ *
+ * @param s the detail message
+ */
+ public AddressException(String s) {
+ super(s);
+ }
+
+ /**
+ * Constructs an AddressException with the specified detail message
+ * and reference info.
+ *
+ * @param s the detail message
+ * @param ref the string being parsed
+ */
+ public AddressException(String s, String ref) {
+ super(s);
+ this.ref = ref;
+ }
+
+ /**
+ * Constructs an AddressException with the specified detail message
+ * and reference info.
+ *
+ * @param s the detail message
+ * @param ref the string being parsed
+ * @param pos the position of the error
+ */
+ public AddressException(String s, String ref, int pos) {
+ super(s);
+ this.ref = ref;
+ this.pos = pos;
+ }
+
+ /**
+ * Get the string that was being parsed when the error was detected
+ * (null if not relevant).
+ *
+ * @return the string that was being parsed
+ */
+ public String getRef() {
+ return ref;
+ }
+
+ /**
+ * Get the position with the reference string where the error was
+ * detected (-1 if not relevant).
+ *
+ * @return the position within the string of the error
+ */
+ public int getPos() {
+ return pos;
+ }
+
+ @Override
+ public String toString() {
+ String s = super.toString();
+ if (ref == null)
+ return s;
+ s += " in string ``" + ref + "''";
+ if (pos < 0)
+ return s;
+ return s + " at position " + pos;
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/internet/ContentDisposition.java b/net-mail/src/main/java/jakarta/mail/internet/ContentDisposition.java
new file mode 100644
index 0000000..6dc9034
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/internet/ContentDisposition.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.internet;
+
+/**
+ * This class represents a MIME ContentDisposition value. It provides
+ * methods to parse a ContentDisposition string into individual components
+ * and to generate a MIME style ContentDisposition string.
+ *
+ * @author John Mani
+ */
+
+public class ContentDisposition {
+
+ private static final boolean contentDispositionStrict =
+ MimeUtility.getBooleanSystemProperty("mail.mime.contentdisposition.strict", true);
+
+ private String disposition; // disposition
+ private ParameterList list; // parameter list
+
+ /**
+ * No-arg Constructor.
+ */
+ public ContentDisposition() {
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param disposition disposition
+ * @param list ParameterList
+ * @since JavaMail 1.2
+ */
+ public ContentDisposition(String disposition, ParameterList list) {
+ this.disposition = disposition;
+ this.list = list;
+ }
+
+ /**
+ * Constructor that takes a ContentDisposition string. The String
+ * is parsed into its constituents: dispostion and parameters.
+ * A ParseException is thrown if the parse fails.
+ *
+ * @param s the ContentDisposition string.
+ * @throws ParseException if the parse fails.
+ * @since JavaMail 1.2
+ */
+ public ContentDisposition(String s) throws ParseException {
+ HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME);
+ HeaderTokenizer.Token tk;
+
+ // First "disposition" ..
+ tk = h.next();
+ if (tk.getType() != HeaderTokenizer.Token.ATOM) {
+ if (contentDispositionStrict) {
+ throw new ParseException("Expected disposition, got " +
+ tk.getValue());
+ }
+ } else {
+ disposition = tk.getValue();
+ }
+
+ // Then parameters ..
+ String rem = h.getRemainder();
+ if (rem != null) {
+ try {
+ list = new ParameterList(rem);
+ } catch (ParseException px) {
+ if (contentDispositionStrict) {
+ throw px;
+ }
+ }
+ }
+ }
+
+ /**
+ * Return the disposition value.
+ *
+ * @return the disposition
+ * @since JavaMail 1.2
+ */
+ public String getDisposition() {
+ return disposition;
+ }
+
+ /**
+ * Set the disposition. Replaces the existing disposition.
+ *
+ * @param disposition the disposition
+ * @since JavaMail 1.2
+ */
+ public void setDisposition(String disposition) {
+ this.disposition = disposition;
+ }
+
+ /**
+ * Return the specified parameter value. Returns null
+ * if this parameter is absent.
+ *
+ * @param name the parameter name
+ * @return parameter value
+ * @since JavaMail 1.2
+ */
+ public String getParameter(String name) {
+ if (list == null)
+ return null;
+
+ return list.get(name);
+ }
+
+ /**
+ * Return a ParameterList object that holds all the available
+ * parameters. Returns null if no parameters are available.
+ *
+ * @return ParameterList
+ * @since JavaMail 1.2
+ */
+ public ParameterList getParameterList() {
+ return list;
+ }
+
+ /**
+ * Set a new ParameterList.
+ *
+ * @param list ParameterList
+ * @since JavaMail 1.2
+ */
+ public void setParameterList(ParameterList list) {
+ this.list = list;
+ }
+
+ /**
+ * Set the specified parameter. If this parameter already exists,
+ * it is replaced by this new value.
+ *
+ * @param name parameter name
+ * @param value parameter value
+ * @since JavaMail 1.2
+ */
+ public void setParameter(String name, String value) {
+ if (list == null)
+ list = new ParameterList();
+
+ list.set(name, value);
+ }
+
+ /**
+ * Retrieve a RFC2045 style string representation of
+ * this ContentDisposition. Returns an empty string if
+ * the conversion failed.
+ *
+ * @return RFC2045 style string
+ * @since JavaMail 1.2
+ */
+ @Override
+ public String toString() {
+ if (disposition == null)
+ return "";
+
+ if (list == null)
+ return disposition;
+
+ StringBuilder sb = new StringBuilder(disposition);
+
+ // append the parameter list
+ // use the length of the string buffer + the length of
+ // the header name formatted as follows "Content-Disposition: "
+ sb.append(list.toString(sb.length() + 21));
+ return sb.toString();
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/internet/ContentType.java b/net-mail/src/main/java/jakarta/mail/internet/ContentType.java
new file mode 100644
index 0000000..cb4a467
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/internet/ContentType.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.internet;
+
+/**
+ * This class represents a MIME Content-Type value. It provides
+ * methods to parse a Content-Type string into individual components
+ * and to generate a MIME style Content-Type string.
+ *
+ * @author John Mani
+ */
+
+public class ContentType {
+
+ private String primaryType; // primary type
+ private String subType; // subtype
+ private ParameterList list; // parameter list
+
+ /**
+ * No-arg Constructor.
+ */
+ public ContentType() {
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param primaryType primary type
+ * @param subType subType
+ * @param list ParameterList
+ */
+ public ContentType(String primaryType, String subType,
+ ParameterList list) {
+ this.primaryType = primaryType;
+ this.subType = subType;
+ this.list = list;
+ }
+
+ /**
+ * Constructor that takes a Content-Type string. The String
+ * is parsed into its constituents: primaryType, subType
+ * and parameters. A ParseException is thrown if the parse fails.
+ *
+ * @param s the Content-Type string.
+ * @throws ParseException if the parse fails.
+ */
+ public ContentType(String s) throws ParseException {
+ HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME);
+ HeaderTokenizer.Token tk;
+
+ // First "type" ..
+ tk = h.next();
+ if (tk.getType() != HeaderTokenizer.Token.ATOM)
+ throw new ParseException("In Content-Type string <" + s + ">" +
+ ", expected MIME type, got " +
+ tk.getValue());
+ primaryType = tk.getValue();
+
+ // The '/' separator ..
+ tk = h.next();
+ if ((char) tk.getType() != '/')
+ throw new ParseException("In Content-Type string <" + s + ">" +
+ ", expected '/', got " + tk.getValue());
+
+ // Then "subType" ..
+ tk = h.next();
+ if (tk.getType() != HeaderTokenizer.Token.ATOM)
+ throw new ParseException("In Content-Type string <" + s + ">" +
+ ", expected MIME subtype, got " +
+ tk.getValue());
+ subType = tk.getValue();
+
+ // Finally parameters ..
+ String rem = h.getRemainder();
+ if (rem != null)
+ list = new ParameterList(rem);
+ }
+
+ /**
+ * Return the primary type.
+ *
+ * @return the primary type
+ */
+ public String getPrimaryType() {
+ return primaryType;
+ }
+
+ /**
+ * Set the primary type. Overrides existing primary type.
+ *
+ * @param primaryType primary type
+ */
+ public void setPrimaryType(String primaryType) {
+ this.primaryType = primaryType;
+ }
+
+ /**
+ * Return the subType.
+ *
+ * @return the subType
+ */
+ public String getSubType() {
+ return subType;
+ }
+
+ /**
+ * Set the subType. Replaces the existing subType.
+ *
+ * @param subType the subType
+ */
+ public void setSubType(String subType) {
+ this.subType = subType;
+ }
+
+ /**
+ * Return the MIME type string, without the parameters.
+ * The returned value is basically the concatenation of
+ * the primaryType, the '/' character and the secondaryType.
+ *
+ * @return the type
+ */
+ public String getBaseType() {
+ if (primaryType == null || subType == null)
+ return "";
+ return primaryType + '/' + subType;
+ }
+
+ /**
+ * Return the specified parameter value. Returns null
+ * if this parameter is absent.
+ *
+ * @param name the parameter name
+ * @return parameter value
+ */
+ public String getParameter(String name) {
+ if (list == null)
+ return null;
+
+ return list.get(name);
+ }
+
+ /**
+ * Return a ParameterList object that holds all the available
+ * parameters. Returns null if no parameters are available.
+ *
+ * @return ParameterList
+ */
+ public ParameterList getParameterList() {
+ return list;
+ }
+
+ /**
+ * Set a new ParameterList.
+ *
+ * @param list ParameterList
+ */
+ public void setParameterList(ParameterList list) {
+ this.list = list;
+ }
+
+ /**
+ * Set the specified parameter. If this parameter already exists,
+ * it is replaced by this new value.
+ *
+ * @param name parameter name
+ * @param value parameter value
+ */
+ public void setParameter(String name, String value) {
+ if (list == null)
+ list = new ParameterList();
+
+ list.set(name, value);
+ }
+
+ /**
+ * Retrieve a RFC2045 style string representation of
+ * this Content-Type. Returns an empty string if
+ * the conversion failed.
+ *
+ * @return RFC2045 style string
+ */
+ @Override
+ public String toString() {
+ if (primaryType == null || subType == null) // need both
+ return "";
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(primaryType).append('/').append(subType);
+ if (list != null)
+ // append the parameter list
+ // use the length of the string buffer + the length of
+ // the header name formatted as follows "Content-Type: "
+ sb.append(list.toString(sb.length() + 14));
+
+ return sb.toString();
+ }
+
+ /**
+ * Match with the specified ContentType object. This method
+ * compares only the primaryType and
+ * subType. The parameters of both operands
+ * are ignored.
+ *
+ * For example, this method will return true when
+ * comparing the ContentTypes for "text/plain"
+ * and "text/plain; charset=foobar".
+ *
+ * If the subType of either operand is the special
+ * character '*', then the subtype is ignored during the match.
+ * For example, this method will return true when
+ * comparing the ContentTypes for "text/plain"
+ * and "text/*"
+ *
+ * @param cType ContentType to compare this against
+ * @return true if it matches
+ */
+ public boolean match(ContentType cType) {
+ // Match primaryType
+ if (!((primaryType == null && cType.getPrimaryType() == null) ||
+ (primaryType != null &&
+ primaryType.equalsIgnoreCase(cType.getPrimaryType()))))
+ return false;
+
+ String sType = cType.getSubType();
+
+ // If either one of the subTypes is wildcarded, return true
+ if ((subType != null && subType.startsWith("*")) ||
+ (sType != null && sType.startsWith("*")))
+ return true;
+
+ // Match subType
+ return (subType == null && sType == null) ||
+ (subType != null && subType.equalsIgnoreCase(sType));
+ }
+
+ /**
+ * Match with the specified content-type string. This method
+ * compares only the primaryType and
+ * subType.
+ * The parameters of both operands are ignored.
+ *
+ * For example, this method will return true when
+ * comparing the ContentType for "text/plain"
+ * with "text/plain; charset=foobar".
+ *
+ * If the subType of either operand is the special
+ * character '*', then the subtype is ignored during the match.
+ * For example, this method will return true when
+ * comparing the ContentType for "text/plain"
+ * with "text/*"
+ *
+ * @param s the content-type string to match
+ * @return true if it matches
+ */
+ public boolean match(String s) {
+ try {
+ return match(new ContentType(s));
+ } catch (ParseException pex) {
+ return false;
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/internet/HeaderTokenizer.java b/net-mail/src/main/java/jakarta/mail/internet/HeaderTokenizer.java
new file mode 100644
index 0000000..239bb2b
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/internet/HeaderTokenizer.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.internet;
+
+/**
+ * This class tokenizes RFC822 and MIME headers into the basic
+ * symbols specified by RFC822 and MIME.
+ *
+ * This class handles folded headers (ie headers with embedded
+ * CRLF SPACE sequences). The folds are removed in the returned
+ * tokens.
+ *
+ * @author John Mani
+ * @author Bill Shannon
+ */
+
+public class HeaderTokenizer {
+
+ /**
+ * RFC822 specials
+ */
+ public final static String RFC822 = "()<>@,;:\\\"\t .[]";
+ /**
+ * MIME specials
+ */
+ public final static String MIME = "()<>@,;:\\\"\t []/?=";
+ // The EOF Token
+ private final static Token EOFToken = new Token(Token.EOF, null);
+ private String string; // the string to be tokenized
+ private boolean skipComments; // should comments be skipped ?
+ private String delimiters; // delimiter string
+ private int currentPos; // current parse position
+ private int maxPos; // string length
+ private int nextPos; // track start of next Token for next()
+ private int peekPos; // track start of next Token for peek()
+
+ /**
+ * Constructor that takes a rfc822 style header.
+ *
+ * @param skipComments If true, comments are skipped and
+ * not returned as tokens
+ * @param header The rfc822 header to be tokenized
+ * @param delimiters Set of delimiter characters
+ * to be used to delimit ATOMS. These
+ * are usually RFC822 or
+ * MIME
+ */
+ public HeaderTokenizer(String header, String delimiters,
+ boolean skipComments) {
+ string = (header == null) ? "" : header; // paranoia ?!
+ this.skipComments = skipComments;
+ this.delimiters = delimiters;
+ currentPos = nextPos = peekPos = 0;
+ maxPos = string.length();
+ }
+
+ /**
+ * Constructor. Comments are ignored and not returned as tokens
+ *
+ * @param header The header that is tokenized
+ * @param delimiters The delimiters to be used
+ */
+ public HeaderTokenizer(String header, String delimiters) {
+ this(header, delimiters, true);
+ }
+
+ /**
+ * Constructor. The RFC822 defined delimiters - RFC822 - are
+ * used to delimit ATOMS. Also comments are skipped and not
+ * returned as tokens
+ *
+ * @param header the header string
+ */
+ public HeaderTokenizer(String header) {
+ this(header, RFC822);
+ }
+
+ // Trim SPACE, HT, CR and NL from end of string
+ private static String trimWhiteSpace(String s) {
+ char c;
+ int i;
+ for (i = s.length() - 1; i >= 0; i--) {
+ if (((c = s.charAt(i)) != ' ') &&
+ (c != '\t') && (c != '\r') && (c != '\n'))
+ break;
+ }
+ if (i <= 0)
+ return "";
+ else
+ return s.substring(0, i + 1);
+ }
+
+ /* Process escape sequences and embedded LWSPs from a comment or
+ * quoted string.
+ */
+ private static String filterToken(String s, int start, int end,
+ boolean keepEscapes) {
+ StringBuilder sb = new StringBuilder();
+ char c;
+ boolean gotEscape = false;
+ boolean gotCR = false;
+
+ for (int i = start; i < end; i++) {
+ c = s.charAt(i);
+ if (c == '\n' && gotCR) {
+ // This LF is part of an unescaped
+ // CRLF sequence (i.e, LWSP). Skip it.
+ gotCR = false;
+ continue;
+ }
+
+ gotCR = false;
+ if (!gotEscape) {
+ // Previous character was NOT '\'
+ if (c == '\\') // skip this character
+ gotEscape = true;
+ else if (c == '\r') // skip this character
+ gotCR = true;
+ else // append this character
+ sb.append(c);
+ } else {
+ // Previous character was '\'. So no need to
+ // bother with any special processing, just
+ // append this character. If keepEscapes is
+ // set, keep the backslash. IE6 fails to escape
+ // backslashes in quoted strings in HTTP headers,
+ // e.g., in the filename parameter.
+ if (keepEscapes)
+ sb.append('\\');
+ sb.append(c);
+ gotEscape = false;
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Parses the next token from this String.
+ *
+ * Clients sit in a loop calling next() to parse successive
+ * tokens until an EOF Token is returned.
+ *
+ * @return the next Token
+ * @throws ParseException if the parse fails
+ */
+ public Token next() throws ParseException {
+ return next('\0', false);
+ }
+
+ /**
+ * Parses the next token from this String.
+ * If endOfAtom is not NUL, the token extends until the
+ * endOfAtom character is seen, or to the end of the header.
+ * This method is useful when parsing headers that don't
+ * obey the MIME specification, e.g., by failing to quote
+ * parameter values that contain spaces.
+ *
+ * @param endOfAtom if not NUL, character marking end of token
+ * @return the next Token
+ * @throws ParseException if the parse fails
+ * @since JavaMail 1.5
+ */
+ public Token next(char endOfAtom) throws ParseException {
+ return next(endOfAtom, false);
+ }
+
+ /**
+ * Parses the next token from this String.
+ * endOfAtom is handled as above. If keepEscapes is true,
+ * any backslash escapes are preserved in the returned string.
+ * This method is useful when parsing headers that don't
+ * obey the MIME specification, e.g., by failing to escape
+ * backslashes in the filename parameter.
+ *
+ * @param endOfAtom if not NUL, character marking end of token
+ * @param keepEscapes keep all backslashes in returned string?
+ * @return the next Token
+ * @throws ParseException if the parse fails
+ * @since JavaMail 1.5
+ */
+ public Token next(char endOfAtom, boolean keepEscapes)
+ throws ParseException {
+ Token tk;
+
+ currentPos = nextPos; // setup currentPos
+ tk = getNext(endOfAtom, keepEscapes);
+ nextPos = peekPos = currentPos; // update currentPos and peekPos
+ return tk;
+ }
+
+ /**
+ * Peek at the next token, without actually removing the token
+ * from the parse stream. Invoking this method multiple times
+ * will return successive tokens, until next() is
+ * called.
+ *
+ * @return the next Token
+ * @throws ParseException if the parse fails
+ */
+ public Token peek() throws ParseException {
+ Token tk;
+
+ currentPos = peekPos; // setup currentPos
+ tk = getNext('\0', false);
+ peekPos = currentPos; // update peekPos
+ return tk;
+ }
+
+ /**
+ * Return the rest of the Header.
+ *
+ * @return String rest of header. null is returned if we are
+ * already at end of header
+ */
+ public String getRemainder() {
+ if (nextPos >= string.length())
+ return null;
+ return string.substring(nextPos);
+ }
+
+ /*
+ * Return the next token starting from 'currentPos'. After the
+ * parse, 'currentPos' is updated to point to the start of the
+ * next token.
+ */
+ private Token getNext(char endOfAtom, boolean keepEscapes)
+ throws ParseException {
+ // If we're already at end of string, return EOF
+ if (currentPos >= maxPos)
+ return EOFToken;
+
+ // Skip white-space, position currentPos beyond the space
+ if (skipWhiteSpace() == Token.EOF)
+ return EOFToken;
+
+ char c;
+ int start;
+ boolean filter = false;
+
+ c = string.charAt(currentPos);
+
+ // Check or Skip comments and position currentPos
+ // beyond the comment
+ while (c == '(') {
+ // Parsing comment ..
+ int nesting;
+ for (start = ++currentPos, nesting = 1;
+ nesting > 0 && currentPos < maxPos;
+ currentPos++) {
+ c = string.charAt(currentPos);
+ if (c == '\\') { // Escape sequence
+ currentPos++; // skip the escaped character
+ filter = true;
+ } else if (c == '\r')
+ filter = true;
+ else if (c == '(')
+ nesting++;
+ else if (c == ')')
+ nesting--;
+ }
+ if (nesting != 0)
+ throw new ParseException("Unbalanced comments");
+
+ if (!skipComments) {
+ // Return the comment, if we are asked to.
+ // Note that the comment start & end markers are ignored.
+ String s;
+ if (filter) // need to go thru the token again.
+ s = filterToken(string, start, currentPos - 1, keepEscapes);
+ else
+ s = string.substring(start, currentPos - 1);
+
+ return new Token(Token.COMMENT, s);
+ }
+
+ // Skip any whitespace after the comment.
+ if (skipWhiteSpace() == Token.EOF)
+ return EOFToken;
+ c = string.charAt(currentPos);
+ }
+
+ // Check for quoted-string and position currentPos
+ // beyond the terminating quote
+ if (c == '"') {
+ currentPos++; // skip initial quote
+ return collectString('"', keepEscapes);
+ }
+
+ // Check for SPECIAL or CTL
+ if (c < 040 || c >= 0177 || delimiters.indexOf(c) >= 0) {
+ if (endOfAtom > 0 && c != endOfAtom) {
+ // not expecting a special character here,
+ // pretend it's a quoted string
+ return collectString(endOfAtom, keepEscapes);
+ }
+ currentPos++; // re-position currentPos
+ char[] ch = new char[1];
+ ch[0] = c;
+ return new Token(c, new String(ch));
+ }
+
+ // Check for ATOM
+ for (start = currentPos; currentPos < maxPos; currentPos++) {
+ c = string.charAt(currentPos);
+ // ATOM is delimited by either SPACE, CTL, "(", <">
+ // or the specified SPECIALS
+ if (c < 040 || c >= 0177 || c == '(' || c == ' ' ||
+ c == '"' || delimiters.indexOf(c) >= 0) {
+ if (endOfAtom > 0 && c != endOfAtom) {
+ // not the expected atom after all;
+ // back up and pretend it's a quoted string
+ currentPos = start;
+ return collectString(endOfAtom, keepEscapes);
+ }
+ break;
+ }
+ }
+ return new Token(Token.ATOM, string.substring(start, currentPos));
+ }
+
+ private Token collectString(char eos, boolean keepEscapes)
+ throws ParseException {
+ int start;
+ boolean filter = false;
+ for (start = currentPos; currentPos < maxPos; currentPos++) {
+ char c = string.charAt(currentPos);
+ if (c == '\\') { // Escape sequence
+ currentPos++;
+ filter = true;
+ } else if (c == '\r')
+ filter = true;
+ else if (c == eos) {
+ currentPos++;
+ String s;
+
+ if (filter)
+ s = filterToken(string, start, currentPos - 1, keepEscapes);
+ else
+ s = string.substring(start, currentPos - 1);
+
+ if (c != '"') { // not a real quoted string
+ s = trimWhiteSpace(s);
+ currentPos--; // back up before the eos char
+ }
+
+ return new Token(Token.QUOTEDSTRING, s);
+ }
+ }
+
+ // ran off the end of the string
+
+ // if we're looking for a matching quote, that's an error
+ if (eos == '"')
+ throw new ParseException("Unbalanced quoted string");
+
+ // otherwise, just return whatever's left
+ String s;
+ if (filter)
+ s = filterToken(string, start, currentPos, keepEscapes);
+ else
+ s = string.substring(start, currentPos);
+ s = trimWhiteSpace(s);
+ return new Token(Token.QUOTEDSTRING, s);
+ }
+
+ // Skip SPACE, HT, CR and NL
+ private int skipWhiteSpace() {
+ char c;
+ for (; currentPos < maxPos; currentPos++)
+ if (((c = string.charAt(currentPos)) != ' ') &&
+ (c != '\t') && (c != '\r') && (c != '\n'))
+ return currentPos;
+ return Token.EOF;
+ }
+
+ /**
+ * The Token class represents tokens returned by the
+ * HeaderTokenizer.
+ */
+ public static class Token {
+
+ /**
+ * Token type indicating an ATOM.
+ */
+ public static final int ATOM = -1;
+ /**
+ * Token type indicating a quoted string. The value
+ * field contains the string without the quotes.
+ */
+ public static final int QUOTEDSTRING = -2;
+ /**
+ * Token type indicating a comment. The value field
+ * contains the comment string without the comment
+ * start and end symbols.
+ */
+ public static final int COMMENT = -3;
+ /**
+ * Token type indicating end of input.
+ */
+ public static final int EOF = -4;
+ private int type;
+ private String value;
+
+ /**
+ * Constructor.
+ *
+ * @param type Token type
+ * @param value Token value
+ */
+ public Token(int type, String value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ /**
+ * Return the type of the token. If the token represents a
+ * delimiter or a control character, the type is that character
+ * itself, converted to an integer. Otherwise, it's value is
+ * one of the following:
+ *
+ *
ATOM A sequence of ASCII characters
+ * delimited by either SPACE, CTL, "(", <"> or the
+ * specified SPECIALS
+ *
QUOTEDSTRING A sequence of ASCII characters
+ * within quotes
+ *
COMMENT A sequence of ASCII characters
+ * within "(" and ")".
+ *
EOF End of header
+ *
+ *
+ * @return the token type
+ */
+ public int getType() {
+ return type;
+ }
+
+ /**
+ * Returns the value of the token just read. When the current
+ * token is a quoted string, this field contains the body of the
+ * string, without the quotes. When the current token is a comment,
+ * this field contains the body of the comment.
+ *
+ * @return token value
+ */
+ public String getValue() {
+ return value;
+ }
+ }
+}
diff --git a/net-mail/src/main/java/jakarta/mail/internet/InternetAddress.java b/net-mail/src/main/java/jakarta/mail/internet/InternetAddress.java
new file mode 100644
index 0000000..3e2c37d
--- /dev/null
+++ b/net-mail/src/main/java/jakarta/mail/internet/InternetAddress.java
@@ -0,0 +1,1508 @@
+/*
+ * Copyright (c) 1997, 2023 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package jakarta.mail.internet;
+
+import jakarta.mail.Address;
+import jakarta.mail.Session;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+/**
+ * This class represents an Internet email address using the syntax
+ * of RFC822.
+ * Typical address syntax is of the form "user@host.domain" or
+ * "Personal Name <user@host.domain>".
+ *
+ * @author Bill Shannon
+ * @author John Mani
+ */
+@SuppressWarnings("serial")
+public class InternetAddress extends Address {
+
+ private static final boolean ignoreBogusGroupName =
+ MimeUtility.getBooleanSystemProperty(
+ "mail.mime.address.ignorebogusgroupname", true);
+ private static final boolean useCanonicalHostName =
+ MimeUtility.getBooleanSystemProperty(
+ "mail.mime.address.usecanonicalhostname", true);
+ private static final boolean allowUtf8 =
+ MimeUtility.getBooleanSystemProperty("mail.mime.allowutf8", false);
+ private static final String rfc822phrase =
+ HeaderTokenizer.RFC822.replace(' ', '\0').replace('\t', '\0');
+ private static final String specialsNoDotNoAt = "()<>,;:\\\"[]";
+ private static final String specialsNoDot = specialsNoDotNoAt + "@";
+ /**
+ * The email address.
+ */
+ protected String address;
+ /**
+ * The personal name.
+ */
+ protected String personal;
+ /**
+ * The RFC 2047 encoded version of the personal name.
+ *
+ * This field and the personal field track each
+ * other, so if a subclass sets one of these fields directly, it
+ * should set the other to null, so that it is
+ * suitably recomputed.
+ */
+ protected String encodedPersonal;
+
+ /**
+ * Default constructor.
+ */
+ public InternetAddress() {
+ }
+
+ /**
+ * Constructor.
+ *
+ * Parse the given string and create an InternetAddress.
+ * See the parse method for details of the parsing.
+ * The address is parsed using "strict" parsing.
+ * This constructor does not perform the additional
+ * syntax checks that the
+ * InternetAddress(String address, boolean strict)
+ * constructor does when strict is true.
+ * This constructor is equivalent to
+ * InternetAddress(address, false).
+ *
+ * @param address the address in RFC822 format
+ * @throws AddressException if the parse failed
+ */
+ public InternetAddress(String address) throws AddressException {
+ // use our address parsing utility routine to parse the string
+ InternetAddress[] a = parse(address, true);
+ // if we got back anything other than a single address, it's an error
+ if (a.length != 1) {
+ throw new AddressException("Illegal address", address);
+ }
+ /*
+ * Now copy the contents of the single address we parsed
+ * into the current object, which will be returned from the
+ * constructor.
+ * XXX - this sure is a round-about way of getting this done.
+ */
+ this.address = a[0].address;
+ this.personal = a[0].personal;
+ this.encodedPersonal = a[0].encodedPersonal;
+ }
+
+ /**
+ * Parse the given string and create an InternetAddress.
+ * If strict is false, the detailed syntax of the
+ * address isn't checked.
+ *
+ * @param address the address in RFC822 format
+ * @param strict enforce RFC822 syntax
+ * @throws AddressException if the parse failed
+ * @since JavaMail 1.3
+ */
+ @SuppressWarnings("this-escape")
+ public InternetAddress(String address, boolean strict)
+ throws AddressException {
+ this(address);
+ if (strict) {
+ if (isGroup()) {
+ getGroup(true); // throw away the result
+ } else {
+ checkAddress(this.address, true, true);
+ }
+ }
+ }
+
+ /**
+ * Construct an InternetAddress given the address and personal name.
+ * The address is assumed to be a syntactically valid RFC822 address.
+ *
+ * @param address the address in RFC822 format
+ * @param personal the personal name
+ * @throws UnsupportedEncodingException if the personal name
+ * can't be encoded in the given charset
+ */
+ public InternetAddress(String address, String personal)
+ throws UnsupportedEncodingException {
+ this(address, personal, null);
+ }
+
+ /**
+ * Construct an InternetAddress given the address and personal name.
+ * The address is assumed to be a syntactically valid RFC822 address.
+ *
+ * @param address the address in RFC822 format
+ * @param personal the personal name
+ * @param charset the MIME charset for the name
+ * @throws UnsupportedEncodingException if the personal name
+ * can't be encoded in the given charset
+ */
+ public InternetAddress(String address, String personal, String charset)
+ throws UnsupportedEncodingException {
+ this.address = address;
+ setPersonal(personal, charset);
+ }
+
+ private static String quotePhrase(String phrase) {
+ int len = phrase.length();
+ boolean needQuoting = false;
+
+ for (int i = 0; i < len; i++) {
+ char c = phrase.charAt(i);
+ if (c == '"' || c == '\\') {
+ // need to escape them and then quote the whole string
+ StringBuilder sb = new StringBuilder(len + 3);
+ sb.append('"');
+ for (int j = 0; j < len; j++) {
+ char cc = phrase.charAt(j);
+ if (cc == '"' || cc == '\\')
+ // Escape the character
+ sb.append('\\');
+ sb.append(cc);
+ }
+ sb.append('"');
+ return sb.toString();
+ } else if ((c < 040 && c != '\r' && c != '\n' && c != '\t') ||
+ (c >= 0177 && !allowUtf8) || rfc822phrase.indexOf(c) >= 0)
+ // These characters cause the string to be quoted
+ needQuoting = true;
+ }
+
+ if (needQuoting) {
+ StringBuilder sb = new StringBuilder(len + 2);
+ sb.append('"').append(phrase).append('"');
+ return sb.toString();
+ } else
+ return phrase;
+ }
+
+ private static String unquote(String s) {
+ if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 1) {
+ s = s.substring(1, s.length() - 1);
+ // check for any escaped characters
+ if (s.indexOf('\\') >= 0) {
+ StringBuilder sb = new StringBuilder(s.length()); // approx
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == '\\' && i < s.length() - 1)
+ c = s.charAt(++i);
+ sb.append(c);
+ }
+ s = sb.toString();
+ }
+ }
+ return s;
+ }
+
+ /**
+ * Convert the given array of InternetAddress objects into
+ * a comma separated sequence of address strings. The
+ * resulting string contains only US-ASCII characters, and
+ * hence is mail-safe.
+ *
+ * @param addresses array of InternetAddress objects
+ * @return comma separated string of addresses
+ * @throws ClassCastException if any address object in the
+ * given array is not an InternetAddress object. Note
+ * that this is a RuntimeException.
+ */
+ public static String toString(Address[] addresses) {
+ return toString(addresses, 0);
+ }
+
+ /**
+ * Convert the given array of InternetAddress objects into
+ * a comma separated sequence of address strings. The
+ * resulting string contains Unicode characters.
+ *
+ * @param addresses array of InternetAddress objects
+ * @return comma separated string of addresses
+ * @throws ClassCastException if any address object in the
+ * given array is not an InternetAddress object. Note
+ * that this is a RuntimeException.
+ * @since JavaMail 1.6
+ */
+ public static String toUnicodeString(Address[] addresses) {
+ return toUnicodeString(addresses, 0);
+ }
+
+ /**
+ * Convert the given array of InternetAddress objects into
+ * a comma separated sequence of address strings. The
+ * resulting string contains only US-ASCII characters, and
+ * hence is mail-safe.
+ *
+ * The 'used' parameter specifies the number of character positions
+ * already taken up in the field into which the resulting address
+ * sequence string is to be inserted. It is used to determine the
+ * line-break positions in the resulting address sequence string.
+ *
+ * @param addresses array of InternetAddress objects
+ * @param used number of character positions already used, in
+ * the field into which the address string is to
+ * be inserted.
+ * @return comma separated string of addresses
+ * @throws ClassCastException if any address object in the
+ * given array is not an InternetAddress object. Note
+ * that this is a RuntimeException.
+ */
+ public static String toString(Address[] addresses, int used) {
+ if (addresses == null || addresses.length == 0)
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < addresses.length; i++) {
+ if (i != 0) { // need to append comma
+ sb.append(", ");
+ used += 2;
+ }
+
+ // prefer not to split a single address across lines so used=0 below
+ String s = MimeUtility.fold(0, addresses[i].toString());
+ int len = lengthOfFirstSegment(s); // length till CRLF
+ if (used + len > 76) { // overflows ...
+ // smash trailing space from ", " above
+ int curlen = sb.length();
+ if (curlen > 0 && sb.charAt(curlen - 1) == ' ')
+ sb.setLength(curlen - 1);
+ sb.append("\r\n\t"); // .. start new continuation line
+ used = 8; // account for the starting char
+ }
+ sb.append(s);
+ used = lengthOfLastSegment(s, used);
+ }
+
+ return sb.toString();
+ }
+
+ /*
+ * quotePhrase() quotes the words within a RFC822 phrase.
+ *
+ * This is tricky, since a phrase is defined as 1 or more
+ * RFC822 words, separated by LWSP. Now, a word that contains
+ * LWSP is supposed to be quoted, and this is exactly what the
+ * MimeUtility.quote() method does. However, when dealing with
+ * a phrase, any LWSP encountered can be construed to be the
+ * separator between words, and not part of the words themselves.
+ * To deal with this funkiness, we have the below variant of
+ * MimeUtility.quote(), which essentially ignores LWSP when
+ * deciding whether to quote a word.
+ *
+ * It aint pretty, but it gets the job done :)
+ */
+
+ /**
+ * Convert the given array of InternetAddress objects into
+ * a comma separated sequence of address strings. The
+ * resulting string contains Unicode characters.
+ *
+ * The 'used' parameter specifies the number of character positions
+ * already taken up in the field into which the resulting address
+ * sequence string is to be inserted. It is used to determine the
+ * line-break positions in the resulting address sequence string.
+ *
+ * @param addresses array of InternetAddress objects
+ * @param used number of character positions already used, in
+ * the field into which the address string is to
+ * be inserted.
+ * @return comma separated string of addresses
+ * @throws ClassCastException if any address object in the
+ * given array is not an InternetAddress object. Note
+ * that this is a RuntimeException.
+ * @since JavaMail 1.6
+ */
+ /*
+ * XXX - This is exactly the same as the above, except it uses
+ * toUnicodeString instead of toString.
+ * XXX - Since the line length restrictions are in bytes, not characters,
+ * we convert all non-ASCII addresses to UTF-8 byte strings,
+ * which we then convert to ISO-8859-1 Strings where every
+ * character respresents one UTF-8 byte. At the end we reverse
+ * the conversion to get back to a correct Unicode string.
+ * This is a hack to allow all the other character-based methods
+ * to work properly with UTF-8 bytes.
+ */
+ public static String toUnicodeString(Address[] addresses, int used) {
+ if (addresses == null || addresses.length == 0)
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+
+ boolean sawNonAscii = false;
+ for (int i = 0; i < addresses.length; i++) {
+ if (i != 0) { // need to append comma
+ sb.append(", ");
+ used += 2;
+ }
+
+ // prefer not to split a single address across lines so used=0 below
+ String as = ((InternetAddress) addresses[i]).toUnicodeString();
+ if (MimeUtility.checkAscii(as) != MimeUtility.ALL_ASCII) {
+ sawNonAscii = true;
+ as = new String(as.getBytes(StandardCharsets.UTF_8),
+ StandardCharsets.ISO_8859_1);
+ }
+ String s = MimeUtility.fold(0, as);
+ int len = lengthOfFirstSegment(s); // length till CRLF
+ if (used + len > 76) { // overflows ...
+ // smash trailing space from ", " above
+ int curlen = sb.length();
+ if (curlen > 0 && sb.charAt(curlen - 1) == ' ')
+ sb.setLength(curlen - 1);
+ sb.append("\r\n\t"); // .. start new continuation line
+ used = 8; // account for the starting char
+ }
+ sb.append(s);
+ used = lengthOfLastSegment(s, used);
+ }
+
+ String ret = sb.toString();
+ if (sawNonAscii)
+ ret = new String(ret.getBytes(StandardCharsets.ISO_8859_1),
+ StandardCharsets.UTF_8);
+ return ret;
+ }
+
+ /*
+ * Return the length of the first segment within this string.
+ * If no segments exist, the length of the whole line is returned.
+ */
+ private static int lengthOfFirstSegment(String s) {
+ int pos;
+ if ((pos = s.indexOf("\r\n")) != -1)
+ return pos;
+ else
+ return s.length();
+ }
+
+ /*
+ * Return the length of the last segment within this string.
+ * If no segments exist, the length of the whole line plus
+ * used is returned.
+ */
+ private static int lengthOfLastSegment(String s, int used) {
+ int pos;
+ if ((pos = s.lastIndexOf("\r\n")) != -1)
+ return s.length() - pos - 2;
+ else
+ return s.length() + used;
+ }
+
+ /**
+ * Return an InternetAddress object representing the current user.
+ * The entire email address may be specified in the "mail.from"
+ * property. If not set, the "mail.user" and "mail.host" properties
+ * are tried. If those are not set, the "user.name" property and
+ * InetAddress.getLocalHost method are tried.
+ * If it is not possible to determine an email address,
+ * null is returned.
+ *
+ * @param session Session object used for property lookup
+ * @return current user's email address
+ */
+ public static InternetAddress getLocalAddress(Session session) {
+ try {
+ return _getLocalAddress(session);
+ } catch (AddressException | UnknownHostException sex) { // ignore it
+ } // ignore it
+ return null;
+ }
+
+ /**
+ * A package-private version of getLocalAddress that doesn't swallow
+ * the exception. Used by MimeMessage.setFrom() to report the reason
+ * for the failure.
+ */
+ // package-private
+ static InternetAddress _getLocalAddress(Session session)
+ throws AddressException, UnknownHostException {
+ String user = null, host = null, address = null;
+ if (session == null) {
+ user = System.getProperty("user.name");
+ host = getLocalHostName();
+ } else {
+ address = session.getProperty("mail.from");
+ if (address == null) {
+ user = session.getProperty("mail.user");
+ if (user == null || user.isEmpty())
+ user = session.getProperty("user.name");
+ if (user == null || user.isEmpty())
+ user = System.getProperty("user.name");
+ host = session.getProperty("mail.host");
+ if (host == null || host.isEmpty())
+ host = getLocalHostName();
+ }
+ }
+
+ if (address == null && user != null && user.length() != 0 &&
+ host != null && host.length() != 0)
+ address = MimeUtility.quote(user.trim(), specialsNoDot + "\t ") +
+ "@" + host;
+
+ if (address == null)
+ return null;
+
+ return new InternetAddress(address);
+ }
+
+ /**
+ * Get the local host name from InetAddress and return it in a form
+ * suitable for use in an email address.
+ */
+ private static String getLocalHostName() throws UnknownHostException {
+ String host = null;
+ InetAddress me = InetAddress.getLocalHost();
+ if (me != null) {
+ // try canonical host name first
+ if (useCanonicalHostName)
+ host = me.getCanonicalHostName();
+ if (host == null)
+ host = me.getHostName();
+ // if we can't get our name, use local address literal
+ if (host == null)
+ host = me.getHostAddress();
+ if (host != null && host.length() > 0 && isInetAddressLiteral(host))
+ host = '[' + host + ']';
+ }
+ return host;
+ }
+
+ /**
+ * Is the address an IPv4 or IPv6 address literal, which needs to
+ * be enclosed in "[]" in an email address? IPv4 literals contain
+ * decimal digits and dots, IPv6 literals contain hex digits, dots,
+ * and colons. We're lazy and don't check the exact syntax, just
+ * the allowed characters; strings that have only the allowed
+ * characters in a literal but don't meet the syntax requirements
+ * for a literal definitely can't be a host name and thus will fail
+ * later when used as an address literal.
+ */
+ private static boolean isInetAddressLiteral(String addr) {
+ boolean sawHex = false, sawColon = false;
+ for (int i = 0; i < addr.length(); i++) {
+ char c = addr.charAt(i);
+ if (c >= '0' && c <= '9')
+ ; // digits always ok
+ else if (c == '.')
+ ; // dot always ok
+ else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
+ sawHex = true; // need to see a colon too
+ else if (c == ':')
+ sawColon = true;
+ else
+ return false; // anything else, definitely not a literal
+ }
+ return !sawHex || sawColon;
+ }
+
+ /**
+ * Parse the given comma separated sequence of addresses into
+ * InternetAddress objects. Addresses must follow RFC822 syntax.
+ *
+ * @param addresslist comma separated address strings
+ * @return array of InternetAddress objects
+ * @throws AddressException if the parse failed
+ */
+ public static InternetAddress[] parse(String addresslist)
+ throws AddressException {
+ return parse(addresslist, true);
+ }
+
+ /**
+ * Parse the given sequence of addresses into InternetAddress
+ * objects. If strict is false, simple email addresses
+ * separated by spaces are also allowed. If strict is
+ * true, many (but not all) of the RFC822 syntax rules are enforced.
+ * In particular, even if strict is true, addresses
+ * composed of simple names (with no "@domain" part) are allowed.
+ * Such "illegal" addresses are not uncommon in real messages.
+ *
+ * Non-strict parsing is typically used when parsing a list of
+ * mail addresses entered by a human. Strict parsing is typically
+ * used when parsing address headers in mail messages.
+ *
+ * @param addresslist comma separated address strings
+ * @param strict enforce RFC822 syntax
+ * @return array of InternetAddress objects
+ * @throws AddressException if the parse failed
+ */
+ public static InternetAddress[] parse(String addresslist, boolean strict)
+ throws AddressException {
+ return parse(addresslist, strict, false);
+ }
+
+ /**
+ * Parse the given sequence of addresses into InternetAddress
+ * objects. If strict is false, the full syntax rules for
+ * individual addresses are not enforced. If strict is
+ * true, many (but not all) of the RFC822 syntax rules are enforced.
+ *
+ * To better support the range of "invalid" addresses seen in real
+ * messages, this method enforces fewer syntax rules than the
+ * parse method when the strict flag is false
+ * and enforces more rules when the strict flag is true. If the
+ * strict flag is false and the parse is successful in separating out an
+ * email address or addresses, the syntax of the addresses themselves
+ * is not checked.
+ *
+ * @param addresslist comma separated address strings
+ * @param strict enforce RFC822 syntax
+ * @return array of InternetAddress objects
+ * @throws AddressException if the parse failed
+ * @since JavaMail 1.3
+ */
+ public static InternetAddress[] parseHeader(String addresslist,
+ boolean strict) throws AddressException {
+ return parse(MimeUtility.unfold(addresslist), strict, true);
+ }
+
+ /*
+ * RFC822 Address parser.
+ *
+ * XXX - This is complex enough that it ought to be a real parser,
+ * not this ad-hoc mess, and because of that, this is not perfect.
+ *
+ * XXX - Deal with encoded Headers too.
+ */
+ @SuppressWarnings("fallthrough")
+ private static InternetAddress[] parse(String s, boolean strict,
+ boolean parseHdr) throws AddressException {
+ int start, end, index, nesting;
+ int start_personal = -1, end_personal = -1;
+ int length = s.length();
+ boolean ignoreErrors = parseHdr && !strict;
+ boolean in_group = false; // we're processing a group term
+ boolean route_addr = false; // address came from route-addr term
+ boolean rfc822 = false; // looks like an RFC822 address
+ char c;
+ List v = new ArrayList<>();
+ InternetAddress ma;
+
+ for (start = end = -1, index = 0; index < length; index++) {
+ c = s.charAt(index);
+
+ switch (c) {
+ case '(': // We are parsing a Comment. Ignore everything inside.
+ // XXX - comment fields should be parsed as whitespace,
+ // more than one allowed per address
+ rfc822 = true;
+ if (start >= 0 && end == -1)
+ end = index;
+ int pindex = index;
+ for (index++, nesting = 1; index < length && nesting > 0;
+ index++) {
+ c = s.charAt(index);
+ switch (c) {
+ case '\\':
+ index++; // skip both '\' and the escaped char
+ break;
+ case '(':
+ nesting++;
+ break;
+ case ')':
+ nesting--;
+ break;
+ default:
+ break;
+ }
+ }
+ if (nesting > 0) {
+ if (!ignoreErrors)
+ throw new AddressException("Missing ')'", s, index);
+ // pretend the first paren was a regular character and
+ // continue parsing after it
+ index = pindex + 1;
+ break;
+ }
+ index--; // point to closing paren
+ if (start_personal == -1)
+ start_personal = pindex + 1;
+ if (end_personal == -1)
+ end_personal = index;
+ break;
+
+ case ')':
+ if (!ignoreErrors)
+ throw new AddressException("Missing '('", s, index);
+ // pretend the left paren was a regular character and
+ // continue parsing
+ if (start == -1)
+ start = index;
+ break;
+
+ case '<':
+ rfc822 = true;
+ if (route_addr) {
+ if (!ignoreErrors)
+ throw new AddressException(
+ "Extra route-addr", s, index);
+
+ // assume missing comma between addresses
+ if (start == -1) {
+ route_addr = false;
+ rfc822 = false;
+ start = end = -1;
+ break; // nope, nothing there
+ }
+ if (!in_group) {
+ // got a token, add this to our InternetAddress list
+ if (end == -1) // should never happen
+ end = index;
+ String addr = s.substring(start, end).trim();
+
+ ma = new InternetAddress();
+ ma.setAddress(addr);
+ if (start_personal >= 0) {
+ ma.encodedPersonal = unquote(
+ s.substring(start_personal, end_personal).
+ trim());
+ }
+ v.add(ma);
+
+ route_addr = false;
+ rfc822 = false;
+ start = end = -1;
+ start_personal = end_personal = -1;
+ // continue processing this new address...
+ }
+ }
+
+ int rindex = index;
+ boolean inquote = false;
+ outf:
+ for (index++; index < length; index++) {
+ c = s.charAt(index);
+ switch (c) {
+ case '\\': // XXX - is this needed?
+ index++; // skip both '\' and the escaped char
+ break;
+ case '"':
+ inquote = !inquote;
+ break;
+ case '>':
+ if (inquote)
+ continue;
+ break outf; // out of for loop
+ default:
+ break;
+ }
+ }
+
+ // did we find a matching quote?
+ if (inquote) {
+ if (!ignoreErrors)
+ throw new AddressException("Missing '\"'", s, index);
+ // didn't find matching quote, try again ignoring quotes
+ // (e.g., ``<"@foo.com>'')
+ outq:
+ for (index = rindex + 1; index < length; index++) {
+ c = s.charAt(index);
+ if (c == '\\') // XXX - is this needed?
+ index++; // skip both '\' and the escaped char
+ else if (c == '>')
+ break;
+ }
+ }
+
+ // did we find a terminating '>'?
+ if (index >= length) {
+ if (!ignoreErrors)
+ throw new AddressException("Missing '>'", s, index);
+ // pretend the "<" was a regular character and
+ // continue parsing after it (e.g., ``<@foo.com'')
+ index = rindex + 1;
+ if (start == -1)
+ start = rindex; // back up to include "<"
+ break;
+ }
+
+ if (!in_group) {
+ if (start >= 0) {
+ // seen some characters? use them as the personal name
+ start_personal = start;
+ end_personal = rindex;
+ }
+ start = rindex + 1;
+ }
+ route_addr = true;
+ end = index;
+ break;
+
+ case '>':
+ if (!ignoreErrors)
+ throw new AddressException("Missing '<'", s, index);
+ // pretend the ">" was a regular character and
+ // continue parsing (e.g., ``>@foo.com'')
+ if (start == -1)
+ start = index;
+ break;
+
+ case '"': // parse quoted string
+ int qindex = index;
+ rfc822 = true;
+ if (start == -1)
+ start = index;
+ outq:
+ for (index++; index < length; index++) {
+ c = s.charAt(index);
+ switch (c) {
+ case '\\':
+ index++; // skip both '\' and the escaped char
+ break;
+ case '"':
+ break outq; // out of for loop
+ default:
+ break;
+ }
+ }
+ if (index >= length) {
+ if (!ignoreErrors)
+ throw new AddressException("Missing '\"'", s, index);
+ // pretend the quote was a regular character and
+ // continue parsing after it (e.g., ``"@foo.com'')
+ index = qindex + 1;
+ }
+ break;
+
+ case '[': // a domain-literal, probably
+ int lindex = index;
+ rfc822 = true;
+ if (start == -1)
+ start = index;
+ outb:
+ for (index++; index < length; index++) {
+ c = s.charAt(index);
+ switch (c) {
+ case '\\':
+ index++; // skip both '\' and the escaped char
+ break;
+ case ']':
+ break outb; // out of for loop
+ default:
+ break;
+ }
+ }
+ if (index >= length) {
+ if (!ignoreErrors)
+ throw new AddressException("Missing ']'", s, index);
+ // pretend the "[" was a regular character and
+ // continue parsing after it (e.g., ``[@foo.com'')
+ index = lindex + 1;
+ }
+ break;
+
+ case ';':
+ if (start == -1) {
+ route_addr = false;
+ rfc822 = false;
+ start = end = -1;
+ break; // nope, nothing there
+ }
+ if (in_group) {
+ in_group = false;
+ /*
+ * If parsing headers, but not strictly, peek ahead.
+ * If next char is "@", treat the group name
+ * like the local part of the address, e.g.,
+ * "Undisclosed-Recipient:;@java.sun.com".
+ */
+ if (parseHdr && !strict &&
+ index + 1 < length && s.charAt(index + 1) == '@')
+ break;
+ ma = new InternetAddress();
+ end = index + 1;
+ ma.setAddress(s.substring(start, end).trim());
+ v.add(ma);
+
+ route_addr = false;
+ rfc822 = false;
+ start = end = -1;
+ start_personal = end_personal = -1;
+ break;
+ }
+ if (!ignoreErrors)
+ throw new AddressException(
+ "Illegal semicolon, not in group", s, index);
+
+ // otherwise, parsing a header; treat semicolon like comma
+ // fall through to comma case...
+
+ case ',': // end of an address, probably
+ if (start == -1) {
+ route_addr = false;
+ rfc822 = false;
+ start = end = -1;
+ break; // nope, nothing there
+ }
+ if (in_group) {
+ route_addr = false;
+ break;
+ }
+ // got a token, add this to our InternetAddress list
+ if (end == -1)
+ end = index;
+
+ String addr = s.substring(start, end).trim();
+ String pers = null;
+ if (rfc822 && start_personal >= 0) {
+ pers = unquote(
+ s.substring(start_personal, end_personal).trim());
+ if (pers.trim().isEmpty())
+ pers = null;
+ }
+
+ /*
+ * If the personal name field has an "@" and the address
+ * field does not, assume they were reversed, e.g.,
+ * ``"joe doe" (john.doe@example.com)''.
+ */
+ if (parseHdr && !strict && pers != null &&
+ pers.indexOf('@') >= 0 &&
+ addr.indexOf('@') < 0 && addr.indexOf('!') < 0) {
+ String tmp = addr;
+ addr = pers;
+ pers = tmp;
+ }
+ if (rfc822 || strict || parseHdr) {
+ if (!ignoreErrors)
+ checkAddress(addr, route_addr, false);
+ ma = new InternetAddress();
+ ma.setAddress(addr);
+ if (pers != null)
+ ma.encodedPersonal = pers;
+ v.add(ma);
+ } else {
+ // maybe we passed over more than one space-separated addr
+ StringTokenizer st = new StringTokenizer(addr);
+ while (st.hasMoreTokens()) {
+ String a = st.nextToken();
+ checkAddress(a, false, false);
+ ma = new InternetAddress();
+ ma.setAddress(a);
+ v.add(ma);
+ }
+ }
+
+ route_addr = false;
+ rfc822 = false;
+ start = end = -1;
+ start_personal = end_personal = -1;
+ break;
+
+ case ':':
+ rfc822 = true;
+ if (in_group)
+ if (!ignoreErrors)
+ throw new AddressException("Nested group", s, index);
+ if (start == -1)
+ start = index;
+ if (parseHdr && !strict) {
+ /*
+ * If next char is a special character that can't occur at
+ * the start of a valid address, treat the group name
+ * as the entire address, e.g., "Date:, Tue", "Re:@foo".
+ */
+ if (index + 1 < length) {
+ String addressSpecials = ")>[]:@\\,.";
+ char nc = s.charAt(index + 1);
+ if (addressSpecials.indexOf(nc) >= 0) {
+ if (nc != '@')
+ break; // don't change in_group
+ /*
+ * Handle a common error:
+ * ``Undisclosed-Recipient:@example.com;''
+ *
+ * Scan ahead. If we find a semicolon before
+ * one of these other special characters,
+ * consider it to be a group after all.
+ */
+ for (int i = index + 2; i < length; i++) {
+ nc = s.charAt(i);
+ if (nc == ';')
+ break;
+ if (addressSpecials.indexOf(nc) >= 0)
+ break;
+ }
+ if (nc == ';')
+ break; // don't change in_group
+ }
+ }
+
+ // ignore bogus "mailto:" prefix in front of an address,
+ // or bogus mail header name included in the address field
+ String gname = s.substring(start, index);
+ if (ignoreBogusGroupName &&
+ (gname.equalsIgnoreCase("mailto") ||
+ gname.equalsIgnoreCase("From") ||
+ gname.equalsIgnoreCase("To") ||
+ gname.equalsIgnoreCase("Cc") ||
+ gname.equalsIgnoreCase("Subject") ||
+ gname.equalsIgnoreCase("Re")))
+ start = -1; // we're not really in a group
+ else
+ in_group = true;
+ } else
+ in_group = true;
+ break;
+
+ // Ignore whitespace
+ case ' ':
+ case '\t':
+ case '\r':
+ case '\n':
+ break;
+
+ default:
+ if (start == -1)
+ start = index;
+ break;
+ }
+ }
+
+ if (start >= 0) {
+ /*
+ * The last token, add this to our InternetAddress list.
+ * Note that this block of code should be identical to the
+ * block above for "case ','".
+ */
+ if (end == -1)
+ end = length;
+
+ String addr = s.substring(start, end).trim();
+ String pers = null;
+ if (rfc822 && start_personal >= 0) {
+ pers = unquote(
+ s.substring(start_personal, end_personal).trim());
+ if (pers.trim().isEmpty())
+ pers = null;
+ }
+
+ /*
+ * If the personal name field has an "@" and the address
+ * field does not, assume they were reversed, e.g.,
+ * ``"joe doe" (john.doe@example.com)''.
+ */
+ if (parseHdr && !strict &&
+ pers != null && pers.indexOf('@') >= 0 &&
+ addr.indexOf('@') < 0 && addr.indexOf('!') < 0) {
+ String tmp = addr;
+ addr = pers;
+ pers = tmp;
+ }
+ if (rfc822 || strict || parseHdr) {
+ if (!ignoreErrors)
+ checkAddress(addr, route_addr, false);
+ ma = new InternetAddress();
+ ma.setAddress(addr);
+ if (pers != null)
+ ma.encodedPersonal = pers;
+ v.add(ma);
+ } else {
+ // maybe we passed over more than one space-separated addr
+ StringTokenizer st = new StringTokenizer(addr);
+ while (st.hasMoreTokens()) {
+ String a = st.nextToken();
+ checkAddress(a, false, false);
+ ma = new InternetAddress();
+ ma.setAddress(a);
+ v.add(ma);
+ }
+ }
+ }
+
+ InternetAddress[] a = new InternetAddress[v.size()];
+ v.toArray(a);
+ return a;
+ }
+
+ /**
+ * Check that the address is a valid "mailbox" per RFC822.
+ * (We also allow simple names.)
+ *
+ * XXX - much more to check
+ * XXX - doesn't handle domain-literals properly (but no one uses them)
+ */
+ private static void checkAddress(String addr,
+ boolean routeAddr, boolean validate)
+ throws AddressException {
+ int i, start = 0;
+
+ if (addr == null)
+ throw new AddressException("Address is null");
+ int len = addr.length();
+ if (len == 0)
+ throw new AddressException("Empty address", addr);
+
+ /*
+ * routeAddr indicates that the address is allowed
+ * to have an RFC 822 "route".
+ */
+ if (routeAddr && addr.charAt(0) == '@') {
+ /*
+ * Check for a legal "route-addr":
+ * [@domain[,@domain ...]:]local@domain
+ */
+ for (start = 0; (i = indexOfAny(addr, ",:", start)) >= 0;
+ start = i + 1) {
+ if (addr.charAt(start) != '@')
+ throw new AddressException("Illegal route-addr", addr);
+ if (addr.charAt(i) == ':') {
+ // end of route-addr
+ start = i + 1;
+ break;
+ }
+ }
+ }
+
+ /*
+ * The rest should be "local@domain", but we allow simply "local"
+ * unless called from validate.
+ *
+ * local-part must follow RFC 822 - no specials except '.'
+ * unless quoted.
+ */
+
+ char c = (char) -1;
+ char lastc = (char) -1;
+ boolean inquote = false;
+ for (i = start; i < len; i++) {
+ lastc = c;
+ c = addr.charAt(i);
+ // a quoted-pair is only supposed to occur inside a quoted string,
+ // but some people use it outside so we're more lenient
+ if (c == '\\' || lastc == '\\')
+ continue;
+ if (c == '"') {
+ if (inquote) {
+ // peek ahead, next char must be "@"
+ if (validate && i + 1 < len && addr.charAt(i + 1) != '@')
+ throw new AddressException(
+ "Quote not at end of local address", addr);
+ inquote = false;
+ } else {
+ if (validate && i != 0)
+ throw new AddressException(
+ "Quote not at start of local address", addr);
+ inquote = true;
+ }
+ continue;
+ } else if (c == '\r') {
+ // peek ahead, next char must be LF
+ if (i + 1 < len && addr.charAt(i + 1) != '\n')
+ throw new AddressException(
+ "Quoted local address contains CR without LF", addr);
+ } else if (c == '\n') {
+ /*
+ * CRLF followed by whitespace is allowed in a quoted string.
+ * We allowed naked LF, but ensure LF is always followed by
+ * whitespace to prevent spoofing the end of the header.
+ */
+ if (i + 1 < len && addr.charAt(i + 1) != ' ' &&
+ addr.charAt(i + 1) != '\t')
+ throw new AddressException(
+ "Quoted local address contains newline without whitespace",
+ addr);
+ }
+ if (inquote)
+ continue;
+ // dot rules should not be applied to quoted-string
+ if (c == '.') {
+ if (i == start)
+ throw new AddressException(
+ "Local address starts with dot", addr);
+ if (lastc == '.')
+ throw new AddressException(
+ "Local address contains dot-dot", addr);
+ }
+ if (c == '@') {
+ if (i == 0)
+ throw new AddressException("Missing local name", addr);
+ if (lastc == '.')
+ throw new AddressException(
+ "Local address ends with dot", addr);
+ break; // done with local part
+ }
+ if (c <= 040 || c == 0177)
+ throw new AddressException(
+ "Local address contains control or whitespace", addr);
+ if (specialsNoDot.indexOf(c) >= 0)
+ throw new AddressException(
+ "Local address contains illegal character", addr);
+ }
+ if (inquote)
+ throw new AddressException("Unterminated quote", addr);
+
+ /*
+ * Done with local part, now check domain.
+ *
+ * Note that the MimeMessage class doesn't remember addresses
+ * as separate objects; it writes them out as headers and then
+ * parses the headers when the addresses are requested.
+ * In order to support the case where a "simple" address is used,
+ * but the address also has a personal name and thus looks like
+ * it should be a valid RFC822 address when parsed, we only check
+ * this if we're explicitly called from the validate method.
+ */
+
+ if (c != '@') {
+ if (validate)
+ throw new AddressException("Missing final '@domain'", addr);
+ return;
+ }
+
+ // check for illegal chars in the domain, but ignore domain literals
+
+ start = i + 1;
+ if (start >= len)
+ throw new AddressException("Missing domain", addr);
+
+ if (addr.charAt(start) == '.')
+ throw new AddressException("Domain starts with dot", addr);
+ boolean inliteral = false;
+ for (i = start; i < len; i++) {
+ c = addr.charAt(i);
+ if (c == '[') {
+ if (i != start)
+ throw new AddressException(
+ "Domain literal not at start of domain", addr);
+ inliteral = true; // domain literal, don't validate
+ } else if (c == ']') {
+ if (i != len - 1)
+ throw new AddressException(
+ "Domain literal end not at end of domain", addr);
+ inliteral = false;
+ } else if (c <= 040 || c == 0177) {
+ throw new AddressException(
+ "Domain contains control or whitespace", addr);
+ } else {
+ // RFC 2822 rule
+ //if (specialsNoDot.indexOf(c) >= 0)
+ /*
+ * RFC 1034 rule is more strict
+ * the full rule is:
+ *
+ * ::= | " "
+ * ::=