Selaa lähdekoodia

add email test functionality and fix issue with starttls certificate trusting

Jason Rivard 5 vuotta sitten
vanhempi
commit
3963a427e8

+ 58 - 0
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -25,6 +25,7 @@ import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
+import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SmsItemBean;
 import password.pwm.bean.UserIdentity;
@@ -36,6 +37,7 @@ import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.StoredValue;
+import password.pwm.config.profile.EmailServerProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.StoredConfigItemKey;
@@ -72,6 +74,9 @@ import password.pwm.i18n.Config;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.ldap.LdapBrowser;
+import password.pwm.svc.email.EmailServer;
+import password.pwm.svc.email.EmailServerUtil;
+import password.pwm.svc.email.EmailService;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -86,6 +91,7 @@ import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;
 import password.pwm.ws.server.rest.bean.HealthData;
 
+import javax.mail.MessagingException;
 import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import java.io.ByteArrayInputStream;
@@ -134,6 +140,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         ldapHealthCheck( HttpMethod.POST ),
         databaseHealthCheck( HttpMethod.POST ),
         smsHealthCheck( HttpMethod.POST ),
+        emailHealthCheck( HttpMethod.POST ),
         finishEditing( HttpMethod.POST ),
         executeSettingFunction( HttpMethod.POST ),
         setConfigurationPassword( HttpMethod.POST ),
@@ -744,6 +751,57 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         return ProcessStatus.Halt;
     }
 
+    @ActionHandler( action = "emailHealthCheck" )
+    private ProcessStatus restEmailHealthCheck(
+            final PwmRequest pwmRequest
+    )
+            throws IOException, PwmUnrecoverableException
+    {
+        final Instant startTime = Instant.now();
+        final ConfigManagerBean configManagerBean = getBean( pwmRequest );
+        final String profileID = pwmRequest.readParameterAsString( "profile" );
+
+        LOGGER.debug( pwmRequest, () -> "beginning restEmailHealthCheck" );
+
+        final Map<String, String> params = pwmRequest.readBodyAsJsonStringMap();
+        final EmailItemBean testEmailItem = new EmailItemBean( params.get( "to" ), params.get( "from" ), params.get( "subject" ), params.get( "body" ), null );
+
+        final List<HealthRecord> returnRecords = new ArrayList<>();
+
+        final Configuration testConfiguration = new Configuration( configManagerBean.getStoredConfiguration() );
+
+        final EmailServerProfile emailServerProfile = testConfiguration.getEmailServerProfiles().get( profileID );
+        if ( emailServerProfile != null )
+        {
+            final Optional<EmailServer> emailServer = EmailServerUtil.makeEmailServer( testConfiguration, emailServerProfile, null );
+            if ( emailServer.isPresent() )
+            {
+                final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, pwmRequest.getUserInfoIfLoggedIn() );
+
+                try
+                {
+                    EmailService.sendEmailSynchronous( emailServer.get(), testConfiguration, testEmailItem, macroMachine );
+                    returnRecords.add( new HealthRecord( HealthStatus.INFO, HealthTopic.Email, "message sent" ) );
+                }
+                catch ( final MessagingException | PwmException e )
+                {
+                    returnRecords.add( new HealthRecord( HealthStatus.WARN, HealthTopic.Email, JavaHelper.readHostileExceptionMessage( e ) ) );
+                }
+            }
+        }
+
+        if ( returnRecords.isEmpty() )
+        {
+            returnRecords.add( new HealthRecord( HealthStatus.WARN, HealthTopic.Email, "smtp service is not configured." ) );
+        }
+
+        final HealthData healthData = HealthRecord.asHealthDataBean( testConfiguration, pwmRequest.getLocale(), returnRecords );
+        final RestResultBean restResultBean = RestResultBean.withData( healthData );
+        pwmRequest.outputJsonResult( restResultBean );
+        LOGGER.debug( pwmRequest, () -> "completed restEmailHealthCheck in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
+        return ProcessStatus.Halt;
+    }
+
     @ActionHandler( action = "uploadFile" )
     private ProcessStatus doUploadFile(
             final PwmRequest pwmRequest

+ 1 - 1
server/src/main/java/password/pwm/svc/PwmServiceEnum.java

@@ -43,7 +43,7 @@ public enum PwmServiceEnum
     StatisticsManager( password.pwm.svc.stats.StatisticsManager.class, Flag.StartDuringRuntimeInstance ),
     WordlistManager( WordlistService.class, Flag.StartDuringRuntimeInstance ),
     SeedlistManager( SeedlistService.class ),
-    EmailQueueManager( EmailService.class ),
+    EmailQueueManager( EmailService.class, Flag.StartDuringRuntimeInstance ),
     SmsQueueManager( password.pwm.util.queue.SmsQueueManager.class ),
     UrlShortenerService( password.pwm.svc.shorturl.UrlShortenerService.class ),
     TokenService( password.pwm.svc.token.TokenService.class, Flag.StartDuringRuntimeInstance ),

+ 17 - 11
server/src/main/java/password/pwm/svc/email/EmailServerUtil.java

@@ -87,7 +87,7 @@ public class EmailServerUtil
         return returnObj;
     }
 
-    private static Optional<EmailServer> makeEmailServer(
+    public static Optional<EmailServer> makeEmailServer(
             final Configuration configuration,
             final EmailServerProfile profile,
             final TrustManager[] trustManagers
@@ -105,7 +105,10 @@ public class EmailServerUtil
                 && port > 0
         )
         {
-            final Properties properties = makeJavaMailProps( configuration, profile, trustManagers );
+            final TrustManager[] effectiveTrustManagers = trustManagers == null
+                    ? trustManagerForProfile( configuration, profile )
+                    : trustManagers;
+            final Properties properties = makeJavaMailProps( configuration, profile, effectiveTrustManagers );
             final javax.mail.Session session = javax.mail.Session.getInstance( properties, null );
             return Optional.of( EmailServer.builder()
                     .id( id )
@@ -177,16 +180,20 @@ public class EmailServerUtil
 
             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 );
+            if ( smtpServerType == SmtpServerType.SMTPS )
+            {
+                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.port", port );
+            }
+            else if ( smtpServerType == SmtpServerType.START_TLS )
+            {
+                properties.put( "mail.smtp.starttls.enable", true );
+                properties.put( "mail.smtp.starttls.required", true );
+            }
         }
         catch ( final Exception e )
         {
@@ -413,5 +420,4 @@ public class EmailServerUtil
 
         return Collections.emptyList();
     }
-
 }

+ 88 - 32
server/src/main/java/password/pwm/svc/email/EmailService.java

@@ -225,38 +225,52 @@ public class EmailService implements PwmService
 
     private boolean determineIfItemCanBeDelivered( final EmailItemBean emailItem )
     {
-
         if ( servers.isEmpty() )
         {
-            LOGGER.debug( () -> "discarding email send event (no SMTP server address configured) " + emailItem.toDebugString() );
+            LOGGER.debug( () -> "discarding email send event, no email servers configured" );
             return false;
         }
 
-        if ( emailItem.getFrom() == null || emailItem.getFrom().length() < 1 )
+        try
         {
-            LOGGER.error( () -> "discarding email event (no from address): " + emailItem.toDebugString() );
-            return false;
+            validateEmailItem( emailItem );
+            return true;
+        }
+        catch ( final PwmOperationalException e )
+        {
+            LOGGER.debug( () -> "discarding email send event: " + e.getMessage() );
         }
+        return false;
+    }
+
+    private static void validateEmailItem( final EmailItemBean emailItem )
+            throws PwmOperationalException
+    {
+
 
-        if ( emailItem.getTo() == null || emailItem.getTo().length() < 1 )
+        if ( StringUtil.isEmpty( emailItem.getFrom() ) )
         {
-            LOGGER.error( () -> "discarding email event (no to address): " + emailItem.toDebugString() );
-            return false;
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_EMAIL_SEND_FAILURE,
+                    "missing from address in email item" ) );
         }
 
-        if ( emailItem.getSubject() == null || emailItem.getSubject().length() < 1 )
+        if ( StringUtil.isEmpty( emailItem.getTo() ) )
         {
-            LOGGER.error( () -> "discarding email event (no subject): " + emailItem.toDebugString() );
-            return false;
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_EMAIL_SEND_FAILURE,
+                    "missing to address in email item" ) );
         }
 
-        if ( ( emailItem.getBodyPlain() == null || emailItem.getBodyPlain().length() < 1 ) && ( emailItem.getBodyHtml() == null || emailItem.getBodyHtml().length() < 1 ) )
+        if ( StringUtil.isEmpty( emailItem.getSubject() ) )
         {
-            LOGGER.error( () -> "discarding email event (no body): " + emailItem.toDebugString() );
-            return false;
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_EMAIL_SEND_FAILURE,
+                    "missing subject in email item" ) );
         }
 
-        return true;
+        if ( StringUtil.isEmpty( emailItem.getBodyPlain() ) && StringUtil.isEmpty( emailItem.getBodyHtml() ) )
+        {
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_EMAIL_SEND_FAILURE,
+                    "missing body in email item" ) );
+        }
     }
 
     public void submitEmail(
@@ -306,7 +320,7 @@ public class EmailService implements PwmService
                 workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
             }
 
-            if ( workingItemBean.getTo() == null || workingItemBean.getTo().length() < 1 )
+            if ( StringUtil.isEmpty( workingItemBean.getTo() ) )
             {
                 LOGGER.error( () -> "no destination address available for email, skipping; email: " + emailItem.toDebugString() );
             }
@@ -335,6 +349,36 @@ public class EmailService implements PwmService
         }
     }
 
+    public static void sendEmailSynchronous(
+            final EmailServer emailServer,
+            final Configuration configuration,
+            final EmailItemBean emailItem,
+            final MacroMachine macroMachine
+    )
+            throws PwmOperationalException, PwmUnrecoverableException, MessagingException
+
+    {
+        validateEmailItem( emailItem );
+        EmailItemBean workingItemBean = emailItem;
+        if ( macroMachine != null )
+        {
+            workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
+        }
+        final Transport transport = EmailServerUtil.makeSmtpTransport( emailServer );
+        final List<Message> messages = EmailServerUtil.convertEmailItemToMessages(
+                workingItemBean,
+                configuration,
+                emailServer
+        );
+
+        for ( final Message message : messages )
+        {
+            message.saveChanges();
+            transport.sendMessage( message, message.getAllRecipients() );
+        }
+        transport.close();
+    }
+
     private final AtomicInteger newThreadLocalTransport = new AtomicInteger();
     private final AtomicInteger useExistingConnection = new AtomicInteger();
     private final AtomicInteger useExistingTransport = new AtomicInteger();
@@ -351,12 +395,37 @@ public class EmailService implements PwmService
     }
 
     private WorkQueueProcessor.ProcessResult sendItem( final EmailItemBean emailItemBean )
+    {
+        try
+        {
+            executeEmailSend( emailItemBean );
+            return WorkQueueProcessor.ProcessResult.SUCCESS;
+        }
+        catch ( final MessagingException | PwmException e )
+        {
+            if ( EmailServerUtil.examineSendFailure( e, retryableStatusResponses ) )
+            {
+                LOGGER.error( () -> "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", will retry" );
+                StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_FAILURES );
+                return WorkQueueProcessor.ProcessResult.RETRY;
+            }
+            else
+            {
+                LOGGER.error( () -> "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", permanent failure, discarding message" );
+                StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_DISCARDS );
+                return WorkQueueProcessor.ProcessResult.FAILED;
+            }
+        }
+    }
+
+    private void executeEmailSend( final EmailItemBean emailItemBean )
+            throws PwmUnrecoverableException, MessagingException
     {
         EmailConnection serverTransport = null;
 
-        // create a new MimeMessage object (using the Session created above)
         try
         {
+            // create a new MimeMessage object (using the Session created above)
             if ( threadLocalTransport.get() == null )
             {
 
@@ -401,11 +470,10 @@ public class EmailService implements PwmService
 
             LOGGER.debug( () -> "sent email: " + emailItemBean.toDebugString() );
             StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_SUCCESSES );
-            return WorkQueueProcessor.ProcessResult.SUCCESS;
+
         }
         catch ( final MessagingException | PwmException e )
         {
-
             final ErrorInformation errorInformation;
             if ( e instanceof PwmException )
             {
@@ -429,19 +497,7 @@ public class EmailService implements PwmService
                 serverErrors.put( serverTransport.getEmailServer(), errorInformation );
             }
             LOGGER.error( errorInformation );
-
-            if ( EmailServerUtil.examineSendFailure( e, retryableStatusResponses ) )
-            {
-                LOGGER.error( () -> "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", will retry" );
-                StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_FAILURES );
-                return WorkQueueProcessor.ProcessResult.RETRY;
-            }
-            else
-            {
-                LOGGER.error( () -> "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", permanent failure, discarding message" );
-                StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_DISCARDS );
-                return WorkQueueProcessor.ProcessResult.FAILED;
-            }
+            throw e;
         }
     }
 

+ 5 - 1
server/src/main/java/password/pwm/util/secure/PwmTrustManager.java

@@ -135,7 +135,11 @@ public class PwmTrustManager implements X509TrustManager
             {
                 try
                 {
-                    testCertificate.verify( rootCA.getPublicKey() );
+                    // first check certificate equality.  if certificate is same, we don't need to verify it signed itself
+                    if ( !testCertificate.equals( rootCA ) )
+                    {
+                        testCertificate.verify( rootCA.getPublicKey() );
+                    }
                     passed = true;
                 }
                 catch ( final NoSuchAlgorithmException | SignatureException | NoSuchProviderException | InvalidKeyException | CertificateException e )

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

@@ -133,6 +133,7 @@ Warning_InvalidFormat=The value does not have the correct format.
 Warning_UploadIE9=This feature is not available when using Internet Explorer 9 and earlier.  Please use a different browser or a newer version of Internet Explorer.
 Warning_ValueIncorrectFormat=The value does not have the correct format.
 Warning_SmsTestData=The test that will be performed will include resolving configured macros (if any).  The macros will be resolved using data of the logged in user, and thus may include sensitive data.  The message should be formatted as required by the SMS gateway service.
+Warning_EmailTestData=Email Test Data
 Warning_NoEndUserModules=End user functionality is not available while the configuration is open.
 Tooltip_ResetButton=Return this setting to its default value.
 Tooltip_HelpButton=Show description for this setting.

+ 35 - 0
webapp/src/main/webapp/public/resources/js/configeditor.js

@@ -780,6 +780,35 @@ PWM_CFGEDIT.smsHealthCheck = function() {
     });
 };
 
+PWM_CFGEDIT.emailHealthCheck = function() {
+    require(["dojo/dom-form"], function(domForm){
+        var dialogBody = '<p>' + PWM_CONFIG.showString('Warning_EmailTestData') + '</p><form id="emailCheckParametersForm"><table>';
+        dialogBody += '<tr><td>To</td><td><input name="to" type="text" value="test@example.com"/></td></tr>';
+        dialogBody += '<tr><td>From</td><td><input name="from" type="text" value="@DefaultEmailFromAddress@"/></td></tr>';
+        dialogBody += '<tr><td>Subject</td><td><input name="subject" type="text" value="Test Email"/></td></tr>';
+        dialogBody += '<tr><td>Body</td><td><input name="body" type="text" value="Test Email""/></td></tr>';
+        dialogBody += '</table></form>';
+        PWM_MAIN.showDialog({text:dialogBody,showCancel:true,title:'Test Email Connection',closeOnOk:false,okAction:function(){
+                var formElement = PWM_MAIN.getObject("emailCheckParametersForm");
+                var formData = domForm.toObject(formElement);
+                var url =  "editor?processAction=emailHealthCheck";
+                url = PWM_MAIN.addParamToUrl(url,'profile',PWM_CFGEDIT.readCurrentProfile());
+                PWM_MAIN.showWaitDialog({loadFunction:function(){
+                        var loadFunction = function(data) {
+                            if (data['error']) {
+                                PWM_MAIN.showErrorDialog(data);
+                            } else {
+                                var bodyText = PWM_ADMIN.makeHealthHtml(data['data'],false,false);
+                                var titleText = 'Email Send Message Status';
+                                PWM_MAIN.showDialog({text:bodyText,title:titleText,showCancel:true});
+                            }
+                        };
+                        PWM_MAIN.ajaxRequest(url,loadFunction,{content:formData});
+                    }});
+            }});
+    });
+};
+
 PWM_CFGEDIT.selectTemplate = function(newTemplate) {
     PWM_MAIN.showConfirmDialog({
         text: PWM_CONFIG.showString('Warning_ChangeTemplate'),
@@ -838,6 +867,10 @@ PWM_CFGEDIT.displaySettingsCategory = function(category) {
         htmlSettingBody += '<div style="width: 100%; text-align: center">'
             + '<button class="btn" id="button-test-SMS"><span class="btn-icon pwm-icon pwm-icon-bolt"></span>Test SMS Settings</button>'
             + '</div>';
+    } else if (category === 'EMAIL_SERVERS') {
+        htmlSettingBody += '<div style="width: 100%; text-align: center">'
+            + '<button class="btn" id="button-test-EMAIL"><span class="btn-icon pwm-icon pwm-icon-bolt"></span>Test Email Settings</button>'
+            + '</div>';
     }
 
     PWM_VAR['skippedSettingCount'] = 0;
@@ -866,6 +899,8 @@ PWM_CFGEDIT.displaySettingsCategory = function(category) {
         PWM_MAIN.addEventHandler('button-test-DATABASE_SETTINGS', 'click', function(){PWM_CFGEDIT.databaseHealthCheck();});
     } else if (category === 'SMS_GATEWAY') {
         PWM_MAIN.addEventHandler('button-test-SMS', 'click', function(){PWM_CFGEDIT.smsHealthCheck();});
+    } else if (category === 'EMAIL_SERVERS') {
+        PWM_MAIN.addEventHandler('button-test-EMAIL', 'click', function(){PWM_CFGEDIT.emailHealthCheck();});
     } else if (category === 'HTTPS_SERVER') {
         PWM_MAIN.addEventHandler('button-test-HTTPS_SERVER', 'click', function(){PWM_CFGEDIT.httpsCertificateView();});
     }