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 }