    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 jakarta.mail.Authenticator;
    025import jakarta.mail.Message;
    026import jakarta.mail.MessagingException;
    027import jakarta.mail.PasswordAuthentication;
    028import jakarta.mail.Session;
    029import jakarta.mail.Transport;
    030import jakarta.mail.internet.AddressException;
    031import jakarta.mail.internet.InternetAddress;
    032import jakarta.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>jakarta.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="jakarta.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="jakarta.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 jakarta.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 jakarta.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 jakarta.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}