Browse Source

Merge branch 'smtps'

Jason Rivard 6 năm trước cách đây
mục cha
commit
cac88e4219

+ 1 - 1
build/checkstyle-import.xml

@@ -86,7 +86,7 @@
     <allow pkg="javax.net"/>
     <allow pkg="javax.crypto"/>
     <allow pkg="javax.mail"/>
-    <allow class="com.sun.mail.smtp.SMTPSendFailedException"/>
+    <allow pkg="com.sun.mail"/>
     <allow pkg="org.xeustechnologies"/>
     <allow pkg="net.glxn"/>
     <allow pkg="org.webjars"/>

+ 2 - 2
data-service/pom.xml

@@ -136,8 +136,8 @@
         </dependency>
         <dependency>
             <groupId>com.sun.mail</groupId>
-            <artifactId>javax.mail</artifactId>
-            <version>1.6.2</version>
+            <artifactId>jakarta.mail</artifactId>
+            <version>1.6.3</version>
         </dependency>
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>

+ 1 - 0
pom.xml

@@ -264,6 +264,7 @@
                     <excludeFilterFile>${project.root.basedir}/build/spotbugs-exclude.xml</excludeFilterFile>
                     <includeTests>false</includeTests>
                     <skip>${skipSpotbugs}</skip>
+                    <effort>max</effort>
                 </configuration>
                 <executions>
                     <execution>

+ 2 - 2
server/pom.xml

@@ -226,8 +226,8 @@
         </dependency>
         <dependency>
             <groupId>com.sun.mail</groupId>
-            <artifactId>javax.mail</artifactId>
-            <version>1.6.2</version>
+            <artifactId>jakarta.mail</artifactId>
+            <version>1.6.3</version>
         </dependency>
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>

+ 2 - 0
server/src/main/java/password/pwm/AppProperty.java

@@ -329,6 +329,8 @@ public enum AppProperty
     SECURITY_DEFAULT_EPHEMERAL_BLOCK_ALG            ( "security.defaultEphemeralBlockAlg" ),
     SECURITY_DEFAULT_EPHEMERAL_HASH_ALG             ( "security.defaultEphemeralHashAlg" ),
     SEEDLIST_BUILTIN_PATH                           ( "seedlist.builtin.path" ),
+    SMTP_IO_CONNECT_TIMEOUT                         ( "smtp.io.connectTimeoutMs" ),
+    SMTP_IO_READ_TIMEOUT                            ( "smtp.io.readTimeoutMs" ),
     SMTP_SUBJECT_ENCODING_CHARSET                   ( "smtp.subjectEncodingCharset" ),
     SMTP_RETRYABLE_SEND_RESPONSE_STATUSES           ( "smtp.retryableSendResponseStatus" ),
     TOKEN_CLEANER_INTERVAL_SECONDS                  ( "token.cleaner.intervalSeconds" ),

+ 4 - 0
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -310,8 +310,12 @@ public enum PwmSetting
             "email.profile.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.INTERNAL ),
     EMAIL_SERVER_ADDRESS(
             "email.smtp.address", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SERVERS ),
+    EMAIL_SERVER_TYPE(
+            "email.smtp.type", PwmSettingSyntax.SELECT, PwmSettingCategory.EMAIL_SERVERS ),
     EMAIL_SERVER_PORT(
             "email.smtp.port", PwmSettingSyntax.NUMERIC, PwmSettingCategory.EMAIL_SERVERS ),
+    EMAIL_SERVER_CERTS(
+            "email.smtp.serverCerts", PwmSettingSyntax.X509CERT, PwmSettingCategory.EMAIL_SERVERS ),
     EMAIL_USERNAME(
             "email.smtp.username", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SERVERS ),
     EMAIL_PASSWORD(

+ 64 - 0
server/src/main/java/password/pwm/config/function/SmtpCertImportFunction.java

@@ -0,0 +1,64 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.function;
+
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.SettingUIFunction;
+import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.value.X509CertificateValue;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmSession;
+import password.pwm.i18n.Message;
+import password.pwm.svc.email.EmailServerUtil;
+import password.pwm.util.java.JavaHelper;
+
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+public class SmtpCertImportFunction implements SettingUIFunction
+{
+    @Override
+    public String provideFunction(
+            final PwmRequest pwmRequest,
+            final StoredConfigurationImpl storedConfiguration,
+            final PwmSetting setting,
+            final String profile,
+            final String extraData
+    )
+            throws PwmUnrecoverableException
+    {
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+
+        final Configuration configuration = new Configuration( storedConfiguration );
+        final List<X509Certificate> certs = EmailServerUtil.readCertificates( configuration, profile );
+        if ( !JavaHelper.isEmpty( certs ) )
+        {
+            final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
+            storedConfiguration.writeSetting( PwmSetting.EMAIL_SERVER_CERTS, profile, new X509CertificateValue( certs ), userIdentity );
+        }
+
+        return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmRequest.getConfig() );
+    }
+
+}

+ 28 - 0
server/src/main/java/password/pwm/config/option/SmtpServerType.java

@@ -0,0 +1,28 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.option;
+
+public enum SmtpServerType
+{
+    SMTP,
+    START_TLS,
+    SMTPS,
+}

+ 8 - 1
server/src/main/java/password/pwm/http/filter/SessionFilter.java

@@ -157,7 +157,7 @@ public class SessionFilter extends AbstractPwmFilter
         // debug the http session headers
         if ( !pwmSession.getSessionStateBean().isDebugInitialized() )
         {
-            LOGGER.trace( pwmSession, () -> pwmRequest.debugHttpHeaders() );
+            LOGGER.trace( pwmSession, pwmRequest::debugHttpHeaders );
             pwmSession.getSessionStateBean().setDebugInitialized( true );
         }
 
@@ -309,6 +309,13 @@ public class SessionFilter extends AbstractPwmFilter
 
         if ( pwmRequest.getURL().isCommandServletURL() )
         {
+            LOGGER.debug( pwmRequest, () -> "session is unvalidated but can not be validated during a command servlet request, will allow" );
+            return ProcessStatus.Continue;
+        }
+
+        if ( pwmRequest.getURL().isResourceURL() )
+        {
+            LOGGER.debug( pwmRequest, () -> "session is unvalidated but can not be validated during a resource request, will allow" );
             return ProcessStatus.Continue;
         }
 

+ 10 - 7
server/src/main/java/password/pwm/svc/email/EmailServer.java

@@ -22,6 +22,7 @@ package password.pwm.svc.email;
 
 import lombok.Builder;
 import lombok.Value;
+import password.pwm.config.option.SmtpServerType;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.StringUtil;
 
@@ -33,19 +34,21 @@ import java.util.Properties;
 @Builder
 public class EmailServer
 {
-    private final String id;
-    private final String host;
-    private final int port;
-    private final String username;
-    private final PasswordData password;
-    private final Properties javaMailProps;
-    private final javax.mail.Session session;
+    private String id;
+    private String host;
+    private int port;
+    private String username;
+    private PasswordData password;
+    private Properties javaMailProps;
+    private javax.mail.Session session;
+    private SmtpServerType type;
 
     public String toDebugString()
     {
         final Map<String, String> debugProps = new LinkedHashMap<>(  );
         debugProps.put( "id", id );
         debugProps.put( "host", host );
+        debugProps.put( "type", type.name() );
         debugProps.put( "port", String.valueOf( port ) );
         if ( !StringUtil.isEmpty( username ) )
         {

+ 142 - 35
server/src/main/java/password/pwm/svc/email/EmailServerUtil.java

@@ -22,11 +22,13 @@
 package password.pwm.svc.email;
 
 import com.sun.mail.smtp.SMTPSendFailedException;
+import com.sun.mail.util.MailSSLSocketFactory;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.option.SmtpServerType;
 import password.pwm.config.profile.EmailServerProfile;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
@@ -36,6 +38,7 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.secure.X509Utils;
 
 import javax.mail.Message;
 import javax.mail.MessagingException;
@@ -45,10 +48,13 @@ import javax.mail.internet.InternetAddress;
 import javax.mail.internet.MimeBodyPart;
 import javax.mail.internet.MimeMessage;
 import javax.mail.internet.MimeMultipart;
+import javax.net.ssl.TrustManager;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -61,6 +67,7 @@ public class EmailServerUtil
     private static final PwmLogger LOGGER = PwmLogger.forClass( EmailServerUtil.class );
 
     static List<EmailServer> makeEmailServersMap( final Configuration configuration )
+            throws PwmUnrecoverableException
     {
         final List<EmailServer> returnObj = new ArrayList<>(  );
 
@@ -68,57 +75,128 @@ public class EmailServerUtil
 
         for ( final EmailServerProfile profile : profiles )
         {
-            final String id = profile.getIdentifier();
-            final String address = profile.readSettingAsString( PwmSetting.EMAIL_SERVER_ADDRESS );
-            final int port = (int) profile.readSettingAsLong( PwmSetting.EMAIL_SERVER_PORT );
-            final String username = profile.readSettingAsString( PwmSetting.EMAIL_USERNAME );
-            final PasswordData password = profile.readSettingAsPassword( PwmSetting.EMAIL_PASSWORD );
-            if ( !StringUtil.isEmpty( address )
-                    && port > 0
-                    )
-            {
-                final Properties properties = makeJavaMailProps( configuration, address, port );
-                final javax.mail.Session session = javax.mail.Session.getInstance( properties, null );
-                final EmailServer emailServer = EmailServer.builder()
-                        .id( id )
-                        .host( address )
-                        .port( port )
-                        .username( username )
-                        .password( password )
-                        .javaMailProps( properties )
-                        .session( session )
-                        .build();
-                returnObj.add( emailServer );
-            }
-            else
-            {
-                LOGGER.warn( "discarding incompletely configured email address for smtp server profile " + id );
-            }
+            final TrustManager[] trustManager = trustManagerForProfile( configuration, profile );
+
+            final Optional<EmailServer> emailServer = makeEmailServer( configuration, profile, trustManager );
+
+            emailServer.ifPresent( returnObj::add );
         }
 
         return returnObj;
     }
 
+    private static Optional<EmailServer> makeEmailServer(
+            final Configuration configuration,
+            final EmailServerProfile profile,
+            final TrustManager[] trustManagers
+    )
+            throws PwmUnrecoverableException
+    {
+        final String id = profile.getIdentifier();
+        final String address = profile.readSettingAsString( PwmSetting.EMAIL_SERVER_ADDRESS );
+        final int port = (int) profile.readSettingAsLong( PwmSetting.EMAIL_SERVER_PORT );
+        final String username = profile.readSettingAsString( PwmSetting.EMAIL_USERNAME );
+        final PasswordData password = profile.readSettingAsPassword( PwmSetting.EMAIL_PASSWORD );
+
+        final SmtpServerType smtpServerType = profile.readSettingAsEnum( PwmSetting.EMAIL_SERVER_TYPE, SmtpServerType.class );
+        if ( !StringUtil.isEmpty( address )
+                && port > 0
+        )
+        {
+            final Properties properties = makeJavaMailProps( configuration, profile, trustManagers );
+            final javax.mail.Session session = javax.mail.Session.getInstance( properties, null );
+            return Optional.of( EmailServer.builder()
+                    .id( id )
+                    .host( address )
+                    .port( port )
+                    .username( username )
+                    .password( password )
+                    .javaMailProps( properties )
+                    .session( session )
+                    .type( smtpServerType )
+                    .build() );
+        }
+        else
+        {
+            LOGGER.warn( "discarding incompletely configured email address for smtp server profile " + id );
+        }
+
+        return Optional.empty();
+    }
+
+    private static TrustManager[] trustManagerForProfile( final Configuration configuration, final EmailServerProfile emailServerProfile )
+            throws PwmUnrecoverableException
+    {
+        final List<X509Certificate> configuredCerts = emailServerProfile.readSettingAsCertificate( PwmSetting.EMAIL_SERVER_CERTS );
+        if ( JavaHelper.isEmpty( configuredCerts ) )
+        {
+            return X509Utils.getDefaultJavaTrustManager( configuration );
+        }
+        final TrustManager certMatchingTrustManager = new X509Utils.CertMatchingTrustManager( configuration, configuredCerts );
+        return new TrustManager[]
+                {
+                        certMatchingTrustManager,
+                };
+    }
+
+
     private static Properties makeJavaMailProps(
             final Configuration config,
-            final String host,
-            final int port
+            final EmailServerProfile profile,
+            final TrustManager[] trustManager
     )
+            throws PwmUnrecoverableException
     {
         //Create a properties item to start setting up the mail
-        final Properties props = new Properties();
+        final Properties properties = new Properties();
 
         //Specify the desired SMTP server
-        props.put( "mail.smtp.host", host );
+        final String address = profile.readSettingAsString( PwmSetting.EMAIL_SERVER_ADDRESS );
+        properties.put( "mail.smtp.host", address );
 
         //Specify SMTP server port
-        props.put( "mail.smtp.port", port );
+        final int port = (int) profile.readSettingAsLong( PwmSetting.EMAIL_SERVER_PORT );
+        properties.put( "mail.smtp.port", port );
+        properties.put( "mail.smtp.socketFactory.port", port );
+
+        //set connection properties
+        properties.put( "mail.smtp.connectiontimeout", JavaHelper.silentParseInt( config.readAppProperty( AppProperty.SMTP_IO_CONNECT_TIMEOUT ), 10_000 ) );
+        properties.put( "mail.smtp.timeout", JavaHelper.silentParseInt( config.readAppProperty( AppProperty.SMTP_IO_CONNECT_TIMEOUT ), 30_000 ) );
+
+        properties.put( "mail.smtp.sendpartial", true );
+
+        try
+        {
+            final SmtpServerType smtpServerType = profile.readSettingAsEnum( PwmSetting.EMAIL_SERVER_TYPE, SmtpServerType.class );
+            if ( smtpServerType == SmtpServerType.SMTP )
+            {
+                return properties;
+            }
+
+            final MailSSLSocketFactory mailSSLSocketFactory = new MailSSLSocketFactory();
+            mailSSLSocketFactory.setTrustManagers( trustManager );
+
+            properties.put( "mail.smtp.ssl.enable", true );
+            properties.put( "mail.smtp.ssl.checkserveridentity", true );
+            properties.put( "mail.smtp.socketFactory.fallback", false );
+            properties.put( "mail.smtp.ssl.socketFactory", mailSSLSocketFactory );
+            properties.put( "mail.smtp.ssl.socketFactory.port", port );
+
+            final boolean useStartTls = smtpServerType == SmtpServerType.START_TLS;
+            properties.put( "mail.smtp.starttls.enable", useStartTls );
+            properties.put( "mail.smtp.starttls.required", useStartTls );
+        }
+        catch ( Exception e )
+        {
+            final String msg = "unable to create message transport properties: " + e.getMessage();
+            throw new PwmUnrecoverableException( PwmError.CONFIG_FORMAT_ERROR, msg );
+        }
 
         //Specify configured advanced settings.
         final Map<String, String> advancedSettingValues = StringUtil.convertStringListToNameValuePair( config.readSettingAsStringArray( PwmSetting.EMAIL_ADVANCED_SETTINGS ), "=" );
-        props.putAll( advancedSettingValues );
+        properties.putAll( advancedSettingValues );
 
-        return props;
+        return properties;
     }
 
     private static InternetAddress makeInternetAddress( final String input )
@@ -279,10 +357,10 @@ public class EmailServerUtil
             throws MessagingException, PwmUnrecoverableException
     {
         // Login to SMTP server first if both username and password is given
-        final Transport transport = server.getSession().getTransport( "smtp" );
-
         final boolean authenticated = !StringUtil.isEmpty( server.getUsername() ) && server.getPassword() != null;
 
+        final Transport transport = server.getSession().getTransport( );
+
         if ( authenticated )
         {
             // create a new Session object for the message
@@ -303,4 +381,33 @@ public class EmailServerUtil
         return transport;
     }
 
+
+    public static List<X509Certificate> readCertificates( final Configuration configuration, final String profile )
+            throws PwmUnrecoverableException
+    {
+        final EmailServerProfile emailServerProfile = configuration.getEmailServerProfiles().get( profile );
+        final X509Utils.CertReaderTrustManager certReaderTm = new X509Utils.CertReaderTrustManager( X509Utils.ReadCertificateFlag.ReadOnlyRootCA );
+        final TrustManager[] trustManagers =  new TrustManager[]
+                {
+                        certReaderTm,
+                };
+        final Optional<EmailServer> emailServer = makeEmailServer( configuration, emailServerProfile, trustManagers );
+        if ( emailServer.isPresent() )
+        {
+            try ( Transport transport = makeSmtpTransport( emailServer.get() ); )
+            {
+                return certReaderTm.getCertificates();
+            }
+            catch ( Exception e )
+            {
+                final String exceptionMessage = JavaHelper.readHostileExceptionMessage( e );
+                final String errorMsg = "error connecting to secure server while reading SMTP certificates: " + exceptionMessage;
+                LOGGER.debug( () -> errorMsg );
+                throw new PwmUnrecoverableException( PwmError.ERROR_SERVICE_UNREACHABLE, errorMsg );
+            }
+        }
+
+        return Collections.emptyList();
+    }
+
 }

+ 29 - 22
server/src/main/java/password/pwm/svc/email/EmailService.java

@@ -59,7 +59,6 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -69,11 +68,11 @@ import java.util.concurrent.atomic.AtomicInteger;
  */
 public class EmailService implements PwmService
 {
-
     private static final PwmLogger LOGGER = PwmLogger.forClass( EmailService.class );
 
     private PwmApplication pwmApplication;
-    private final Map<EmailServer, Optional<ErrorInformation>> serverErrors = new ConcurrentHashMap<>( );
+    private final Map<EmailServer, ErrorInformation> serverErrors = new ConcurrentHashMap<>( );
+    private ErrorInformation startupError;
     private final List<EmailServer> servers = new ArrayList<>( );
     private WorkQueueProcessor<EmailItemBean> workQueueProcessor;
     private AtomicLoopIntIncrementer serverIncrementer;
@@ -89,18 +88,23 @@ public class EmailService implements PwmService
         status = STATUS.OPENING;
         this.pwmApplication = pwmApplication;
 
-        servers.addAll( EmailServerUtil.makeEmailServersMap( pwmApplication.getConfig() ) );
-
-        if ( servers.isEmpty() )
+        try
         {
+            servers.addAll( EmailServerUtil.makeEmailServersMap( pwmApplication.getConfig() ) );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            startupError = e.getErrorInformation();
+            LOGGER.error( "unable to startup email service: " + e.getMessage() );
             status = STATUS.CLOSED;
-            LOGGER.debug( () -> "no email servers configured, will remain closed" );
             return;
         }
 
-        for ( final EmailServer emailServer : servers )
+        if ( servers.isEmpty() )
         {
-            serverErrors.put( emailServer, Optional.empty() );
+            status = STATUS.CLOSED;
+            LOGGER.debug( () -> "no email servers configured, will remain closed" );
+            return;
         }
 
         serverIncrementer = new AtomicLoopIntIncrementer( servers.size() - 1 );
@@ -127,7 +131,6 @@ public class EmailService implements PwmService
 
         retryableStatusResponses = readRetryableStatusCodes( pwmApplication.getConfig() );
 
-
         status = STATUS.OPEN;
     }
 
@@ -148,6 +151,11 @@ public class EmailService implements PwmService
 
     public List<HealthRecord> healthCheck( )
     {
+        if ( startupError != null )
+        {
+            return Collections.singletonList( HealthRecord.forMessage( HealthMessage.ServiceClosed, this.getClass().getSimpleName(), startupError.toDebugStr() ) );
+        }
+
         if ( pwmApplication.getLocalDB() == null || pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN )
         {
             return Collections.singletonList( HealthRecord.forMessage( HealthMessage.ServiceClosed_LocalDBUnavail, this.getClass().getSimpleName() ) );
@@ -159,13 +167,11 @@ public class EmailService implements PwmService
         }
 
         final List<HealthRecord> records = new ArrayList<>( );
-        for ( final Map.Entry<EmailServer, Optional<ErrorInformation>> entry : serverErrors.entrySet() )
+        final Map<EmailServer, ErrorInformation> localMap = new HashMap<>( serverErrors );
+        for ( final Map.Entry<EmailServer, ErrorInformation> entry : localMap.entrySet() )
         {
-            if ( entry.getValue().isPresent() )
-            {
-                final ErrorInformation errorInformation = entry.getValue().get();
-                records.add( HealthRecord.forMessage( HealthMessage.Email_SendFailure, errorInformation.toDebugStr() ) );
-            }
+            final ErrorInformation errorInformation = entry.getValue();
+            records.add( HealthRecord.forMessage( HealthMessage.Email_SendFailure, errorInformation.toDebugStr() ) );
         }
 
         return records;
@@ -391,7 +397,7 @@ public class EmailService implements PwmService
                 serverTransport.getTransport().sendMessage( message, message.getAllRecipients() );
             }
 
-            serverErrors.put( serverTransport.getEmailServer(), Optional.empty() );
+            serverErrors.remove( serverTransport.getEmailServer() );
 
             LOGGER.debug( () -> "sent email: " + emailItemBean.toDebugString() );
             StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_SUCCESSES );
@@ -420,7 +426,7 @@ public class EmailService implements PwmService
 
             if ( serverTransport != null )
             {
-                serverErrors.put( serverTransport.getEmailServer(), Optional.of( errorInformation ) );
+                serverErrors.put( serverTransport.getEmailServer(), errorInformation );
             }
             LOGGER.error( errorInformation );
 
@@ -457,14 +463,15 @@ public class EmailService implements PwmService
             {
                 final Transport transport = EmailServerUtil.makeSmtpTransport( server );
 
-                serverErrors.put( server, Optional.empty() );
+                serverErrors.remove( server );
                 return new EmailConnection( server, transport );
             }
-            catch ( MessagingException e )
+            catch ( Exception e )
             {
-                final String msg = "unable to connect to email server '" + server.toDebugString() + "', error: " + e.getMessage();
+                final String exceptionMsg = JavaHelper.readHostileExceptionMessage( e );
+                final String msg = "unable to connect to email server '" + server.toDebugString() + "', error: " + exceptionMsg;
                 final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_UNREACHABLE, msg );
-                serverErrors.put( server, Optional.of( errorInformation ) );
+                serverErrors.put( server, errorInformation );
                 LOGGER.warn( errorInformation.toDebugStr() );
             }
         }

+ 0 - 1
server/src/main/java/password/pwm/util/localdb/WorkQueueProcessor.java

@@ -58,7 +58,6 @@ import java.util.concurrent.locks.LockSupport;
  */
 public final class WorkQueueProcessor<W extends Serializable>
 {
-
     private static final TimeDuration SUBMIT_QUEUE_FULL_RETRY_CYCLE_INTERVAL = TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS );
     private static final TimeDuration CLOSE_RETRY_CYCLE_INTERVAL = TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS );
 

+ 31 - 18
server/src/main/java/password/pwm/util/secure/X509Utils.java

@@ -229,13 +229,13 @@ public abstract class X509Utils
         return false;
     }
 
-    private static class CertReaderTrustManager implements X509TrustManager
+    public static class CertReaderTrustManager implements X509TrustManager
     {
         private final ReadCertificateFlag[] readCertificateFlags;
 
-        private X509Certificate[] certificates;
+        private List<X509Certificate> certificates = new ArrayList<>();
 
-        CertReaderTrustManager( final ReadCertificateFlag[] readCertificateFlags )
+        public CertReaderTrustManager( final ReadCertificateFlag... readCertificateFlags )
         {
             this.readCertificateFlags = readCertificateFlags;
         }
@@ -254,9 +254,10 @@ public abstract class X509Utils
         public void checkServerTrusted( final X509Certificate[] chain, final String authType )
                 throws CertificateException
         {
-            certificates = chain;
-            final List<Map<String, String>> certDebugInfo = X509Utils.makeDebugInfoMap( Arrays.asList( certificates ) );
-            LOGGER.debug( () -> "read certificates from remote server via httpclient: "
+            final List<X509Certificate> asList = Arrays.asList( chain );
+            certificates.addAll( asList );
+            final List<Map<String, String>> certDebugInfo = X509Utils.makeDebugInfoMap( certificates );
+            LOGGER.debug( () -> "read certificates from remote server: "
                     + JsonUtil.serialize( new ArrayList<>( certDebugInfo ) ) );
         }
 
@@ -264,9 +265,9 @@ public abstract class X509Utils
         {
             if ( JavaHelper.enumArrayContainsValue( readCertificateFlags, ReadCertificateFlag.ReadOnlyRootCA ) )
             {
-                return Collections.unmodifiableList( identifyRootCACertificate( Arrays.asList( certificates ) ) );
+                return Collections.unmodifiableList( identifyRootCACertificate( certificates ) );
             }
-            return Collections.unmodifiableList( Arrays.asList( certificates ) );
+            return Collections.unmodifiableList( certificates );
         }
     }
 
@@ -443,10 +444,11 @@ public abstract class X509Utils
             throws CertificateEncodingException, PwmUnrecoverableException
     {
         return x509Certificate.toString()
-                + "\n:MD5 checksum: " + hash( x509Certificate, PwmHashAlgorithm.MD5 )
-                + "\n:SHA1 checksum: " + hash( x509Certificate, PwmHashAlgorithm.SHA1 )
-                + "\n:SHA2-256 checksum: " + hash( x509Certificate, PwmHashAlgorithm.SHA256 )
-                + "\n:SHA2-512 checksum: " + hash( x509Certificate, PwmHashAlgorithm.SHA512 );
+                + "\nMD5: " + hash( x509Certificate, PwmHashAlgorithm.MD5 )
+                + "\nSHA1: " + hash( x509Certificate, PwmHashAlgorithm.SHA1 )
+                + "\nSHA2-256: " + hash( x509Certificate, PwmHashAlgorithm.SHA256 )
+                + "\nSHA2-512: " + hash( x509Certificate, PwmHashAlgorithm.SHA512 )
+                + "\n:IsRootCA: " + certIsRootCA( x509Certificate );
     }
 
     public static String makeDebugText( final X509Certificate x509Certificate )
@@ -545,21 +547,32 @@ public abstract class X509Utils
 
     private static List<X509Certificate> identifyRootCACertificate( final List<X509Certificate> certificates )
     {
-        final int keyCertSignBitPosition = 5;
         for ( final X509Certificate certificate : certificates )
         {
             final boolean[] keyUsages = certificate.getKeyUsage();
-            if ( keyUsages != null && keyUsages.length > keyCertSignBitPosition - 1 )
+            if ( certIsRootCA( certificate ) )
             {
-                if ( keyUsages[keyCertSignBitPosition] )
-                {
-                    return Collections.singletonList( certificate );
-                }
+                return Collections.singletonList( certificate );
             }
         }
         return Collections.emptyList();
     }
 
+    private static boolean certIsRootCA( final X509Certificate certificate )
+    {
+        final int keyCertSignBitPosition = 5;
+        final boolean[] keyUsages = certificate.getKeyUsage();
+        if ( keyUsages != null && keyUsages.length > keyCertSignBitPosition - 1 )
+        {
+            if ( keyUsages[keyCertSignBitPosition] )
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     public static TrustManager[] getDefaultJavaTrustManager( final Configuration configuration )
             throws PwmUnrecoverableException
     {

+ 2 - 0
server/src/main/resources/password/pwm/AppProperty.properties

@@ -309,6 +309,8 @@ security.defaultEphemeralBlockAlg=AES128_GCM
 security.defaultEphemeralHashAlg=SHA512
 security.config.minSecurityKeyLength=32
 seedlist.builtin.path=/WEB-INF/seedlist.zip
+smtp.io.connectTimeoutMs=10000
+smtp.io.readTimeoutMs=30000
 smtp.subjectEncodingCharset=UTF8
 smtp.retryableSendResponseStatus=400,420,421
 telemetry.senderImplementation=password.pwm.svc.telemetry.HttpTelemetrySender

+ 16 - 0
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -711,6 +711,22 @@
             <property key="Maximum">65535</property>
         </properties>
     </setting>
+    <setting hidden="false" key="email.smtp.type" level="1">
+        <default>
+            <value>SMTP</value>
+        </default>
+        <options>
+            <option value="SMTP">SMTP (Plaintext)</option>
+            <option value="START_TLS">StartTLS</option>
+            <option value="SMTPS">SMTPS (SSL/TLS)</option>
+        </options>
+    </setting>
+    <setting hidden="false" key="email.smtp.serverCerts" level="0" required="true">
+        <default/>
+        <properties>
+            <property key="Cert_ImportHandler">password.pwm.config.function.SmtpCertImportFunction</property>
+        </properties>
+    </setting>
     <setting hidden="false" key="email.smtp.username" level="1">
         <default>
             <value />

+ 1 - 1
server/src/main/resources/password/pwm/i18n/Health.properties

@@ -78,7 +78,7 @@ HealthMessage_LocalDBLogger_NOTOPEN=LocalDBLogger is not open, status is %1%
 HealthMessage_LocalDBLogger_HighRecordCount=LocalDBLogger event log record count of %1% records, is more than the configured maximum of %2%.  Excess records are being purged.
 HealthMessage_LocalDBLogger_OldRecordPresent=Oldest LocalDBLogger event log record is %1%, configured maximum is %2%.  Excess records are being purged.
 HealthMessage_NewUser_PwTemplateBad=The setting %1% is set to a LDAP DN value that is invalid
-HealthMessage_ServiceClosed=unable to start %1% service
+HealthMessage_ServiceClosed=unable to start %1% service   %2%
 HealthMessage_ServiceClosed_LocalDBUnavail=unable to start %1% service, LocalDB is not available
 HealthMessage_ServiceClosed_AppReadOnly=unable to start %1% service, application is in read-only mode
 HealthMessage_Wordlist_AutoImportFailure=Configured word list (%1%) failed to import due to error: %2% at timestamp %3%

+ 4 - 0
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -334,6 +334,8 @@ Setting_Description_email.sendUsername=Define this template to send an email for
 Setting_Description_email.profile.list=List of SMTP email servers to be used.  @PwmAppName@ will alternate among the servers in the list when a server becomes unreachable.
 Setting_Description_email.smtp.address=Specify an SMTP server address that sends the emails @PwmAppName@ generates.  Removing this setting prevents @PwmAppName@ from sending any emails.  Ensure that the server specified here allows relaying.  For best results, use a local SMTP server.
 Setting_Description_email.smtp.port=Specify the network port number for the SMTP server.
+Setting_Description_email.smtp.type=The type of connection to use for the SMTP session.
+Setting_Description_email.smtp.serverCerts=Certificates used for secure communication with server.  If no certificates are specfied, the default Java trust store will be used for certificate validation.
 Setting_Description_email.smtp.username=Specify an SMTP user that logs in to the SMTP server so that it can send the emails @PwmAppName@ generates.  A blank value here sends SMTP messages without authentication.
 Setting_Description_email.smtp.userpassword=Specify the password for the SMTP user.  A blank value here sends SMTP messages without authentication.
 Setting_Description_email.smtp.advancedSettings=Add Name/Value settings to control the behavior of the mail agent. Available settings are defined as part of the <a href\="https\://javamail.java.net/nonav/docs/api/com/sun/mail/smtp/package-summary.html">JavaMail API</a>. The settings must be in "name\=value" format, where name is the key value of a valid JavaMail API setting.
@@ -856,6 +858,8 @@ Setting_Label_email.sendUsername=Send User Name Email
 Setting_Label_email.profile.list=Email Servers
 Setting_Label_email.smtp.address=SMTP Server Address
 Setting_Label_email.smtp.port=SMTP Server Port
+Setting_Label_email.smtp.type=SMTP Connection Type
+Setting_Label_email.smtp.serverCerts=SMTP Server Certificates
 Setting_Label_email.smtp.username=SMTP Server User Name
 Setting_Label_email.smtp.userpassword=SMTP Server Password
 Setting_Label_email.smtp.advancedSettings=SMTP Email Advanced Settings