From 6851b5083ef9610bae320bf07e1f24d2aa08851b Mon Sep 17 00:00:00 2001
From: Matt Sicker <boards@gmail.com>
Date: Mon, 13 Apr 2020 17:59:43 -0500
Subject: [PATCH] [LOG4J2-2819] Add support for specifying an SSL configuration
 for SmtpAppender

Signed-off-by: Matt Sicker <boards@gmail.com>
---
 log4j-core/revapi.json                        |  29 +-
 .../log4j/core/appender/SmtpAppender.java     | 251 +++++++++++++++---
 .../logging/log4j/core/net/SmtpManager.java   |  31 ++-
 .../log4j/core/appender/SmtpAppenderTest.java |  20 +-
 4 files changed, 275 insertions(+), 56 deletions(-)

Index: apache-log4j-2.13.0-src/log4j-core/revapi.json
===================================================================
--- apache-log4j-2.13.0-src.orig/log4j-core/revapi.json
+++ apache-log4j-2.13.0-src/log4j-core/revapi.json
@@ -107,7 +107,34 @@
         "new": "method <B extends org.apache.logging.log4j.core.appender.WriterAppender.Builder<B extends org.apache.logging.log4j.core.appender.WriterAppender.Builder<B>>> B org.apache.logging.log4j.core.appender.WriterAppender::newBuilder()",
         "typeParameter": "B extends org.apache.logging.log4j.core.appender.WriterAppender.Builder<B extends org.apache.logging.log4j.core.appender.WriterAppender.Builder<B>>",
         "justification": "LOG4J2-2491 - Allow all appenders to optionally carry a property array"
+      },
+      {
+        "code": "java.annotation.removed",
+        "old": "method org.apache.logging.log4j.core.appender.SmtpAppender org.apache.logging.log4j.core.appender.SmtpAppender::createAppender(org.apache.logging.log4j.core.config.Configuration, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, org.apache.logging.log4j.core.Layout<? extends java.io.Serializable>, org.apache.logging.log4j.core.Filter, java.lang.String)",
+        "new": "method org.apache.logging.log4j.core.appender.SmtpAppender org.apache.logging.log4j.core.appender.SmtpAppender::createAppender(org.apache.logging.log4j.core.config.Configuration, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, org.apache.logging.log4j.core.Layout<? extends java.io.Serializable>, org.apache.logging.log4j.core.Filter, java.lang.String)",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginFactory",
+        "justification": "Plugin is created through builder class now. No practical change here."
+      },
+      {
+        "code": "java.method.numberOfParametersChanged",
+        "old": "method org.apache.logging.log4j.core.net.SmtpManager org.apache.logging.log4j.core.net.SmtpManager::getSmtpManager(org.apache.logging.log4j.core.config.Configuration, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, int, java.lang.String, java.lang.String, boolean, java.lang.String, int)",
+        "new": "method org.apache.logging.log4j.core.net.SmtpManager org.apache.logging.log4j.core.net.SmtpManager::getSmtpManager(org.apache.logging.log4j.core.config.Configuration, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, int, java.lang.String, java.lang.String, boolean, java.lang.String, int, org.apache.logging.log4j.core.net.ssl.SslConfiguration)",
+        "justification": "LOG4J2-2819 - Add support for specifying an SSL configuration for SmtpAppender"
+      },
+      {
+        "code": "java.annotation.added",
+        "old": "parameter org.apache.logging.log4j.core.net.ssl.SslConfiguration org.apache.logging.log4j.core.net.ssl.SslConfiguration::createSSLConfiguration(java.lang.String, org.apache.logging.log4j.core.net.ssl.KeyStoreConfiguration, org.apache.logging.log4j.core.net.ssl.TrustStoreConfiguration, ===boolean===)",
+        "new": "parameter org.apache.logging.log4j.core.net.ssl.SslConfiguration org.apache.logging.log4j.core.net.ssl.SslConfiguration::createSSLConfiguration(java.lang.String, org.apache.logging.log4j.core.net.ssl.KeyStoreConfiguration, org.apache.logging.log4j.core.net.ssl.TrustStoreConfiguration, ===boolean===)",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginAttribute(\"verifyHostName\")",
+        "justification": "LOG4J2-TODO"
+      },
+      {
+        "code": "java.annotation.removed",
+        "old": "parameter org.apache.logging.log4j.core.net.ssl.SslConfiguration org.apache.logging.log4j.core.net.ssl.SslConfiguration::createSSLConfiguration(java.lang.String, org.apache.logging.log4j.core.net.ssl.KeyStoreConfiguration, org.apache.logging.log4j.core.net.ssl.TrustStoreConfiguration, ===boolean===)",
+        "new": "parameter org.apache.logging.log4j.core.net.ssl.SslConfiguration org.apache.logging.log4j.core.net.ssl.SslConfiguration::createSSLConfiguration(java.lang.String, org.apache.logging.log4j.core.net.ssl.KeyStoreConfiguration, org.apache.logging.log4j.core.net.ssl.TrustStoreConfiguration, ===boolean===)",
+        "annotation": "@org.apache.logging.log4j.core.config.plugins.PluginElement(\"verifyHostName\")",
+        "justification": "LOG4J2-TODO"
       }
     ]
   }
-]
\ No newline at end of file
+]
Index: apache-log4j-2.13.0-src/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/SmtpAppender.java
===================================================================
--- apache-log4j-2.13.0-src.orig/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/SmtpAppender.java
+++ apache-log4j-2.13.0-src/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/SmtpAppender.java
@@ -17,8 +17,6 @@
 
 package org.apache.logging.log4j.core.appender;
 
-import java.io.Serializable;
-
 import org.apache.logging.log4j.core.Appender;
 import org.apache.logging.log4j.core.Core;
 import org.apache.logging.log4j.core.Filter;
@@ -29,16 +27,20 @@ import org.apache.logging.log4j.core.con
 import org.apache.logging.log4j.core.config.Property;
 import org.apache.logging.log4j.core.config.plugins.Plugin;
 import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
 import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
 import org.apache.logging.log4j.core.config.plugins.PluginElement;
-import org.apache.logging.log4j.core.config.plugins.PluginFactory;
 import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
 import org.apache.logging.log4j.core.config.plugins.validation.constraints.ValidPort;
 import org.apache.logging.log4j.core.filter.ThresholdFilter;
 import org.apache.logging.log4j.core.layout.HtmlLayout;
 import org.apache.logging.log4j.core.net.SmtpManager;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
 import org.apache.logging.log4j.core.util.Booleans;
 
+import java.io.Serializable;
+
 /**
  * Send an e-mail when a specific logging event occurs, typically on errors or
  * fatal errors.
@@ -72,45 +74,212 @@ public final class SmtpAppender extends
     }
 
     /**
+     * @since 2.13.2
+     */
+    public static class Builder extends AbstractAppender.Builder<Builder>
+            implements org.apache.logging.log4j.core.util.Builder<SmtpAppender> {
+        @PluginBuilderAttribute
+        private String to;
+
+        @PluginBuilderAttribute
+        private String cc;
+
+        @PluginBuilderAttribute
+        private String bcc;
+
+        @PluginBuilderAttribute
+        private String from;
+
+        @PluginBuilderAttribute
+        private String replyTo;
+
+        @PluginBuilderAttribute
+        private String subject;
+
+        @PluginBuilderAttribute
+        private String smtpProtocol = "smtp";
+
+        @PluginBuilderAttribute
+        private String smtpHost;
+
+        @PluginBuilderAttribute
+        @ValidPort
+        private int smtpPort;
+
+        @PluginBuilderAttribute
+        private String smtpUsername;
+
+        @PluginBuilderAttribute(sensitive = true)
+        private String smtpPassword;
+
+        @PluginBuilderAttribute
+        private boolean smtpDebug;
+
+        @PluginBuilderAttribute
+        private int bufferSize = DEFAULT_BUFFER_SIZE;
+
+        @PluginElement("SSL")
+        private SslConfiguration sslConfiguration;
+
+        /**
+         * Comma-separated list of recipient email addresses.
+         */
+        public Builder setTo(final String to) {
+            this.to = to;
+            return this;
+        }
+
+        /**
+         * Comma-separated list of CC email addresses.
+         */
+        public Builder setCc(final String cc) {
+            this.cc = cc;
+            return this;
+        }
+
+        /**
+         * Comma-separated list of BCC email addresses.
+         */
+        public Builder setBcc(final String bcc) {
+            this.bcc = bcc;
+            return this;
+        }
+
+        /**
+         * Email address of the sender.
+         */
+        public Builder setFrom(final String from) {
+            this.from = from;
+            return this;
+        }
+
+        /**
+         * Comma-separated list of Reply-To email addresses.
+         */
+        public Builder setReplyTo(final String replyTo) {
+            this.replyTo = replyTo;
+            return this;
+        }
+
+        /**
+         * Subject template for the email messages.
+         * @see org.apache.logging.log4j.core.layout.PatternLayout
+         */
+        public Builder setSubject(final String subject) {
+            this.subject = subject;
+            return this;
+        }
+
+        /**
+         * Transport protocol to use for SMTP such as "smtp" or "smtps". Defaults to "smtp".
+         */
+        public Builder setSmtpProtocol(final String smtpProtocol) {
+            this.smtpProtocol = smtpProtocol;
+            return this;
+        }
+
+        /**
+         * Host name of SMTP server to send messages through.
+         */
+        public Builder setSmtpHost(final String smtpHost) {
+            this.smtpHost = smtpHost;
+            return this;
+        }
+
+        /**
+         * Port number of SMTP server to send messages through.
+         */
+        public Builder setSmtpPort(final int smtpPort) {
+            this.smtpPort = smtpPort;
+            return this;
+        }
+
+        /**
+         * Username to authenticate with SMTP server.
+         */
+        public Builder setSmtpUsername(final String smtpUsername) {
+            this.smtpUsername = smtpUsername;
+            return this;
+        }
+
+        /**
+         * Password to authenticate with SMTP server.
+         */
+        public Builder setSmtpPassword(final String smtpPassword) {
+            this.smtpPassword = smtpPassword;
+            return this;
+        }
+
+        /**
+         * Enables or disables mail session debugging on STDOUT. Disabled by default.
+         */
+        public Builder setSmtpDebug(final boolean smtpDebug) {
+            this.smtpDebug = smtpDebug;
+            return this;
+        }
+
+        /**
+         * Number of log events to buffer before sending an email. Defaults to {@value #DEFAULT_BUFFER_SIZE}.
+         */
+        public Builder setBufferSize(final int bufferSize) {
+            this.bufferSize = bufferSize;
+            return this;
+        }
+
+        /**
+         * Specifies an SSL configuration for smtps connections.
+         */
+        public Builder setSslConfiguration(final SslConfiguration sslConfiguration) {
+            this.sslConfiguration = sslConfiguration;
+            return this;
+        }
+
+        /**
+         * Specifies the layout used for the email message body. By default, this uses the
+         * {@linkplain HtmlLayout#createDefaultLayout() default HTML layout}.
+         */
+        @Override
+        public Builder setLayout(final Layout<? extends Serializable> layout) {
+            return super.setLayout(layout);
+        }
+
+        /**
+         * Specifies the filter used for this appender. By default, uses a {@link ThresholdFilter} with a level of
+         * ERROR.
+         */
+        @Override
+        public Builder setFilter(final Filter filter) {
+            return super.setFilter(filter);
+        }
+
+        @Override
+        public SmtpAppender build() {
+            if (getLayout() == null) {
+                setLayout(HtmlLayout.createDefaultLayout());
+            }
+            if (getFilter() == null) {
+                setFilter(ThresholdFilter.createFilter(null, null, null));
+            }
+            final SmtpManager smtpManager = SmtpManager.getSmtpManager(getConfiguration(), to, cc, bcc, from, replyTo,
+                    subject, smtpProtocol, smtpHost, smtpPort, smtpUsername, smtpPassword, smtpDebug,
+                    getFilter().toString(), bufferSize, sslConfiguration);
+            return new SmtpAppender(getName(), getFilter(), getLayout(), smtpManager, isIgnoreExceptions(), getPropertyArray());
+        }
+    }
+
+    /**
+     * @since 2.13.2
+     */
+    @PluginBuilderFactory
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /**
      * Create a SmtpAppender.
-     *
-     * @param name
-     *            The name of the Appender.
-     * @param to
-     *            The comma-separated list of recipient email addresses.
-     * @param cc
-     *            The comma-separated list of CC email addresses.
-     * @param bcc
-     *            The comma-separated list of BCC email addresses.
-     * @param from
-     *            The email address of the sender.
-     * @param replyTo
-     *            The comma-separated list of reply-to email addresses.
-     * @param subject The subject of the email message.
-     * @param smtpProtocol The SMTP transport protocol (such as "smtps", defaults to "smtp").
-     * @param smtpHost
-     *            The SMTP hostname to send to.
-     * @param smtpPortStr
-     *            The SMTP port to send to.
-     * @param smtpUsername
-     *            The username required to authenticate against the SMTP server.
-     * @param smtpPassword
-     *            The password required to authenticate against the SMTP server.
-     * @param smtpDebug
-     *            Enable mail session debuging on STDOUT.
-     * @param bufferSizeStr
-     *            How many log events should be buffered for inclusion in the
-     *            message?
-     * @param layout
-     *            The layout to use (defaults to HtmlLayout).
-     * @param filter
-     *            The Filter or null (defaults to ThresholdFilter, level of
-     *            ERROR).
-     * @param ignore If {@code "true"} (default) exceptions encountered when appending events are logged; otherwise
-     *               they are propagated to the caller.
-     * @return The SmtpAppender.
+     * @deprecated Use {@link #newBuilder()} to create and configure a {@link Builder} instance.
+     * @see Builder
      */
-    @PluginFactory
     public static SmtpAppender createAppender(
             @PluginConfiguration final Configuration config,
             @PluginAttribute("name") @Required final String name,
@@ -149,7 +318,7 @@ public final class SmtpAppender extends
         final Configuration configuration = config != null ? config : new DefaultConfiguration();
 
         final SmtpManager manager = SmtpManager.getSmtpManager(configuration, to, cc, bcc, from, replyTo, subject, smtpProtocol,
-            smtpHost, smtpPort, smtpUsername, smtpPassword, isSmtpDebug, filter.toString(),  bufferSize);
+            smtpHost, smtpPort, smtpUsername, smtpPassword, isSmtpDebug, filter.toString(),  bufferSize, null);
         if (manager == null) {
             return null;
         }
Index: apache-log4j-2.13.0-src/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SmtpManager.java
===================================================================
--- apache-log4j-2.13.0-src.orig/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SmtpManager.java
+++ apache-log4j-2.13.0-src/log4j-core/src/main/java/org/apache/logging/log4j/core/net/SmtpManager.java
@@ -35,6 +35,7 @@ import javax.mail.internet.MimeMessage;
 import javax.mail.internet.MimeMultipart;
 import javax.mail.internet.MimeUtility;
 import javax.mail.util.ByteArrayDataSource;
+import javax.net.ssl.SSLSocketFactory;
 
 import org.apache.logging.log4j.LoggingException;
 import org.apache.logging.log4j.core.Layout;
@@ -46,6 +47,7 @@ import org.apache.logging.log4j.core.imp
 import org.apache.logging.log4j.core.impl.MutableLogEvent;
 import org.apache.logging.log4j.core.layout.AbstractStringLayout.Serializer;
 import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
 import org.apache.logging.log4j.core.util.CyclicBuffer;
 import org.apache.logging.log4j.core.util.NameUtil;
 import org.apache.logging.log4j.core.util.NetUtils;
@@ -99,7 +101,8 @@ public class SmtpManager extends Abstrac
                                              final String from, final String replyTo,
                                              final String subject, String protocol, final String host,
                                              final int port, final String username, final String password,
-                                             final boolean isDebug, final String filterName, final int numElements) {
+                                             final boolean isDebug, final String filterName, final int numElements,
+                                             final SslConfiguration sslConfiguration) {
         if (Strings.isEmpty(protocol)) {
             protocol = "smtp";
         }
@@ -144,7 +147,7 @@ public class SmtpManager extends Abstrac
         final Serializer subjectSerializer = PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(subject).build();
 
         return getManager(name, FACTORY, new FactoryData(to, cc, bcc, from, replyTo, subjectSerializer,
-            protocol, host, port, username, password, isDebug, numElements));
+            protocol, host, port, username, password, isDebug, numElements, sslConfiguration));
     }
 
     /**
@@ -275,10 +278,12 @@ public class SmtpManager extends Abstrac
         private final String password;
         private final boolean isDebug;
         private final int numElements;
+        private final SslConfiguration sslConfiguration;
 
         public FactoryData(final String to, final String cc, final String bcc, final String from, final String replyTo,
                            final Serializer subjectSerializer, final String protocol, final String host, final int port,
-                           final String username, final String password, final boolean isDebug, final int numElements) {
+                           final String username, final String password, final boolean isDebug, final int numElements,
+                           final SslConfiguration sslConfiguration) {
             this.to = to;
             this.cc = cc;
             this.bcc = bcc;
@@ -292,6 +297,7 @@ public class SmtpManager extends Abstrac
             this.password = password;
             this.isDebug = isDebug;
             this.numElements = numElements;
+            this.sslConfiguration = sslConfiguration;
         }
     }
 
@@ -317,22 +323,31 @@ public class SmtpManager extends Abstrac
             final String prefix = "mail." + data.protocol;
 
             final Properties properties = PropertiesUtil.getSystemProperties();
-            properties.put("mail.transport.protocol", data.protocol);
+            properties.setProperty("mail.transport.protocol", data.protocol);
             if (properties.getProperty("mail.host") == null) {
                 // Prevent an UnknownHostException in Java 7
-                properties.put("mail.host", NetUtils.getLocalHostname());
+                properties.setProperty("mail.host", NetUtils.getLocalHostname());
             }
 
             if (null != data.host) {
-                properties.put(prefix + ".host", data.host);
+                properties.setProperty(prefix + ".host", data.host);
             }
             if (data.port > 0) {
-                properties.put(prefix + ".port", String.valueOf(data.port));
+                properties.setProperty(prefix + ".port", String.valueOf(data.port));
             }
 
             final Authenticator authenticator = buildAuthenticator(data.username, data.password);
             if (null != authenticator) {
-                properties.put(prefix + ".auth", "true");
+                properties.setProperty(prefix + ".auth", "true");
+            }
+
+            if (data.protocol.equals("smtps")) {
+                final SslConfiguration sslConfiguration = data.sslConfiguration;
+                if (sslConfiguration != null) {
+                    final SSLSocketFactory sslSocketFactory = sslConfiguration.getSslSocketFactory();
+                    properties.put(prefix + ".ssl.socketFactory", sslSocketFactory);
+                    properties.setProperty(prefix + ".ssl.checkserveridentity", Boolean.toString(sslConfiguration.isVerifyHostName()));
+                }
             }
 
             final Session session = Session.getInstance(properties, authenticator);
Index: apache-log4j-2.13.0-src/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/SmtpAppenderTest.java
===================================================================
--- apache-log4j-2.13.0-src.orig/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/SmtpAppenderTest.java
+++ apache-log4j-2.13.0-src/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/SmtpAppenderTest.java
@@ -40,8 +40,6 @@ import static org.junit.Assert.*;
 public class SmtpAppenderTest {
 
     private static final String HOST = "localhost";
-    private static final int PORTNUM = AvailablePortFinder.getNextAvailable();
-    private static final String PORT = String.valueOf(PORTNUM);
 
     @Test
     public void testMessageFactorySetFrom() throws MessagingException {
@@ -114,9 +112,19 @@ public class SmtpAppenderTest {
         final String subjectKey = getClass().getName();
         final String subjectValue = "SubjectValue1";
         ThreadContext.put(subjectKey, subjectValue);
-        final SmtpAppender appender = SmtpAppender.createAppender(null, "Test", "to@example.com", "cc@example.com",
-                "bcc@example.com", "from@example.com", "replyTo@example.com", "Subject Pattern %X{" + subjectKey + "}",
-                null, HOST, PORT, null, null, "false", "3", null, null, "true");
+        final int smtpPort = AvailablePortFinder.getNextAvailable();
+        final SmtpAppender appender = SmtpAppender.newBuilder()
+                .setName("Test")
+                .setTo("to@example.com")
+                .setCc("cc@example.com")
+                .setBcc("bcc@example.com")
+                .setFrom("from@example.com")
+                .setReplyTo("replyTo@example.com")
+                .setSubject("Subject Pattern %X{" + subjectKey + "}")
+                .setSmtpHost(HOST)
+                .setSmtpPort(smtpPort)
+                .setBufferSize(3)
+                .build();
         appender.start();
 
         final LoggerContext context = LoggerContext.getContext();
@@ -125,7 +133,7 @@ public class SmtpAppenderTest {
         root.setAdditive(false);
         root.setLevel(Level.DEBUG);
 
-        final SimpleSmtpServer server = SimpleSmtpServer.start(PORTNUM);
+        final SimpleSmtpServer server = SimpleSmtpServer.start(smtpPort);
 
         root.debug("Debug message #1");
         root.debug("Debug message #2");
