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 }