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