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 (recommdended) 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>&lt;resource-ref&gt;</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>&lt;resource-ref&gt;
140 *   &lt;description>Resource reference to a container-managed JNDI JavaMail factory for sending e-mails.&lt;/description&gt;
141 *   &lt;res-ref-name>mail/Session&lt;/res-ref-name&gt;
142 *   &lt;res-type>javax.mail.Session&lt;/res-type&gt;
143 *   &lt;res-auth>Container&lt;/res-auth&gt;
144 * &lt;/resource-ref&gt;</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>&lt;Resource&gt;</code>
149 * element inside the JSPWiki webapp's <code>&lt;Context&gt;</code> declaration. Here's an example shows
150 * how to do it:</p>
151 * <pre>&lt;Context ...&gt;
152 * ...
153 * &lt;Resource name="mail/Session" auth="Container"
154 *           type="javax.mail.Session"
155 *           mail.smtp.host="127.0.0.1"/&gt;
156 *           mail.smtp.port="25"/&gt;
157 *           mail.smtp.account="your-account-name"/&gt;
158 *           mail.smtp.password="your-password"/&gt;
159 *           mail.from="Snoop Dogg &lt;snoop@dogg.org&gt;"/&gt;
160 *           mail.smtp.timeout="5000"/&gt;
161 *           mail.smtp.connectiontimeout="5000"/&gt;
162 *           mail.smtp.starttls.enable="true"/&gt;
163 * ...
164 * &lt;/Context&gt;</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>&lt;GlobalNamingResources&gt;
170 *   &lt;Resource name="mail/Session" auth="Container"
171 *             type="javax.mail.Session"
172 *             ...
173 *             mail.smtp.starttls.enable="true"/&gt;
174 * &lt;/GlobalNamingResources&gt;</pre>
175 * <p>This approach &#8212; creating a global JNDI resource &#8212; 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 &lt;snoop.dog@shizzle.net&gt;<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        {
274            // Create and address the message
275            final MimeMessage msg = new MimeMessage(session);
276            msg.setFrom(new InternetAddress(c_fromAddress));
277            msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to, false));
278            msg.setSubject(subject, StandardCharsets.UTF_8.name());
279            msg.setText(content, StandardCharsets.UTF_8.name());
280            msg.setSentDate(new Date());
281
282            // Send and log it
283            Transport.send(msg);
284            if (log.isInfoEnabled())
285            {
286                log.info("Sent e-mail to=" + to + ", subject=\"" + subject + "\", used "
287                         + (c_useJndi ? "JNDI" : "standalone") + " mail session.");
288            }
289        }
290        catch (final MessagingException e)
291        {
292            log.error(e);
293            throw e;
294        }
295    }
296    
297    // --------- JavaMail Session Helper methods  --------------------------------
298
299    /**
300     * Gets the Sender's email address from JNDI Session if available, otherwise
301     * from the jspwiki.properties or lastly the default value.
302     * @param pSession <code>Session</code>
303     * @param pProperties <code>Properties</code>
304     * @return <code>String</code>
305     */
306    static String getSenderEmailAddress(final Session pSession, final Properties pProperties)
307    {
308        if( c_fromAddress == null )
309        {
310            // First, attempt to get the email address from the JNDI Mail
311            // Session.
312            if( pSession != null && c_useJndi )
313            {
314                c_fromAddress = pSession.getProperty( MailUtil.PROP_MAIL_SENDER );
315            }
316            // If unsuccessful, get the email address from the properties or
317            // default.
318            if( c_fromAddress == null )
319            {
320                c_fromAddress = pProperties.getProperty( PROP_MAIL_SENDER, DEFAULT_SENDER ).trim();
321                if( log.isDebugEnabled() )
322                    log.debug( "Attempt to get the sender's mail address from the JNDI mail session failed, will use \""
323                               + c_fromAddress + "\" (configured via jspwiki.properties or the internal default)." );
324            }
325            else
326            {
327                if( log.isDebugEnabled() )
328                    log.debug( "Attempt to get the sender's mail address from the JNDI mail session was successful (" + c_fromAddress
329                               + ")." );
330            }
331        }
332        return c_fromAddress;
333    }
334
335    /**
336     * Returns the Mail Session from either JNDI or creates a stand-alone.
337     * @param props a the properties that contain mail session properties
338     * @return <code>Session</code>
339     */
340    private static Session getMailSession(final Properties props)
341    {
342        Session result = null;
343        final String jndiName = props.getProperty(PROP_MAIL_JNDI_NAME, DEFAULT_MAIL_JNDI_NAME).trim();
344
345        if (c_useJndi)
346        {
347            // Try getting the Session from the JNDI factory first
348            if ( log.isDebugEnabled() )
349                log.debug("Try getting a mail session via JNDI name \"" + jndiName + "\".");
350            try
351            {
352                result = getJNDIMailSession(jndiName);
353            }
354            catch (final NamingException e)
355            {
356                // Oops! JNDI factory must not be set up
357                c_useJndi = false;
358                if ( log.isInfoEnabled() )
359                    log.info("Unable to get a mail session via JNDI, will use custom settings at least until next startup.");
360            }
361        }
362
363        // JNDI failed; so, get the Session from the standalone factory
364        if (result == null)
365        {
366            if ( log.isDebugEnabled() )
367                log.debug("Getting a standalone mail session configured by jspwiki.properties and/or internal default values.");
368            result = getStandaloneMailSession(props);
369        }
370        return result;
371    }
372
373    /**
374     * Returns a stand-alone JavaMail Session by looking up the correct
375     * mail account, password and host from a supplied set of properties.
376     * If the JavaMail property {@value #PROP_MAIL_ACCOUNT} is set to
377     * a value that is non-<code>null</code> and of non-zero length, the
378     * Session will be initialized with an instance of
379     * {@link javax.mail.Authenticator}.
380     * @param props the properties that contain mail session properties
381     * @return the initialized JavaMail Session
382     */
383    static Session getStandaloneMailSession(final Properties props ) {
384        // Read the JSPWiki settings from the properties
385        final String host     = props.getProperty( PROP_MAIL_HOST, DEFAULT_MAIL_HOST );
386        final String port     = props.getProperty( PROP_MAIL_PORT, DEFAULT_MAIL_PORT );
387        final String account  = props.getProperty( PROP_MAIL_ACCOUNT );
388        final String password = props.getProperty( PROP_MAIL_PASSWORD );
389        final String timeout  = props.getProperty( PROP_MAIL_TIMEOUT, DEFAULT_MAIL_TIMEOUT);
390        final String conntimeout = props.getProperty( PROP_MAIL_CONNECTION_TIMEOUT, DEFAULT_MAIL_CONN_TIMEOUT );
391        final boolean starttls = TextUtil.getBooleanProperty( props, PROP_MAIL_STARTTLS, true);
392        
393        final boolean useAuthentication = account != null && !account.isEmpty();
394
395        final Properties mailProps = new Properties();
396
397        // Set JavaMail properties
398        mailProps.put( PROP_MAIL_HOST, host );
399        mailProps.put( PROP_MAIL_PORT, port );
400        mailProps.put( PROP_MAIL_TIMEOUT, timeout );
401        mailProps.put( PROP_MAIL_CONNECTION_TIMEOUT, conntimeout );
402        mailProps.put( PROP_MAIL_STARTTLS, starttls ? TRUE : FALSE );
403
404        // Add SMTP authentication if required
405        Session session = null;
406        if ( useAuthentication )
407        {
408            mailProps.put( PROP_MAIL_AUTH, TRUE );
409            final SmtpAuthenticator auth = new SmtpAuthenticator( account, password );
410
411            session = Session.getInstance( mailProps, auth );
412        }
413        else
414        {
415            session = Session.getInstance( mailProps );
416        }
417
418        if ( log.isDebugEnabled() )
419        {
420            final String mailServer = host + ":" + port + ", account=" + account + ", password not displayed, timeout="
421            + timeout + ", connectiontimeout=" + conntimeout + ", starttls.enable=" + starttls
422            + ", use authentication=" + ( useAuthentication ? TRUE : FALSE );
423            log.debug( "JavaMail session obtained from standalone mail factory: " + mailServer );
424        }
425        return session;
426    }
427
428
429    /**
430     * Returns a JavaMail Session instance from a JNDI container-managed factory.
431     * @param jndiName the JNDI name for the resource. If <code>null</code>, the default value
432     * of <code>mail/Session</code> will be used
433     * @return the initialized JavaMail Session
434     * @throws NamingException if the Session cannot be obtained; for example, if the factory is not configured
435     */
436    static Session getJNDIMailSession(final String jndiName ) throws NamingException
437    {
438        Session session = null;
439        try
440        {
441            final Context initCtx = new InitialContext();
442            final Context ctx = (Context) initCtx.lookup( JAVA_COMP_ENV );
443            session = (Session) ctx.lookup( jndiName );
444        }
445        catch( final NamingException e )
446        {
447            log.warn( "JNDI mail session initialization error: " + e.getMessage() );
448            throw e;
449        }
450        if ( log.isDebugEnabled() )
451        {
452            log.debug( "mail session obtained from JNDI mail factory: " + jndiName );
453        }
454        return session;
455    }
456
457    /**
458     * Simple {@link javax.mail.Authenticator} subclass that authenticates a user to
459     * an SMTP server.
460     */
461    protected static class SmtpAuthenticator extends Authenticator {
462
463        private static final String BLANK = "";
464        private final String m_pass;
465        private final String m_login;
466
467        /**
468         * Constructs a new SmtpAuthenticator with a supplied username and password.
469         * @param login the user name
470         * @param pass the password
471         */
472        public SmtpAuthenticator(final String login, final String pass)
473        {
474            super();
475            m_login =   login == null ? BLANK : login;
476            m_pass =     pass == null ? BLANK : pass;
477        }
478
479        /**
480         * Returns the password used to authenticate to the SMTP server.
481         * @return <code>PasswordAuthentication</code>
482         */
483        public PasswordAuthentication getPasswordAuthentication()
484        {
485            if ( BLANK.equals(m_pass) )
486            {
487                return null;
488            }
489
490            return new PasswordAuthentication( m_login, m_pass );
491        }
492
493    }
494
495}