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