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