/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package javax.mail; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.InetAddress; import java.net.URL; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import java.util.WeakHashMap; /** * OK, so we have a final class in the API with a heck of a lot of implementation required... * let's try and figure out what it is meant to do. * <p/> * It is supposed to collect together properties and defaults so that they can be * shared by multiple applications on a desktop; with process isolation and no * real concept of shared memory, this seems challenging. These properties and * defaults rely on system properties, making management in a app server harder, * and on resources loaded from "mail.jar" which may lead to skew between * differnet independent implementations of this API. * * @version $Rev$ $Date$ */ public final class Session { private static final Class[] PARAM_TYPES = {Session.class, URLName.class}; private static final WeakHashMap addressMapsByClassLoader = new WeakHashMap(); private static Session DEFAULT_SESSION; private Map passwordAuthentications = new HashMap(); private final Properties properties; private final Authenticator authenticator; private boolean debug; private PrintStream debugOut = System.out; private static final WeakHashMap providersByClassLoader = new WeakHashMap(); /** * No public constrcutor allowed. */ private Session(Properties properties, Authenticator authenticator) { this.properties = properties; this.authenticator = authenticator; debug = Boolean.valueOf(properties.getProperty("mail.debug")).booleanValue(); } /** * Create a new session initialized with the supplied properties which uses the supplied authenticator. * Clients should ensure the properties listed in Appendix A of the JavaMail specification are * set as the defaults are unlikey to work in most scenarios; particular attention should be given * to: * <ul> * <li>mail.store.protocol</li> * <li>mail.transport.protocol</li> * <li>mail.host</li> * <li>mail.user</li> * <li>mail.from</li> * </ul> * * @param properties the session properties * @param authenticator an authenticator for callbacks to the user * @return a new session */ public static Session getInstance(Properties properties, Authenticator authenticator) { return new Session(new Properties(properties), authenticator); } /** * Create a new session initialized with the supplied properties with no authenticator. * * @param properties the session properties * @return a new session * @see #getInstance(java.util.Properties, Authenticator) */ public static Session getInstance(Properties properties) { return getInstance(properties, null); } /** * Get the "default" instance assuming no authenticator is required. * * @param properties the session properties * @return if "default" session * @throws SecurityException if the does not have permission to access the default session */ public synchronized static Session getDefaultInstance(Properties properties) { return getDefaultInstance(properties, null); } /** * Get the "default" session. * If there is not current "default", a new Session is created and installed as the default. * * @param properties * @param authenticator * @return if "default" session * @throws SecurityException if the does not have permission to access the default session */ public synchronized static Session getDefaultInstance(Properties properties, Authenticator authenticator) { if (DEFAULT_SESSION == null) { DEFAULT_SESSION = getInstance(properties, authenticator); } else { if (authenticator != DEFAULT_SESSION.authenticator) { if (authenticator == null || DEFAULT_SESSION.authenticator == null || authenticator.getClass().getClassLoader() != DEFAULT_SESSION.authenticator.getClass().getClassLoader()) { throw new SecurityException(); } } // todo we should check with the SecurityManager here as well } return DEFAULT_SESSION; } /** * Enable debugging for this session. * Debugging can also be enabled by setting the "mail.debug" property to true when * the session is being created. * * @param debug the debug setting */ public void setDebug(boolean debug) { this.debug = debug; } /** * Get the debug setting for this session. * * @return the debug setting */ public boolean getDebug() { return debug; } /** * Set the output stream where debug information should be sent. * If set to null, System.out will be used. * * @param out the stream to write debug information to */ public void setDebugOut(PrintStream out) { debugOut = out == null ? System.out : out; } /** * Return the debug output stream. * * @return the debug output stream */ public PrintStream getDebugOut() { return debugOut; } /** * Return the list of providers available to this application. * This method searches for providers that are defined in the javamail.providers * and javamail.default.providers resources available through the current context * classloader, or if that is not available, the classloader that loaded this class. * <p/> * As searching for providers is potentially expensive, this implementation maintains * a WeakHashMap of providers indexed by ClassLoader. * * @return an array of providers */ public Provider[] getProviders() { ProviderInfo info = getProviderInfo(); return (Provider[]) info.all.toArray(new Provider[info.all.size()]); } /** * Return the provider for a specific protocol. * This implementation initially looks in the Session properties for an property with the name * "mail.<protocol>.class"; if found it attempts to create an instance of the class named in that * property throwing a NoSuchProviderException if the class cannot be loaded. * If this property is not found, it searches the providers returned by {@link #getProviders()} * for a entry for the specified protocol. * * @param protocol the protocol to get a provider for * @return a provider for that protocol * @throws NoSuchProviderException */ public Provider getProvider(String protocol) throws NoSuchProviderException { //If we are deployed into an OSGi environment, leverage it Class<? extends Provider> providerClass = org.apache.servicemix.specs.locator.OsgiLocator.locate(Provider.class, protocol); if (providerClass != null) { try { return (Provider) providerClass.newInstance(); } catch (InstantiationException e) { throw new NoSuchProviderException(e.getMessage()); } catch (IllegalAccessException e) { throw new NoSuchProviderException(e.getMessage()); } } ProviderInfo info = getProviderInfo(); Provider provider = null; String providerName = properties.getProperty("mail." + protocol + ".class"); if (providerName != null) { provider = (Provider) info.byClassName.get(providerName); if (debug) { writeDebug("DEBUG: new provider loaded: " + provider.toString()); } } // if not able to locate this by class name, just grab a registered protocol. if (provider == null) { provider = (Provider) info.byProtocol.get(protocol); } if (provider == null) { throw new NoSuchProviderException("Unable to locate provider for protocol: " + protocol); } if (debug) { writeDebug("DEBUG: getProvider() returning provider " + provider.toString()); } return provider; } /** * Make the supplied Provider the default for its protocol. * * @param provider the new default Provider * @throws NoSuchProviderException */ public void setProvider(Provider provider) throws NoSuchProviderException { ProviderInfo info = getProviderInfo(); info.byProtocol.put(provider.getProtocol(), provider); } /** * Return a Store for the default protocol defined by the mail.store.protocol property. * * @return the store for the default protocol * @throws NoSuchProviderException */ public Store getStore() throws NoSuchProviderException { String protocol = properties.getProperty("mail.store.protocol"); if (protocol == null) { throw new NoSuchProviderException("mail.store.protocol property is not set"); } return getStore(protocol); } /** * Return a Store for the specified protocol. * * @param protocol the protocol to get a Store for * @return a Store * @throws NoSuchProviderException if no provider is defined for the specified protocol */ public Store getStore(String protocol) throws NoSuchProviderException { Provider provider = getProvider(protocol); return getStore(provider); } /** * Return a Store for the protocol specified in the given URL * * @param url the URL of the Store * @return a Store * @throws NoSuchProviderException if no provider is defined for the specified protocol */ public Store getStore(URLName url) throws NoSuchProviderException { return (Store) getService(getProvider(url.getProtocol()), url); } /** * Return the Store specified by the given provider. * * @param provider the provider to create from * @return a Store * @throws NoSuchProviderException if there was a problem creating the Store */ public Store getStore(Provider provider) throws NoSuchProviderException { if (Provider.Type.STORE != provider.getType()) { throw new NoSuchProviderException("Not a Store Provider: " + provider); } return (Store) getService(provider, null); } /** * Return a closed folder for the supplied URLName, or null if it cannot be obtained. * <p/> * The scheme portion of the URL is used to locate the Provider and create the Store; * the returned Store is then used to obtain the folder. * * @param name the location of the folder * @return the requested folder, or null if it is unavailable * @throws NoSuchProviderException if there is no provider * @throws MessagingException if there was a problem accessing the Store */ public Folder getFolder(URLName name) throws MessagingException { Store store = getStore(name); return store.getFolder(name); } /** * Return a Transport for the default protocol specified by the * <code>mail.transport.protocol</code> property. * * @return a Transport * @throws NoSuchProviderException */ public Transport getTransport() throws NoSuchProviderException { String protocol = properties.getProperty("mail.transport.protocol"); if (protocol == null) { throw new NoSuchProviderException("mail.transport.protocol property is not set"); } return getTransport(protocol); } /** * Return a Transport for the specified protocol. * * @param protocol the protocol to use * @return a Transport * @throws NoSuchProviderException */ public Transport getTransport(String protocol) throws NoSuchProviderException { Provider provider = getProvider(protocol); return getTransport(provider); } /** * Return a transport for the protocol specified in the URL. * * @param name the URL whose scheme specifies the protocol * @return a Transport * @throws NoSuchProviderException */ public Transport getTransport(URLName name) throws NoSuchProviderException { return (Transport) getService(getProvider(name.getProtocol()), name); } /** * Return a transport for the protocol associated with the type of this address. * * @param address the address we are trying to deliver to * @return a Transport * @throws NoSuchProviderException */ public Transport getTransport(Address address) throws NoSuchProviderException { String type = address.getType(); // load the address map from the resource files. Map addressMap = getAddressMap(); String protocolName = (String)addressMap.get(type); if (protocolName == null) { throw new NoSuchProviderException("No provider for address type " + type); } return getTransport(protocolName); } /** * Return the Transport specified by a Provider * * @param provider the defining Provider * @return a Transport * @throws NoSuchProviderException */ public Transport getTransport(Provider provider) throws NoSuchProviderException { return (Transport) getService(provider, null); } /** * Set the password authentication associated with a URL. * * @param name the url * @param authenticator the authenticator */ public void setPasswordAuthentication(URLName name, PasswordAuthentication authenticator) { if (authenticator == null) { passwordAuthentications.remove(name); } else { passwordAuthentications.put(name, authenticator); } } /** * Get the password authentication associated with a URL * * @param name the URL * @return any authenticator for that url, or null if none */ public PasswordAuthentication getPasswordAuthentication(URLName name) { return (PasswordAuthentication) passwordAuthentications.get(name); } /** * Call back to the application supplied authenticator to get the needed username add password. * * @param host the host we are trying to connect to, may be null * @param port the port on that host * @param protocol the protocol trying to be used * @param prompt a String to show as part of the prompt, may be null * @param defaultUserName the default username, may be null * @return the authentication information collected by the authenticator; may be null */ public PasswordAuthentication requestPasswordAuthentication(InetAddress host, int port, String protocol, String prompt, String defaultUserName) { if (authenticator == null) { return null; } return authenticator.authenticate(host, port, protocol, prompt, defaultUserName); } /** * Return the properties object for this Session; this is a live collection. * * @return the properties for the Session */ public Properties getProperties() { return properties; } /** * Return the specified property. * * @param property the property to get * @return its value, or null if not present */ public String getProperty(String property) { return getProperties().getProperty(property); } /** * Add a provider to the Session managed provider list. * * @param provider The new provider to add. */ public synchronized void addProvider(Provider provider) { ProviderInfo info = getProviderInfo(); info.addProvider(provider); } /** * Add a mapping between an address type and a protocol used * to process that address type. * * @param addressType * The address type identifier. * @param protocol The protocol name mapping. */ public void setProtocolForAddress(String addressType, String protocol) { Map addressMap = getAddressMap(); // no protocol specified is a removal if (protocol == null) { addressMap.remove(addressType); } else { addressMap.put(addressType, protocol); } } private Service getService(Provider provider, URLName name) throws NoSuchProviderException { try { if (name == null) { name = new URLName(provider.getProtocol(), null, -1, null, null, null); } //If we are deployed into an OSGi environment, leverage it Class<? extends Service> providerClass = org.apache.servicemix.specs.locator.OsgiLocator.locate(Service.class, provider.getClassName()); if (providerClass != null) { try { Constructor ctr = providerClass.getConstructor(PARAM_TYPES); return (Service) ctr.newInstance(this, name); } catch (InstantiationException e) { throw new NoSuchProviderException(e.getMessage()); } catch (IllegalAccessException e) { throw new NoSuchProviderException(e.getMessage()); } } ClassLoader cl = getClassLoader(); Class clazz = cl.loadClass(provider.getClassName()); Constructor ctr = clazz.getConstructor(PARAM_TYPES); return (Service) ctr.newInstance(this, name); } catch (ClassNotFoundException e) { throw (NoSuchProviderException) new NoSuchProviderException("Unable to load class for provider: " + provider).initCause(e); } catch (NoSuchMethodException e) { throw (NoSuchProviderException) new NoSuchProviderException("Provider class does not have a constructor(Session, URLName): " + provider).initCause(e); } catch (InstantiationException e) { throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e); } catch (IllegalAccessException e) { throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e); } catch (InvocationTargetException e) { throw (NoSuchProviderException) new NoSuchProviderException("Exception from constructor of provider class: " + provider).initCause(e.getCause()); } } private ProviderInfo getProviderInfo() { ClassLoader cl = getClassLoader(); ProviderInfo info = (ProviderInfo) providersByClassLoader.get(cl); if (info == null) { info = loadProviders(cl); } return info; } private Map getAddressMap() { ClassLoader cl = getClassLoader(); Map addressMap = (Map)addressMapsByClassLoader.get(cl); if (addressMap == null) { addressMap = loadAddressMap(cl); } return addressMap; } /** * Resolve a class loader used to resolve context resources. The * class loader used is either a current thread context class * loader (if set), the class loader used to load an authenticator * we've been initialized with, or the class loader used to load * this class instance (which may be a subclass of Session). * * @return The class loader used to load resources. */ private ClassLoader getClassLoader() { ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (cl == null) { if (authenticator != null) { cl = authenticator.getClass().getClassLoader(); } else { cl = this.getClass().getClassLoader(); } } return cl; } private ProviderInfo loadProviders(ClassLoader cl) { // we create a merged map from reading all of the potential address map entries. The locations // searched are: // 1. java.home/lib/javamail.address.map // 2. META-INF/javamail.address.map // 3. META-INF/javamail.default.address.map // ProviderInfo info = new ProviderInfo(); // make sure this is added to the global map. providersByClassLoader.put(cl, info); // NOTE: Unlike the addressMap, we process these in the defined order. The loading routine // will not overwrite entries if they already exist in the map. try { File file = new File(System.getProperty("java.home"), "lib/javamail.providers"); InputStream is = new FileInputStream(file); try { loadProviders(info, is); if (debug) { writeDebug("Loaded lib/javamail.providers from " + file.toString()); } } finally{ is.close(); } } catch (SecurityException e) { // ignore } catch (IOException e) { // ignore } try { Enumeration e = cl.getResources("META-INF/javamail.providers"); while (e.hasMoreElements()) { URL url = (URL) e.nextElement(); if (debug) { writeDebug("Loading META-INF/javamail.providers from " + url.toString()); } InputStream is = url.openStream(); try { loadProviders(info, is); } finally{ is.close(); } } } catch (SecurityException e) { // ignore } catch (IOException e) { // ignore } try { Enumeration e = cl.getResources("META-INF/javamail.default.providers"); while (e.hasMoreElements()) { URL url = (URL) e.nextElement(); if (debug) { writeDebug("Loading javamail.default.providers from " + url.toString()); } InputStream is = url.openStream(); try { loadProviders(info, is); } finally{ is.close(); } } } catch (SecurityException e) { // ignore } catch (IOException e) { // ignore } return info; } private void loadProviders(ProviderInfo info, InputStream is) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String line; while ((line = reader.readLine()) != null) { // Lines beginning with "#" are just comments. if (line.startsWith("#")) { continue; } StringTokenizer tok = new StringTokenizer(line, ";"); String protocol = null; Provider.Type type = null; String className = null; String vendor = null; String version = null; while (tok.hasMoreTokens()) { String property = tok.nextToken(); int index = property.indexOf('='); if (index == -1) { continue; } String key = property.substring(0, index).trim().toLowerCase(); String value = property.substring(index+1).trim(); if (protocol == null && "protocol".equals(key)) { protocol = value; } else if (type == null && "type".equals(key)) { if ("store".equals(value)) { type = Provider.Type.STORE; } else if ("transport".equals(value)) { type = Provider.Type.TRANSPORT; } } else if (className == null && "class".equals(key)) { className = value; } else if ("vendor".equals(key)) { vendor = value; } else if ("version".equals(key)) { version = value; } } if (protocol == null || type == null || className == null) { //todo should we log a warning? continue; } if (debug) { writeDebug("DEBUG: loading new provider protocol=" + protocol + ", className=" + className + ", vendor=" + vendor + ", version=" + version); } Provider provider = new Provider(type, protocol, className, vendor, version); // add to the info list. info.addProvider(provider); } } /** * Load up an address map associated with a using class loader * instance. * * @param cl The class loader used to resolve the address map. * * @return A map containing the entries associated with this classloader * instance. */ private static Map loadAddressMap(ClassLoader cl) { // we create a merged map from reading all of the potential address map entries. The locations // searched are: // 1. java.home/lib/javamail.address.map // 2. META-INF/javamail.address.map // 3. META-INF/javamail.default.address.map // // if all of the above searches fail, we just set up some "default" defaults. // the format of the address.map file is defined as a property file. We can cheat and // just use Properties.load() to read in the files. Properties addressMap = new Properties(); // add this to the tracking map. addressMapsByClassLoader.put(cl, addressMap); // NOTE: We are reading these resources in reverse order of what's cited above. This allows // user defined entries to overwrite default entries if there are similarly named items. try { Enumeration e = cl.getResources("META-INF/javamail.default.address.map"); while (e.hasMoreElements()) { URL url = (URL) e.nextElement(); InputStream is = url.openStream(); try { // load as a property file addressMap.load(is); } finally{ is.close(); } } } catch (SecurityException e) { // ignore } catch (IOException e) { // ignore } try { Enumeration e = cl.getResources("META-INF/javamail.address.map"); while (e.hasMoreElements()) { URL url = (URL) e.nextElement(); InputStream is = url.openStream(); try { // load as a property file addressMap.load(is); } finally{ is.close(); } } } catch (SecurityException e) { // ignore } catch (IOException e) { // ignore } try { File file = new File(System.getProperty("java.home"), "lib/javamail.address.map"); InputStream is = new FileInputStream(file); try { // load as a property file addressMap.load(is); } finally{ is.close(); } } catch (SecurityException e) { // ignore } catch (IOException e) { // ignore } try { Enumeration e = cl.getResources("META-INF/javamail.address.map"); while (e.hasMoreElements()) { URL url = (URL) e.nextElement(); InputStream is = url.openStream(); try { // load as a property file addressMap.load(is); } finally{ is.close(); } } } catch (SecurityException e) { // ignore } catch (IOException e) { // ignore } // if unable to load anything, at least create the MimeMessage-smtp protocol mapping. if (addressMap.isEmpty()) { addressMap.put("rfc822", "smtp"); } return addressMap; } /** * Private convenience routine for debug output. * * @param msg The message to write out to the debug stream. */ private void writeDebug(String msg) { debugOut.println(msg); } private static class ProviderInfo { private final Map byClassName = new HashMap(); private final Map byProtocol = new HashMap(); private final List all = new ArrayList(); public void addProvider(Provider provider) { String className = provider.getClassName(); if (!byClassName.containsKey(className)) { byClassName.put(className, provider); } String protocol = provider.getProtocol(); if (!byProtocol.containsKey(protocol)) { byProtocol.put(protocol, provider); } all.add(provider); } } }