001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019 020 package javax.mail; 021 022 import java.io.BufferedReader; 023 import java.io.File; 024 import java.io.FileInputStream; 025 import java.io.IOException; 026 import java.io.InputStream; 027 import java.io.InputStreamReader; 028 import java.io.PrintStream; 029 import java.lang.reflect.Constructor; 030 import java.lang.reflect.InvocationTargetException; 031 import java.net.InetAddress; 032 import java.net.URL; 033 import java.util.ArrayList; 034 import java.util.Enumeration; 035 import java.util.HashMap; 036 import java.util.List; 037 import java.util.Map; 038 import java.util.Properties; 039 import java.util.StringTokenizer; 040 import java.util.WeakHashMap; 041 042 043 /** 044 * OK, so we have a final class in the API with a heck of a lot of implementation required... 045 * let's try and figure out what it is meant to do. 046 * <p/> 047 * It is supposed to collect together properties and defaults so that they can be 048 * shared by multiple applications on a desktop; with process isolation and no 049 * real concept of shared memory, this seems challenging. These properties and 050 * defaults rely on system properties, making management in a app server harder, 051 * and on resources loaded from "mail.jar" which may lead to skew between 052 * differnet independent implementations of this API. 053 * 054 * @version $Rev: 510527 $ $Date: 2007-02-22 15:17:48 +0100 (Do, 22. Feb 2007) $ 055 */ 056 public final class Session { 057 private static final Class[] PARAM_TYPES = {Session.class, URLName.class}; 058 private static final WeakHashMap addressMapsByClassLoader = new WeakHashMap(); 059 private static Session DEFAULT_SESSION; 060 061 private Map passwordAuthentications = new HashMap(); 062 063 private final Properties properties; 064 private final Authenticator authenticator; 065 private boolean debug; 066 private PrintStream debugOut = System.out; 067 068 private static final WeakHashMap providersByClassLoader = new WeakHashMap(); 069 070 /** 071 * No public constrcutor allowed. 072 */ 073 private Session(Properties properties, Authenticator authenticator) { 074 this.properties = properties; 075 this.authenticator = authenticator; 076 debug = Boolean.valueOf(properties.getProperty("mail.debug")).booleanValue(); 077 } 078 079 /** 080 * Create a new session initialized with the supplied properties which uses the supplied authenticator. 081 * Clients should ensure the properties listed in Appendix A of the JavaMail specification are 082 * set as the defaults are unlikey to work in most scenarios; particular attention should be given 083 * to: 084 * <ul> 085 * <li>mail.store.protocol</li> 086 * <li>mail.transport.protocol</li> 087 * <li>mail.host</li> 088 * <li>mail.user</li> 089 * <li>mail.from</li> 090 * </ul> 091 * 092 * @param properties the session properties 093 * @param authenticator an authenticator for callbacks to the user 094 * @return a new session 095 */ 096 public static Session getInstance(Properties properties, Authenticator authenticator) { 097 return new Session(new Properties(properties), authenticator); 098 } 099 100 /** 101 * Create a new session initialized with the supplied properties with no authenticator. 102 * 103 * @param properties the session properties 104 * @return a new session 105 * @see #getInstance(java.util.Properties, Authenticator) 106 */ 107 public static Session getInstance(Properties properties) { 108 return getInstance(properties, null); 109 } 110 111 /** 112 * Get the "default" instance assuming no authenticator is required. 113 * 114 * @param properties the session properties 115 * @return if "default" session 116 * @throws SecurityException if the does not have permission to access the default session 117 */ 118 public synchronized static Session getDefaultInstance(Properties properties) { 119 return getDefaultInstance(properties, null); 120 } 121 122 /** 123 * Get the "default" session. 124 * If there is not current "default", a new Session is created and installed as the default. 125 * 126 * @param properties 127 * @param authenticator 128 * @return if "default" session 129 * @throws SecurityException if the does not have permission to access the default session 130 */ 131 public synchronized static Session getDefaultInstance(Properties properties, Authenticator authenticator) { 132 if (DEFAULT_SESSION == null) { 133 DEFAULT_SESSION = getInstance(properties, authenticator); 134 } else { 135 if (authenticator != DEFAULT_SESSION.authenticator) { 136 if (authenticator == null || DEFAULT_SESSION.authenticator == null || authenticator.getClass().getClassLoader() != DEFAULT_SESSION.authenticator.getClass().getClassLoader()) { 137 throw new SecurityException(); 138 } 139 } 140 // todo we should check with the SecurityManager here as well 141 } 142 return DEFAULT_SESSION; 143 } 144 145 /** 146 * Enable debugging for this session. 147 * Debugging can also be enabled by setting the "mail.debug" property to true when 148 * the session is being created. 149 * 150 * @param debug the debug setting 151 */ 152 public void setDebug(boolean debug) { 153 this.debug = debug; 154 } 155 156 /** 157 * Get the debug setting for this session. 158 * 159 * @return the debug setting 160 */ 161 public boolean getDebug() { 162 return debug; 163 } 164 165 /** 166 * Set the output stream where debug information should be sent. 167 * If set to null, System.out will be used. 168 * 169 * @param out the stream to write debug information to 170 */ 171 public void setDebugOut(PrintStream out) { 172 debugOut = out == null ? System.out : out; 173 } 174 175 /** 176 * Return the debug output stream. 177 * 178 * @return the debug output stream 179 */ 180 public PrintStream getDebugOut() { 181 return debugOut; 182 } 183 184 /** 185 * Return the list of providers available to this application. 186 * This method searches for providers that are defined in the javamail.providers 187 * and javamail.default.providers resources available through the current context 188 * classloader, or if that is not available, the classloader that loaded this class. 189 * <p/> 190 * As searching for providers is potentially expensive, this implementation maintains 191 * a WeakHashMap of providers indexed by ClassLoader. 192 * 193 * @return an array of providers 194 */ 195 public Provider[] getProviders() { 196 ProviderInfo info = getProviderInfo(); 197 return (Provider[]) info.all.toArray(new Provider[info.all.size()]); 198 } 199 200 /** 201 * Return the provider for a specific protocol. 202 * This implementation initially looks in the Session properties for an property with the name 203 * "mail.<protocol>.class"; if found it attempts to create an instance of the class named in that 204 * property throwing a NoSuchProviderException if the class cannot be loaded. 205 * If this property is not found, it searches the providers returned by {@link #getProviders()} 206 * for a entry for the specified protocol. 207 * 208 * @param protocol the protocol to get a provider for 209 * @return a provider for that protocol 210 * @throws NoSuchProviderException 211 */ 212 public Provider getProvider(String protocol) throws NoSuchProviderException { 213 ProviderInfo info = getProviderInfo(); 214 Provider provider = null; 215 String providerName = properties.getProperty("mail." + protocol + ".class"); 216 if (providerName != null) { 217 provider = (Provider) info.byClassName.get(providerName); 218 if (debug) { 219 writeDebug("DEBUG: new provider loaded: " + provider.toString()); 220 } 221 } 222 223 // if not able to locate this by class name, just grab a registered protocol. 224 if (provider == null) { 225 provider = (Provider) info.byProtocol.get(protocol); 226 } 227 228 if (provider == null) { 229 throw new NoSuchProviderException("Unable to locate provider for protocol: " + protocol); 230 } 231 if (debug) { 232 writeDebug("DEBUG: getProvider() returning provider " + provider.toString()); 233 } 234 return provider; 235 } 236 237 /** 238 * Make the supplied Provider the default for its protocol. 239 * 240 * @param provider the new default Provider 241 * @throws NoSuchProviderException 242 */ 243 public void setProvider(Provider provider) throws NoSuchProviderException { 244 ProviderInfo info = getProviderInfo(); 245 info.byProtocol.put(provider.getProtocol(), provider); 246 } 247 248 /** 249 * Return a Store for the default protocol defined by the mail.store.protocol property. 250 * 251 * @return the store for the default protocol 252 * @throws NoSuchProviderException 253 */ 254 public Store getStore() throws NoSuchProviderException { 255 String protocol = properties.getProperty("mail.store.protocol"); 256 if (protocol == null) { 257 throw new NoSuchProviderException("mail.store.protocol property is not set"); 258 } 259 return getStore(protocol); 260 } 261 262 /** 263 * Return a Store for the specified protocol. 264 * 265 * @param protocol the protocol to get a Store for 266 * @return a Store 267 * @throws NoSuchProviderException if no provider is defined for the specified protocol 268 */ 269 public Store getStore(String protocol) throws NoSuchProviderException { 270 Provider provider = getProvider(protocol); 271 return getStore(provider); 272 } 273 274 /** 275 * Return a Store for the protocol specified in the given URL 276 * 277 * @param url the URL of the Store 278 * @return a Store 279 * @throws NoSuchProviderException if no provider is defined for the specified protocol 280 */ 281 public Store getStore(URLName url) throws NoSuchProviderException { 282 return (Store) getService(getProvider(url.getProtocol()), url); 283 } 284 285 /** 286 * Return the Store specified by the given provider. 287 * 288 * @param provider the provider to create from 289 * @return a Store 290 * @throws NoSuchProviderException if there was a problem creating the Store 291 */ 292 public Store getStore(Provider provider) throws NoSuchProviderException { 293 if (Provider.Type.STORE != provider.getType()) { 294 throw new NoSuchProviderException("Not a Store Provider: " + provider); 295 } 296 return (Store) getService(provider, null); 297 } 298 299 /** 300 * Return a closed folder for the supplied URLName, or null if it cannot be obtained. 301 * <p/> 302 * The scheme portion of the URL is used to locate the Provider and create the Store; 303 * the returned Store is then used to obtain the folder. 304 * 305 * @param name the location of the folder 306 * @return the requested folder, or null if it is unavailable 307 * @throws NoSuchProviderException if there is no provider 308 * @throws MessagingException if there was a problem accessing the Store 309 */ 310 public Folder getFolder(URLName name) throws MessagingException { 311 Store store = getStore(name); 312 return store.getFolder(name); 313 } 314 315 /** 316 * Return a Transport for the default protocol specified by the 317 * <code>mail.transport.protocol</code> property. 318 * 319 * @return a Transport 320 * @throws NoSuchProviderException 321 */ 322 public Transport getTransport() throws NoSuchProviderException { 323 String protocol = properties.getProperty("mail.transport.protocol"); 324 if (protocol == null) { 325 throw new NoSuchProviderException("mail.transport.protocol property is not set"); 326 } 327 return getTransport(protocol); 328 } 329 330 /** 331 * Return a Transport for the specified protocol. 332 * 333 * @param protocol the protocol to use 334 * @return a Transport 335 * @throws NoSuchProviderException 336 */ 337 public Transport getTransport(String protocol) throws NoSuchProviderException { 338 Provider provider = getProvider(protocol); 339 return getTransport(provider); 340 } 341 342 /** 343 * Return a transport for the protocol specified in the URL. 344 * 345 * @param name the URL whose scheme specifies the protocol 346 * @return a Transport 347 * @throws NoSuchProviderException 348 */ 349 public Transport getTransport(URLName name) throws NoSuchProviderException { 350 return (Transport) getService(getProvider(name.getProtocol()), name); 351 } 352 353 /** 354 * Return a transport for the protocol associated with the type of this address. 355 * 356 * @param address the address we are trying to deliver to 357 * @return a Transport 358 * @throws NoSuchProviderException 359 */ 360 public Transport getTransport(Address address) throws NoSuchProviderException { 361 String type = address.getType(); 362 // load the address map from the resource files. 363 Map addressMap = getAddressMap(); 364 String protocolName = (String)addressMap.get(type); 365 if (protocolName == null) { 366 throw new NoSuchProviderException("No provider for address type " + type); 367 } 368 return getTransport(protocolName); 369 } 370 371 /** 372 * Return the Transport specified by a Provider 373 * 374 * @param provider the defining Provider 375 * @return a Transport 376 * @throws NoSuchProviderException 377 */ 378 public Transport getTransport(Provider provider) throws NoSuchProviderException { 379 return (Transport) getService(provider, null); 380 } 381 382 /** 383 * Set the password authentication associated with a URL. 384 * 385 * @param name the url 386 * @param authenticator the authenticator 387 */ 388 public void setPasswordAuthentication(URLName name, PasswordAuthentication authenticator) { 389 if (authenticator == null) { 390 passwordAuthentications.remove(name); 391 } else { 392 passwordAuthentications.put(name, authenticator); 393 } 394 } 395 396 /** 397 * Get the password authentication associated with a URL 398 * 399 * @param name the URL 400 * @return any authenticator for that url, or null if none 401 */ 402 public PasswordAuthentication getPasswordAuthentication(URLName name) { 403 return (PasswordAuthentication) passwordAuthentications.get(name); 404 } 405 406 /** 407 * Call back to the application supplied authenticator to get the needed username add password. 408 * 409 * @param host the host we are trying to connect to, may be null 410 * @param port the port on that host 411 * @param protocol the protocol trying to be used 412 * @param prompt a String to show as part of the prompt, may be null 413 * @param defaultUserName the default username, may be null 414 * @return the authentication information collected by the authenticator; may be null 415 */ 416 public PasswordAuthentication requestPasswordAuthentication(InetAddress host, int port, String protocol, String prompt, String defaultUserName) { 417 if (authenticator == null) { 418 return null; 419 } 420 return authenticator.authenticate(host, port, protocol, prompt, defaultUserName); 421 } 422 423 /** 424 * Return the properties object for this Session; this is a live collection. 425 * 426 * @return the properties for the Session 427 */ 428 public Properties getProperties() { 429 return properties; 430 } 431 432 /** 433 * Return the specified property. 434 * 435 * @param property the property to get 436 * @return its value, or null if not present 437 */ 438 public String getProperty(String property) { 439 return getProperties().getProperty(property); 440 } 441 442 private Service getService(Provider provider, URLName name) throws NoSuchProviderException { 443 try { 444 if (name == null) { 445 name = new URLName(provider.getProtocol(), null, -1, null, null, null); 446 } 447 ClassLoader cl = getClassLoader(); 448 Class clazz = cl.loadClass(provider.getClassName()); 449 Constructor ctr = clazz.getConstructor(PARAM_TYPES); 450 return (Service) ctr.newInstance(new Object[]{this, name}); 451 } catch (ClassNotFoundException e) { 452 throw (NoSuchProviderException) new NoSuchProviderException("Unable to load class for provider: " + provider).initCause(e); 453 } catch (NoSuchMethodException e) { 454 throw (NoSuchProviderException) new NoSuchProviderException("Provider class does not have a constructor(Session, URLName): " + provider).initCause(e); 455 } catch (InstantiationException e) { 456 throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e); 457 } catch (IllegalAccessException e) { 458 throw (NoSuchProviderException) new NoSuchProviderException("Unable to instantiate provider class: " + provider).initCause(e); 459 } catch (InvocationTargetException e) { 460 throw (NoSuchProviderException) new NoSuchProviderException("Exception from constructor of provider class: " + provider).initCause(e.getCause()); 461 } 462 } 463 464 private ProviderInfo getProviderInfo() { 465 ClassLoader cl = getClassLoader(); 466 ProviderInfo info = (ProviderInfo) providersByClassLoader.get(cl); 467 if (info == null) { 468 info = loadProviders(cl); 469 } 470 return info; 471 } 472 473 private Map getAddressMap() { 474 ClassLoader cl = getClassLoader(); 475 Map addressMap = (Map)addressMapsByClassLoader.get(cl); 476 if (addressMap == null) { 477 addressMap = loadAddressMap(cl); 478 } 479 return addressMap; 480 } 481 482 483 /** 484 * Resolve a class loader used to resolve context resources. The 485 * class loader used is either a current thread context class 486 * loader (if set), the class loader used to load an authenticator 487 * we've been initialized with, or the class loader used to load 488 * this class instance (which may be a subclass of Session). 489 * 490 * @return The class loader used to load resources. 491 */ 492 private ClassLoader getClassLoader() { 493 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 494 if (cl == null) { 495 if (authenticator != null) { 496 cl = authenticator.getClass().getClassLoader(); 497 } 498 else { 499 cl = this.getClass().getClassLoader(); 500 } 501 } 502 return cl; 503 } 504 505 private ProviderInfo loadProviders(ClassLoader cl) { 506 // we create a merged map from reading all of the potential address map entries. The locations 507 // searched are: 508 // 1. java.home/lib/javamail.address.map 509 // 2. META-INF/javamail.address.map 510 // 3. META-INF/javamail.default.address.map 511 // 512 ProviderInfo info = new ProviderInfo(); 513 514 // make sure this is added to the global map. 515 providersByClassLoader.put(cl, info); 516 517 518 // NOTE: Unlike the addressMap, we process these in the defined order. The loading routine 519 // will not overwrite entries if they already exist in the map. 520 521 try { 522 Enumeration e = cl.getResources("META-INF/javamail.default.providers"); 523 while (e.hasMoreElements()) { 524 URL url = (URL) e.nextElement(); 525 if (debug) { 526 writeDebug("Loading javamail.default.providers from " + url.toString()); 527 } 528 529 InputStream is = url.openStream(); 530 try { 531 loadProviders(info, is); 532 } finally{ 533 is.close(); 534 } 535 } 536 } catch (SecurityException e) { 537 // ignore 538 } catch (IOException e) { 539 // ignore 540 } 541 542 543 try { 544 File file = new File(System.getProperty("java.home"), "lib/javamail.providers"); 545 InputStream is = new FileInputStream(file); 546 try { 547 loadProviders(info, is); 548 if (debug) { 549 writeDebug("Loaded lib/javamail.providers from " + file.toString()); 550 } 551 } finally{ 552 is.close(); 553 } 554 } catch (SecurityException e) { 555 // ignore 556 } catch (IOException e) { 557 // ignore 558 } 559 560 try { 561 Enumeration e = cl.getResources("META-INF/javamail.providers"); 562 while (e.hasMoreElements()) { 563 URL url = (URL) e.nextElement(); 564 if (debug) { 565 writeDebug("Loading META-INF/javamail.providers from " + url.toString()); 566 } 567 InputStream is = url.openStream(); 568 try { 569 loadProviders(info, is); 570 } finally{ 571 is.close(); 572 } 573 } 574 } catch (SecurityException e) { 575 // ignore 576 } catch (IOException e) { 577 // ignore 578 } 579 580 return info; 581 } 582 583 private void loadProviders(ProviderInfo info, InputStream is) throws IOException { 584 BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 585 String line; 586 while ((line = reader.readLine()) != null) { 587 StringTokenizer tok = new StringTokenizer(line, ";"); 588 String protocol = null; 589 Provider.Type type = null; 590 String className = null; 591 String vendor = null; 592 String version = null; 593 while (tok.hasMoreTokens()) { 594 String property = tok.nextToken(); 595 int index = property.indexOf('='); 596 if (index == -1) { 597 continue; 598 } 599 String key = property.substring(0, index).trim().toLowerCase(); 600 String value = property.substring(index+1).trim(); 601 if (protocol == null && "protocol".equals(key)) { 602 protocol = value; 603 } else if (type == null && "type".equals(key)) { 604 if ("store".equals(value)) { 605 type = Provider.Type.STORE; 606 } else if ("transport".equals(value)) { 607 type = Provider.Type.TRANSPORT; 608 } 609 } else if (className == null && "class".equals(key)) { 610 className = value; 611 } else if ("vendor".equals(key)) { 612 vendor = value; 613 } else if ("version".equals(key)) { 614 version = value; 615 } 616 } 617 if (protocol == null || type == null || className == null) { 618 //todo should we log a warning? 619 continue; 620 } 621 622 if (debug) { 623 writeDebug("DEBUG: loading new provider protocol=" + protocol + ", className=" + className + ", vendor=" + vendor + ", version=" + version); 624 } 625 Provider provider = new Provider(protocol, className, type, vendor, version); 626 if (!info.byClassName.containsKey(className)) { 627 info.byClassName.put(className, provider); 628 } 629 if (!info.byProtocol.containsKey(protocol)) { 630 info.byProtocol.put(protocol, provider); 631 } 632 info.all.add(provider); 633 } 634 } 635 636 /** 637 * Load up an address map associated with a using class loader 638 * instance. 639 * 640 * @param cl The class loader used to resolve the address map. 641 * 642 * @return A map containing the entries associated with this classloader 643 * instance. 644 */ 645 private static Map loadAddressMap(ClassLoader cl) { 646 // we create a merged map from reading all of the potential address map entries. The locations 647 // searched are: 648 // 1. java.home/lib/javamail.address.map 649 // 2. META-INF/javamail.address.map 650 // 3. META-INF/javamail.default.address.map 651 // 652 // if all of the above searches fail, we just set up some "default" defaults. 653 654 // the format of the address.map file is defined as a property file. We can cheat and 655 // just use Properties.load() to read in the files. 656 Properties addressMap = new Properties(); 657 658 // add this to the tracking map. 659 addressMapsByClassLoader.put(cl, addressMap); 660 661 // NOTE: We are reading these resources in reverse order of what's cited above. This allows 662 // user defined entries to overwrite default entries if there are similarly named items. 663 664 try { 665 Enumeration e = cl.getResources("META-INF/javamail.default.address.map"); 666 while (e.hasMoreElements()) { 667 URL url = (URL) e.nextElement(); 668 InputStream is = url.openStream(); 669 try { 670 // load as a property file 671 addressMap.load(is); 672 } finally{ 673 is.close(); 674 } 675 } 676 } catch (SecurityException e) { 677 // ignore 678 } catch (IOException e) { 679 // ignore 680 } 681 682 683 try { 684 Enumeration e = cl.getResources("META-INF/javamail.address.map"); 685 while (e.hasMoreElements()) { 686 URL url = (URL) e.nextElement(); 687 InputStream is = url.openStream(); 688 try { 689 // load as a property file 690 addressMap.load(is); 691 } finally{ 692 is.close(); 693 } 694 } 695 } catch (SecurityException e) { 696 // ignore 697 } catch (IOException e) { 698 // ignore 699 } 700 701 702 try { 703 File file = new File(System.getProperty("java.home"), "lib/javamail.address.map"); 704 InputStream is = new FileInputStream(file); 705 try { 706 // load as a property file 707 addressMap.load(is); 708 } finally{ 709 is.close(); 710 } 711 } catch (SecurityException e) { 712 // ignore 713 } catch (IOException e) { 714 // ignore 715 } 716 717 try { 718 Enumeration e = cl.getResources("META-INF/javamail.address.map"); 719 while (e.hasMoreElements()) { 720 URL url = (URL) e.nextElement(); 721 InputStream is = url.openStream(); 722 try { 723 // load as a property file 724 addressMap.load(is); 725 } finally{ 726 is.close(); 727 } 728 } 729 } catch (SecurityException e) { 730 // ignore 731 } catch (IOException e) { 732 // ignore 733 } 734 735 736 // if unable to load anything, at least create the MimeMessage-smtp protocol mapping. 737 if (addressMap.isEmpty()) { 738 addressMap.put("rfc822", "smtp"); 739 } 740 741 return addressMap; 742 } 743 744 /** 745 * Private convenience routine for debug output. 746 * 747 * @param msg The message to write out to the debug stream. 748 */ 749 private void writeDebug(String msg) { 750 debugOut.println(msg); 751 } 752 753 754 private static class ProviderInfo { 755 private final Map byClassName = new HashMap(); 756 private final Map byProtocol = new HashMap(); 757 private final List all = new ArrayList(); 758 } 759 }