Jelajahi Sumber

Merge pull request #274 from pwm-project/email

Email branch
Jason 7 tahun lalu
induk
melakukan
0ced6a0538

+ 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;

+ 16 - 0
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;
@@ -1070,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<>();
@@ -1142,6 +1154,10 @@ public class Configuration implements SettingReader
                 newProfile = DeleteAccountProfile.makeFromStoredConfiguration( storedConfiguration, profileID );
                 break;
 
+            case EmailServers:
+                newProfile = EmailServerProfile.makeFromStoredConfiguration( storedConfiguration, profileID );
+                break;
+
             case SetupOTPProfile:
                 newProfile = SetupOtpProfile.makeFromStoredConfiguration( storedConfiguration, profileID );
                 break;

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

@@ -260,6 +260,7 @@ public enum PwmSetting
             "ldap.addObjectClasses", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.LDAP_ATTRIBUTES ),
 
 
+
     // ldap global settings
     LDAP_PROFILE_LIST(
             "ldap.profile.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.INTERNAL ),
@@ -278,24 +279,27 @@ public enum PwmSetting
     LDAP_ENABLE_WIRE_TRACE(
             "ldap.wireTrace.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.LDAP_GLOBAL ),
 
-
-    // email settings
+    // New multiple email settings
+    EMAIL_SERVERS(
+            "email.profile.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.INTERNAL ),
     EMAIL_SERVER_ADDRESS(
-            "email.smtp.address", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SETTINGS ),
+            "email.smtp.address", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SERVERS ),
     EMAIL_SERVER_PORT(
-            "email.smtp.port", PwmSettingSyntax.NUMERIC, PwmSettingCategory.EMAIL_SETTINGS ),
-    EMAIL_DEFAULT_FROM_ADDRESS(
-            "email.default.fromAddress", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SETTINGS ),
+            "email.smtp.port", PwmSettingSyntax.NUMERIC, PwmSettingCategory.EMAIL_SERVERS ),
     EMAIL_USERNAME(
-            "email.smtp.username", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SETTINGS ),
+            "email.smtp.username", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SERVERS ),
     EMAIL_PASSWORD(
-            "email.smtp.userpassword", PwmSettingSyntax.PASSWORD, PwmSettingCategory.EMAIL_SETTINGS ),
+            "email.smtp.userpassword", PwmSettingSyntax.PASSWORD, PwmSettingCategory.EMAIL_SERVERS ),
+
+    // system wide email settings
+
+    EMAIL_DEFAULT_FROM_ADDRESS(
+            "email.default.fromAddress", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SETTINGS ),
     EMAIL_MAX_QUEUE_AGE(
             "email.queueMaxAge", PwmSettingSyntax.DURATION, PwmSettingCategory.EMAIL_SETTINGS ),
     EMAIL_ADVANCED_SETTINGS(
             "email.smtp.advancedSettings", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.EMAIL_SETTINGS ),
 
-
     // email template
     EMAIL_CHANGEPASSWORD(
             "email.changePassword", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES ),

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

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

+ 60 - 0
server/src/main/java/password/pwm/config/profile/EmailServerProfile.java

@@ -0,0 +1,60 @@
+/*
+ * 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.config.profile;
+
+import password.pwm.config.PwmSetting;
+import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfiguration;
+
+import java.util.Locale;
+import java.util.Map;
+
+public class EmailServerProfile extends AbstractProfile
+{
+
+    private static final ProfileType PROFILE_TYPE = ProfileType.EmailServers;
+
+    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 );
+    }
+
+    @Override
+    public ProfileType profileType( )
+    {
+        return PROFILE_TYPE;
+    }
+
+    @Override
+    public String getDisplayName( final Locale locale )
+    {
+        final String value = this.readSettingAsLocalizedString( PwmSetting.EMAIL_SERVERS, locale );
+        return value != null && !value.isEmpty() ? value : this.getIdentifier();
+    }
+}

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

@@ -27,13 +27,15 @@ 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 ),
-    SetupOTPProfile( true, PwmSettingCategory.OTP_PROFILE, PwmSetting.OTP_SETUP_USER_PERMISSION ),;
+    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;
     private final PwmSetting queryMatch;

+ 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
+public class EmailServer
+{
+    private final String id;
+    private final String host;
+    private final int port;
+    private final String username;
+    private final PasswordData password;
+    private final Properties javaMailProps;
+    private final javax.mail.Session session;
+
+    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_USERNAME );
+            final PasswordData password = profile.readSettingAsPassword( PwmSetting.EMAIL_PASSWORD );
+            if ( !StringUtil.isEmpty( address )
+                    && port > 0
+                    )
+            {
+                final Properties properties = makeJavaMailProps( configuration, address, port );
+                final javax.mail.Session session = javax.mail.Session.getInstance( properties, null );
+                final EmailServer emailServer = EmailServer.builder()
+                        .id( id )
+                        .host( address )
+                        .port( port )
+                        .username( username )
+                        .password( password )
+                        .javaMailProps( properties )
+                        .session( session )
+                        .build();
+                returnObj.add( emailServer );
+            }
+            else
+            {
+                LOGGER.warn( "discarding incompletely configured email address for smtp server profile " + id );
+            }
+        }
+
+        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;
+    }
+}

+ 72 - 201
server/src/main/java/password/pwm/util/queue/EmailQueueManager.java → server/src/main/java/password/pwm/svc/email/EmailService.java

@@ -20,14 +20,12 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util.queue;
+package password.pwm.svc.email;
 
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
-import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
-import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.error.ErrorInformation;
@@ -37,12 +35,11 @@ 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.AtomicLoopIntIncrementer;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -55,52 +52,54 @@ 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.Optional;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * @author Jason D. Rivard
  */
-public class EmailQueueManager implements PwmService
+public class EmailService implements PwmService
 {
 
-    private static final PwmLogger LOGGER = PwmLogger.forClass( EmailQueueManager.class );
+    private static final PwmLogger LOGGER = PwmLogger.forClass( EmailService.class );
 
     private PwmApplication pwmApplication;
-    private Properties javaMailProps = new Properties();
+    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 ErrorInformation lastError;
 
-    private final ThreadLocal<Transport> threadLocalTransport = new ThreadLocal<>();
+    private final ThreadLocal<EmailConnection> threadLocalTransport = new ThreadLocal<>();
 
     public void init( final PwmApplication pwmApplication )
             throws PwmException
     {
         status = STATUS.OPENING;
         this.pwmApplication = pwmApplication;
-        javaMailProps = makeJavaMailProps( pwmApplication.getConfig() );
+
+        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, EmailQueueManager will remain closed" );
+            LOGGER.warn( "localdb is not open, EmailService will remain closed" );
             status = STATUS.CLOSED;
             return;
         }
@@ -144,12 +143,17 @@ public class EmailQueueManager implements PwmService
             return Collections.singletonList( HealthRecord.forMessage( HealthMessage.ServiceClosed_AppReadOnly, this.getClass().getSimpleName() ) );
         }
 
-        if ( lastError != null )
+        final List<HealthRecord> records = new ArrayList<>( );
+        for ( final Map.Entry<EmailServer, Optional<ErrorInformation>> entry : serverErrors.entrySet() )
         {
-            return Collections.singletonList( HealthRecord.forMessage( HealthMessage.Email_SendFailure, lastError.toDebugStr() ) );
+            if ( entry.getValue().isPresent() )
+            {
+                final ErrorInformation errorInformation = entry.getValue().get();
+                records.add( HealthRecord.forMessage( HealthMessage.Email_SendFailure, errorInformation.toDebugStr() ) );
+            }
         }
 
-        return Collections.emptyList();
+        return records;
     }
 
     @Override
@@ -200,9 +204,8 @@ public class EmailQueueManager implements PwmService
 
     private boolean determineIfItemCanBeDelivered( final EmailItemBean emailItem )
     {
-        final String serverAddress = pwmApplication.getConfig().readSettingAsString( PwmSetting.EMAIL_SERVER_ADDRESS );
 
-        if ( serverAddress == null || serverAddress.length() < 1 )
+        if ( servers.isEmpty() )
         {
             LOGGER.debug( "discarding email send event (no SMTP server address configured) " + emailItem.toDebugString() );
             return false;
@@ -274,12 +277,12 @@ public class EmailQueueManager implements PwmService
             if ( ( emailItem.getTo() == null || emailItem.getTo().isEmpty() ) && userInfo != null )
             {
                 final String toAddress = userInfo.getUserEmailAddress();
-                workingItemBean = newEmailToAddress( workingItemBean, toAddress );
+                workingItemBean = EmailServerUtil.newEmailToAddress( workingItemBean, toAddress );
             }
 
             if ( macroMachine != null )
             {
-                workingItemBean = applyMacrosToEmail( workingItemBean, macroMachine );
+                workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
             }
 
             if ( workingItemBean.getTo() == null || workingItemBean.getTo().length() < 1 )
@@ -328,14 +331,16 @@ public class EmailQueueManager implements PwmService
 
     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() );
+                threadLocalTransport.set( getSmtpTransport( ) );
                 newThreadLocalTransport.getAndIncrement();
             }
             else
@@ -343,11 +348,14 @@ public class EmailQueueManager implements PwmService
                 LOGGER.trace( "using existing threadLocal transport, stats: " + stats() );
                 useExistingTransport.getAndIncrement();
             }
-            final Transport transport = threadLocalTransport.get();
-            if ( !transport.isConnected() )
+
+            serverTransport = threadLocalTransport.get();
+
+            if ( !serverTransport.getTransport().isConnected() )
             {
                 LOGGER.trace( "connecting threadLocal transport, stats: " + stats() );
-                transport.connect();
+                threadLocalTransport.set( getSmtpTransport( ) );
+                serverTransport = threadLocalTransport.get();
                 newConnectionCounter.getAndIncrement();
             }
             else
@@ -356,15 +364,19 @@ public class EmailQueueManager implements PwmService
                 useExistingConnection.getAndIncrement();
             }
 
-            final List<Message> messages = convertEmailItemToMessages( emailItemBean, this.pwmApplication.getConfig() );
+            final List<Message> messages = EmailServerUtil.convertEmailItemToMessages(
+                    emailItemBean,
+                    this.pwmApplication.getConfig(),
+                    serverTransport.getEmailServer()
+            );
 
             for ( final Message message : messages )
             {
                 message.saveChanges();
-                transport.sendMessage( message, message.getAllRecipients() );
+                serverTransport.getTransport().sendMessage( message, message.getAllRecipients() );
             }
 
-            lastError = null;
+            serverErrors.put( serverTransport.getEmailServer(), Optional.empty() );
 
             LOGGER.debug( "sent email: " + emailItemBean.toDebugString() );
             StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_SUCCESSES );
@@ -391,10 +403,13 @@ public class EmailQueueManager implements PwmService
                 );
             }
 
-            lastError = errorInformation;
+            if ( serverTransport != null )
+            {
+                serverErrors.put( serverTransport.getEmailServer(), Optional.of( errorInformation ) );
+            }
             LOGGER.error( errorInformation );
 
-            if ( sendIsRetryable( e ) )
+            if ( EmailServerUtil.sendIsRetryable( e ) )
             {
                 LOGGER.error( "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", will retry" );
                 StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_FAILURES );
@@ -402,189 +417,45 @@ public class EmailQueueManager implements PwmService
             }
             else
             {
-                LOGGER.error(
-                        "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", permanent failure, discarding message" );
+                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
-    {
-        final String mailUser = this.pwmApplication.getConfig().readSettingAsString( PwmSetting.EMAIL_USERNAME );
-        final PasswordData mailPassword = this.pwmApplication.getConfig().readSettingAsPassword( PwmSetting.EMAIL_PASSWORD );
-        final String mailhost = this.pwmApplication.getConfig().readSettingAsString( PwmSetting.EMAIL_SERVER_ADDRESS );
-        final int mailport = ( int ) this.pwmApplication.getConfig().readSettingAsLong( PwmSetting.EMAIL_SERVER_PORT );
-
-        // Login to SMTP server first if both username and password is given
-        final javax.mail.Session session = javax.mail.Session.getInstance( javaMailProps, null );
-        final Transport 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)" ) );
-        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 )
+    private EmailConnection getSmtpTransport( )
+            throws PwmUnrecoverableException
     {
-        //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", 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;
-        }
+        // the global server incrementer rotates the server list by 1 offset each attempt to get an smtp transport.
+        int nextSlot = serverIncrementer.next();
 
-        if ( input.matches( "^.*<.*>$" ) )
+        for ( int i = 0; i < servers.size(); i++ )
         {
-            // check for format like: John Doe <jdoe@example.com>
-            final String[] splitString = input.split( "<|>" );
-            if ( splitString.length < 2 )
-            {
-                return new InternetAddress( input );
-            }
+            nextSlot = nextSlot >= ( servers.size() - 1 )
+                    ? 0
+                    : nextSlot + 1;
 
-            final InternetAddress address = new InternetAddress();
-            address.setAddress( splitString[ 1 ].trim() );
+            final EmailServer server = servers.get( nextSlot );
             try
             {
-                address.setPersonal( splitString[ 0 ].trim(), PwmConstants.DEFAULT_CHARSET.toString() );
+                final Transport transport = EmailServerUtil.makeSmtpTransport( server );
+
+                serverErrors.put( server, Optional.empty() );
+                return new EmailConnection( server, transport );
             }
-            catch ( UnsupportedEncodingException e )
+            catch ( MessagingException e )
             {
-                LOGGER.error( "unsupported encoding error while parsing internet address '" + input + "', error: " + e.getMessage() );
+                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() );
             }
-            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;
+        throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_UNREACHABLE, "unable to reach any configured email server" );
     }
 
-    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;
-    }
 }
 

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

@@ -682,6 +682,47 @@
             <value><![CDATA[User]]></value>
         </default>
     </setting>
+
+    <setting hidden="true" key="email.profile.list" level="1">
+        <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>default</value>
+        </default>
+    </setting>
+
+    <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.port" level="1">
+        <default>
+            <value>25</value>
+        </default>
+        <properties>
+            <property key="Minimum">1</property>
+            <property key="Maximum">65535</property>
+        </properties>
+    </setting>
+    <setting hidden="false" key="email.default.fromAddresses" level="1">
+        <flag>emailSyntax</flag>
+        <default>
+            <value>noreply@example.org</value>
+        </default>
+    </setting>
+    <setting hidden="false" key="email.smtp.username" level="1">
+        <default>
+            <value />
+        </default>
+    </setting>
+    <setting hidden="false" key="email.smtp.userpassword" level="1">
+    </setting>
+
     <setting hidden="false" key="email.smtp.address" level="1">
         <regex>^[a-zA-Z0-9.-]*$</regex>
         <default>
@@ -3895,6 +3936,11 @@
     <category hidden="false" key="CHALLENGE_POLICY">
         <profile setting="challenge.profile.list"/>
     </category>
+    <category hidden="false" key="EMAIL_SERVERS">
+        <profile setting="email.profile.list"/>
+    </category>
+    <category hidden="false" key="EMAIL_PROFILE_SETTING">
+    </category>
     <category hidden="false" key="EMAIL">
     </category>
     <category hidden="false" key="EMAIL_SETTINGS">

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

@@ -46,6 +46,7 @@ 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_FORGOTTEN_USERNAME=Allows a user to search for a forgotten user name using a configurable search filter and attributes.
@@ -143,6 +144,7 @@ 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_FORGOTTEN_USERNAME=Forgotten User Name
@@ -324,11 +326,12 @@ 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=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.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.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.
 Setting_Description_email.updateProfile=Define this template to send an email to users after a profile update.
@@ -810,11 +813,12 @@ 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.smtp.address=SMTP Email Server Address
+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.smtp.port=SMTP Email Server Port
-Setting_Label_email.smtp.username=SMTP Email Server User Name
-Setting_Label_email.smtp.userpassword=SMTP Email Server Password
 Setting_Label_email.unlock=Unlock Account Email
 Setting_Label_email.updateguest=Guest Registration Update Email
 Setting_Label_email.updateProfile.token=Update Profile Email Verification

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

@@ -22,34 +22,39 @@
 
 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.EmailServer;
+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;
+import java.util.Properties;
 
 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);
+        EmailServer emailServer = EmailServer.builder()
+                .javaMailProps( new Properties(  ) )
+                .build();
+
+        List<Message> messages = EmailServerUtil.convertEmailItemToMessages(emailItemBean, config, emailServer);
         Assert.assertEquals(2, messages.size());
 
         Message message = messages.get(0);