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 package org.apache.wiki.util;
020
021 import java.util.Date;
022 import java.util.Properties;
023
024 import javax.mail.Authenticator;
025 import javax.mail.Message;
026 import javax.mail.MessagingException;
027 import javax.mail.PasswordAuthentication;
028 import javax.mail.Session;
029 import javax.mail.Transport;
030 import javax.mail.internet.AddressException;
031 import javax.mail.internet.InternetAddress;
032 import javax.mail.internet.MimeMessage;
033 import javax.naming.Context;
034 import javax.naming.InitialContext;
035 import javax.naming.NamingException;
036
037 import 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 */
192 public 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);
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 }