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>&lt;resource-ref&gt;</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>&lt;resource-ref&gt;
144 *   &lt;description>Resource reference to a container-managed JNDI JavaMail factory for sending e-mails.&lt;/description&gt;
145 *   &lt;res-ref-name>mail/Session&lt;/res-ref-name&gt;
146 *   &lt;res-type>javax.mail.Session&lt;/res-type&gt;
147 *   &lt;res-auth>Container&lt;/res-auth&gt;
148 * &lt;/resource-ref&gt;</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>&lt;Resource&gt;</code>
153 * element inside the JSPWiki webapp's <code>&lt;Context&gt;</code> declaration. Here's an example shows
154 * how to do it:</p>
155 * <pre>&lt;Context ...&gt;
156 * ...
157 * &lt;Resource name="mail/Session" auth="Container"
158 *           type="javax.mail.Session"
159 *           mail.smtp.host="127.0.0.1"/&gt;
160 *           mail.smtp.port="25"/&gt;
161 *           mail.smtp.account="your-account-name"/&gt;
162 *           mail.smtp.password="your-password"/&gt;
163 *           mail.from="Snoop Dogg &lt;snoop@dogg.org&gt;"/&gt;
164 *           mail.smtp.timeout="5000"/&gt;
165 *           mail.smtp.connectiontimeout="5000"/&gt;
166 *           mail.smtp.starttls.enable="true"/&gt;
167 * ...
168 * &lt;/Context&gt;</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>&lt;GlobalNamingResources&gt;
174 *   &lt;Resource name="mail/Session" auth="Container"
175 *             type="javax.mail.Session"
176 *             ...
177 *             mail.smtp.starttls.enable="true"/&gt;
178 * &lt;/GlobalNamingResources&gt;</pre>
179 * <p>This approach &#8212; creating a global JNDI resource &#8212; 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 &lt;snoop.dog@shizzle.net&gt;<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}