1 /**
2 *
3 * Copyright 2003-2006 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: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 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
441 /**
442 * Add a provider to the Session managed provider list.
443 *
444 * @param provider The new provider to add.
445 */
446 public synchronized void addProvider(Provider provider) {
447 ProviderInfo info = getProviderInfo();
448 info.addProvider(provider);
449 }
450
451
452
453 /**
454 * Add a mapping between an address type and a protocol used
455 * to process that address type.
456 *
457 * @param addressType
458 * The address type identifier.
459 * @param protocol The protocol name mapping.
460 */
461 public void setProtocolForAddress(String addressType, String protocol) {
462 Map addressMap = getAddressMap();
463
464
465 if (protocol == null) {
466 addressMap.remove(addressType);
467 }
468 else {
469 addressMap.put(addressType, protocol);
470 }
471 }
472
473
474 private Service getService(Provider provider, URLName name) throws NoSuchProviderException {
475 try {
476 ClassLoader cl = getClassLoader();
477 Class clazz = cl.loadClass(provider.getClassName());
478 Constructor ctr = clazz.getConstructor(PARAM_TYPES);
479 return (Service) ctr.newInstance(new Object[]{this, name});
480 } catch (ClassNotFoundException e) {
481 throw (NoSuchProviderException) new NoSuchProviderException("Unable to load class for provider: " + provider).initCause(e);
482 } catch (NoSuchMethodException e) {
483 throw (NoSuchProviderException) new NoSuchProviderException("Provider class does not have a constructor(Session, URLName): " + provider).initCause(e);
484 } catch (InstantiationException e) {
485 throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e);
486 } catch (IllegalAccessException e) {
487 throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e);
488 } catch (InvocationTargetException e) {
489 throw (NoSuchProviderException) new NoSuchProviderException("Exception from constructor of provider class: " + provider).initCause(e.getCause());
490 }
491 }
492
493 private ProviderInfo getProviderInfo() {
494 ClassLoader cl = getClassLoader();
495 ProviderInfo info = (ProviderInfo) providersByClassLoader.get(cl);
496 if (info == null) {
497 info = loadProviders(cl);
498 }
499 return info;
500 }
501
502 private Map getAddressMap() {
503 ClassLoader cl = getClassLoader();
504 Map addressMap = (Map)addressMapsByClassLoader.get(cl);
505 if (addressMap == null) {
506 addressMap = loadAddressMap(cl);
507 }
508 return addressMap;
509 }
510
511
512 /**
513 * Resolve a class loader used to resolve context resources. The
514 * class loader used is either a current thread context class
515 * loader (if set), the class loader used to load an authenticator
516 * we've been initialized with, or the class loader used to load
517 * this class instance (which may be a subclass of Session).
518 *
519 * @return The class loader used to load resources.
520 */
521 private ClassLoader getClassLoader() {
522 ClassLoader cl = Thread.currentThread().getContextClassLoader();
523 if (cl == null) {
524 if (authenticator != null) {
525 cl = authenticator.getClass().getClassLoader();
526 }
527 else {
528 cl = this.getClass().getClassLoader();
529 }
530 }
531 return cl;
532 }
533
534 private ProviderInfo loadProviders(ClassLoader cl) {
535
536
537
538
539
540
541 ProviderInfo info = new ProviderInfo();
542
543
544 providersByClassLoader.put(cl, info);
545
546
547
548
549
550 try {
551 Enumeration e = cl.getResources("META-INF/javamail.default.providers");
552 while (e.hasMoreElements()) {
553 URL url = (URL) e.nextElement();
554 if (debug) {
555 writeDebug("Loading javamail.default.providers from " + url.toString());
556 }
557
558 InputStream is = url.openStream();
559 try {
560 loadProviders(info, is);
561 } finally{
562 is.close();
563 }
564 }
565 } catch (SecurityException e) {
566
567 } catch (IOException e) {
568
569 }
570
571
572 try {
573 File file = new File(System.getProperty("java.home"), "lib/javamail.providers");
574 InputStream is = new FileInputStream(file);
575 try {
576 loadProviders(info, is);
577 if (debug) {
578 writeDebug("Loaded lib/javamail.providers from " + file.toString());
579 }
580 } finally{
581 is.close();
582 }
583 } catch (SecurityException e) {
584
585 } catch (IOException e) {
586
587 }
588
589 try {
590 Enumeration e = cl.getResources("META-INF/javamail.providers");
591 while (e.hasMoreElements()) {
592 URL url = (URL) e.nextElement();
593 if (debug) {
594 writeDebug("Loading META-INF/javamail.providers from " + url.toString());
595 }
596 InputStream is = url.openStream();
597 try {
598 loadProviders(info, is);
599 } finally{
600 is.close();
601 }
602 }
603 } catch (SecurityException e) {
604
605 } catch (IOException e) {
606
607 }
608
609 return info;
610 }
611
612 private void loadProviders(ProviderInfo info, InputStream is) throws IOException {
613 BufferedReader reader = new BufferedReader(new InputStreamReader(is));
614 String line;
615 while ((line = reader.readLine()) != null) {
616 StringTokenizer tok = new StringTokenizer(line, ";");
617 String protocol = null;
618 Provider.Type type = null;
619 String className = null;
620 String vendor = null;
621 String version = null;
622 while (tok.hasMoreTokens()) {
623 String property = tok.nextToken();
624 int index = property.indexOf('=');
625 if (index == -1) {
626 continue;
627 }
628 String key = property.substring(0, index).trim().toLowerCase();
629 String value = property.substring(index+1).trim();
630 if (protocol == null && "protocol".equals(key)) {
631 protocol = value;
632 } else if (type == null && "type".equals(key)) {
633 if ("store".equals(value)) {
634 type = Provider.Type.STORE;
635 } else if ("transport".equals(value)) {
636 type = Provider.Type.TRANSPORT;
637 }
638 } else if (className == null && "class".equals(key)) {
639 className = value;
640 } else if ("vendor".equals(key)) {
641 vendor = value;
642 } else if ("version".equals(key)) {
643 version = value;
644 }
645 }
646 if (protocol == null || type == null || className == null) {
647
648 continue;
649 }
650
651 if (debug) {
652 writeDebug("DEBUG: loading new provider protocol=" + protocol + ", className=" + className + ", vendor=" + vendor + ", version=" + version);
653 }
654 Provider provider = new Provider(type, protocol, className, vendor, version);
655
656 info.addProvider(provider);
657 }
658 }
659
660 /**
661 * Load up an address map associated with a using class loader
662 * instance.
663 *
664 * @param cl The class loader used to resolve the address map.
665 *
666 * @return A map containing the entries associated with this classloader
667 * instance.
668 */
669 private static Map loadAddressMap(ClassLoader cl) {
670
671
672
673
674
675
676
677
678
679
680 Properties addressMap = new Properties();
681
682
683 addressMapsByClassLoader.put(cl, addressMap);
684
685
686
687
688 try {
689 Enumeration e = cl.getResources("META-INF/javamail.default.address.map");
690 while (e.hasMoreElements()) {
691 URL url = (URL) e.nextElement();
692 InputStream is = url.openStream();
693 try {
694
695 addressMap.load(is);
696 } finally{
697 is.close();
698 }
699 }
700 } catch (SecurityException e) {
701
702 } catch (IOException e) {
703
704 }
705
706
707 try {
708 Enumeration e = cl.getResources("META-INF/javamail.address.map");
709 while (e.hasMoreElements()) {
710 URL url = (URL) e.nextElement();
711 InputStream is = url.openStream();
712 try {
713
714 addressMap.load(is);
715 } finally{
716 is.close();
717 }
718 }
719 } catch (SecurityException e) {
720
721 } catch (IOException e) {
722
723 }
724
725
726 try {
727 File file = new File(System.getProperty("java.home"), "lib/javamail.address.map");
728 InputStream is = new FileInputStream(file);
729 try {
730
731 addressMap.load(is);
732 } finally{
733 is.close();
734 }
735 } catch (SecurityException e) {
736
737 } catch (IOException e) {
738
739 }
740
741 try {
742 Enumeration e = cl.getResources("META-INF/javamail.address.map");
743 while (e.hasMoreElements()) {
744 URL url = (URL) e.nextElement();
745 InputStream is = url.openStream();
746 try {
747
748 addressMap.load(is);
749 } finally{
750 is.close();
751 }
752 }
753 } catch (SecurityException e) {
754
755 } catch (IOException e) {
756
757 }
758
759
760
761 if (addressMap.isEmpty()) {
762 addressMap.put("rfc822", "smtp");
763 }
764
765 return addressMap;
766 }
767
768 /**
769 * Private convenience routine for debug output.
770 *
771 * @param msg The message to write out to the debug stream.
772 */
773 private void writeDebug(String msg) {
774 debugOut.println(msg);
775 }
776
777
778 private static class ProviderInfo {
779 private final Map byClassName = new HashMap();
780 private final Map byProtocol = new HashMap();
781 private final List all = new ArrayList();
782
783 public void addProvider(Provider provider) {
784 String className = provider.getClassName();
785
786 if (!byClassName.containsKey(className)) {
787 byClassName.put(className, provider);
788 }
789
790 String protocol = provider.getProtocol();
791 if (!byProtocol.containsKey(protocol)) {
792 byProtocol.put(protocol, provider);
793 }
794 all.add(provider);
795 }
796 }
797 }