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
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
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
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
502
503
504
505
506
507 ProviderInfo info = new ProviderInfo();
508
509
510 providersByClassLoader.put(cl, info);
511
512
513
514
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
533 } catch (IOException e) {
534
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
551 } catch (IOException e) {
552
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
571 } catch (IOException e) {
572
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
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
642
643
644
645
646
647
648
649
650
651 Properties addressMap = new Properties();
652
653
654 addressMapsByClassLoader.put(cl, addressMap);
655
656
657
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
666 addressMap.load(is);
667 } finally{
668 is.close();
669 }
670 }
671 } catch (SecurityException e) {
672
673 } catch (IOException e) {
674
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
685 addressMap.load(is);
686 } finally{
687 is.close();
688 }
689 }
690 } catch (SecurityException e) {
691
692 } catch (IOException e) {
693
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
702 addressMap.load(is);
703 } finally{
704 is.close();
705 }
706 } catch (SecurityException e) {
707
708 } catch (IOException e) {
709
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
719 addressMap.load(is);
720 } finally{
721 is.close();
722 }
723 }
724 } catch (SecurityException e) {
725
726 } catch (IOException e) {
727
728 }
729
730
731
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 }