View Javadoc

1   /**
2    *
3    * Copyright 2003-2004 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package javax.mail;
19  
20  import java.io.BufferedReader;
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.PrintStream;
27  import java.lang.reflect.Constructor;
28  import java.lang.reflect.InvocationTargetException;
29  import java.net.InetAddress;
30  import java.net.URL;
31  import java.util.ArrayList;
32  import java.util.Enumeration;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Properties;
37  import java.util.StringTokenizer;
38  import java.util.WeakHashMap;
39  
40  
41  /**
42   * OK, so we have a final class in the API with a heck of a lot of implementation required...
43   * let's try and figure out what it is meant to do.
44   * <p/>
45   * It is supposed to collect together properties and defaults so that they can be
46   * shared by multiple applications on a desktop; with process isolation and no
47   * real concept of shared memory, this seems challenging. These properties and
48   * defaults rely on system properties, making management in a app server harder,
49   * and on resources loaded from "mail.jar" which may lead to skew between
50   * differnet independent implementations of this API.
51   *
52   * @version $Rev: 399294 $ $Date: 2006-05-03 06:28:41 -0700 (Wed, 03 May 2006) $
53   */
54  public final class Session {
55      private static final Class[] PARAM_TYPES = {Session.class, URLName.class};
56      private static final WeakHashMap addressMapsByClassLoader = new WeakHashMap();
57      private static Session DEFAULT_SESSION;
58  
59      private Map passwordAuthentications = new HashMap();
60  
61      private final Properties properties;
62      private final Authenticator authenticator;
63      private boolean debug;
64      private PrintStream debugOut = System.out;
65  
66      private static final WeakHashMap providersByClassLoader = new WeakHashMap();
67  
68      /**
69       * No public constrcutor allowed.
70       */
71      private Session(Properties properties, Authenticator authenticator) {
72          this.properties = properties;
73          this.authenticator = authenticator;
74          debug = Boolean.valueOf(properties.getProperty("mail.debug")).booleanValue();
75      }
76  
77      /**
78       * Create a new session initialized with the supplied properties which uses the supplied authenticator.
79       * Clients should ensure the properties listed in Appendix A of the JavaMail specification are
80       * set as the defaults are unlikey to work in most scenarios; particular attention should be given
81       * to:
82       * <ul>
83       * <li>mail.store.protocol</li>
84       * <li>mail.transport.protocol</li>
85       * <li>mail.host</li>
86       * <li>mail.user</li>
87       * <li>mail.from</li>
88       * </ul>
89       *
90       * @param properties    the session properties
91       * @param authenticator an authenticator for callbacks to the user
92       * @return a new session
93       */
94      public static Session getInstance(Properties properties, Authenticator authenticator) {
95          return new Session(new Properties(properties), authenticator);
96      }
97  
98      /**
99       * 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 }