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>&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     */
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 &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);
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    }