Переглянути джерело

update/refactoring for multiple email support

Jason Rivard 7 роки тому
батько
коміт
06056e6b0e

+ 3 - 3
server/src/main/java/password/pwm/PwmApplication.java

@@ -43,6 +43,7 @@ import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmServiceManager;
 import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.cluster.ClusterService;
+import password.pwm.svc.email.EmailService;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.AuditService;
@@ -75,7 +76,6 @@ import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.CrService;
 import password.pwm.util.operations.OtpService;
-import password.pwm.util.queue.EmailQueueManager;
 import password.pwm.util.queue.SmsQueueManager;
 import password.pwm.util.secure.HttpsServerCertificateManager;
 import password.pwm.util.secure.PwmRandom;
@@ -550,9 +550,9 @@ public class PwmApplication
         return ( ReportService ) pwmServiceManager.getService( ReportService.class );
     }
 
-    public EmailQueueManager getEmailQueue( )
+    public EmailService getEmailQueue( )
     {
-        return ( EmailQueueManager ) pwmServiceManager.getService( EmailQueueManager.class );
+        return ( EmailService ) pwmServiceManager.getService( EmailService.class );
     }
 
     public AuditService getAuditManager( )

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

@@ -23,12 +23,14 @@
 package password.pwm.bean;
 
 import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Getter;
 
 import java.io.Serializable;
 
 @Getter
 @AllArgsConstructor
+@Builder
 public class EmailItemBean implements Serializable
 {
     private final String to;

+ 0 - 27
server/src/main/java/password/pwm/bean/pub/EmailServerBean.java

@@ -1,27 +0,0 @@
-package password.pwm.bean.pub;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import password.pwm.util.PasswordData;
-
-@Getter
-@AllArgsConstructor
-public class EmailServerBean {
-    private final String serverAddress;
-    private final int port;
-    private final String defaultFrom;
-    private final String username;
-    private final PasswordData password;
-    private boolean tried;
-
-    public String toDebugString() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append("ServerAddress: ").append(serverAddress).append(", port: ").append(port).append(", Username: ").append(username);
-        return sb.toString();
-    }
-
-    public void setTried(final boolean newTried) {
-        tried = newTried;
-    }
-}
-

+ 20 - 6
server/src/main/java/password/pwm/config/Configuration.java

@@ -33,6 +33,7 @@ import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.TokenStorageMethod;
 import password.pwm.config.profile.ChallengeProfile;
 import password.pwm.config.profile.DeleteAccountProfile;
+import password.pwm.config.profile.EmailServerProfile;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.profile.LdapProfile;
@@ -1048,11 +1049,13 @@ public class Configuration implements SettingReader
     /* generic profile stuff */
 
 
-    public Map<String,NewUserProfile> getNewUserProfiles() {
-        final Map<String,NewUserProfile> returnMap = new LinkedHashMap<>();
-        final Map<String,Profile> profileMap = profileMap(ProfileType.NewUser);
-        for (final Map.Entry<String,Profile> entry : profileMap.entrySet()) {
-            returnMap.put(entry.getKey(), (NewUserProfile) entry.getValue());
+    public Map<String, NewUserProfile> getNewUserProfiles( )
+    {
+        final Map<String, NewUserProfile> returnMap = new LinkedHashMap<>();
+        final Map<String, Profile> profileMap = profileMap( ProfileType.NewUser );
+        for ( final Map.Entry<String, Profile> entry : profileMap.entrySet() )
+        {
+            returnMap.put( entry.getKey(), ( NewUserProfile ) entry.getValue() );
         }
         return returnMap;
     }
@@ -1068,6 +1071,17 @@ public class Configuration implements SettingReader
         return returnMap;
     }
 
+    public Map<String, EmailServerProfile> getEmailServerProfiles( )
+    {
+        final Map<String, EmailServerProfile> returnMap = new LinkedHashMap<>();
+        final Map<String, Profile> profileMap = profileMap( ProfileType.EmailServers );
+        for ( final Map.Entry<String, Profile> entry : profileMap.entrySet() )
+        {
+            returnMap.put( entry.getKey(), ( EmailServerProfile ) entry.getValue() );
+        }
+        return returnMap;
+    }
+
     public Map<String, SetupOtpProfile> getSetupOTPProfiles( )
     {
         final Map<String, SetupOtpProfile> returnMap = new LinkedHashMap<>();
@@ -1141,7 +1155,7 @@ public class Configuration implements SettingReader
                 break;
 
             case EmailServers:
-                newProfile = EmailServerProfile.makeFromStoredConfiguration(storedConfiguration, profileID);
+                newProfile = EmailServerProfile.makeFromStoredConfiguration( storedConfiguration, profileID );
                 break;
 
             case SetupOTPProfile:

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

@@ -280,22 +280,21 @@ public enum PwmSetting
             "ldap.wireTrace.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.LDAP_GLOBAL ),
 
     // New multiple email settings
-    EMAIL_PROFILES(
-            "email.profile.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.INTERNAL),
-    EMAIL_SERVER_ADDRESSES(
-            "email.smtp.addresses", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_PROFILES),
-    EMAIL_SERVER_PORTS(
-            "email.smtp.ports", PwmSettingSyntax.NUMERIC, PwmSettingCategory.EMAIL_PROFILES),
+    EMAIL_SERVERS(
+            "email.profile.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.INTERNAL ),
+    EMAIL_SERVER_ADDRESS(
+            "email.smtp.address", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SERVERS ),
+    EMAIL_SERVER_PORT(
+            "email.smtp.port", PwmSettingSyntax.NUMERIC, PwmSettingCategory.EMAIL_SERVERS ),
     EMAIL_USERNAMES(
-            "email.smtp.usernames", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_PROFILES),
+            "email.smtp.username", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SERVERS ),
     EMAIL_PASSWORDS(
-            "email.smtp.userpasswords", PwmSettingSyntax.PASSWORD, PwmSettingCategory.EMAIL_PROFILES),
+            "email.smtp.userpassword", PwmSettingSyntax.PASSWORD, PwmSettingCategory.EMAIL_SERVERS ),
 
-    // system wideemail settings
+    // system wide email settings
 
     EMAIL_DEFAULT_FROM_ADDRESS(
-            "email.default.fromAddress", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SETTINGS),
-
+            "email.default.fromAddress", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SETTINGS ),
     EMAIL_MAX_QUEUE_AGE(
             "email.queueMaxAge", PwmSettingSyntax.DURATION, PwmSettingCategory.EMAIL_SETTINGS ),
     EMAIL_ADVANCED_SETTINGS(
@@ -673,8 +672,9 @@ public enum PwmSetting
     AUDIT_SYSLOG_SERVERS(
             "audit.syslog.servers", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.AUDIT_FORWARD ),
     AUDIT_SYSLOG_CERTIFICATES(
-            "audit.syslog.certificates", PwmSettingSyntax.X509CERT, PwmSettingCategory.AUDIT_FORWARD),
-
+            "audit.syslog.certificates", PwmSettingSyntax.X509CERT, PwmSettingCategory.AUDIT_FORWARD ),
+    AUDIT_SYSLOG_OUTPUT_FORMAT(
+            "audit.syslog.outputFormat", PwmSettingSyntax.SELECT, PwmSettingCategory.AUDIT_FORWARD ),
 
     // challenge settings
     CHALLENGE_ENABLE(

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

@@ -89,10 +89,10 @@ public enum PwmSettingCategory
     UI_FEATURES( USER_INTERFACE ),
     UI_WEB( USER_INTERFACE ),
 
-    EMAIL                       (SETTINGS),
-    EMAIL_SETTINGS              (EMAIL),
-    EMAIL_TEMPLATES             (EMAIL),
-    EMAIL_PROFILES              (EMAIL),
+    EMAIL                       ( SETTINGS ),
+    EMAIL_SETTINGS              ( EMAIL ),
+    EMAIL_TEMPLATES             ( EMAIL ),
+    EMAIL_SERVERS               ( EMAIL ),
 
     SMS( SETTINGS ),
     SMS_GATEWAY( SMS ),

+ 15 - 90
server/src/main/java/password/pwm/config/profile/EmailServerProfile.java

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2017 The PWM Project
+ * Copyright (c) 2009-2018 The PWM Project
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -29,107 +29,32 @@ import password.pwm.config.stored.StoredConfiguration;
 import java.util.Locale;
 import java.util.Map;
 
-public class EmailServerProfile extends AbstractProfile {
+public class EmailServerProfile extends AbstractProfile
+{
 
     private static final ProfileType PROFILE_TYPE = ProfileType.EmailServers;
 
-//    private Instant newUserPasswordPolicyCacheTime;
-//    private final Map<Locale,PwmPasswordPolicy> newUserPasswordPolicyCache = new HashMap<>();
-
-    protected EmailServerProfile(final String identifier, final Map<PwmSetting, StoredValue> storedValueMap) {
-        super(identifier, storedValueMap);
+    protected EmailServerProfile( final String identifier, final Map<PwmSetting, StoredValue> storedValueMap )
+    {
+        super( identifier, storedValueMap );
     }
 
-    public static EmailServerProfile makeFromStoredConfiguration(final StoredConfiguration storedConfiguration, final String identifier) {
-        final Map<PwmSetting,StoredValue> valueMap = makeValueMap(storedConfiguration, identifier, PROFILE_TYPE.getCategory());
-        return new EmailServerProfile(identifier, valueMap);
-
+    public static EmailServerProfile makeFromStoredConfiguration( final StoredConfiguration storedConfiguration, final String identifier )
+    {
+        final Map<PwmSetting, StoredValue> valueMap = makeValueMap( storedConfiguration, identifier, PROFILE_TYPE.getCategory() );
+        return new EmailServerProfile( identifier, valueMap );
     }
 
     @Override
-    public ProfileType profileType() {
+    public ProfileType profileType( )
+    {
         return PROFILE_TYPE;
     }
 
     @Override
-    public String getDisplayName(final Locale locale) {
-        final String value = this.readSettingAsLocalizedString(PwmSetting.EMAIL_PROFILES, locale);
-        return value != null && !value.isEmpty() ? value : this.getIdentifier();
-    }
-/*
-    public PwmPasswordPolicy getNewUserPasswordPolicy(final PwmApplication pwmApplication, final Locale userLocale)
-            throws PwmUnrecoverableException
+    public String getDisplayName( final Locale locale )
     {
-        final Configuration config = pwmApplication.getConfig();
-        final long maxNewUserCacheMS = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.CONFIG_NEWUSER_PASSWORD_POLICY_CACHE_MS));
-        if (newUserPasswordPolicyCacheTime != null && TimeDuration.fromCurrent(newUserPasswordPolicyCacheTime).isLongerThan(maxNewUserCacheMS)) {
-            newUserPasswordPolicyCacheTime = Instant.now();
-            newUserPasswordPolicyCache.clear();
-        }
-
-        final PwmPasswordPolicy cachedPolicy = newUserPasswordPolicyCache.get(userLocale);
-        if (cachedPolicy != null) {
-            return cachedPolicy;
-        }
-        
-        final PwmPasswordPolicy thePolicy;
-        final LdapProfile defaultLdapProfile = config.getDefaultLdapProfile();
-        final String configuredNewUserPasswordDN = readSettingAsString(PwmSetting.NEWUSER_PASSWORD_POLICY_USER);
-        if (configuredNewUserPasswordDN == null || configuredNewUserPasswordDN.length() < 1) {
-            final String errorMsg = "the setting " + PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug(this.getIdentifier(),PwmConstants.DEFAULT_LOCALE) + " must have a value";
-            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,errorMsg));
-        } else {
-
-            final String lookupDN;
-            if ("TESTUSER".equalsIgnoreCase(configuredNewUserPasswordDN)) {
-                lookupDN = defaultLdapProfile.readSettingAsString(PwmSetting.LDAP_TEST_USER_DN);
-                if (lookupDN == null || lookupDN.isEmpty()) {
-                    final String errorMsg ="setting " 
-                            + PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug(defaultLdapProfile.getIdentifier(),PwmConstants.DEFAULT_LOCALE) 
-                            + " must be configured since setting "
-                            + PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug(this.getIdentifier(),PwmConstants.DEFAULT_LOCALE)
-                            + " is set to TESTUSER";
-                    throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,errorMsg));
-                }
-            } else {
-                lookupDN = configuredNewUserPasswordDN;
-            }
-
-            if (lookupDN.isEmpty()) {
-                throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,"user ldap dn in setting " + PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug(null,PwmConstants.DEFAULT_LOCALE) + " can not be resolved"));
-            } else {
-                try {
-                    final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(defaultLdapProfile.getIdentifier());
-                    final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser(lookupDN);
-                    final UserIdentity userIdentity = new UserIdentity(lookupDN, defaultLdapProfile.getIdentifier());
-                    thePolicy = PasswordUtility.readPasswordPolicyForUser(pwmApplication, null, userIdentity, chaiUser, userLocale);
-                } catch (ChaiUnavailableException e) {
-                    throw new PwmUnrecoverableException(PwmError.forChaiError(e.getErrorCode()));
-                }
-            }
-        }
-        newUserPasswordPolicyCache.put(userLocale,thePolicy);
-        return thePolicy;
-    }
-*/
-/*
-    public TimeDuration getTokenDurationEmail(final Configuration configuration) {
-        final long newUserDuration = readSettingAsLong(PwmSetting.NEWUSER_TOKEN_LIFETIME_EMAIL);
-        if (newUserDuration < 1) {
-            final long defaultDuration = configuration.readSettingAsLong(PwmSetting.TOKEN_LIFETIME);
-            return new TimeDuration(defaultDuration, TimeUnit.SECONDS);
-        }
-        return new TimeDuration(newUserDuration, TimeUnit.SECONDS);
-    }
-*/
-/*
-    public TimeDuration getTokenDurationSMS(final Configuration configuration) {
-        final long newUserDuration = readSettingAsLong(PwmSetting.NEWUSER_TOKEN_LIFETIME_SMS);
-        if (newUserDuration < 1) {
-            final long defaultDuration = configuration.readSettingAsLong(PwmSetting.TOKEN_LIFETIME);
-            return new TimeDuration(defaultDuration, TimeUnit.SECONDS);
-        }
-        return new TimeDuration(newUserDuration, TimeUnit.SECONDS);
+        final String value = this.readSettingAsLocalizedString( PwmSetting.EMAIL_SERVERS, locale );
+        return value != null && !value.isEmpty() ? value : this.getIdentifier();
     }
-*/
 }

+ 10 - 8
server/src/main/java/password/pwm/config/profile/ProfileType.java

@@ -25,14 +25,16 @@ package password.pwm.config.profile;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 
-public enum ProfileType {
-    Helpdesk            (true,  PwmSettingCategory.HELPDESK_PROFILE,    PwmSetting.HELPDESK_PROFILE_QUERY_MATCH),
-    ForgottenPassword   (false, PwmSettingCategory.RECOVERY_PROFILE,    PwmSetting.RECOVERY_PROFILE_QUERY_MATCH),
-    NewUser             (false, PwmSettingCategory.NEWUSER_PROFILE,     null),
-    UpdateAttributes    (true,  PwmSettingCategory.UPDATE_PROFILE,      PwmSetting.UPDATE_PROFILE_QUERY_MATCH),
-    DeleteAccount(true,  PwmSettingCategory.DELETE_ACCOUNT_PROFILE, PwmSetting.DELETE_ACCOUNT_PERMISSION),
-    EmailServers(true, PwmSettingCategory.EMAIL_PROFILES, null),
-    ;
+public enum ProfileType
+{
+    Helpdesk            ( true,  PwmSettingCategory.HELPDESK_PROFILE,    PwmSetting.HELPDESK_PROFILE_QUERY_MATCH ),
+    ForgottenPassword   ( false, PwmSettingCategory.RECOVERY_PROFILE,    PwmSetting.RECOVERY_PROFILE_QUERY_MATCH ),
+    NewUser             ( false, PwmSettingCategory.NEWUSER_PROFILE,     null ),
+    UpdateAttributes    ( true,  PwmSettingCategory.UPDATE_PROFILE,      PwmSetting.UPDATE_PROFILE_QUERY_MATCH ),
+    DeleteAccount       ( true,  PwmSettingCategory.DELETE_ACCOUNT_PROFILE, PwmSetting.DELETE_ACCOUNT_PERMISSION ),
+    SetupOTPProfile     ( true, PwmSettingCategory.OTP_PROFILE, PwmSetting.OTP_SETUP_USER_PERMISSION ),
+    EmailServers        ( true, PwmSettingCategory.EMAIL_SERVERS, null ),;
+
     
     private final boolean authenticated;
     private final PwmSettingCategory category;

+ 9 - 124
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -1091,9 +1091,14 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
         LOGGER.trace( pwmRequest, "all recovery checks passed, proceeding to configured recovery action" );
 
-        final RecoveryAction recoveryAction = ForgottenPasswordUtil.getRecoveryAction(config, forgottenPasswordBean);
-        if (recoveryAction == RecoveryAction.SENDNEWPW || recoveryAction == RecoveryAction.SENDNEWPW_AND_EXPIRE) {
-            processSendNewPassword(pwmRequest);
+        final RecoveryAction recoveryAction = ForgottenPasswordUtil.getRecoveryAction( config, forgottenPasswordBean );
+        if ( recoveryAction == RecoveryAction.SENDNEWPW || recoveryAction == RecoveryAction.SENDNEWPW_AND_EXPIRE )
+        {
+            if ( disallowAllButUnlock )
+            {
+                PasswordUtility.throwPasswordTooSoonException( userInfo, pwmRequest.getSessionLabel() );
+            }
+            ForgottenPasswordUtil.doActionSendNewPassword( pwmRequest );
             return;
         }
 
@@ -1216,128 +1221,8 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         }
     }
 
-    private static void processSendNewPassword(final PwmRequest pwmRequest)
-            throws ChaiUnavailableException, IOException, ServletException, PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
-        final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile(pwmRequest);
-        final RecoveryAction recoveryAction = ForgottenPasswordUtil.getRecoveryAction(pwmApplication.getConfig(), forgottenPasswordBean);
-
-        LOGGER.trace(pwmRequest,"beginning process to send new password to user");
-
-        if (!forgottenPasswordBean.getProgress().isAllPassed()) {
-            return;
-        }
-
-        final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
-        final ChaiUser theUser = pwmRequest.getPwmApplication().getProxiedChaiUser(userIdentity);
-
-        try { // try unlocking user
-            theUser.unlockPassword();
-            LOGGER.trace(pwmRequest, "unlock account succeeded");
-        } catch (ChaiOperationException e) {
-            final String errorMsg = "unable to unlock user " + theUser.getEntryDN() + " error: " + e.getMessage();
-            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNLOCK_FAILURE,errorMsg);
-            LOGGER.error(pwmRequest.getPwmSession(), errorInformation.toDebugStr());
-            pwmRequest.respondWithError(errorInformation);
-            return;
-        }
-
-        try {
-            pwmSession.getLoginInfoBean().setAuthenticated(true);
-            pwmSession.getLoginInfoBean().getAuthFlags().add(AuthenticationType.AUTH_FROM_PUBLIC_MODULE);
-            pwmSession.getLoginInfoBean().setUserIdentity(userIdentity);
-
-            LOGGER.info(pwmRequest, "user successfully supplied password recovery responses, emailing new password to: " + theUser.getEntryDN());
-
-            // add post change actions
-            addPostChangeAction(pwmRequest, userIdentity);
-
-            // create newpassword
-            final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword(pwmSession, pwmApplication);
-
-            // set the password
-            LOGGER.trace(pwmRequest.getPwmSession(), "setting user password to system generated random value");
-            PasswordUtility.setActorPassword(pwmSession, pwmApplication, newPassword);
-
-            if (recoveryAction == RecoveryAction.SENDNEWPW_AND_EXPIRE) {
-                LOGGER.debug(pwmSession, "marking user password as expired");
-                theUser.expirePassword();
-            }
-
-            // mark the event log
-            pwmApplication.getAuditManager().submit(AuditEvent.RECOVER_PASSWORD, pwmSession.getUserInfo(), pwmSession);
-
-            final MessageSendMethod messageSendMethod = forgottenPasswordProfile.readSettingAsEnum(PwmSetting.RECOVERY_SENDNEWPW_METHOD,MessageSendMethod.class);
-
-            // send email or SMS
-            final String toAddress = PasswordUtility.sendNewPassword(
-                    pwmSession.getUserInfo(),
-                    pwmApplication,
-                    pwmSession.getSessionManager().getMacroMachine(pwmApplication),
-                    newPassword,
-                    pwmSession.getSessionStateBean().getLocale(),
-                    messageSendMethod
-            );
-
-            pwmRequest.getPwmResponse().forwardToSuccessPage(Message.Success_PasswordSend, toAddress);
-        } catch (PwmException e) {
-            LOGGER.warn(pwmSession,"unexpected error setting new password during recovery process for user: " + e.getMessage());
-            pwmRequest.respondWithError(e.getErrorInformation());
-        } catch (ChaiOperationException e) {
-            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN,"unexpected ldap error while processing recovery action " + recoveryAction + ", error: " + e.getMessage());
-            LOGGER.warn(pwmSession,errorInformation.toDebugStr());
-            pwmRequest.respondWithError(errorInformation);
-        } finally {
-            clearForgottenPasswordBean(pwmRequest);
-            pwmSession.unauthenticateUser(pwmRequest);
-            pwmSession.getSessionStateBean().setPasswordModified(false);
-        }
-    }
-
-
-    private static List<FormConfiguration> figureAttributeForm(
-            final ForgottenPasswordProfile forgottenPasswordProfile,
-            final ForgottenPasswordBean forgottenPasswordBean,
-            final PwmRequest pwmRequest,
-            final UserIdentity userIdentity
-    )
-            throws ChaiUnavailableException, PwmOperationalException, PwmUnrecoverableException
-    {
-        final List<FormConfiguration> requiredAttributesForm = forgottenPasswordProfile.readSettingAsForm(PwmSetting.RECOVERY_ATTRIBUTE_FORM);
-        if (requiredAttributesForm.isEmpty()) {
-            return requiredAttributesForm;
-        }
-
-        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo(pwmRequest, forgottenPasswordBean);
-        final List<FormConfiguration> returnList = new ArrayList<>();
-        for (final FormConfiguration formItem : requiredAttributesForm) {
-            if (formItem.isRequired()) {
-                returnList.add(formItem);
-            } else {
-                try {
-                    final String currentValue = userInfo.readStringAttribute(formItem.getName());
-                    if (currentValue != null && currentValue.length() > 0) {
-                        returnList.add(formItem);
-                    } else {
-                        LOGGER.trace(pwmRequest, "excluding optional required attribute(" + formItem.getName() + "), user has no value");
-                    }
-                } catch (PwmUnrecoverableException e) {
-                    throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_NO_CHALLENGES, "unexpected error reading value for attribute " + formItem.getName()));
-                }
-            }
-        }
-
-        if (returnList.isEmpty()) {
-            throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_NO_CHALLENGES, "user has no values for any optional attribute"));
-        }
-
-        return returnList;
-    }
 
-    private static void addPostChangeAction(
+    static void addPostChangeAction(
             final PwmRequest pwmRequest,
             final UserIdentity userIdentity
     )

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

@@ -22,6 +22,7 @@
 
 package password.pwm.svc;
 
+import password.pwm.svc.email.EmailService;
 import password.pwm.util.java.JavaHelper;
 
 import java.util.ArrayList;
@@ -39,7 +40,7 @@ public enum PwmServiceEnum
     StatisticsManager( password.pwm.svc.stats.StatisticsManager.class, Flag.StartDuringRuntimeInstance ),
     WordlistManager( password.pwm.svc.wordlist.WordlistManager.class ),
     SeedlistManager( password.pwm.svc.wordlist.SeedlistManager.class ),
-    EmailQueueManager( password.pwm.util.queue.EmailQueueManager.class ),
+    EmailQueueManager( EmailService.class ),
     SmsQueueManager( password.pwm.util.queue.SmsQueueManager.class ),
     UrlShortenerService( password.pwm.svc.shorturl.UrlShortenerService.class ),
     TokenService( password.pwm.svc.token.TokenService.class, Flag.StartDuringRuntimeInstance ),

+ 34 - 0
server/src/main/java/password/pwm/svc/email/EmailConnection.java

@@ -0,0 +1,34 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.email;
+
+import lombok.Value;
+
+import javax.mail.Transport;
+
+@Value
+class EmailConnection
+{
+    private final EmailServer emailServer;
+    private final Transport transport;
+}

+ 58 - 0
server/src/main/java/password/pwm/svc/email/EmailServer.java

@@ -0,0 +1,58 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.email;
+
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.util.PasswordData;
+import password.pwm.util.java.StringUtil;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+
+@Value
+@Builder
+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;
+
+    public String toDebugString()
+    {
+        final Map<String, String> debugProps = new LinkedHashMap<>(  );
+        debugProps.put( "id", id );
+        debugProps.put( "host", host );
+        debugProps.put( "port", String.valueOf( port ) );
+        if ( !StringUtil.isEmpty( username ) )
+        {
+            debugProps.put( "username", username );
+        }
+        return StringUtil.mapToString( debugProps );
+    }
+}

+ 289 - 0
server/src/main/java/password/pwm/svc/email/EmailServerUtil.java

@@ -0,0 +1,289 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+package password.pwm.svc.email;
+
+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.profile.EmailServerProfile;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpContentType;
+import password.pwm.util.PasswordData;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Transport;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+public class EmailServerUtil
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( EmailServerUtil.class );
+
+    static List<EmailServer> makeEmailServersMap( final Configuration configuration )
+    {
+        final List<EmailServer> returnObj = new ArrayList<>(  );
+
+        final Collection<EmailServerProfile> profiles = configuration.getEmailServerProfiles().values();
+
+        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_USERNAMES );
+            final PasswordData password = profile.readSettingAsPassword( PwmSetting.EMAIL_PASSWORDS );
+            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 );
+            }
+        }
+
+        return returnObj;
+    }
+
+    private static Properties makeJavaMailProps(
+            final Configuration config,
+            final String host,
+            final int port
+    )
+    {
+        //Create a properties item to start setting up the mail
+        final Properties props = new Properties();
+
+        //Specify the desired SMTP server
+        props.put( "mail.smtp.host", host );
+
+        //Specify SMTP server port
+        props.put( "mail.smtp.port", port );
+
+        //Specify configured advanced settings.
+        final Map<String, String> advancedSettingValues = StringUtil.convertStringListToNameValuePair( config.readSettingAsStringArray( PwmSetting.EMAIL_ADVANCED_SETTINGS ), "=" );
+        props.putAll( advancedSettingValues );
+
+        return props;
+    }
+
+    private static InternetAddress makeInternetAddress( final String input )
+            throws AddressException
+    {
+        if ( input == null )
+        {
+            return null;
+        }
+
+        if ( input.matches( "^.*<.*>$" ) )
+        {
+            // check for format like: John Doe <jdoe@example.com>
+            final String[] splitString = input.split( "<|>" );
+            if ( splitString.length < 2 )
+            {
+                return new InternetAddress( input );
+            }
+
+            final InternetAddress address = new InternetAddress();
+            address.setAddress( splitString[ 1 ].trim() );
+            try
+            {
+                address.setPersonal( splitString[ 0 ].trim(), PwmConstants.DEFAULT_CHARSET.toString() );
+            }
+            catch ( UnsupportedEncodingException e )
+            {
+                LOGGER.error( "unsupported encoding error while parsing internet address '" + input + "', error: " + e.getMessage() );
+            }
+            return address;
+        }
+        return new InternetAddress( input );
+    }
+
+    static EmailItemBean applyMacrosToEmail( final EmailItemBean emailItem, final MacroMachine macroMachine )
+    {
+        final EmailItemBean expandedEmailItem;
+        expandedEmailItem = new EmailItemBean(
+                macroMachine.expandMacros( emailItem.getTo() ),
+                macroMachine.expandMacros( emailItem.getFrom() ),
+                macroMachine.expandMacros( emailItem.getSubject() ),
+                macroMachine.expandMacros( emailItem.getBodyPlain() ),
+                macroMachine.expandMacros( emailItem.getBodyHtml() )
+        );
+        return expandedEmailItem;
+    }
+
+    static EmailItemBean newEmailToAddress( final EmailItemBean emailItem, final String toAddress )
+    {
+        final EmailItemBean expandedEmailItem;
+        expandedEmailItem = new EmailItemBean(
+                toAddress,
+                emailItem.getFrom(),
+                emailItem.getSubject(),
+                emailItem.getBodyPlain(),
+                emailItem.getBodyHtml()
+        );
+        return expandedEmailItem;
+    }
+
+    static boolean sendIsRetryable( final Exception e )
+    {
+        if ( e != null )
+        {
+            final Throwable cause = e.getCause();
+            if ( cause instanceof IOException )
+            {
+                LOGGER.trace( "message send failure cause is due to an IOException: " + e.getMessage() );
+                return true;
+            }
+            if ( e instanceof PwmUnrecoverableException )
+            {
+                if ( ( ( PwmUnrecoverableException ) e ).getError() == PwmError.ERROR_SERVICE_UNREACHABLE )
+                {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public static List<Message> convertEmailItemToMessages(
+            final EmailItemBean emailItemBean,
+            final Configuration config,
+            final EmailServer emailServer
+    )
+            throws MessagingException
+    {
+        final List<Message> messages = new ArrayList<>();
+        final boolean hasPlainText = emailItemBean.getBodyPlain() != null && emailItemBean.getBodyPlain().length() > 0;
+        final boolean hasHtml = emailItemBean.getBodyHtml() != null && emailItemBean.getBodyHtml().length() > 0;
+        final String subjectEncodingCharset = config.readAppProperty( AppProperty.SMTP_SUBJECT_ENCODING_CHARSET );
+
+        // create a new Session object for the messagejavamail
+        final String emailTo = emailItemBean.getTo();
+        if ( emailTo != null )
+        {
+            final InternetAddress[] recipients = InternetAddress.parse( emailTo );
+            for ( final InternetAddress recipient : recipients )
+            {
+                final MimeMessage message = new MimeMessage( emailServer.getSession() );
+                message.setFrom( makeInternetAddress( emailItemBean.getFrom() ) );
+                message.setRecipient( Message.RecipientType.TO, recipient );
+                {
+                    if ( subjectEncodingCharset != null && !subjectEncodingCharset.isEmpty() )
+                    {
+                        message.setSubject( emailItemBean.getSubject(), subjectEncodingCharset );
+                    }
+                    else
+                    {
+                        message.setSubject( emailItemBean.getSubject() );
+                    }
+                }
+                message.setSentDate( new Date() );
+
+                if ( hasPlainText && hasHtml )
+                {
+                    final MimeMultipart content = new MimeMultipart( "alternative" );
+                    final MimeBodyPart text = new MimeBodyPart();
+                    final MimeBodyPart html = new MimeBodyPart();
+                    text.setContent( emailItemBean.getBodyPlain(), HttpContentType.plain.getHeaderValue() );
+                    html.setContent( emailItemBean.getBodyHtml(), HttpContentType.html.getHeaderValue() );
+                    content.addBodyPart( text );
+                    content.addBodyPart( html );
+                    message.setContent( content );
+                }
+                else if ( hasPlainText )
+                {
+                    message.setContent( emailItemBean.getBodyPlain(), HttpContentType.plain.getHeaderValue() );
+                }
+                else if ( hasHtml )
+                {
+                    message.setContent( emailItemBean.getBodyHtml(), HttpContentType.html.getHeaderValue() );
+                }
+
+                messages.add( message );
+            }
+        }
+
+        return messages;
+    }
+
+    static Transport makeSmtpTransport( final EmailServer server )
+            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;
+
+        if ( authenticated )
+        {
+            // create a new Session object for the message
+            transport.connect(
+                    server.getHost(),
+                    server.getPort(),
+                    server.getUsername(),
+                    server.getPassword().getStringValue()
+            );
+        }
+        else
+        {
+            transport.connect();
+        }
+
+        LOGGER.debug( "connected to " + server.toDebugString() + " " + ( authenticated ? "(authenticated)" : "(unauthenticated)" ) );
+
+        return transport;
+    }
+}

+ 461 - 0
server/src/main/java/password/pwm/svc/email/EmailService.java

@@ -0,0 +1,461 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.email;
+
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmApplicationMode;
+import password.pwm.bean.EmailItemBean;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.DataStorageMethod;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmException;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.health.HealthMessage;
+import password.pwm.health.HealthRecord;
+import password.pwm.ldap.UserInfo;
+import password.pwm.svc.PwmService;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.java.AtomicLoopIntIncrementer;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.localdb.LocalDB;
+import password.pwm.util.localdb.LocalDBStoredQueue;
+import password.pwm.util.localdb.WorkQueueProcessor;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Transport;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author Jason D. Rivard
+ */
+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 List<EmailServer> servers = new ArrayList<>( );
+    private WorkQueueProcessor<EmailItemBean> workQueueProcessor;
+    private AtomicLoopIntIncrementer serverIncrementer;
+
+    private PwmService.STATUS status = STATUS.NEW;
+
+    private final ThreadLocal<EmailConnection> threadLocalTransport = new ThreadLocal<>();
+
+    public void init( final PwmApplication pwmApplication )
+            throws PwmException
+    {
+        status = STATUS.OPENING;
+        this.pwmApplication = pwmApplication;
+
+        servers.addAll( EmailServerUtil.makeEmailServersMap( pwmApplication.getConfig() ) );
+
+        for ( final EmailServer emailServer : servers )
+        {
+            serverErrors.put( emailServer, Optional.empty() );
+        }
+
+        serverIncrementer = new AtomicLoopIntIncrementer( servers.size() - 1 );
+
+        if ( pwmApplication.getLocalDB() == null || pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN )
+        {
+            LOGGER.warn( "localdb is not open, EmailService will remain closed" );
+            status = STATUS.CLOSED;
+            return;
+        }
+
+        final WorkQueueProcessor.Settings settings = WorkQueueProcessor.Settings.builder()
+                .maxEvents( Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.QUEUE_EMAIL_MAX_COUNT ) ) )
+                .retryDiscardAge( new TimeDuration( pwmApplication.getConfig().readSettingAsLong( PwmSetting.EMAIL_MAX_QUEUE_AGE ), TimeUnit.SECONDS ) )
+                .retryInterval( new TimeDuration( Long.parseLong( pwmApplication.getConfig().readAppProperty( AppProperty.QUEUE_EMAIL_RETRY_TIMEOUT_MS ) ) ) )
+                .preThreads( Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.QUEUE_EMAIL_MAX_THREADS ) ) )
+                .build();
+        final LocalDBStoredQueue localDBStoredQueue = LocalDBStoredQueue.createLocalDBStoredQueue( pwmApplication, pwmApplication.getLocalDB(), LocalDB.DB.EMAIL_QUEUE );
+
+        workQueueProcessor = new WorkQueueProcessor<>( pwmApplication, localDBStoredQueue, settings, new EmailItemProcessor(), this.getClass() );
+        status = STATUS.OPEN;
+    }
+
+    public void close( )
+    {
+        status = STATUS.CLOSED;
+        if ( workQueueProcessor != null )
+        {
+            workQueueProcessor.close();
+        }
+    }
+
+    @Override
+    public STATUS status( )
+    {
+        return status;
+    }
+
+    public List<HealthRecord> healthCheck( )
+    {
+        if ( pwmApplication.getLocalDB() == null || pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN )
+        {
+            return Collections.singletonList( HealthRecord.forMessage( HealthMessage.ServiceClosed_LocalDBUnavail, this.getClass().getSimpleName() ) );
+        }
+
+        if ( pwmApplication.getApplicationMode() == PwmApplicationMode.READ_ONLY )
+        {
+            return Collections.singletonList( HealthRecord.forMessage( HealthMessage.ServiceClosed_AppReadOnly, this.getClass().getSimpleName() ) );
+        }
+
+        final List<HealthRecord> records = new ArrayList<>( );
+        for ( final Map.Entry<EmailServer, Optional<ErrorInformation>> entry : serverErrors.entrySet() )
+        {
+            if ( entry.getValue().isPresent() )
+            {
+                final ErrorInformation errorInformation = entry.getValue().get();
+                records.add( HealthRecord.forMessage( HealthMessage.Email_SendFailure, errorInformation.toDebugStr() ) );
+            }
+        }
+
+        return records;
+    }
+
+    @Override
+    public ServiceInfoBean serviceInfo( )
+    {
+        final Map<String, String> debugItems = new LinkedHashMap<>();
+        if ( workQueueProcessor != null )
+        {
+            debugItems.putAll( workQueueProcessor.debugInfo() );
+        }
+        if ( status() == STATUS.OPEN )
+        {
+            return new ServiceInfoBean( Collections.singletonList( DataStorageMethod.LOCALDB ), debugItems );
+        }
+        else
+        {
+            return new ServiceInfoBean( Collections.emptyList(), debugItems );
+        }
+    }
+
+    public int queueSize( )
+    {
+        return workQueueProcessor == null
+                ? 0
+                : workQueueProcessor.queueSize();
+    }
+
+    public Instant eldestItem( )
+    {
+        return workQueueProcessor == null
+                ? null
+                : workQueueProcessor.eldestItem();
+    }
+
+    private class EmailItemProcessor implements WorkQueueProcessor.ItemProcessor<EmailItemBean>
+    {
+        @Override
+        public WorkQueueProcessor.ProcessResult process( final EmailItemBean workItem )
+        {
+            return sendItem( workItem );
+        }
+
+        public String convertToDebugString( final EmailItemBean emailItemBean )
+        {
+            return emailItemBean.toDebugString();
+        }
+    }
+
+    private boolean determineIfItemCanBeDelivered( final EmailItemBean emailItem )
+    {
+
+        if ( servers.isEmpty() )
+        {
+            LOGGER.debug( "discarding email send event (no SMTP server address configured) " + emailItem.toDebugString() );
+            return false;
+        }
+
+        if ( emailItem.getFrom() == null || emailItem.getFrom().length() < 1 )
+        {
+            LOGGER.error( "discarding email event (no from address): " + emailItem.toDebugString() );
+            return false;
+        }
+
+        if ( emailItem.getTo() == null || emailItem.getTo().length() < 1 )
+        {
+            LOGGER.error( "discarding email event (no to address): " + emailItem.toDebugString() );
+            return false;
+        }
+
+        if ( emailItem.getSubject() == null || emailItem.getSubject().length() < 1 )
+        {
+            LOGGER.error( "discarding email event (no subject): " + emailItem.toDebugString() );
+            return false;
+        }
+
+        if ( ( emailItem.getBodyPlain() == null || emailItem.getBodyPlain().length() < 1 ) && ( emailItem.getBodyHtml() == null || emailItem.getBodyHtml().length() < 1 ) )
+        {
+            LOGGER.error( "discarding email event (no body): " + emailItem.toDebugString() );
+            return false;
+        }
+
+        return true;
+    }
+
+    public void submitEmail(
+            final EmailItemBean emailItem,
+            final UserInfo userInfo,
+            final MacroMachine macroMachine
+    )
+            throws PwmUnrecoverableException
+    {
+        submitEmailImpl( emailItem, userInfo, macroMachine, false );
+    }
+
+    public void submitEmailImmediate(
+            final EmailItemBean emailItem,
+            final UserInfo userInfo,
+            final MacroMachine macroMachine
+    )
+            throws PwmUnrecoverableException
+    {
+        submitEmailImpl( emailItem, userInfo, macroMachine, true );
+    }
+
+    private void submitEmailImpl(
+            final EmailItemBean emailItem,
+            final UserInfo userInfo,
+            final MacroMachine macroMachine,
+            final boolean immediate
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( emailItem == null )
+        {
+            return;
+        }
+
+        final EmailItemBean finalBean;
+        {
+            EmailItemBean workingItemBean = emailItem;
+            if ( ( emailItem.getTo() == null || emailItem.getTo().isEmpty() ) && userInfo != null )
+            {
+                final String toAddress = userInfo.getUserEmailAddress();
+                workingItemBean = EmailServerUtil.newEmailToAddress( workingItemBean, toAddress );
+            }
+
+            if ( macroMachine != null )
+            {
+                workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
+            }
+
+            if ( workingItemBean.getTo() == null || workingItemBean.getTo().length() < 1 )
+            {
+                LOGGER.error( "no destination address available for email, skipping; email: " + emailItem.toDebugString() );
+            }
+
+            if ( !determineIfItemCanBeDelivered( emailItem ) )
+            {
+                return;
+            }
+            finalBean = workingItemBean;
+        }
+
+        try
+        {
+            if ( immediate )
+            {
+                workQueueProcessor.submitImmediate( finalBean );
+            }
+            else
+            {
+                workQueueProcessor.submit( finalBean );
+            }
+        }
+        catch ( PwmOperationalException e )
+        {
+            LOGGER.warn( "unable to add email to queue: " + e.getMessage() );
+        }
+    }
+
+    private final AtomicInteger newThreadLocalTransport = new AtomicInteger();
+    private final AtomicInteger useExistingConnection = new AtomicInteger();
+    private final AtomicInteger useExistingTransport = new AtomicInteger();
+    private final AtomicInteger newConnectionCounter = new AtomicInteger();
+
+    private String stats( )
+    {
+        final Map<String, Integer> map = new HashMap<>();
+        map.put( "newThreadLocalTransport", newThreadLocalTransport.get() );
+        map.put( "useExistingConnection", newThreadLocalTransport.get() );
+        map.put( "useExistingTransport", useExistingTransport.get() );
+        map.put( "newConnectionCounter", newConnectionCounter.get() );
+        return StringUtil.mapToString( map );
+    }
+
+    private WorkQueueProcessor.ProcessResult sendItem( final EmailItemBean emailItemBean )
+    {
+        EmailConnection serverTransport = null;
+
+        // create a new MimeMessage object (using the Session created above)
+        try
+        {
+            if ( threadLocalTransport.get() == null )
+            {
+
+                LOGGER.trace( "initializing new threadLocal transport, stats: " + stats() );
+                threadLocalTransport.set( getSmtpTransport( ) );
+                newThreadLocalTransport.getAndIncrement();
+            }
+            else
+            {
+                LOGGER.trace( "using existing threadLocal transport, stats: " + stats() );
+                useExistingTransport.getAndIncrement();
+            }
+
+            serverTransport = threadLocalTransport.get();
+
+            if ( !serverTransport.getTransport().isConnected() )
+            {
+                LOGGER.trace( "connecting threadLocal transport, stats: " + stats() );
+                threadLocalTransport.set( getSmtpTransport( ) );
+                serverTransport = threadLocalTransport.get();
+                newConnectionCounter.getAndIncrement();
+            }
+            else
+            {
+                LOGGER.trace( "using existing threadLocal: stats: " + stats() );
+                useExistingConnection.getAndIncrement();
+            }
+
+            final List<Message> messages = EmailServerUtil.convertEmailItemToMessages(
+                    emailItemBean,
+                    this.pwmApplication.getConfig(),
+                    serverTransport.getEmailServer()
+            );
+
+            for ( final Message message : messages )
+            {
+                message.saveChanges();
+                serverTransport.getTransport().sendMessage( message, message.getAllRecipients() );
+            }
+
+            serverErrors.put( serverTransport.getEmailServer(), Optional.empty() );
+
+            LOGGER.debug( "sent email: " + emailItemBean.toDebugString() );
+            StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_SUCCESSES );
+            return WorkQueueProcessor.ProcessResult.SUCCESS;
+        }
+        catch ( MessagingException | PwmException e )
+        {
+
+            final ErrorInformation errorInformation;
+            if ( e instanceof PwmException )
+            {
+                errorInformation = ( ( PwmException ) e ).getErrorInformation();
+            }
+            else
+            {
+                final String errorMsg = "error sending email: " + e.getMessage();
+                errorInformation = new ErrorInformation(
+                        PwmError.ERROR_EMAIL_SEND_FAILURE,
+                        errorMsg,
+                        new String[] {
+                                emailItemBean.toDebugString(),
+                                JavaHelper.readHostileExceptionMessage( e ),
+                        }
+                );
+            }
+
+            if ( serverTransport != null )
+            {
+                serverErrors.put( serverTransport.getEmailServer(), Optional.of( errorInformation ) );
+            }
+            LOGGER.error( errorInformation );
+
+            if ( EmailServerUtil.sendIsRetryable( e ) )
+            {
+                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 EmailConnection getSmtpTransport( )
+            throws PwmUnrecoverableException
+    {
+
+        // the global server incrementer rotates the server list by 1 offset each attempt to get an smtp transport.
+        int nextSlot = serverIncrementer.next();
+
+        for ( int i = 0; i < servers.size(); i++ )
+        {
+            nextSlot = nextSlot >= ( servers.size() - 1 )
+                    ? 0
+                    : nextSlot + 1;
+
+            final EmailServer server = servers.get( nextSlot );
+            try
+            {
+                final Transport transport = EmailServerUtil.makeSmtpTransport( server );
+
+                serverErrors.put( server, Optional.empty() );
+                return new EmailConnection( server, transport );
+            }
+            catch ( MessagingException e )
+            {
+                final String msg = "unable to connect to email server '" + server.toDebugString() + "', error: " + e.getMessage();
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_UNREACHABLE, msg );
+                serverErrors.put( server, Optional.of( errorInformation ) );
+                LOGGER.warn( errorInformation.toDebugStr() );
+            }
+        }
+
+        throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_UNREACHABLE, "unable to reach any configured email server" );
+    }
+
+}
+

+ 0 - 666
server/src/main/java/password/pwm/util/queue/EmailQueueManager.java

@@ -1,666 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package password.pwm.util.queue;
-
-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.pub.EmailServerBean;
-import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.StoredValue;
-import password.pwm.config.option.DataStorageMethod;
-import password.pwm.config.stored.StoredConfiguration;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmException;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.health.HealthMessage;
-import password.pwm.health.HealthRecord;
-import password.pwm.http.HttpContentType;
-import password.pwm.ldap.UserInfo;
-import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.Statistic;
-import password.pwm.svc.stats.StatisticsManager;
-import password.pwm.util.PasswordData;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.TimeDuration;
-import password.pwm.util.localdb.LocalDB;
-import password.pwm.util.localdb.LocalDBStoredQueue;
-import password.pwm.util.localdb.WorkQueueProcessor;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
-
-import javax.mail.Message;
-import javax.mail.MessagingException;
-import javax.mail.Transport;
-import javax.mail.internet.AddressException;
-import javax.mail.internet.InternetAddress;
-import javax.mail.internet.MimeBodyPart;
-import javax.mail.internet.MimeMessage;
-import javax.mail.internet.MimeMultipart;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * @author Jason D. Rivard
- */
-public class EmailQueueManager implements PwmService
-{
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( EmailQueueManager.class );
-
-    private PwmApplication pwmApplication;
-    private List<EmailServerBean> emailServers = new ArrayList<>();
-    private static List<EmailServerBean> staticEmailServers = new ArrayList<>();
-    private Properties javaMailProps = new Properties();
-    private WorkQueueProcessor<EmailItemBean> workQueueProcessor;
-
-    private PwmService.STATUS status = STATUS.NEW;
-    private ErrorInformation lastError;
-
-    private final ThreadLocal<Transport> threadLocalTransport = new ThreadLocal<>();
-
-    public void init( final PwmApplication pwmApplication )
-            throws PwmException
-    {
-        status = STATUS.OPENING;
-        this.pwmApplication = pwmApplication;
-        javaMailProps = makeJavaMailProps( pwmApplication.getConfig() );
-        String hostAddress = "";
-        String emailFromAddress = "";
-        String emailUsername = "";
-        PasswordData emailPassword = null;
-        int hostPort = 0;
-        boolean allAvailable = true;
-
-        final List<String> profiles = pwmApplication.getConfig().readSettingAsStringArray(PwmSetting.EMAIL_PROFILES);
-
-        try {
-            final StoredConfiguration storedConfiguration = pwmApplication.getConfig().getStoredConfiguration();
-
-            for (int i = 0; i < profiles.size(); i++) {
-                final String profileName = profiles.get(i);
-                final StoredValue address = storedConfiguration.readSetting(PwmSetting.EMAIL_SERVER_ADDRESSES, profileName);
-                if (null != address) {
-                    hostAddress = address.toString();
-                } else {
-                    allAvailable = false;
-                }
-                final StoredValue port = storedConfiguration.readSetting(PwmSetting.EMAIL_SERVER_PORTS, profileName);
-                if (null != port) {
-                    hostPort = Integer.parseInt(port.toString());
-                } else {
-                    allAvailable = false;
-                }
-                final StoredValue fromAddress = storedConfiguration.readSetting(PwmSetting.EMAIL_DEFAULT_FROM_ADDRESS);
-                if (null != fromAddress) {
-                    emailFromAddress = fromAddress.toString();
-                } else {
-                    allAvailable = false;
-                }
-                final StoredValue username = storedConfiguration.readSetting(PwmSetting.EMAIL_USERNAMES, profileName);
-                if (null != username) {
-                    emailUsername = username.toString();
-                } else {
-                    allAvailable = false;
-                }
-                final StoredValue mailPassword = storedConfiguration.readSetting(PwmSetting.EMAIL_PASSWORDS, profileName);
-                if (null != mailPassword) {
-                    emailPassword = Configuration.JavaTypeConverter.valueToPassword(mailPassword);
-                } else {
-                    allAvailable = false;
-                }
-                if (allAvailable) {
-                    final EmailServerBean emailServerBean = new EmailServerBean(hostAddress, hostPort, emailFromAddress, emailUsername, emailPassword, false);
-                    emailServers.add(emailServerBean);
-                    staticEmailServers.add(emailServerBean);
-                }
-            }
-        } catch (PwmUnrecoverableException ure) {
-            return;
-        }
-
-        if ( pwmApplication.getLocalDB() == null || pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN )
-        {
-            LOGGER.warn( "localdb is not open, EmailQueueManager will remain closed" );
-            status = STATUS.CLOSED;
-            return;
-        }
-
-        final WorkQueueProcessor.Settings settings = WorkQueueProcessor.Settings.builder()
-                .maxEvents( Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.QUEUE_EMAIL_MAX_COUNT ) ) )
-                .retryDiscardAge( new TimeDuration( pwmApplication.getConfig().readSettingAsLong( PwmSetting.EMAIL_MAX_QUEUE_AGE ), TimeUnit.SECONDS ) )
-                .retryInterval( new TimeDuration( Long.parseLong( pwmApplication.getConfig().readAppProperty( AppProperty.QUEUE_EMAIL_RETRY_TIMEOUT_MS ) ) ) )
-                .preThreads( Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.QUEUE_EMAIL_MAX_THREADS ) ) )
-                .build();
-        final LocalDBStoredQueue localDBStoredQueue = LocalDBStoredQueue.createLocalDBStoredQueue( pwmApplication, pwmApplication.getLocalDB(), LocalDB.DB.EMAIL_QUEUE );
-
-        workQueueProcessor = new WorkQueueProcessor<>( pwmApplication, localDBStoredQueue, settings, new EmailItemProcessor(), this.getClass() );
-        status = STATUS.OPEN;
-    }
-
-    public void close( )
-    {
-        status = STATUS.CLOSED;
-        if ( workQueueProcessor != null )
-        {
-            workQueueProcessor.close();
-        }
-    }
-
-    @Override
-    public STATUS status( )
-    {
-        return status;
-    }
-
-    public List<HealthRecord> healthCheck( )
-    {
-        if ( pwmApplication.getLocalDB() == null || pwmApplication.getLocalDB().status() != LocalDB.Status.OPEN )
-        {
-            return Collections.singletonList( HealthRecord.forMessage( HealthMessage.ServiceClosed_LocalDBUnavail, this.getClass().getSimpleName() ) );
-        }
-
-        if ( pwmApplication.getApplicationMode() == PwmApplicationMode.READ_ONLY )
-        {
-            return Collections.singletonList( HealthRecord.forMessage( HealthMessage.ServiceClosed_AppReadOnly, this.getClass().getSimpleName() ) );
-        }
-
-        if ( lastError != null )
-        {
-            return Collections.singletonList( HealthRecord.forMessage( HealthMessage.Email_SendFailure, lastError.toDebugStr() ) );
-        }
-
-        return Collections.emptyList();
-    }
-
-    @Override
-    public ServiceInfoBean serviceInfo( )
-    {
-        final Map<String, String> debugItems = new LinkedHashMap<>();
-        if ( workQueueProcessor != null )
-        {
-            debugItems.putAll( workQueueProcessor.debugInfo() );
-        }
-        if ( status() == STATUS.OPEN )
-        {
-            return new ServiceInfoBean( Collections.singletonList( DataStorageMethod.LOCALDB ), debugItems );
-        }
-        else
-        {
-            return new ServiceInfoBean( Collections.emptyList(), debugItems );
-        }
-    }
-
-    public int queueSize( )
-    {
-        return workQueueProcessor == null
-                ? 0
-                : workQueueProcessor.queueSize();
-    }
-
-    public Instant eldestItem( )
-    {
-        return workQueueProcessor == null
-                ? null
-                : workQueueProcessor.eldestItem();
-    }
-
-    private class EmailItemProcessor implements WorkQueueProcessor.ItemProcessor<EmailItemBean>
-    {
-        @Override
-        public WorkQueueProcessor.ProcessResult process( final EmailItemBean workItem )
-        {
-            return sendItem( workItem );
-        }
-
-        public String convertToDebugString( final EmailItemBean emailItemBean )
-        {
-            return emailItemBean.toDebugString();
-        }
-    }
-
-    private boolean determineIfItemCanBeDelivered(final EmailItemBean emailItem) {
-
-        String serverAddress = "";
-
-        for (int i = 0; i < emailServers.size(); i++) {
-            serverAddress = emailServers.get(i).getServerAddress();if (serverAddress == null || serverAddress.length() < 1)
-        {    LOGGER.debug("discarding email send event (no SMTP server address configured) " + emailItem.toDebugString());
-            return false;} else {
-                break;
-            }
-        }
-
-        if ( emailItem.getFrom() == null || emailItem.getFrom().length() < 1 )
-        {
-            LOGGER.error( "discarding email event (no from address): " + emailItem.toDebugString() );
-            return false;
-        }
-
-        if ( emailItem.getTo() == null || emailItem.getTo().length() < 1 )
-        {
-            LOGGER.error( "discarding email event (no to address): " + emailItem.toDebugString() );
-            return false;
-        }
-
-        if ( emailItem.getSubject() == null || emailItem.getSubject().length() < 1 )
-        {
-            LOGGER.error( "discarding email event (no subject): " + emailItem.toDebugString() );
-            return false;
-        }
-
-        if ( ( emailItem.getBodyPlain() == null || emailItem.getBodyPlain().length() < 1 ) && ( emailItem.getBodyHtml() == null || emailItem.getBodyHtml().length() < 1 ) )
-        {
-            LOGGER.error( "discarding email event (no body): " + emailItem.toDebugString() );
-            return false;
-        }
-
-        return true;
-    }
-
-    public void submitEmail(
-            final EmailItemBean emailItem,
-            final UserInfo userInfo,
-            final MacroMachine macroMachine
-    )
-            throws PwmUnrecoverableException
-    {
-        submitEmailImpl( emailItem, userInfo, macroMachine, false );
-    }
-
-    public void submitEmailImmediate(
-            final EmailItemBean emailItem,
-            final UserInfo userInfo,
-            final MacroMachine macroMachine
-    )
-            throws PwmUnrecoverableException
-    {
-        submitEmailImpl( emailItem, userInfo, macroMachine, true );
-    }
-
-    private void submitEmailImpl(
-            final EmailItemBean emailItem,
-            final UserInfo userInfo,
-            final MacroMachine macroMachine,
-            final boolean immediate
-    )
-            throws PwmUnrecoverableException
-    {
-        if ( emailItem == null )
-        {
-            return;
-        }
-
-        final EmailItemBean finalBean;
-        {
-            EmailItemBean workingItemBean = emailItem;
-            if ( ( emailItem.getTo() == null || emailItem.getTo().isEmpty() ) && userInfo != null )
-            {
-                final String toAddress = userInfo.getUserEmailAddress();
-                workingItemBean = newEmailToAddress( workingItemBean, toAddress );
-            }
-
-            if ( macroMachine != null )
-            {
-                workingItemBean = applyMacrosToEmail( workingItemBean, macroMachine );
-            }
-
-            if ( workingItemBean.getTo() == null || workingItemBean.getTo().length() < 1 )
-            {
-                LOGGER.error( "no destination address available for email, skipping; email: " + emailItem.toDebugString() );
-            }
-
-            if ( !determineIfItemCanBeDelivered( emailItem ) )
-            {
-                return;
-            }
-            finalBean = workingItemBean;
-        }
-
-        try
-        {
-            if ( immediate )
-            {
-                workQueueProcessor.submitImmediate( finalBean );
-            }
-            else
-            {
-                workQueueProcessor.submit( finalBean );
-            }
-        }
-        catch ( PwmOperationalException e )
-        {
-            LOGGER.warn( "unable to add email to queue: " + e.getMessage() );
-        }
-    }
-
-    private final AtomicInteger newThreadLocalTransport = new AtomicInteger();
-    private final AtomicInteger useExistingConnection = new AtomicInteger();
-    private final AtomicInteger useExistingTransport = new AtomicInteger();
-    private final AtomicInteger newConnectionCounter = new AtomicInteger();
-
-    private String stats( )
-    {
-        final Map<String, Integer> map = new HashMap<>();
-        map.put( "newThreadLocalTransport", newThreadLocalTransport.get() );
-        map.put( "useExistingConnection", newThreadLocalTransport.get() );
-        map.put( "useExistingTransport", useExistingTransport.get() );
-        map.put( "newConnectionCounter", newConnectionCounter.get() );
-        return StringUtil.mapToString( map );
-    }
-
-    private WorkQueueProcessor.ProcessResult sendItem( final EmailItemBean emailItemBean )
-    {
-
-        // create a new MimeMessage object (using the Session created above)
-        try
-        {
-            if ( threadLocalTransport.get() == null )
-            {
-                LOGGER.trace( "initializing new threadLocal transport, stats: " + stats() );
-                threadLocalTransport.set( getSmtpTransport() );
-                newThreadLocalTransport.getAndIncrement();
-            }
-            else
-            {
-                LOGGER.trace( "using existing threadLocal transport, stats: " + stats() );
-                useExistingTransport.getAndIncrement();
-            }
-            final Transport transport = threadLocalTransport.get();
-            if ( !transport.isConnected() )
-            {
-                LOGGER.trace( "connecting threadLocal transport, stats: " + stats() );
-                transport.connect();
-                newConnectionCounter.getAndIncrement();
-            }
-            else
-            {
-                LOGGER.trace( "using existing threadLocal: stats: " + stats() );
-                useExistingConnection.getAndIncrement();
-            }
-
-            final List<Message> messages = convertEmailItemToMessages( emailItemBean, this.pwmApplication.getConfig() );
-
-            for ( final Message message : messages )
-            {
-                message.saveChanges();
-                transport.sendMessage( message, message.getAllRecipients() );
-            }
-
-            lastError = null;
-
-            LOGGER.debug( "sent email: " + emailItemBean.toDebugString() );
-            StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_SUCCESSES );
-            return WorkQueueProcessor.ProcessResult.SUCCESS;
-        }
-        catch ( MessagingException | PwmException e )
-        {
-
-            final ErrorInformation errorInformation;
-            if ( e instanceof PwmException )
-            {
-                errorInformation = ( ( PwmException ) e ).getErrorInformation();
-            }
-            else
-            {
-                final String errorMsg = "error sending email: " + e.getMessage();
-                errorInformation = new ErrorInformation(
-                        PwmError.ERROR_EMAIL_SEND_FAILURE,
-                        errorMsg,
-                        new String[] {
-                                emailItemBean.toDebugString(),
-                                JavaHelper.readHostileExceptionMessage( e ),
-                        }
-                );
-            }
-
-            lastError = errorInformation;
-            LOGGER.error( errorInformation );
-
-            if ( sendIsRetryable( e ) )
-            {
-                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 Transport getSmtpTransport( )
-            throws MessagingException, PwmUnrecoverableException
-    {
-        Transport tr = null;
-        String mailHost;
-        int mailPort; String mailUser ;
-         PasswordData mailPassword ;
-
-        for ( int i = 0; i < emailServers.size(); i++) {
-            if (emailServers.get(i).isTried()) {
-                emailServers.get(i).setTried(false);
-                continue;
-            } else {
-                mailHost = emailServers.get(i).getServerAddress();
-                mailPort = emailServers.get(i).getPort();
-                mailUser = emailServers.get(i).getUsername();
-                mailPassword = emailServers.get(i).getPassword();
-                emailServers.get(i).setTried(true);
-            }
-
-            // Login to SMTP server first if both username and password is given
-            final javax.mail.Session session = javax.mail.Session.getInstance( javaMailProps, null );
-            tr = session.getTransport( "smtp" );
-
-            final boolean authenticated = !( mailUser == null || mailUser.length() < 1 || mailPassword == null );
-
-        if (authenticated)
-        {    // create a new Session object for the message
-            tr.connect(mailHost, mailPort, mailUser, mailPassword.getStringValue());
-        } else {
-            tr.connect();
-        }
-
-            LOGGER.debug( "connected to " + mailHost + ":" + mailPort + " " + ( authenticated ? "(secure)" : "(plaintext)" ) );
-            emailServers.get(i).setTried(false);
-            break;
-        }
-        return tr;
-    }
-
-    public List<Message> convertEmailItemToMessages( final EmailItemBean emailItemBean, final Configuration config )
-            throws MessagingException
-    {
-        final List<Message> messages = new ArrayList<>();
-        final boolean hasPlainText = emailItemBean.getBodyPlain() != null && emailItemBean.getBodyPlain().length() > 0;
-        final boolean hasHtml = emailItemBean.getBodyHtml() != null && emailItemBean.getBodyHtml().length() > 0;
-        final String subjectEncodingCharset = config.readAppProperty( AppProperty.SMTP_SUBJECT_ENCODING_CHARSET );
-
-        // create a new Session object for the messagejavamail
-        final javax.mail.Session session = javax.mail.Session.getInstance( javaMailProps, null );
-
-        final String emailTo = emailItemBean.getTo();
-        if ( emailTo != null )
-        {
-            final InternetAddress[] recipients = InternetAddress.parse( emailTo );
-            for ( final InternetAddress recipient : recipients )
-            {
-                final MimeMessage message = new MimeMessage( session );
-                message.setFrom( makeInternetAddress( emailItemBean.getFrom() ) );
-                message.setRecipient( Message.RecipientType.TO, recipient );
-                {
-                    if ( subjectEncodingCharset != null && !subjectEncodingCharset.isEmpty() )
-                    {
-                        message.setSubject( emailItemBean.getSubject(), subjectEncodingCharset );
-                    }
-                    else
-                    {
-                        message.setSubject( emailItemBean.getSubject() );
-                    }
-                }
-                message.setSentDate( new Date() );
-
-                if ( hasPlainText && hasHtml )
-                {
-                    final MimeMultipart content = new MimeMultipart( "alternative" );
-                    final MimeBodyPart text = new MimeBodyPart();
-                    final MimeBodyPart html = new MimeBodyPart();
-                    text.setContent( emailItemBean.getBodyPlain(), HttpContentType.plain.getHeaderValue() );
-                    html.setContent( emailItemBean.getBodyHtml(), HttpContentType.html.getHeaderValue() );
-                    content.addBodyPart( text );
-                    content.addBodyPart( html );
-                    message.setContent( content );
-                }
-                else if ( hasPlainText )
-                {
-                    message.setContent( emailItemBean.getBodyPlain(), HttpContentType.plain.getHeaderValue() );
-                }
-                else if ( hasHtml )
-                {
-                    message.setContent( emailItemBean.getBodyHtml(), HttpContentType.html.getHeaderValue() );
-                }
-
-                messages.add( message );
-            }
-        }
-
-        return messages;
-    }
-
-    private static Properties makeJavaMailProps( final Configuration config )
-    {
-        //Create a properties item to start setting up the mail
-        final Properties props = new Properties();
-
-        for (int i = 0; i < staticEmailServers.size(); i++) {
-            props.put("mail.smtp.host", staticEmailServers.get(i).getServerAddress());
-            props.put("mail.smtp.port",staticEmailServers.get(i).getPort());
-        }
-        //Specify the desired SMTP server
-        //props.put( "mail.smtp.host", config.readSettingAsString( PwmSetting.EMAIL_SERVER_ADDRESS ) );
-
-        //Specify SMTP server port
-        //props.put( "mail.smtp.port", ( int ) config.readSettingAsLong( PwmSetting.EMAIL_SERVER_PORT ) );
-
-        //Specify configured advanced settings.
-        final Map<String, String> advancedSettingValues = StringUtil.convertStringListToNameValuePair( config.readSettingAsStringArray( PwmSetting.EMAIL_ADVANCED_SETTINGS ), "=" );
-        props.putAll( advancedSettingValues );
-
-        return props;
-    }
-
-    private static InternetAddress makeInternetAddress( final String input )
-            throws AddressException
-    {
-        if ( input == null )
-        {
-            return null;
-        }
-
-        if ( input.matches( "^.*<.*>$" ) )
-        {
-            // check for format like: John Doe <jdoe@example.com>
-            final String[] splitString = input.split( "<|>" );
-            if ( splitString.length < 2 )
-            {
-                return new InternetAddress( input );
-            }
-
-            final InternetAddress address = new InternetAddress();
-            address.setAddress( splitString[ 1 ].trim() );
-            try
-            {
-                address.setPersonal( splitString[ 0 ].trim(), PwmConstants.DEFAULT_CHARSET.toString() );
-            }
-            catch ( UnsupportedEncodingException e )
-            {
-                LOGGER.error( "unsupported encoding error while parsing internet address '" + input + "', error: " + e.getMessage() );
-            }
-            return address;
-        }
-        return new InternetAddress( input );
-    }
-
-    private static EmailItemBean applyMacrosToEmail( final EmailItemBean emailItem, final MacroMachine macroMachine )
-    {
-        final EmailItemBean expandedEmailItem;
-        expandedEmailItem = new EmailItemBean(
-                macroMachine.expandMacros( emailItem.getTo() ),
-                macroMachine.expandMacros( emailItem.getFrom() ),
-                macroMachine.expandMacros( emailItem.getSubject() ),
-                macroMachine.expandMacros( emailItem.getBodyPlain() ),
-                macroMachine.expandMacros( emailItem.getBodyHtml() )
-        );
-        return expandedEmailItem;
-    }
-
-    private static EmailItemBean newEmailToAddress( final EmailItemBean emailItem, final String toAddress )
-    {
-        final EmailItemBean expandedEmailItem;
-        expandedEmailItem = new EmailItemBean(
-                toAddress,
-                emailItem.getFrom(),
-                emailItem.getSubject(),
-                emailItem.getBodyPlain(),
-                emailItem.getBodyHtml()
-        );
-        return expandedEmailItem;
-    }
-
-    private static boolean sendIsRetryable( final Exception e )
-    {
-        if ( e != null )
-        {
-            final Throwable cause = e.getCause();
-            if ( cause instanceof IOException )
-            {
-                LOGGER.trace( "message send failure cause is due to an IOException: " + e.getMessage() );
-                return true;
-            }
-        }
-        return false;
-    }
-}
-

+ 7 - 6
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -687,19 +687,20 @@
         <regex>^(?!.*all.*)([a-zA-Z][a-zA-Z0-9-]{2,15})$</regex>
         <properties>
             <property key="Minimum">1</property>
+            <property key="Minimum">5</property>
         </properties>
         <default>
-            <value>Email</value>
+            <value>default</value>
         </default>
     </setting>
 
-    <setting hidden="false" key="email.smtp.addresses" level="1">
+    <setting hidden="false" key="email.smtp.address" level="1">
         <regex>^[a-zA-Z0-9.-]*$</regex>
         <default>
             <value />
         </default>
     </setting>
-    <setting hidden="false" key="email.smtp.ports" level="1">
+    <setting hidden="false" key="email.smtp.port" level="1">
         <default>
             <value>25</value>
         </default>
@@ -714,12 +715,12 @@
             <value>noreply@example.org</value>
         </default>
     </setting>
-    <setting hidden="false" key="email.smtp.usernames" level="1">
+    <setting hidden="false" key="email.smtp.username" level="1">
         <default>
             <value />
         </default>
     </setting>
-    <setting hidden="false" key="email.smtp.userpasswords" level="1">
+    <setting hidden="false" key="email.smtp.userpassword" level="1">
     </setting>
 
     <setting hidden="false" key="email.smtp.address" level="1">
@@ -3935,7 +3936,7 @@
     <category hidden="false" key="CHALLENGE_POLICY">
         <profile setting="challenge.profile.list"/>
     </category>
-    <category hidden="false" key="EMAIL_PROFILES">
+    <category hidden="false" key="EMAIL_SERVERS">
         <profile setting="email.profile.list"/>
     </category>
     <category hidden="false" key="EMAIL_PROFILE_SETTING">

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

@@ -46,9 +46,9 @@ Category_Description_EDIR_CR_SETTINGS=NetIQ eDirectory CR specific settings.
 Category_Description_EDIRECTORY=NetIQ eDirectory specific settings.
 Category_Description_EDIR_SETTINGS=NetIQ eDirectory specific settings.
 Category_Description_EMAIL=<p>Configuration settings for all sent emails.   The settings for the email body configuration are for both plaintext and HTML.  We encourage that for each configured setting and locale for the email body, that you configure both plaintext and HTML. @PwmAppName@ delivers the email in both formats and the email client can choose which to display.</p> <p>Email definitions might use macros.  For more information about macros, see the "View" menu "Show Macro Help".</p>
+Category_Description_EMAIL_SERVERS=Email Servers
 Category_Description_EMAIL_SETTINGS=
 Category_Description_EMAIL_TEMPLATES=
-Category_Description_EMAIL_PROFILES=Email Profile Settings
 Category_Description_FORGOTTEN_USERNAME=Allows a user to search for a forgotten user name using a configurable search filter and attributes.
 Category_Description_GENERAL=General settings for the application.  Settings here control the functionality and behavior of the system overall.
 Category_Description_GUEST=Note\: The guest user registration module requires that the logged in user has sufficient permissions to create users and if so configured, to check for duplicate values.
@@ -144,9 +144,9 @@ Category_Label_EDIR_CR_SETTINGS=eDirectory Challenge Sets
 Category_Label_EDIRECTORY=NetIQ eDirectory
 Category_Label_EDIR_SETTINGS=eDirectory Settings
 Category_Label_EMAIL=Email
+Category_Label_EMAIL_SERVERS=Email Servers
 Category_Label_EMAIL_SETTINGS=Email Settings
 Category_Label_EMAIL_TEMPLATES=Email Templates
-Category_Label_EMAIL_PROFILES=Email Profile Settings
 Category_Label_FORGOTTEN_USERNAME=Forgotten User Name
 Category_Label_GENERAL=Application
 Category_Label_GUEST=Guest Registration
@@ -314,7 +314,7 @@ Setting_Description_email.adminAlert.toAddress=Define this template to send an e
 Setting_Description_email.challenge.token=Define this template to send an email during the forgotten password verification process.  You can use %TOKEN% to insert the token value into the email.
 Setting_Description_email.changePassword=Define this template to send an email to the users when password changes occur. @PwmAppName@ only sends this email when the users change their own passwords.
 Setting_Description_email.changePassword.helpdesk=Define this template to send an email to users when the Help Desk changes the users' passwords. @PwmAppName@ expands macros for this setting based on the user who is changing their password, not the Help Desk user. 
-Setting_Description_email.default.fromAddress=Specify a default From Address for the email templates. This value will be used for all sent email unless indicated in the template.
+Setting_Description_email.default.fromAddress=Specify a default From Address for the email templates.
 Setting_Description_email.deleteAccount=Define this template to send an email to the users after the Account Delete action.
 Setting_Description_email.guest=Define this template to send an email to newly created guest users.
 Setting_Description_email.helpdesk.token=Define this template to send an email during the Help Desk verification process.  You can use %TOKEN% to insert the token value into the email.
@@ -326,11 +326,11 @@ Setting_Description_email.pwExpirationNotice=Email sent to users to notify the u
 Setting_Description_email.queueMaxAge=Specify the maximum age (in seconds) an email can wait in the send queue.  If an email is in the send queue longer than this time, @PwmAppName@ discards it.  Emails only persist in the send queue if there is an IO or network error to the SMTP server while sending the email.
 Setting_Description_email.sendpassword=Define this template to send an email during forgotten password reset process if you enabled the send password functionality.
 Setting_Description_email.sendUsername=Define this template to send an email for the forgotten user name process.
-Setting_Description_email.profile.list=Email profiles for multiple email servers.
-Setting_Description_email.smtp.addresses=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.ports=Specify the network port number for the SMTP server.
-Setting_Description_email.smtp.usernames=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.userpasswords=Specify the password for the SMTP user.  A blank value here sends SMTP messages without authentication.
+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.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.
 Setting_Description_email.unlock=Define this template to send an email to users who unlock their own account.
 Setting_Description_email.updateguest=Define this template to send an email to updated guest users.
@@ -813,11 +813,11 @@ Setting_Label_email.pwExpirationNotice=Password Expiration Notification Email
 Setting_Label_email.queueMaxAge=Maximum Email Queue Age
 Setting_Label_email.sendpassword=Send Password Email
 Setting_Label_email.sendUsername=Send User Name Email
-Setting_Label_email.profile.list=Email Profiles
-Setting_Label_email.smtp.addresses=Email Server Address
-Setting_Label_email.smtp.ports=Email Server Port
-Setting_Label_email.smtp.usernames=SMTP Email Server User Name
-Setting_Label_email.smtp.userpasswords=Email Server Password
+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.username=SMTP Server User Name
+Setting_Label_email.smtp.userpassword=SMTP Server Password
 Setting_Label_email.smtp.advancedSettings=SMTP Email Advanced Settings
 Setting_Label_email.unlock=Unlock Account Email
 Setting_Label_email.updateguest=Guest Registration Update Email

+ 10 - 11
server/src/test/java/password/pwm/util/queue/EmailQueueManagerTest.java

@@ -22,34 +22,33 @@
 
 package password.pwm.util.queue;
 
-import java.io.IOException;
-import java.util.List;
-
-import javax.mail.Message;
-import javax.mail.MessagingException;
-import javax.mail.internet.InternetAddress;
-
 import junit.framework.Assert;
-
 import org.apache.commons.io.IOUtils;
 import org.junit.Test;
 import org.mockito.Mockito;
-
 import password.pwm.AppProperty;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.config.Configuration;
+import password.pwm.svc.email.EmailServerUtil;
+import password.pwm.svc.email.EmailService;
+
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.internet.InternetAddress;
+import java.io.IOException;
+import java.util.List;
 
 public class EmailQueueManagerTest {
     @Test
     public void testConvertEmailItemToMessage() throws MessagingException, IOException {
-        EmailQueueManager emailQueueManager = new EmailQueueManager();
+        EmailService emailService = new EmailService();
 
         Configuration config = Mockito.mock(Configuration.class);
         Mockito.when(config.readAppProperty(AppProperty.SMTP_SUBJECT_ENCODING_CHARSET)).thenReturn("UTF8");
 
         EmailItemBean emailItemBean = new EmailItemBean("fred@flintstones.tv, barney@flintstones.tv", "bedrock-admin@flintstones.tv", "Test Subject", "bodyPlain", "bodyHtml");
 
-        List<Message> messages = emailQueueManager.convertEmailItemToMessages(emailItemBean, config);
+        List<Message> messages = EmailServerUtil.convertEmailItemToMessages(emailItemBean, config, null);
         Assert.assertEquals(2, messages.size());
 
         Message message = messages.get(0);