001    /**
002     *
003     * Copyright 2003-2004 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package javax.mail;
019    
020    import java.io.BufferedReader;
021    import java.io.File;
022    import java.io.FileInputStream;
023    import java.io.IOException;
024    import java.io.InputStream;
025    import java.io.InputStreamReader;
026    import java.io.PrintStream;
027    import java.lang.reflect.Constructor;
028    import java.lang.reflect.InvocationTargetException;
029    import java.net.InetAddress;
030    import java.net.URL;
031    import java.util.ArrayList;
032    import java.util.Enumeration;
033    import java.util.HashMap;
034    import java.util.List;
035    import java.util.Map;
036    import java.util.Properties;
037    import java.util.StringTokenizer;
038    import java.util.WeakHashMap;
039    
040    
041    /**
042     * OK, so we have a final class in the API with a heck of a lot of implementation required...
043     * let's try and figure out what it is meant to do.
044     * <p/>
045     * It is supposed to collect together properties and defaults so that they can be
046     * shared by multiple applications on a desktop; with process isolation and no
047     * real concept of shared memory, this seems challenging. These properties and
048     * defaults rely on system properties, making management in a app server harder,
049     * and on resources loaded from "mail.jar" which may lead to skew between
050     * differnet independent implementations of this API.
051     *
052     * @version $Rev: 399294 $ $Date: 2006-05-03 06:28:41 -0700 (Wed, 03 May 2006) $
053     */
054    public final class Session {
055        private static final Class[] PARAM_TYPES = {Session.class, URLName.class};
056        private static final WeakHashMap addressMapsByClassLoader = new WeakHashMap();
057        private static Session DEFAULT_SESSION;
058    
059        private Map passwordAuthentications = new HashMap();
060    
061        private final Properties properties;
062        private final Authenticator authenticator;
063        private boolean debug;
064        private PrintStream debugOut = System.out;
065    
066        private static final WeakHashMap providersByClassLoader = new WeakHashMap();
067    
068        /**
069         * No public constrcutor allowed.
070         */
071        private Session(Properties properties, Authenticator authenticator) {
072            this.properties = properties;
073            this.authenticator = authenticator;
074            debug = Boolean.valueOf(properties.getProperty("mail.debug")).booleanValue();
075        }
076    
077        /**
078         * Create a new session initialized with the supplied properties which uses the supplied authenticator.
079         * Clients should ensure the properties listed in Appendix A of the JavaMail specification are
080         * set as the defaults are unlikey to work in most scenarios; particular attention should be given
081         * to:
082         * <ul>
083         * <li>mail.store.protocol</li>
084         * <li>mail.transport.protocol</li>
085         * <li>mail.host</li>
086         * <li>mail.user</li>
087         * <li>mail.from</li>
088         * </ul>
089         *
090         * @param properties    the session properties
091         * @param authenticator an authenticator for callbacks to the user
092         * @return a new session
093         */
094        public static Session getInstance(Properties properties, Authenticator authenticator) {
095            return new Session(new Properties(properties), authenticator);
096        }
097    
098        /**
099         * Create a new session initialized with the supplied properties with no authenticator.
100         *
101         * @param properties the session properties
102         * @return a new session
103         * @see #getInstance(java.util.Properties, Authenticator)
104         */
105        public static Session getInstance(Properties properties) {
106            return getInstance(properties, null);
107        }
108    
109        /**
110         * Get the "default" instance assuming no authenticator is required.
111         *
112         * @param properties the session properties
113         * @return if "default" session
114         * @throws SecurityException if the does not have permission to access the default session
115         */
116        public synchronized static Session getDefaultInstance(Properties properties) {
117            return getDefaultInstance(properties, null);
118        }
119    
120        /**
121         * Get the "default" session.
122         * If there is not current "default", a new Session is created and installed as the default.
123         *
124         * @param properties
125         * @param authenticator
126         * @return if "default" session
127         * @throws SecurityException if the does not have permission to access the default session
128         */
129        public synchronized static Session getDefaultInstance(Properties properties, Authenticator authenticator) {
130            if (DEFAULT_SESSION == null) {
131                DEFAULT_SESSION = getInstance(properties, authenticator);
132            } else {
133                if (authenticator != DEFAULT_SESSION.authenticator) {
134                    if (authenticator == null || DEFAULT_SESSION.authenticator == null || authenticator.getClass().getClassLoader() != DEFAULT_SESSION.authenticator.getClass().getClassLoader()) {
135                        throw new SecurityException();
136                    }
137                }
138                // todo we should check with the SecurityManager here as well
139            }
140            return DEFAULT_SESSION;
141        }
142    
143        /**
144         * Enable debugging for this session.
145         * Debugging can also be enabled by setting the "mail.debug" property to true when
146         * the session is being created.
147         *
148         * @param debug the debug setting
149         */
150        public void setDebug(boolean debug) {
151            this.debug = debug;
152        }
153    
154        /**
155         * Get the debug setting for this session.
156         *
157         * @return the debug setting
158         */
159        public boolean getDebug() {
160            return debug;
161        }
162    
163        /**
164         * Set the output stream where debug information should be sent.
165         * If set to null, System.out will be used.
166         *
167         * @param out the stream to write debug information to
168         */
169        public void setDebugOut(PrintStream out) {
170            debugOut = out == null ? System.out : out;
171        }
172    
173        /**
174         * Return the debug output stream.
175         *
176         * @return the debug output stream
177         */
178        public PrintStream getDebugOut() {
179            return debugOut;
180        }
181    
182        /**
183         * Return the list of providers available to this application.
184         * This method searches for providers that are defined in the javamail.providers
185         * and javamail.default.providers resources available through the current context
186         * classloader, or if that is not available, the classloader that loaded this class.
187         * <p/>
188         * As searching for providers is potentially expensive, this implementation maintains
189         * a WeakHashMap of providers indexed by ClassLoader.
190         *
191         * @return an array of providers
192         */
193        public Provider[] getProviders() {
194            ProviderInfo info = getProviderInfo();
195            return (Provider[]) info.all.toArray(new Provider[info.all.size()]);
196        }
197    
198        /**
199         * Return the provider for a specific protocol.
200         * This implementation initially looks in the Session properties for an property with the name
201         * "mail.<protocol>.class"; if found it attempts to create an instance of the class named in that
202         * property throwing a NoSuchProviderException if the class cannot be loaded.
203         * If this property is not found, it searches the providers returned by {@link #getProviders()}
204         * for a entry for the specified protocol.
205         *
206         * @param protocol the protocol to get a provider for
207         * @return a provider for that protocol
208         * @throws NoSuchProviderException
209         */
210        public Provider getProvider(String protocol) throws NoSuchProviderException {
211            ProviderInfo info = getProviderInfo();
212            Provider provider = null;
213            String providerName = properties.getProperty("mail." + protocol + ".class");
214            if (providerName != null) {
215                provider = (Provider) info.byClassName.get(providerName);
216                if (debug) {
217                    writeDebug("DEBUG: new provider loaded: " + provider.toString());
218                }
219            }
220    
221            // if not able to locate this by class name, just grab a registered protocol.
222            if (provider == null) {
223                provider = (Provider) info.byProtocol.get(protocol);
224            }
225    
226            if (provider == null) {
227                throw new NoSuchProviderException("Unable to locate provider for protocol: " + protocol);
228            }
229            if (debug) {
230                writeDebug("DEBUG: getProvider() returning provider " + provider.toString());
231            }
232            return provider;
233        }
234    
235        /**
236         * Make the supplied Provider the default for its protocol.
237         *
238         * @param provider the new default Provider
239         * @throws NoSuchProviderException
240         */
241        public void setProvider(Provider provider) throws NoSuchProviderException {
242            ProviderInfo info = getProviderInfo();
243            info.byProtocol.put(provider.getProtocol(), provider);
244        }
245    
246        /**
247         * Return a Store for the default protocol defined by the mail.store.protocol property.
248         *
249         * @return the store for the default protocol
250         * @throws NoSuchProviderException
251         */
252        public Store getStore() throws NoSuchProviderException {
253            String protocol = properties.getProperty("mail.store.protocol");
254            if (protocol == null) {
255                throw new NoSuchProviderException("mail.store.protocol property is not set");
256            }
257            return getStore(protocol);
258        }
259    
260        /**
261         * Return a Store for the specified protocol.
262         *
263         * @param protocol the protocol to get a Store for
264         * @return a Store
265         * @throws NoSuchProviderException if no provider is defined for the specified protocol
266         */
267        public Store getStore(String protocol) throws NoSuchProviderException {
268            Provider provider = getProvider(protocol);
269            return getStore(provider);
270        }
271    
272        /**
273         * Return a Store for the protocol specified in the given URL
274         *
275         * @param url the URL of the Store
276         * @return a Store
277         * @throws NoSuchProviderException if no provider is defined for the specified protocol
278         */
279        public Store getStore(URLName url) throws NoSuchProviderException {
280            return (Store) getService(getProvider(url.getProtocol()), url);
281        }
282    
283        /**
284         * Return the Store specified by the given provider.
285         *
286         * @param provider the provider to create from
287         * @return a Store
288         * @throws NoSuchProviderException if there was a problem creating the Store
289         */
290        public Store getStore(Provider provider) throws NoSuchProviderException {
291            if (Provider.Type.STORE != provider.getType()) {
292                throw new NoSuchProviderException("Not a Store Provider: " + provider);
293            }
294            return (Store) getService(provider, null);
295        }
296    
297        /**
298         * Return a closed folder for the supplied URLName, or null if it cannot be obtained.
299         * <p/>
300         * The scheme portion of the URL is used to locate the Provider and create the Store;
301         * the returned Store is then used to obtain the folder.
302         *
303         * @param name the location of the folder
304         * @return the requested folder, or null if it is unavailable
305         * @throws NoSuchProviderException if there is no provider
306         * @throws MessagingException      if there was a problem accessing the Store
307         */
308        public Folder getFolder(URLName name) throws MessagingException {
309            Store store = getStore(name);
310            return store.getFolder(name);
311        }
312    
313        /**
314         * Return a Transport for the default protocol specified by the
315         * <code>mail.transport.protocol</code> property.
316         *
317         * @return a Transport
318         * @throws NoSuchProviderException
319         */
320        public Transport getTransport() throws NoSuchProviderException {
321            String protocol = properties.getProperty("mail.transport.protocol");
322            if (protocol == null) {
323                throw new NoSuchProviderException("mail.transport.protocol property is not set");
324            }
325            return getTransport(protocol);
326        }
327    
328        /**
329         * Return a Transport for the specified protocol.
330         *
331         * @param protocol the protocol to use
332         * @return a Transport
333         * @throws NoSuchProviderException
334         */
335        public Transport getTransport(String protocol) throws NoSuchProviderException {
336            Provider provider = getProvider(protocol);
337            return getTransport(provider);
338        }
339    
340        /**
341         * Return a transport for the protocol specified in the URL.
342         *
343         * @param name the URL whose scheme specifies the protocol
344         * @return a Transport
345         * @throws NoSuchProviderException
346         */
347        public Transport getTransport(URLName name) throws NoSuchProviderException {
348            return (Transport) getService(getProvider(name.getProtocol()), name);
349        }
350    
351        /**
352         * Return a transport for the protocol associated with the type of this address.
353         *
354         * @param address the address we are trying to deliver to
355         * @return a Transport
356         * @throws NoSuchProviderException
357         */
358        public Transport getTransport(Address address) throws NoSuchProviderException {
359            String type = address.getType();
360            // load the address map from the resource files.
361            Map addressMap = getAddressMap();
362            String protocolName = (String)addressMap.get(type);
363            if (protocolName == null) {
364                throw new NoSuchProviderException("No provider for address type " + type);
365            }
366            return getTransport(protocolName);
367        }
368    
369        /**
370         * Return the Transport specified by a Provider
371         *
372         * @param provider the defining Provider
373         * @return a Transport
374         * @throws NoSuchProviderException
375         */
376        public Transport getTransport(Provider provider) throws NoSuchProviderException {
377            return (Transport) getService(provider, null);
378        }
379    
380        /**
381         * Set the password authentication associated with a URL.
382         *
383         * @param name          the url
384         * @param authenticator the authenticator
385         */
386        public void setPasswordAuthentication(URLName name, PasswordAuthentication authenticator) {
387            if (authenticator == null) {
388                passwordAuthentications.remove(name);
389            } else {
390                passwordAuthentications.put(name, authenticator);
391            }
392        }
393    
394        /**
395         * Get the password authentication associated with a URL
396         *
397         * @param name the URL
398         * @return any authenticator for that url, or null if none
399         */
400        public PasswordAuthentication getPasswordAuthentication(URLName name) {
401            return (PasswordAuthentication) passwordAuthentications.get(name);
402        }
403    
404        /**
405         * Call back to the application supplied authenticator to get the needed username add password.
406         *
407         * @param host            the host we are trying to connect to, may be null
408         * @param port            the port on that host
409         * @param protocol        the protocol trying to be used
410         * @param prompt          a String to show as part of the prompt, may be null
411         * @param defaultUserName the default username, may be null
412         * @return the authentication information collected by the authenticator; may be null
413         */
414        public PasswordAuthentication requestPasswordAuthentication(InetAddress host, int port, String protocol, String prompt, String defaultUserName) {
415            if (authenticator == null) {
416                return null;
417            }
418            return authenticator.authenticate(host, port, protocol, prompt, defaultUserName);
419        }
420    
421        /**
422         * Return the properties object for this Session; this is a live collection.
423         *
424         * @return the properties for the Session
425         */
426        public Properties getProperties() {
427            return properties;
428        }
429    
430        /**
431         * Return the specified property.
432         *
433         * @param property the property to get
434         * @return its value, or null if not present
435         */
436        public String getProperty(String property) {
437            return getProperties().getProperty(property);
438        }
439    
440        private Service getService(Provider provider, URLName name) throws NoSuchProviderException {
441            try {
442                ClassLoader cl = getClassLoader();
443                Class clazz = cl.loadClass(provider.getClassName());
444                Constructor ctr = clazz.getConstructor(PARAM_TYPES);
445                return (Service) ctr.newInstance(new Object[]{this, name});
446            } catch (ClassNotFoundException e) {
447                throw (NoSuchProviderException) new NoSuchProviderException("Unable to load class for provider: " + provider).initCause(e);
448            } catch (NoSuchMethodException e) {
449                throw (NoSuchProviderException) new NoSuchProviderException("Provider class does not have a constructor(Session, URLName): " + provider).initCause(e);
450            } catch (InstantiationException e) {
451                throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e);
452            } catch (IllegalAccessException e) {
453                throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e);
454            } catch (InvocationTargetException e) {
455                throw (NoSuchProviderException) new NoSuchProviderException("Exception from constructor of provider class: " + provider).initCause(e.getCause());
456            }
457        }
458    
459        private ProviderInfo getProviderInfo() {
460            ClassLoader cl = getClassLoader();
461            ProviderInfo info = (ProviderInfo) providersByClassLoader.get(cl);
462            if (info == null) {
463                info = loadProviders(cl);
464            }
465            return info;
466        }
467    
468        private Map getAddressMap() {
469            ClassLoader cl = getClassLoader();
470            Map addressMap = (Map)addressMapsByClassLoader.get(cl);
471            if (addressMap == null) {
472                addressMap = loadAddressMap(cl);
473            }
474            return addressMap;
475        }
476    
477    
478        /**
479         * Resolve a class loader used to resolve context resources.  The
480         * class loader used is either a current thread context class
481         * loader (if set), the class loader used to load an authenticator
482         * we've been initialized with, or the class loader used to load
483         * this class instance (which may be a subclass of Session).
484         *
485         * @return The class loader used to load resources.
486         */
487        private ClassLoader getClassLoader() {
488            ClassLoader cl = Thread.currentThread().getContextClassLoader();
489            if (cl == null) {
490                if (authenticator != null) {
491                    cl = authenticator.getClass().getClassLoader();
492                }
493                else {
494                    cl = this.getClass().getClassLoader();
495                }
496            }
497            return cl;
498        }
499    
500        private ProviderInfo loadProviders(ClassLoader cl) {
501            // we create a merged map from reading all of the potential address map entries.  The locations
502            // searched are:
503            //   1.   java.home/lib/javamail.address.map
504            //   2. META-INF/javamail.address.map
505            //   3. META-INF/javamail.default.address.map
506            //
507            ProviderInfo info = new ProviderInfo();
508    
509            // make sure this is added to the global map.
510            providersByClassLoader.put(cl, info);
511    
512    
513            // NOTE:  Unlike the addressMap, we process these in the defined order.  The loading routine
514            // will not overwrite entries if they already exist in the map.
515    
516            try {
517                Enumeration e = cl.getResources("META-INF/javamail.default.providers");
518                while (e.hasMoreElements()) {
519                    URL url = (URL) e.nextElement();
520                    if (debug) {
521                        writeDebug("Loading javamail.default.providers from " + url.toString());
522                    }
523    
524                    InputStream is = url.openStream();
525                    try {
526                        loadProviders(info, is);
527                    } finally{
528                        is.close();
529                    }
530                }
531            } catch (SecurityException e) {
532                // ignore
533            } catch (IOException e) {
534                // ignore
535            }
536    
537    
538            try {
539                File file = new File(System.getProperty("java.home"), "lib/javamail.providers");
540                InputStream is = new FileInputStream(file);
541                try {
542                    loadProviders(info, is);
543                    if (debug) {
544                        writeDebug("Loaded lib/javamail.providers from " + file.toString());
545                    }
546                } finally{
547                    is.close();
548                }
549            } catch (SecurityException e) {
550                // ignore
551            } catch (IOException e) {
552                // ignore
553            }
554    
555            try {
556                Enumeration e = cl.getResources("META-INF/javamail.providers");
557                while (e.hasMoreElements()) {
558                    URL url = (URL) e.nextElement();
559                    if (debug) {
560                        writeDebug("Loading META-INF/javamail.providers from " + url.toString());
561                    }
562                    InputStream is = url.openStream();
563                    try {
564                        loadProviders(info, is);
565                    } finally{
566                        is.close();
567                    }
568                }
569            } catch (SecurityException e) {
570                // ignore
571            } catch (IOException e) {
572                // ignore
573            }
574    
575            return info;
576        }
577    
578        private void loadProviders(ProviderInfo info, InputStream is) throws IOException {
579            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
580            String line;
581            while ((line = reader.readLine()) != null) {
582                StringTokenizer tok = new StringTokenizer(line, ";");
583                String protocol = null;
584                Provider.Type type = null;
585                String className = null;
586                String vendor = null;
587                String version = null;
588                while (tok.hasMoreTokens()) {
589                    String property = tok.nextToken();
590                    int index = property.indexOf('=');
591                    if (index == -1) {
592                        continue;
593                    }
594                    String key = property.substring(0, index).trim().toLowerCase();
595                    String value = property.substring(index+1).trim();
596                    if (protocol == null && "protocol".equals(key)) {
597                        protocol = value;
598                    } else if (type == null && "type".equals(key)) {
599                        if ("store".equals(value)) {
600                            type = Provider.Type.STORE;
601                        } else if ("transport".equals(value)) {
602                            type = Provider.Type.TRANSPORT;
603                        }
604                    } else if (className == null && "class".equals(key)) {
605                        className = value;
606                    } else if ("vendor".equals(key)) {
607                        vendor = value;
608                    } else if ("version".equals(key)) {
609                        version = value;
610                    }
611                }
612                if (protocol == null || type == null || className == null) {
613                    //todo should we log a warning?
614                    continue;
615                }
616    
617                if (debug) {
618                    writeDebug("DEBUG: loading new provider protocol=" + protocol + ", className=" + className + ", vendor=" + vendor + ", version=" + version);
619                }
620                Provider provider = new Provider(protocol, className, type, vendor, version);
621                if (!info.byClassName.containsKey(className)) {
622                    info.byClassName.put(className, provider);
623                }
624                if (!info.byProtocol.containsKey(protocol)) {
625                    info.byProtocol.put(protocol, provider);
626                }
627                info.all.add(provider);
628            }
629        }
630    
631        /**
632         * Load up an address map associated with a using class loader
633         * instance.
634         *
635         * @param cl     The class loader used to resolve the address map.
636         *
637         * @return A map containing the entries associated with this classloader
638         *         instance.
639         */
640        private static Map loadAddressMap(ClassLoader cl) {
641            // we create a merged map from reading all of the potential address map entries.  The locations
642            // searched are:
643            //   1.   java.home/lib/javamail.address.map
644            //   2. META-INF/javamail.address.map
645            //   3. META-INF/javamail.default.address.map
646            //
647            // if all of the above searches fail, we just set up some "default" defaults.
648    
649            // the format of the address.map file is defined as a property file.  We can cheat and
650            // just use Properties.load() to read in the files.
651            Properties addressMap = new Properties();
652    
653            // add this to the tracking map.
654            addressMapsByClassLoader.put(cl, addressMap);
655    
656            // NOTE:  We are reading these resources in reverse order of what's cited above.  This allows
657            // user defined entries to overwrite default entries if there are similarly named items.
658    
659            try {
660                Enumeration e = cl.getResources("META-INF/javamail.default.address.map");
661                while (e.hasMoreElements()) {
662                    URL url = (URL) e.nextElement();
663                    InputStream is = url.openStream();
664                    try {
665                        // load as a property file
666                        addressMap.load(is);
667                    } finally{
668                        is.close();
669                    }
670                }
671            } catch (SecurityException e) {
672                // ignore
673            } catch (IOException e) {
674                // ignore
675            }
676    
677    
678            try {
679                Enumeration e = cl.getResources("META-INF/javamail.address.map");
680                while (e.hasMoreElements()) {
681                    URL url = (URL) e.nextElement();
682                    InputStream is = url.openStream();
683                    try {
684                        // load as a property file
685                        addressMap.load(is);
686                    } finally{
687                        is.close();
688                    }
689                }
690            } catch (SecurityException e) {
691                // ignore
692            } catch (IOException e) {
693                // ignore
694            }
695    
696    
697            try {
698                File file = new File(System.getProperty("java.home"), "lib/javamail.address.map");
699                InputStream is = new FileInputStream(file);
700                try {
701                    // load as a property file
702                    addressMap.load(is);
703                } finally{
704                    is.close();
705                }
706            } catch (SecurityException e) {
707                // ignore
708            } catch (IOException e) {
709                // ignore
710            }
711    
712            try {
713                Enumeration e = cl.getResources("META-INF/javamail.address.map");
714                while (e.hasMoreElements()) {
715                    URL url = (URL) e.nextElement();
716                    InputStream is = url.openStream();
717                    try {
718                        // load as a property file
719                        addressMap.load(is);
720                    } finally{
721                        is.close();
722                    }
723                }
724            } catch (SecurityException e) {
725                // ignore
726            } catch (IOException e) {
727                // ignore
728            }
729    
730    
731            // if unable to load anything, at least create the MimeMessage-smtp protocol mapping.
732            if (addressMap.isEmpty()) {
733                addressMap.put("rfc822", "smtp");
734            }
735    
736            return addressMap;
737        }
738    
739        /**
740         * Private convenience routine for debug output.
741         *
742         * @param msg    The message to write out to the debug stream.
743         */
744        private void writeDebug(String msg) {
745            debugOut.println(msg);
746        }
747    
748    
749        private static class ProviderInfo {
750            private final Map byClassName = new HashMap();
751            private final Map byProtocol = new HashMap();
752            private final List all = new ArrayList();
753        }
754    }