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 */ 019package org.apache.wiki.util; 020 021import java.util.Date; 022import java.util.Properties; 023 024import javax.mail.Authenticator; 025import javax.mail.Message; 026import javax.mail.MessagingException; 027import javax.mail.PasswordAuthentication; 028import javax.mail.Session; 029import javax.mail.Transport; 030import javax.mail.internet.AddressException; 031import javax.mail.internet.InternetAddress; 032import javax.mail.internet.MimeMessage; 033import javax.naming.Context; 034import javax.naming.InitialContext; 035import javax.naming.NamingException; 036 037import org.apache.log4j.Logger; 038 039 040/** 041 * <p>Contains static methods for sending e-mails to recipients using JNDI-supplied 042 * <a href="http://java.sun.com/products/javamail/">JavaMail</a> 043 * Sessions supplied by a web container (preferred) or configured via 044 * <code>jspwiki.properties</code>; both methods are described below. 045 * Because most e-mail servers require authentication, 046 * for security reasons implementors are <em>strongly</em> encouraged to use 047 * container-managed JavaMail Sessions so that passwords are not exposed in 048 * <code>jspwiki.properties</code>.</p> 049 * <p>To enable e-mail functions within JSPWiki, administrators must do three things: 050 * ensure that the required JavaMail JARs are on the runtime classpath, configure 051 * JavaMail appropriately, and (recommdended) configure the JNDI JavaMail session factory.</p> 052 * <strong>JavaMail runtime JARs</strong> 053 * <p>The first step is easy: JSPWiki bundles 054 * recent versions of the required JavaMail <code>mail.jar</code> and 055 * <code>activation.jar</code> into the JSPWiki WAR file; so, out of the box 056 * this is already taken care of. However, when using JNDI-supplied 057 * Session factories, these should be moved, <em>not copied</em>, to a classpath location 058 * where the JARs can be shared by both the JSPWiki webapp and the container. For example, 059 * Tomcat 5 provides the directory <code><var>$CATALINA_HOME</var>/common/lib</code> 060 * for storage of shared JARs; move <code>mail.jar</code> and <code>activation.jar</code> 061 * there instead of keeping them in <code>/WEB-INF/lib</code>.</p> 062 * <strong>JavaMail configuration</strong> 063 * <p>Regardless of the method used for supplying JavaMail sessions (JNDI container-managed 064 * or via <code>jspwiki.properties</code>, JavaMail needs certain properties 065 * set in order to work correctly. Configurable properties are these:</p> 066 * <table border="1"> 067 * <tr> 068 * <thead> 069 * <th>Property</th> 070 * <th>Default</th> 071 * <th>Definition</th> 072 * <thead> 073 * </tr> 074 * <tr> 075 * <td><code>jspwiki.mail.jndiname</code></td> 076 * <td><code>mail/Session</code></td> 077 * <td>The JNDI name of the JavaMail session factory</td> 078 * </tr> 079 * <tr> 080 * <td><code>mail.smtp.host</code></td> 081 * <td><code>127.0.0.1</code></td> 082 * <td>The SMTP mail server from which messages will be sent.</td> 083 * </tr> 084 * <tr> 085 * <td><code>mail.smtp.port</code></td> 086 * <td><code>25</code></td> 087 * <td>The port number of the SMTP mail service.</td> 088 * </tr> 089 * <tr> 090 * <td><code>mail.smtp.account</code></td> 091 * <td>(not set)</td> 092 * <td>The user name of the sender. If this value is supplied, the JavaMail 093 * session will attempt to authenticate to the mail server before sending 094 * the message. If not supplied, JavaMail will attempt to send the message 095 * without authenticating (i.e., it will use the server as an open relay). 096 * In real-world scenarios, you should set this value.</td> 097 * </tr> 098 * <tr> 099 * <td><code>mail.smtp.password</code></td> 100 * <td>(not set)</td> 101 * <td>The password of the sender. In real-world scenarios, you 102 * should set this value.</td> 103 * </tr> 104 * <tr> 105 * <td><code>mail.from</code></td> 106 * <td><code><var>${user.name}</var>@<var>${mail.smtp.host}</var>*</code></td> 107 * <td>The e-mail address of the sender.</td> 108 * </tr> 109 * <tr> 110 * <td><code>mail.smtp.timeout</code></td> 111 * <td><code>5000*</code></td> 112 * <td>Socket I/O timeout value, in milliseconds. The default is 5 seconds.</td> 113 * </tr> 114 * <tr> 115 * <td><code>mail.smtp.connectiontimeout</code></td> 116 * <td><code>5000*</code></td> 117 * <td>Socket connection timeout value, in milliseconds. The default is 5 seconds.</td> 118 * </tr> 119 * <tr> 120 * <td><code>mail.smtp.starttls.enable</code></td> 121 * <td><code>true*</code></td> 122 * <td>If true, enables the use of the STARTTLS command (if 123 * supported by the server) to switch the connection to a 124 * TLS-protected connection before issuing any login commands. 125 * Note that an appropriate trust store must configured so that 126 * the client will trust the server's certificate. By default, 127 * the JRE trust store contains root CAs for most public certificate 128 * authorities.</td> 129 * </tr> 130 * </table> 131 * <p>*These defaults apply only if the stand-alone Session factory is used 132 * (that is, these values are obtained from <code>jspwiki.properties</code>). 133 * If using a container-managed JNDI Session factory, the container will 134 * likely supply its own default values, and you should probably override 135 * them (see the next section).</p> 136 * <strong>Container JNDI Session factory configuration</strong> 137 * <p>You are strongly encouraged to use a container-managed JNDI factory for 138 * JavaMail sessions, rather than configuring JavaMail through <code>jspwiki.properties</code>. 139 * To do this, you need to two things: uncomment the <code><resource-ref></code> block 140 * in <code>/WEB-INF/web.xml</code> that enables container-managed JavaMail, and 141 * configure your container's JavaMail resource factory. The <code>web.xml</code> 142 * part is easy: just uncomment the section that looks like this:</p> 143 * <pre><resource-ref> 144 * <description>Resource reference to a container-managed JNDI JavaMail factory for sending e-mails.</description> 145 * <res-ref-name>mail/Session</res-ref-name> 146 * <res-type>javax.mail.Session</res-type> 147 * <res-auth>Container</res-auth> 148 * </resource-ref></pre> 149 * <p>To configure your container's resource factory, follow the directions supplied by 150 * your container's documentation. For example, the 151 * <a href="http://tomcat.apache.org/tomcat-5.5-doc/jndi-resources-howto.html#JavaMail%20Sessions">Tomcat 152 * 5.5 docs</a> state that you need a properly configured <code><Resource></code> 153 * element inside the JSPWiki webapp's <code><Context></code> declaration. Here's an example shows 154 * how to do it:</p> 155 * <pre><Context ...> 156 * ... 157 * <Resource name="mail/Session" auth="Container" 158 * type="javax.mail.Session" 159 * mail.smtp.host="127.0.0.1"/> 160 * mail.smtp.port="25"/> 161 * mail.smtp.account="your-account-name"/> 162 * mail.smtp.password="your-password"/> 163 * mail.from="Snoop Dogg <snoop@dogg.org>"/> 164 * mail.smtp.timeout="5000"/> 165 * mail.smtp.connectiontimeout="5000"/> 166 * mail.smtp.starttls.enable="true"/> 167 * ... 168 * </Context></pre> 169 * <p>Note that with Tomcat (and most other application containers) you can also declare the JavaMail 170 * JNDI factory as a global resource, shared by all applications, instead of as a local JSPWiki 171 * resource as we have done here. For example, the following entry in 172 * <code><var>$CATALINA_HOME</var>/conf/server.xml</code> creates a global resource:</p> 173 * <pre><GlobalNamingResources> 174 * <Resource name="mail/Session" auth="Container" 175 * type="javax.mail.Session" 176 * ... 177 * mail.smtp.starttls.enable="true"/> 178 * </GlobalNamingResources></pre> 179 * <p>This approach — creating a global JNDI resource — yields somewhat decreased 180 * deployment complexity because the JSPWiki webapp no longer needs its own JavaMail resource 181 * declaration. However, it is slightly less secure because it means that all other applications 182 * can now obtain a JavaMail session if they want to. In many cases, this <em>is</em> what 183 * you want.</p> 184 * <p>NOTE: Versions of Tomcat 5.5 later than 5.5.17, and up to and including 5.5.23 have a 185 * b0rked version of <code><var>$CATALINA_HOME</var>/common/lib/naming-factory.jar</code> 186 * that prevents usage of JNDI. To avoid this problem, you should patch your 5.5.23 version 187 * of <code>naming-factory.jar</code> with the one from 5.5.17. This is a known issue 188 * and the bug report (#40668) is 189 * <a href="http://issues.apache.org/bugzilla/show_bug.cgi?id=40668">here</a>. 190 * 191 */ 192public final class MailUtil { 193 194 private static final String JAVA_COMP_ENV = "java:comp/env"; 195 196 private static final String FALSE = "false"; 197 198 private static final String TRUE = "true"; 199 200 private static boolean c_useJndi = true; 201 202 private static final String PROP_MAIL_AUTH = "mail.smtp.auth"; 203 204 protected static final Logger log = Logger.getLogger(MailUtil.class); 205 206 protected static final String DEFAULT_MAIL_JNDI_NAME = "mail/Session"; 207 208 protected static final String DEFAULT_MAIL_HOST = "localhost"; 209 210 protected static final String DEFAULT_MAIL_PORT = "25"; 211 212 protected static final String DEFAULT_MAIL_TIMEOUT = "5000"; 213 214 protected static final String DEFAULT_MAIL_CONN_TIMEOUT = "5000"; 215 216 protected static final String DEFAULT_SENDER = "jspwiki@localhost"; 217 218 protected static final String PROP_MAIL_JNDI_NAME = "jspwiki.mail.jndiname"; 219 220 protected static final String PROP_MAIL_HOST = "mail.smtp.host"; 221 222 protected static final String PROP_MAIL_PORT = "mail.smtp.port"; 223 224 protected static final String PROP_MAIL_ACCOUNT = "mail.smtp.account"; 225 226 protected static final String PROP_MAIL_PASSWORD = "mail.smtp.password"; 227 228 protected static final String PROP_MAIL_TIMEOUT = "mail.smtp.timeout"; 229 230 protected static final String PROP_MAIL_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout"; 231 232 protected static final String PROP_MAIL_TRANSPORT = "smtp"; 233 234 protected static final String PROP_MAIL_SENDER = "mail.from"; 235 236 protected static final String PROP_MAIL_STARTTLS = "mail.smtp.starttls.enable"; 237 238 private static String c_fromAddress = null; 239 240 /** 241 * Private constructor prevents instantiation. 242 */ 243 private MailUtil() 244 { 245 } 246 247 /** 248 * <p>Sends an e-mail to a specified receiver using a JavaMail Session supplied 249 * by a JNDI mail session factory (preferred) or a locally initialized 250 * session based on properties in <code>jspwiki.properties</code>. 251 * See the top-level JavaDoc for this class for a description of 252 * required properties and their default values.</p> 253 * <p>The e-mail address used for the <code>to</code> parameter must be in 254 * RFC822 format, as described in the JavaDoc for {@link javax.mail.internet.InternetAddress} 255 * and more fully at 256 * <a href="http://www.freesoft.org/CIE/RFC/822/index.htm">http://www.freesoft.org/CIE/RFC/822/index.htm</a>. 257 * In other words, e-mail addresses should look like this:</p> 258 * <blockquote><code>Snoop Dog <snoop.dog@shizzle.net><br/> 259 * snoop.dog@shizzle.net</code></blockquote> 260 * <p>Note that the first form allows a "friendly" user name to be supplied 261 * in addition to the actual e-mail address.</p> 262 * 263 * @param props the properties that contain mail session properties 264 * @param to the receiver 265 * @param subject the subject line of the message 266 * @param content the contents of the mail message, as plain text 267 * @throws AddressException If the address is invalid 268 * @throws MessagingException If the message cannot be sent. 269 */ 270 public static void sendMessage( Properties props, String to, String subject, String content) 271 throws AddressException, MessagingException 272 { 273 Session session = getMailSession( props ); 274 getSenderEmailAddress(session, props); 275 276 try 277 { 278 // Create and address the message 279 MimeMessage msg = new MimeMessage(session); 280 msg.setFrom(new InternetAddress(c_fromAddress)); 281 msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to, false)); 282 msg.setSubject(subject, "UTF-8"); 283 msg.setText(content, "UTF-8"); 284 msg.setSentDate(new Date()); 285 286 // Send and log it 287 Transport.send(msg); 288 if (log.isInfoEnabled()) 289 { 290 log.info("Sent e-mail to=" + to + ", subject=\"" + subject + "\", used " 291 + (c_useJndi ? "JNDI" : "standalone") + " mail session."); 292 } 293 } 294 catch (MessagingException e) 295 { 296 log.error(e); 297 throw e; 298 } 299 } 300 301 // --------- JavaMail Session Helper methods -------------------------------- 302 303 /** 304 * Gets the Sender's email address from JNDI Session if available, otherwise 305 * from the jspwiki.properties or lastly the default value. 306 * @param pSession <code>Session</code> 307 * @param pProperties <code>Properties</code> 308 * @return <code>String</code> 309 */ 310 protected static String getSenderEmailAddress(Session pSession, Properties pProperties) 311 { 312 if( c_fromAddress == null ) 313 { 314 // First, attempt to get the email address from the JNDI Mail 315 // Session. 316 if( pSession != null && c_useJndi ) 317 { 318 c_fromAddress = pSession.getProperty( MailUtil.PROP_MAIL_SENDER ); 319 } 320 // If unsuccessful, get the email address from the properties or 321 // default. 322 if( c_fromAddress == null ) 323 { 324 c_fromAddress = pProperties.getProperty( PROP_MAIL_SENDER, DEFAULT_SENDER ).trim(); 325 if( log.isDebugEnabled() ) 326 log.debug( "Attempt to get the sender's mail address from the JNDI mail session failed, will use \"" 327 + c_fromAddress + "\" (configured via jspwiki.properties or the internal default)." ); 328 } 329 else 330 { 331 if( log.isDebugEnabled() ) 332 log.debug( "Attempt to get the sender's mail address from the JNDI mail session was successful (" + c_fromAddress 333 + ")." ); 334 } 335 } 336 return c_fromAddress; 337 } 338 339 /** 340 * Returns the Mail Session from either JNDI or creates a stand-alone. 341 * @param props a the properties that contain mail session properties 342 * @return <code>Session</code> 343 */ 344 private static Session getMailSession(Properties props) 345 { 346 Session result = null; 347 String jndiName = props.getProperty(PROP_MAIL_JNDI_NAME, DEFAULT_MAIL_JNDI_NAME).trim(); 348 349 if (c_useJndi) 350 { 351 // Try getting the Session from the JNDI factory first 352 if ( log.isDebugEnabled() ) 353 log.debug("Try getting a mail session via JNDI name \"" + jndiName + "\"."); 354 try 355 { 356 result = getJNDIMailSession(jndiName); 357 } 358 catch (NamingException e) 359 { 360 // Oops! JNDI factory must not be set up 361 c_useJndi = false; 362 if ( log.isInfoEnabled() ) 363 log.info("Unable to get a mail session via JNDI, will use custom settings at least until next startup."); 364 } 365 } 366 367 // JNDI failed; so, get the Session from the standalone factory 368 if (result == null) 369 { 370 if ( log.isDebugEnabled() ) 371 log.debug("Getting a standalone mail session configured by jspwiki.properties and/or internal default values."); 372 result = getStandaloneMailSession(props); 373 } 374 return result; 375 } 376 377 /** 378 * Returns a stand-alone JavaMail Session by looking up the correct 379 * mail account, password and host from a supplied set of properties. 380 * If the JavaMail property {@value #PROP_MAIL_ACCOUNT} is set to 381 * a value that is non-<code>null</code> and of non-zero length, the 382 * Session will be initialized with an instance of 383 * {@link javax.mail.Authenticator}. 384 * @param props the properties that contain mail session properties 385 * @return the initialized JavaMail Session 386 */ 387 protected static Session getStandaloneMailSession( Properties props ) { 388 // Read the JSPWiki settings from the properties 389 String host = props.getProperty( PROP_MAIL_HOST, DEFAULT_MAIL_HOST ); 390 String port = props.getProperty( PROP_MAIL_PORT, DEFAULT_MAIL_PORT ); 391 String account = props.getProperty( PROP_MAIL_ACCOUNT ); 392 String password = props.getProperty( PROP_MAIL_PASSWORD ); 393 String timeout = props.getProperty( PROP_MAIL_TIMEOUT, DEFAULT_MAIL_TIMEOUT); 394 String conntimeout = props.getProperty( PROP_MAIL_CONNECTION_TIMEOUT, DEFAULT_MAIL_CONN_TIMEOUT ); 395 boolean starttls = TextUtil.getBooleanProperty( props, PROP_MAIL_STARTTLS, true); 396 397 boolean useAuthentication = account != null && account.length() > 0; 398 399 Properties mailProps = new Properties(); 400 401 // Set JavaMail properties 402 mailProps.put( PROP_MAIL_HOST, host ); 403 mailProps.put( PROP_MAIL_PORT, port ); 404 mailProps.put( PROP_MAIL_TIMEOUT, timeout ); 405 mailProps.put( PROP_MAIL_CONNECTION_TIMEOUT, conntimeout ); 406 mailProps.put( PROP_MAIL_STARTTLS, starttls ? TRUE : FALSE ); 407 408 // Add SMTP authentication if required 409 Session session = null; 410 if ( useAuthentication ) 411 { 412 mailProps.put( PROP_MAIL_AUTH, TRUE ); 413 SmtpAuthenticator auth = new SmtpAuthenticator( account, password ); 414 415 session = Session.getInstance( mailProps, auth ); 416 } 417 else 418 { 419 session = Session.getInstance( mailProps ); 420 } 421 422 if ( log.isDebugEnabled() ) 423 { 424 String mailServer = host + ":" + port + ", account=" + account + ", password not displayed, timeout=" 425 + timeout + ", connectiontimeout=" + conntimeout + ", starttls.enable=" + starttls 426 + ", use authentication=" + ( useAuthentication ? TRUE : FALSE ); 427 log.debug( "JavaMail session obtained from standalone mail factory: " + mailServer ); 428 } 429 return session; 430 } 431 432 433 /** 434 * Returns a JavaMail Session instance from a JNDI container-managed factory. 435 * @param jndiName the JNDI name for the resource. If <code>null</code>, the default value 436 * of <code>mail/Session</code> will be used 437 * @return the initialized JavaMail Session 438 * @throws NamingException if the Session cannot be obtained; for example, if the factory is not configured 439 */ 440 protected static Session getJNDIMailSession( String jndiName ) throws NamingException 441 { 442 Session session = null; 443 try 444 { 445 Context initCtx = new InitialContext(); 446 Context ctx = (Context) initCtx.lookup( JAVA_COMP_ENV ); 447 session = (Session) ctx.lookup( jndiName ); 448 } 449 catch( NamingException e ) 450 { 451 log.warn( "JNDI mail session initialization error: " + e.getMessage() ); 452 throw e; 453 } 454 if ( log.isDebugEnabled() ) 455 { 456 log.debug( "mail session obtained from JNDI mail factory: " + jndiName ); 457 } 458 return session; 459 } 460 461 /** 462 * Simple {@link javax.mail.Authenticator} subclass that authenticates a user to 463 * an SMTP server. 464 */ 465 protected static class SmtpAuthenticator extends Authenticator { 466 467 private static final String BLANK = ""; 468 private final String m_pass; 469 private final String m_login; 470 471 /** 472 * Constructs a new SmtpAuthenticator with a supplied username and password. 473 * @param login the user name 474 * @param pass the password 475 */ 476 public SmtpAuthenticator(String login, String pass) 477 { 478 super(); 479 m_login = login == null ? BLANK : login; 480 m_pass = pass == null ? BLANK : pass; 481 } 482 483 /** 484 * Returns the password used to authenticate to the SMTP server. 485 * @return <code>PasswordAuthentication</code> 486 */ 487 public PasswordAuthentication getPasswordAuthentication() 488 { 489 if ( BLANK.equals(m_pass) ) 490 { 491 return null; 492 } 493 494 return new PasswordAuthentication( m_login, m_pass ); 495 } 496 497 } 498 499}