Explorar el Código

Merge remote-tracking branch 'origin/master' into ng-helpdesk

jalbr74 hace 7 años
padre
commit
2ce175cd58
Se han modificado 100 ficheros con 1099 adiciones y 452 borrados
  1. 19 1
      server/pom.xml
  2. 3 3
      server/src/main/java/password/pwm/PwmApplication.java
  3. 1 1
      server/src/main/java/password/pwm/PwmConstants.java
  4. 2 0
      server/src/main/java/password/pwm/bean/EmailItemBean.java
  5. 16 0
      server/src/main/java/password/pwm/config/Configuration.java
  6. 25 11
      server/src/main/java/password/pwm/config/PwmSetting.java
  7. 4 3
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  8. 30 0
      server/src/main/java/password/pwm/config/option/RecoveryMinLifetimeOption.java
  9. 60 0
      server/src/main/java/password/pwm/config/profile/EmailServerProfile.java
  10. 8 6
      server/src/main/java/password/pwm/config/profile/ProfileType.java
  11. 17 30
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  12. 12 14
      server/src/main/java/password/pwm/health/LDAPStatusChecker.java
  13. 1 0
      server/src/main/java/password/pwm/http/PwmRequestAttribute.java
  14. 1 0
      server/src/main/java/password/pwm/http/PwmRequestFlag.java
  15. 19 0
      server/src/main/java/password/pwm/http/bean/PwmSessionBean.java
  16. 17 0
      server/src/main/java/password/pwm/http/servlet/AbstractPwmServlet.java
  17. 1 0
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataBean.java
  18. 1 0
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataReader.java
  19. 2 14
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  20. 17 17
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServletUtil.java
  21. 58 39
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  22. 83 11
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  23. 1 1
      server/src/main/java/password/pwm/http/tag/PwmScriptRefTag.java
  24. 1 1
      server/src/main/java/password/pwm/http/tag/PwmScriptTag.java
  25. 2 0
      server/src/main/java/password/pwm/i18n/Display.java
  26. 2 0
      server/src/main/java/password/pwm/ldap/UserInfo.java
  27. 1 0
      server/src/main/java/password/pwm/ldap/UserInfoBean.java
  28. 12 0
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  29. 7 1
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  30. 2 1
      server/src/main/java/password/pwm/svc/PwmServiceEnum.java
  31. 34 0
      server/src/main/java/password/pwm/svc/email/EmailConnection.java
  32. 58 0
      server/src/main/java/password/pwm/svc/email/EmailServer.java
  33. 289 0
      server/src/main/java/password/pwm/svc/email/EmailServerUtil.java
  34. 72 201
      server/src/main/java/password/pwm/svc/email/EmailService.java
  35. 50 18
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  36. 63 6
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  37. 3 0
      server/src/main/resources/password/pwm/i18n/Display.properties
  38. 14 8
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  39. 1 1
      server/src/main/webapp/WEB-INF/jsp/accountinformation.jsp
  40. 1 1
      server/src/main/webapp/WEB-INF/jsp/activateuser-agreement.jsp
  41. 1 1
      server/src/main/webapp/WEB-INF/jsp/activateuser-entercode.jsp
  42. 1 1
      server/src/main/webapp/WEB-INF/jsp/activateuser.jsp
  43. 1 1
      server/src/main/webapp/WEB-INF/jsp/admin-activity.jsp
  44. 1 1
      server/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp
  45. 1 1
      server/src/main/webapp/WEB-INF/jsp/admin-dashboard.jsp
  46. 1 1
      server/src/main/webapp/WEB-INF/jsp/admin-logview.jsp
  47. 1 1
      server/src/main/webapp/WEB-INF/jsp/admin-tokenlookup.jsp
  48. 1 1
      server/src/main/webapp/WEB-INF/jsp/admin-urlreference.jsp
  49. 9 1
      server/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp
  50. 1 1
      server/src/main/webapp/WEB-INF/jsp/changepassword-agreement.jsp
  51. 1 1
      server/src/main/webapp/WEB-INF/jsp/changepassword-complete.jsp
  52. 1 1
      server/src/main/webapp/WEB-INF/jsp/changepassword-form.jsp
  53. 1 1
      server/src/main/webapp/WEB-INF/jsp/changepassword-wait.jsp
  54. 1 1
      server/src/main/webapp/WEB-INF/jsp/changepassword-warn.jsp
  55. 1 1
      server/src/main/webapp/WEB-INF/jsp/changepassword.jsp
  56. 2 2
      server/src/main/webapp/WEB-INF/jsp/configmanager-certificates.jsp
  57. 1 1
      server/src/main/webapp/WEB-INF/jsp/configmanager-localdb.jsp
  58. 1 1
      server/src/main/webapp/WEB-INF/jsp/configmanager-login.jsp
  59. 1 1
      server/src/main/webapp/WEB-INF/jsp/configmanager-permissions.jsp
  60. 1 1
      server/src/main/webapp/WEB-INF/jsp/configmanager-wordlists.jsp
  61. 1 1
      server/src/main/webapp/WEB-INF/jsp/configmanager.jsp
  62. 1 1
      server/src/main/webapp/WEB-INF/jsp/deleteaccount-agreement.jsp
  63. 1 1
      server/src/main/webapp/WEB-INF/jsp/deleteaccount-confirm.jsp
  64. 1 1
      server/src/main/webapp/WEB-INF/jsp/error-http.jsp
  65. 1 1
      server/src/main/webapp/WEB-INF/jsp/error.jsp
  66. 7 2
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-actionchoice.jsp
  67. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-attributes.jsp
  68. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-enterotp.jsp
  69. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-entertoken.jsp
  70. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-method.jsp
  71. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-remote.jsp
  72. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-responses.jsp
  73. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-search.jsp
  74. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-tokenchoice.jsp
  75. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenusername-complete.jsp
  76. 1 1
      server/src/main/webapp/WEB-INF/jsp/forgottenusername-search.jsp
  77. 1 1
      server/src/main/webapp/WEB-INF/jsp/fragment/footer.jsp
  78. 3 2
      server/src/main/webapp/WEB-INF/jsp/fragment/header.jsp
  79. 1 1
      server/src/main/webapp/WEB-INF/jsp/guest-create.jsp
  80. 1 1
      server/src/main/webapp/WEB-INF/jsp/guest-search.jsp
  81. 1 1
      server/src/main/webapp/WEB-INF/jsp/guest-update.jsp
  82. 1 1
      server/src/main/webapp/WEB-INF/jsp/login-passwordonly.jsp
  83. 13 1
      server/src/main/webapp/WEB-INF/jsp/login.jsp
  84. 1 1
      server/src/main/webapp/WEB-INF/jsp/logout-public.jsp
  85. 1 1
      server/src/main/webapp/WEB-INF/jsp/logout.jsp
  86. 1 1
      server/src/main/webapp/WEB-INF/jsp/newuser-agreement.jsp
  87. 1 1
      server/src/main/webapp/WEB-INF/jsp/newuser-entercode.jsp
  88. 1 1
      server/src/main/webapp/WEB-INF/jsp/newuser-profilechoice.jsp
  89. 1 1
      server/src/main/webapp/WEB-INF/jsp/newuser-wait.jsp
  90. 1 1
      server/src/main/webapp/WEB-INF/jsp/newuser.jsp
  91. 1 1
      server/src/main/webapp/WEB-INF/jsp/setupotpsecret-existing.jsp
  92. 1 1
      server/src/main/webapp/WEB-INF/jsp/setupotpsecret-success.jsp
  93. 1 1
      server/src/main/webapp/WEB-INF/jsp/setupotpsecret-test.jsp
  94. 1 1
      server/src/main/webapp/WEB-INF/jsp/setupotpsecret.jsp
  95. 1 1
      server/src/main/webapp/WEB-INF/jsp/setupresponses-confirm.jsp
  96. 1 1
      server/src/main/webapp/WEB-INF/jsp/setupresponses-existing.jsp
  97. 1 1
      server/src/main/webapp/WEB-INF/jsp/setupresponses-helpdesk.jsp
  98. 1 1
      server/src/main/webapp/WEB-INF/jsp/setupresponses.jsp
  99. 1 1
      server/src/main/webapp/WEB-INF/jsp/shortcut.jsp
  100. 1 1
      server/src/main/webapp/WEB-INF/jsp/success.jsp

+ 19 - 1
server/pom.xml

@@ -332,6 +332,24 @@
                     </execution>
                 </executions>
             </plugin>
+            <plugin>
+                <!-- creates the classes directory early in the build so the attribution plugin doesn't fail -->
+                <artifactId>maven-antrun-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>generate-sources</id>
+                        <phase>generate-sources</phase>
+                        <configuration>
+                            <tasks>
+                                <mkdir dir="${project.build.directory}/classes"/>
+                            </tasks>
+                        </configuration>
+                        <goals>
+                            <goal>run</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
             <plugin>
                 <!-- builds xml file of dependencies and licenses for use in about page -->
                 <groupId>com.github.jinnovations</groupId>
@@ -342,7 +360,7 @@
                         <goals>
                             <goal>generate-attribution-file</goal>
                         </goals>
-                        <phase>process-resources</phase>
+                        <phase>generate-resources</phase>
                     </execution>
                 </executions>
                 <configuration>

+ 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( )

+ 1 - 1
server/src/main/java/password/pwm/PwmConstants.java

@@ -112,7 +112,7 @@ public abstract class PwmConstants
     public static final String SESSION_ATTR_PWM_SESSION = "PwmSession";
     public static final String SESSION_ATTR_BEANS = "SessionBeans";
     public static final String SESSION_ATTR_PWM_APP_NONCE = "PwmApplication-Nonce";
-    public static final String SESSION_ATTR_FORGOTTEN_PW_USERINFO_CACHE = "ForgottenPw-UserInfoCache";
+    public static final String REQUEST_ATTR_FORGOTTEN_PW_USERINFO_CACHE = "ForgottenPw-UserInfoCache";
 
     public static final PwmHashAlgorithm SETTING_CHECKSUM_HASH_METHOD = PwmHashAlgorithm.SHA256;
 

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

+ 25 - 11
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 ),
@@ -724,8 +728,6 @@ public enum PwmSetting
             "response.hashMethod", PwmSettingSyntax.SELECT, PwmSettingCategory.RECOVERY_SETTINGS ),
     FORGOTTEN_USER_POST_ACTIONS(
             "recovery.postActions", PwmSettingSyntax.ACTION, PwmSettingCategory.RECOVERY_SETTINGS ),
-    CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME(
-            "challenge.enforceMinimumPasswordLifetime", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS ),
     RECOVERY_BOGUS_USER_ENABLE(
             "recovery.bogus.user.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS ),
 
@@ -752,6 +754,9 @@ public enum PwmSetting
             "recovery.allowWhenLocked", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_OPTIONS ),
     TOKEN_RESEND_ENABLE(
             "recovery.token.resend.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_OPTIONS ),
+    RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS(
+            "recovery.minimumPasswordLifetimeOptions", PwmSettingSyntax.SELECT, PwmSettingCategory.RECOVERY_OPTIONS ),
+
 
     // recovery oauth
     RECOVERY_OAUTH_ID_LOGIN_URL(
@@ -1157,6 +1162,11 @@ public enum PwmSetting
 
 
     // deprecated.
+
+    // deprecated 2018-02-27
+    RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME(
+            "challenge.enforceMinimumPasswordLifetime", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_OPTIONS ),
+
     UPDATE_PROFILE_CHECK_QUERY_MATCH(
             "updateAttributes.check.queryMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.UPDATE_PROFILE ),
     PASSWORD_POLICY_AD_COMPLEXITY(
@@ -1168,6 +1178,10 @@ public enum PwmSetting
     HELPDESK_ENABLE_OTP_VERIFY(
             "helpdesk.otp.verify", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_BASE ),;
 
+
+
+
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSetting.class );
 
     private final String key;

+ 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 ),

+ 30 - 0
server/src/main/java/password/pwm/config/option/RecoveryMinLifetimeOption.java

@@ -0,0 +1,30 @@
+/*
+ * 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.option;
+
+public enum RecoveryMinLifetimeOption implements ConfigurationOption
+{
+    ALLOW,
+    UNLOCKONLY,
+    NONE
+}

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

+ 17 - 30
server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -1391,39 +1391,26 @@ public class StoredConfigurationImpl implements StoredConfiguration
                 }
             }
 
-            /*
+            for ( final String profileID : storedConfiguration.profilesForSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME ) )
             {
-                if (!storedConfiguration.isDefaultValue(PwmSetting.CHALLENGE_REQUIRE_RESPONSES)) {
-                    final StoredValue configValue = storedConfiguration.readSetting(PwmSetting.RECOVERY_VERIFICATION_METHODS, "default");
-                    final VerificationMethodValue.VerificationMethodSettings existingSettings = (VerificationMethodValue.VerificationMethodSettings)configValue.toNativeObject();
-                    final Map<RecoveryVerificationMethod,VerificationMethodValue.VerificationMethodSetting> newMethods = new HashMap<>();
-                    newMethods.putAll(existingSettings.getMethodSettings());
-                    VerificationMethodValue.VerificationMethodSetting setting = new VerificationMethodValue.VerificationMethodSetting(VerificationMethodValue.EnabledState.disabled);
-                    newMethods.put(RecoveryVerificationMethod.CHALLENGE_RESPONSES,setting);
-                    final VerificationMethodValue.VerificationMethodSettings newSettings = new VerificationMethodValue.VerificationMethodSettings(
-                            newMethods,
-                            existingSettings.getMinOptionalRequired()
-                    );
-                    storedConfiguration.writeSetting(PwmSetting.RECOVERY_VERIFICATION_METHODS, "default", new VerificationMethodValue(newSettings), actor);
-                }
-            }
-
-            {
-                if (!storedConfiguration.isDefaultValue(PwmSetting.FORGOTTEN_PASSWORD_REQUIRE_OTP)) {
-                    final StoredValue configValue = storedConfiguration.readSetting(PwmSetting.RECOVERY_VERIFICATION_METHODS, "default");
-                    final VerificationMethodValue.VerificationMethodSettings existingSettings = (VerificationMethodValue.VerificationMethodSettings)configValue.toNativeObject();
-                    final Map<RecoveryVerificationMethod,VerificationMethodValue.VerificationMethodSetting> newMethods = new HashMap<>();
-                    newMethods.putAll(existingSettings.getMethodSettings());
-                    VerificationMethodValue.VerificationMethodSetting setting = new VerificationMethodValue.VerificationMethodSetting(VerificationMethodValue.EnabledState.required);
-                    newMethods.put(RecoveryVerificationMethod.CHALLENGE_RESPONSES,setting);
-                    final VerificationMethodValue.VerificationMethodSettings newSettings = new VerificationMethodValue.VerificationMethodSettings(
-                            newMethods,
-                            existingSettings.getMinOptionalRequired()
-                    );
-                    storedConfiguration.writeSetting(PwmSetting.FORGOTTEN_PASSWORD_REQUIRE_OTP, "default", new VerificationMethodValue(newSettings), actor);
+                if ( !storedConfiguration.isDefaultValue( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ) )
+                {
+                    final boolean enforceEnabled = ( boolean ) storedConfiguration.readSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ).toNativeObject();
+                    final StoredValue value = enforceEnabled
+                            ? new StringValue( "NONE" )
+                            : new StringValue( "ALLOW" );
+                    final ValueMetaData existingData = storedConfiguration.readSettingMetadata( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID );
+                    LOGGER.warn( "converting deprecated non-default setting "
+                            + PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME.toMenuLocationDebug(profileID,PwmConstants.DEFAULT_LOCALE) + "/" + profileID
+                            + " to replacement setting " + PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE )
+                            + ", value=" + value.toNativeObject().toString() );
+                    final UserIdentity newActor = existingData != null && existingData.getUserIdentity() != null
+                            ? existingData.getUserIdentity()
+                            : actor;
+                    storedConfiguration.writeSetting( PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS, profileID, value, newActor );
+                    storedConfiguration.resetSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID, actor );
                 }
             }
-            */
         }
     }
 

+ 12 - 14
server/src/main/java/password/pwm/health/LDAPStatusChecker.java

@@ -50,7 +50,6 @@ import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
-import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.UserInfo;
@@ -312,20 +311,19 @@ public class LDAPStatusChecker implements HealthChecker
                             passwordStatus = userInfo.getPasswordStatus();
                         }
 
-                        try
-                        {
-                            PasswordUtility.checkIfPasswordWithinMinimumLifetime(
-                                    theUser,
-                                    SessionLabel.HEALTH_SESSION_LABEL,
-                                    passwordPolicy,
-                                    pwdLastModified,
-                                    passwordStatus
-                            );
-                        }
-                        catch ( PwmException e )
                         {
-                            LOGGER.trace( SessionLabel.HEALTH_SESSION_LABEL, "skipping test user password set: " + e.getMessage() );
-                            doPasswordChange = false;
+                            final boolean withinMinLifetime = PasswordUtility.isPasswordWithinMinimumLifetimeImpl(
+                                            theUser,
+                                            SessionLabel.HEALTH_SESSION_LABEL,
+                                            passwordPolicy,
+                                            pwdLastModified,
+                                            passwordStatus
+                                    );
+                            if ( withinMinLifetime )
+                            {
+                                LOGGER.trace( SessionLabel.HEALTH_SESSION_LABEL, "skipping test user password set due to password being within minimum lifetime" );
+                                doPasswordChange = false;
+                            }
                         }
                     }
                     if ( doPasswordChange )

+ 1 - 0
server/src/main/java/password/pwm/http/PwmRequestAttribute.java

@@ -80,6 +80,7 @@ public enum PwmRequestAttribute
     ForgottenPasswordOtpRecord,
     ForgottenPasswordResendTokenEnabled,
     ForgottenPasswordTokenDestItems,
+    ForgottenPasswordInhibitPasswordReset,
 
     GuestCurrentExpirationDate,
     GuestMaximumExpirationDate,

+ 1 - 0
server/src/main/java/password/pwm/http/PwmRequestFlag.java

@@ -35,4 +35,5 @@ public enum PwmRequestFlag
     ALWAYS_EXPAND_MESSAGE_TEXT,
     INCLUDE_CONFIG_CSS,
     INCLUDE_IAS_ANGULAR,
+    INCLUDE_IAS_CSS
 }

+ 19 - 0
server/src/main/java/password/pwm/http/bean/PwmSessionBean.java

@@ -29,6 +29,9 @@ import password.pwm.error.ErrorInformation;
 
 import java.io.Serializable;
 import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 @Getter
@@ -41,6 +44,17 @@ public abstract class PwmSessionBean implements Serializable
         AUTHENTICATED,
     }
 
+    private static List<Class<? extends PwmSessionBean>> publicBeans;
+
+    static
+    {
+        final List<Class<? extends PwmSessionBean>> list = new ArrayList<>(  );
+        list.add( ActivateUserBean.class );
+        list.add( ForgottenPasswordBean.class );
+        list.add( NewUserBean.class );
+        publicBeans = Collections.unmodifiableList( list );
+    }
+
     private String guid;
     private Instant timestamp;
     private ErrorInformation lastError;
@@ -48,4 +62,9 @@ public abstract class PwmSessionBean implements Serializable
     public abstract Type getType( );
 
     public abstract Set<SessionBeanMode> supportedModes( );
+
+    public static List<Class<? extends PwmSessionBean>> getPublicBeans()
+    {
+        return publicBeans;
+    }
 }

+ 17 - 0
server/src/main/java/password/pwm/http/servlet/AbstractPwmServlet.java

@@ -154,6 +154,23 @@ public abstract class AbstractPwmServlet extends HttpServlet implements PwmServl
             }
 
             outputUnrecoverableException( pwmRequest, pue );
+
+            clearModuleBeans( pwmRequest );
+        }
+    }
+
+    private void clearModuleBeans( final PwmRequest pwmRequest )
+    {
+        for ( final Class theClass : PwmSessionBean.getPublicBeans() )
+        {
+            try
+            {
+                pwmRequest.getPwmApplication().getSessionStateService().clearBean( pwmRequest, theClass );
+            }
+            catch ( PwmUnrecoverableException e )
+            {
+                LOGGER.debug( pwmRequest, "error while clearing module bean during after module error output: " + e.getMessage() );
+            }
         }
     }
 

+ 1 - 0
server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataBean.java

@@ -41,6 +41,7 @@ public class UserDebugDataBean implements Serializable
 
     private final PublicUserInfoBean publicUserInfoBean;
     private final boolean passwordReadable;
+    private final boolean passwordWithinMinimumLifetime;
     private final Map<Permission, String> permissions;
 
     private final PwmPasswordPolicy ldapPasswordPolicy;

+ 1 - 0
server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataReader.java

@@ -92,6 +92,7 @@ public class UserDebugDataReader
                 .ldapPasswordPolicy( ldapPasswordPolicy )
                 .configuredPasswordPolicy( configPasswordPolicy )
                 .passwordReadable( readablePassword )
+                .passwordWithinMinimumLifetime( userInfo.isWithinPasswordMinimumLifetime() )
                 .build();
 
         return userDebugData;

+ 2 - 14
server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java

@@ -23,7 +23,6 @@
 package password.pwm.http.servlet.changepw;
 
 import com.novell.ldapchai.ChaiUser;
-import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import password.pwm.AppProperty;
 import password.pwm.Permission;
@@ -441,7 +440,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
     public void nextStep(
             final PwmRequest pwmRequest
     )
-            throws IOException, PwmUnrecoverableException, ChaiUnavailableException, ServletException
+            throws IOException, PwmUnrecoverableException, ServletException
     {
         final ChangePasswordBean changePasswordBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, ChangePasswordBean.class );
 
@@ -546,18 +545,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
             return ProcessStatus.Halt;
         }
 
-        try
-        {
-            ChangePasswordServletUtil.checkMinimumLifetime( pwmApplication, pwmSession, changePasswordBean, pwmSession.getUserInfo() );
-        }
-        catch ( PwmOperationalException e )
-        {
-            throw new PwmUnrecoverableException( e.getErrorInformation() );
-        }
-        catch ( ChaiException e )
-        {
-            throw PwmUnrecoverableException.fromChaiException( e );
-        }
+        ChangePasswordServletUtil.checkMinimumLifetime( pwmApplication, pwmSession, changePasswordBean, pwmSession.getUserInfo() );
 
         return ProcessStatus.Continue;
     }

+ 17 - 17
server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServletUtil.java

@@ -36,12 +36,12 @@ import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmError;
-import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.http.bean.ChangePasswordBean;
+import password.pwm.http.servlet.forgottenpw.ForgottenPasswordUtil;
 import password.pwm.ldap.PasswordChangeProgressChecker;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.auth.AuthenticationType;
@@ -171,34 +171,34 @@ public class ChangePasswordServletUtil
             final ChangePasswordBean changePasswordBean,
             final UserInfo userInfo
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
+            throws PwmUnrecoverableException
     {
         if ( changePasswordBean.isNextAllowedTimePassed() )
         {
             return;
         }
 
-        try
-        {
-            PasswordUtility.checkIfPasswordWithinMinimumLifetime(
-                    pwmSession.getSessionManager().getActor( pwmApplication ),
-                    pwmSession.getLabel(),
-                    userInfo.getPasswordPolicy(),
-                    userInfo.getPasswordLastModifiedTime(),
-                    userInfo.getPasswordStatus()
-            );
-        }
-        catch ( PwmException e )
+        if ( userInfo.isWithinPasswordMinimumLifetime() )
         {
-            final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME );
-            if ( !enforceFromForgotten && userInfo.isRequiresNewPassword() )
+            boolean allowChange = false;
+            if ( pwmSession.getLoginInfoBean().getAuthFlags().contains( AuthenticationType.AUTH_FROM_PUBLIC_MODULE ) )
+            {
+                allowChange = ForgottenPasswordUtil.permitPwChangeDuringMinLifetime(
+                        pwmApplication,
+                        pwmSession.getLabel(),
+                        userInfo.getUserIdentity()
+                );
+
+            }
+
+            if ( allowChange )
             {
                 LOGGER.debug( pwmSession, "current password is too young, but skipping enforcement of minimum lifetime check due to setting "
-                        + PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME.toMenuLocationDebug( null, pwmSession.getSessionStateBean().getLocale() ) );
+                        + PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS.toMenuLocationDebug( null, pwmSession.getSessionStateBean().getLocale() ) );
             }
             else
             {
-                throw new PwmUnrecoverableException( e.getErrorInformation() );
+                PasswordUtility.throwPasswordTooSoonException( userInfo, pwmSession.getLabel() );
             }
         }
 

+ 58 - 39
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -41,6 +41,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.RecoveryAction;
+import password.pwm.config.option.RecoveryMinLifetimeOption;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
@@ -223,19 +224,12 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         pwmRequest.getPwmApplication().getSessionStateService().clearBean( pwmRequest, ForgottenPasswordBean.class );
     }
 
-    static ForgottenPasswordProfile forgottenPasswordProfile( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
-    {
-        final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-        return pwmRequest.getConfig().getForgottenPasswordProfiles().get( forgottenPasswordBean.getForgottenPasswordProfileID() );
-    }
-
-
     @ActionHandler( action = "actionChoice" )
     private ProcessStatus processActionChoice( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException, ServletException, IOException, ChaiUnavailableException
     {
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-        final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest );
+        final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
 
         final boolean resendEnabled = forgottenPasswordProfile.readSettingAsBoolean( PwmSetting.TOKEN_RESEND_ENABLE );
 
@@ -245,6 +239,19 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             forgottenPasswordBean.getProgress().clearTokenSentStatus();
         }
 
+
+        final boolean disallowAllButUnlock;
+        {
+            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+            final RecoveryMinLifetimeOption minLifetimeOption = forgottenPasswordProfile.readSettingAsEnum(
+                    PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS,
+                    RecoveryMinLifetimeOption.class
+            );
+            disallowAllButUnlock = minLifetimeOption == RecoveryMinLifetimeOption.UNLOCKONLY
+                    && userInfo.isPasswordLocked();
+        }
+
+
         if ( forgottenPasswordBean.getProgress().isAllPassed() )
         {
             final String choice = pwmRequest.readParameterAsString( "choice" );
@@ -259,6 +266,11 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                         break;
 
                     case resetPassword:
+                        if ( disallowAllButUnlock )
+                        {
+                            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+                            PasswordUtility.throwPasswordTooSoonException( userInfo, pwmRequest.getSessionLabel() );
+                        }
                         this.executeResetPassword( pwmRequest );
                         break;
 
@@ -772,8 +784,10 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
     private ProcessStatus processResendToken( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException, IOException
     {
+        final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
+
         {
-            final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest );
+            final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
             final boolean resendEnabled = forgottenPasswordProfile.readSettingAsBoolean( PwmSetting.TOKEN_RESEND_ENABLE );
             if ( !resendEnabled )
             {
@@ -783,7 +797,6 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             }
         }
 
-        final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
 
         if ( !forgottenPasswordBean.getProgress().isTokenSent() )
         {
@@ -927,7 +940,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             return;
         }
 
-        final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest );
+        final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
         {
             final Map<String, ForgottenPasswordProfile> profileIDList = pwmRequest.getConfig().getForgottenPasswordProfiles();
             final String profileDebugMsg = forgottenPasswordProfile != null && profileIDList != null && profileIDList.size() > 1
@@ -1051,31 +1064,40 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         }
 
         final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-        try
+        if ( userInfo == null )
         {
-            final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME );
-            if ( enforceFromForgotten )
-            {
-                final ChaiUser theUser = pwmApplication.getProxiedChaiUser( forgottenPasswordBean.getUserIdentity() );
-                PasswordUtility.checkIfPasswordWithinMinimumLifetime(
-                        theUser,
-                        pwmRequest.getSessionLabel(),
-                        userInfo.getPasswordPolicy(),
-                        userInfo.getPasswordLastModifiedTime(),
-                        userInfo.getPasswordStatus()
-                );
-            }
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_UNKNOWN, "unable to load userInfo while processing forgotten password controller" );
         }
-        catch ( PwmOperationalException e )
+
+        // check if user's pw is within min lifetime window
+        final RecoveryMinLifetimeOption minLifetimeOption = forgottenPasswordProfile.readSettingAsEnum(
+                PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS,
+                RecoveryMinLifetimeOption.class
+        );
+        if ( minLifetimeOption == RecoveryMinLifetimeOption.NONE
+                || (
+                !userInfo.isPasswordLocked()
+                        &&  minLifetimeOption == RecoveryMinLifetimeOption.UNLOCKONLY )
+                )
         {
-            throw new PwmUnrecoverableException( e.getErrorInformation() );
+            if ( userInfo.isWithinPasswordMinimumLifetime() )
+            {
+                PasswordUtility.throwPasswordTooSoonException( userInfo, pwmRequest.getSessionLabel() );
+            }
         }
 
+        final boolean disallowAllButUnlock = minLifetimeOption == RecoveryMinLifetimeOption.UNLOCKONLY
+                && userInfo.isPasswordLocked();
+
         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 )
         {
+            if ( disallowAllButUnlock )
+            {
+                PasswordUtility.throwPasswordTooSoonException( userInfo, pwmRequest.getSessionLabel() );
+            }
             ForgottenPasswordUtil.doActionSendNewPassword( pwmRequest );
             return;
         }
@@ -1086,18 +1108,11 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
             if ( !passwordStatus.isExpired() && !passwordStatus.isPreExpired() )
             {
-                try
+                if ( userInfo.isPasswordLocked() )
                 {
-                    final ChaiUser theUser = pwmApplication.getProxiedChaiUser( forgottenPasswordBean.getUserIdentity() );
-                    if ( theUser.isPasswordLocked() )
-                    {
-                        pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_ACTION_CHOICE );
-                        return;
-                    }
-                }
-                catch ( ChaiOperationException e )
-                {
-                    LOGGER.error( pwmRequest, "chai operation error checking user lock status: " + e.getMessage() );
+                    pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordInhibitPasswordReset, Boolean.TRUE );
+                    pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_ACTION_CHOICE );
+                    return;
                 }
             }
         }
@@ -1416,7 +1431,11 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
                 if ( !progress.getSatisfiedMethods().contains( IdentityVerificationMethod.TOKEN ) )
                 {
-                    final boolean resendEnabled = forgottenPasswordProfile( pwmRequest ).readSettingAsBoolean( PwmSetting.TOKEN_RESEND_ENABLE );
+                    final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile(
+                            pwmRequest.getPwmApplication(),
+                            forgottenPasswordBean
+                    );
+                    final boolean resendEnabled = forgottenPasswordProfile.readSettingAsBoolean( PwmSetting.TOKEN_RESEND_ENABLE );
                     pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordResendTokenEnabled, resendEnabled );
                     pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_ENTER_TOKEN );
                     return;
@@ -1455,7 +1474,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
             case OAUTH:
                 forgottenPasswordBean.getProgress().setInProgressVerificationMethod( IdentityVerificationMethod.OAUTH );
-                final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest );
+                final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
                 final OAuthSettings oAuthSettings = OAuthSettings.forForgottenPassword( forgottenPasswordProfile );
                 final OAuthMachine oAuthMachine = new OAuthMachine( oAuthSettings );
                 pwmRequest.getPwmApplication().getSessionStateService().saveSessionBeans( pwmRequest );

+ 83 - 11
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -42,6 +42,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.RecoveryAction;
+import password.pwm.config.option.RecoveryMinLifetimeOption;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.profile.ProfileType;
 import password.pwm.config.profile.ProfileUtility;
@@ -88,7 +89,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-class ForgottenPasswordUtil
+public class ForgottenPasswordUtil
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( ForgottenPasswordUtil.class );
 
@@ -146,12 +147,12 @@ class ForgottenPasswordUtil
             return null;
         }
 
-        final String cacheKey = PwmConstants.SESSION_ATTR_FORGOTTEN_PW_USERINFO_CACHE;
+        final String cacheKey = PwmConstants.REQUEST_ATTR_FORGOTTEN_PW_USERINFO_CACHE;
 
         final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
 
         {
-            final UserInfo userInfoFromSession = ( UserInfo ) pwmRequest.getHttpServletRequest().getSession().getAttribute( cacheKey );
+            final UserInfo userInfoFromSession = ( UserInfo ) pwmRequest.getHttpServletRequest().getAttribute( cacheKey );
             if ( userInfoFromSession != null )
             {
                 if ( userIdentity.equals( userInfoFromSession.getUserIdentity() ) )
@@ -173,7 +174,7 @@ class ForgottenPasswordUtil
                 userIdentity, pwmRequest.getLocale()
         );
 
-        pwmRequest.getHttpServletRequest().getSession().setAttribute( cacheKey, userInfo );
+        pwmRequest.getHttpServletRequest().setAttribute( cacheKey, userInfo );
 
         return userInfo;
     }
@@ -572,7 +573,7 @@ class ForgottenPasswordUtil
     {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final ForgottenPasswordBean forgottenPasswordBean = ForgottenPasswordServlet.forgottenPasswordBean( pwmRequest );
-        final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordServlet.forgottenPasswordProfile( pwmRequest );
+        final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
         final RecoveryAction recoveryAction = ForgottenPasswordUtil.getRecoveryAction( pwmApplication.getConfig(), forgottenPasswordBean );
 
         LOGGER.trace( pwmRequest, "beginning process to send new password to user" );
@@ -744,6 +745,77 @@ class ForgottenPasswordUtil
         forgottenPasswordBean.setRecoveryFlags( recoveryFlags );
     }
 
+    public static boolean permitPwChangeDuringMinLifetime(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        ForgottenPasswordProfile forgottenPasswordProfile = null;
+        try
+        {
+            forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile(
+                    pwmApplication,
+                    sessionLabel,
+                    userIdentity
+            );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.debug( sessionLabel, "can't read user's forgotten password profile - assuming no profile assigned, error: " + e.getMessage() );
+        }
+
+        if ( forgottenPasswordProfile == null )
+        {
+            // default is true.
+            return true;
+        }
+
+        final RecoveryMinLifetimeOption option = forgottenPasswordProfile.readSettingAsEnum(
+                PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS,
+                RecoveryMinLifetimeOption.class
+        );
+        return option == RecoveryMinLifetimeOption.ALLOW;
+    }
+
+    private static ForgottenPasswordProfile forgottenPasswordProfile(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        final String forgottenProfileID = ProfileUtility.discoverProfileIDforUser(
+                pwmApplication,
+                sessionLabel,
+                userIdentity,
+                ProfileType.ForgottenPassword
+        );
+
+        if ( StringUtil.isEmpty( forgottenProfileID ) )
+        {
+            final String msg = "user does not have a forgotten password profile assigned";
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_NO_PROFILE_ASSIGNED, msg );
+        }
+
+        return pwmApplication.getConfig().getForgottenPasswordProfiles().get( forgottenProfileID );
+    }
+
+    static ForgottenPasswordProfile forgottenPasswordProfile(
+            final PwmApplication pwmApplication,
+            final ForgottenPasswordBean forgottenPasswordBean
+    )
+    {
+        final String forgottenProfileID = forgottenPasswordBean.getForgottenPasswordProfileID();
+        if ( StringUtil.isEmpty( forgottenProfileID ) )
+        {
+            throw new IllegalStateException( "cannot load forgotten profile without ID registered in bean" );
+        }
+        return pwmApplication.getConfig().getForgottenPasswordProfiles().get( forgottenProfileID );
+    }
+
+
     static void initForgottenPasswordBean(
             final PwmRequest pwmRequest,
             final UserIdentity userIdentity,
@@ -760,13 +832,13 @@ class ForgottenPasswordUtil
 
         final UserInfo userInfo = readUserInfo( pwmRequest, forgottenPasswordBean );
 
-        final String forgottenProfileID = ProfileUtility.discoverProfileIDforUser( pwmApplication, sessionLabel, userIdentity, ProfileType.ForgottenPassword );
-        if ( forgottenProfileID == null || forgottenProfileID.isEmpty() )
-        {
-            throw new PwmUnrecoverableException( PwmError.ERROR_NO_PROFILE_ASSIGNED.toInfo() );
-        }
+        final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile(
+                pwmApplication,
+                pwmRequest.getSessionLabel(),
+                userIdentity
+        );
+        final String forgottenProfileID = forgottenPasswordProfile.getIdentifier();
         forgottenPasswordBean.setForgottenPasswordProfileID( forgottenProfileID );
-        final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordServlet.forgottenPasswordProfile( pwmRequest );
 
         final ForgottenPasswordBean.RecoveryFlags recoveryFlags = calculateRecoveryFlags(
                 pwmApplication,

+ 1 - 1
server/src/main/java/password/pwm/http/tag/PwmScriptRefTag.java

@@ -59,7 +59,7 @@ public class PwmScriptRefTag extends TagSupport
             url = PwmUrlTag.insertContext( pageContext, url );
             url = PwmUrlTag.insertResourceNonce( pwmRequest.getPwmApplication(), url );
 
-            final String output = "<script type=\"text/javascript\" nonce=\"" + cspNonce + "\" src=\"" + url + "\"></script>";
+            final String output = "<script type=\"text/javascript\" nonce=\"" + cspNonce + "\" src=\"" + url + "\"></script><noscript></noscript>";
             pageContext.getOut().write( output );
         }
         catch ( Exception e )

+ 1 - 1
server/src/main/java/password/pwm/http/tag/PwmScriptTag.java

@@ -60,7 +60,7 @@ public class PwmScriptTag extends BodyTagSupport
                 final String strippedTagBody = stripHtmlScriptTags( tagBody );
                 final String output = "<script type=\"text/javascript\" nonce=\"" + pwmRequest.getCspNonce() + "\">"
                         + strippedTagBody
-                        + "</script>";
+                        + "</script><noscript></noscript>";
                 getPreviousOut().write( output );
             }
         }

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

@@ -169,6 +169,8 @@ public enum Display implements PwmDisplayBundle
     Display_WarnExistingOtpSecret,
     Display_WarnExistingResponseTime,
     Display_WarnExistingResponse,
+    Display_WarnJavaScriptNotEnabledTitle,
+    Display_WarnJavaScriptNotEnabledMessage,
     Display_PleaseVerifyOtp,
     Display_OtpRecoveryInfo,
     Display_OtpClearWarning,

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

@@ -72,6 +72,8 @@ public interface UserInfo
 
     boolean isPasswordLocked( ) throws PwmUnrecoverableException;
 
+    boolean isWithinPasswordMinimumLifetime( ) throws PwmUnrecoverableException;
+
     Instant getPasswordLastModifiedTime( ) throws PwmUnrecoverableException;
 
     String getUserEmailAddress( ) throws PwmUnrecoverableException;

+ 1 - 0
server/src/main/java/password/pwm/ldap/UserInfoBean.java

@@ -89,6 +89,7 @@ public class UserInfoBean implements UserInfo
     private final boolean requiresOtpConfig;
     private final boolean requiresUpdateProfile;
     private final boolean requiresInteraction;
+    private final boolean withinPasswordMinimumLifetime;
 
     @Builder.Default
     private Map<String, String> attributes = Collections.emptyMap();

+ 12 - 0
server/src/main/java/password/pwm/ldap/UserInfoReader.java

@@ -794,4 +794,16 @@ public class UserInfoReader implements UserInfo
                 || selfCachedReference.isRequiresOtpConfig()
                 || selfCachedReference.getPasswordStatus().isWarnPeriod();
     }
+
+    @Override
+    public boolean isWithinPasswordMinimumLifetime( ) throws PwmUnrecoverableException
+    {
+        return PasswordUtility.isPasswordWithinMinimumLifetimeImpl(
+                this.chaiUser,
+                this.sessionLabel,
+                this.getPasswordPolicy(),
+                this.getPasswordLastModifiedTime(),
+                this.getPasswordStatus()
+        );
+    }
 }

+ 7 - 1
server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -45,6 +45,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.servlet.forgottenpw.ForgottenPasswordUtil;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecord;
@@ -512,7 +513,12 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
         {
             final Instant date = OracleDSEntries.convertZuluToDate( oracleDSPrePasswordAllowChangeTime );
 
-            final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME );
+            final boolean enforceFromForgotten = !ForgottenPasswordUtil.permitPwChangeDuringMinLifetime(
+                    pwmApplication,
+                    sessionLabel,
+                    userIdentity
+            );
+
             if ( enforceFromForgotten )
             {
                 if ( Instant.now().isBefore( date ) )

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

+ 50 - 18
server/src/main/java/password/pwm/util/operations/PasswordUtility.java

@@ -1234,14 +1234,55 @@ public class PasswordUtility
         return null;
     }
 
-    public static void checkIfPasswordWithinMinimumLifetime(
+    public static void throwPasswordTooSoonException(
+            final UserInfo userInfo,
+            final SessionLabel sessionLabel
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( !userInfo.isWithinPasswordMinimumLifetime() )
+        {
+            return;
+        }
+
+        final Instant lastModified = userInfo.getPasswordLastModifiedTime();
+        final TimeDuration minimumLifetime;
+        {
+            final int minimumLifetimeSeconds = userInfo.getPasswordPolicy().getRuleHelper().readIntValue( PwmPasswordRule.MinimumLifetime );
+            if ( minimumLifetimeSeconds < 1 )
+            {
+                return;
+            }
+
+            if ( userInfo.getPasswordPolicy() == null )
+            {
+                LOGGER.debug( sessionLabel, "skipping minimum lifetime check, password last set time is unknown" );
+                return;
+            }
+
+            minimumLifetime = new TimeDuration( minimumLifetimeSeconds, TimeUnit.SECONDS );
+
+        }
+        final Instant allowedChangeDate = Instant.ofEpochMilli( lastModified.toEpochMilli() + minimumLifetime.getTotalMilliseconds() );
+        final TimeDuration passwordAge = TimeDuration.fromCurrent( lastModified );
+        final String msg = "last password change was at "
+                + JavaHelper.toIsoDate( lastModified )
+                + " and is too recent (" + passwordAge.asCompactString()
+                + " ago), password cannot be changed within minimum lifetime of "
+                + minimumLifetime.asCompactString()
+                + ", next eligible time to change is after " + JavaHelper.toIsoDate( allowedChangeDate );
+        throw PwmUnrecoverableException.newException( PwmError.PASSWORD_TOO_SOON, msg );
+
+    }
+
+    public static boolean isPasswordWithinMinimumLifetimeImpl(
             final ChaiUser chaiUser,
             final SessionLabel sessionLabel,
             final PwmPasswordPolicy passwordPolicy,
             final Instant lastModified,
             final PasswordStatus passwordStatus
     )
-            throws PwmOperationalException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
 
         // for oracle DS; this check is also handled in UserAuthenticator.
@@ -1261,7 +1302,7 @@ public class PasswordUtility
                         throw new PwmUnrecoverableException( errorInformation );
                     }
                 }
-                return;
+                return false;
             }
         }
         catch ( ChaiException e )
@@ -1274,13 +1315,13 @@ public class PasswordUtility
             final int minimumLifetimeSeconds = passwordPolicy.getRuleHelper().readIntValue( PwmPasswordRule.MinimumLifetime );
             if ( minimumLifetimeSeconds < 1 )
             {
-                return;
+                return false;
             }
 
             if ( lastModified == null )
             {
                 LOGGER.debug( sessionLabel, "skipping minimum lifetime check, password last set time is unknown" );
-                return;
+                return false;
             }
 
             minimumLifetime = new TimeDuration( minimumLifetimeSeconds, TimeUnit.SECONDS );
@@ -1296,31 +1337,22 @@ public class PasswordUtility
         if ( lastModified.isAfter( Instant.now() ) )
         {
             LOGGER.debug( sessionLabel, "skipping minimum lifetime check, password lastModified time is in the future" );
-            return;
+            return false;
         }
 
         final boolean passwordTooSoon = passwordAge.isShorterThan( minimumLifetime );
         if ( !passwordTooSoon )
         {
             LOGGER.trace( sessionLabel, "minimum lifetime check passed, password age " );
-            return;
+            return false;
         }
 
         if ( passwordStatus.isExpired() || passwordStatus.isPreExpired() || passwordStatus.isWarnPeriod() )
         {
             LOGGER.debug( sessionLabel, "current password is too young, but skipping enforcement of minimum lifetime check because current password is expired" );
-            return;
+            return false;
         }
 
-        final Instant allowedChangeDate = Instant.ofEpochMilli( lastModified.toEpochMilli() + minimumLifetime.getTotalMilliseconds() );
-        final String errorMsg = "last password change was at "
-                + JavaHelper.toIsoDate( lastModified )
-                + " and is too recent (" + passwordAge.asCompactString()
-                + " ago), password cannot be changed within minimum lifetime of "
-                + minimumLifetime.asCompactString()
-                + ", next eligible time to change is after " + JavaHelper.toIsoDate( allowedChangeDate );
-
-        final ErrorInformation errorInformation = new ErrorInformation( PwmError.PASSWORD_TOO_SOON, errorMsg );
-        throw new PwmOperationalException( errorInformation );
+        return true;
     }
 }

+ 63 - 6
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>
@@ -2311,11 +2352,6 @@
             <value><![CDATA[{"type":"ldapQuery","ldapProfileID":"all","ldapQuery":"(objectClass=*)"}]]></value>
         </default>
     </setting>
-    <setting hidden="false" key="challenge.enforceMinimumPasswordLifetime" level="2" required="true">
-        <default>
-            <value>false</value>
-        </default>
-    </setting>
     <setting hidden="true" key="challenge.profile.list" level="1">
         <regex>^(?!.*all.*)([a-zA-Z][a-zA-Z0-9-]{2,15})$</regex>
         <properties>
@@ -2342,7 +2378,7 @@
             <option value="TOKEN">SMS/Email Token Verification</option>
             <option value="OTP">OTP (Mobile Device) Verification</option>
             <option value="REMOTE_RESPONSES">External Responses</option>
-            <option value="OAUTH">OAuth2</option>
+            <option value="OAUTH">OAuth</option>
         </options>
     </setting>
     <setting hidden="false" key="recovery.oauth.idserver.loginUrl" level="2">
@@ -2535,6 +2571,16 @@
             <value>true</value>
         </default>
     </setting>
+    <setting hidden="false" key="recovery.minimumPasswordLifetimeOptions" level="1" required="true">
+        <default>
+            <value>ALLOW</value>
+        </default>
+        <options>
+            <option value="ALLOW">Allow - Allow normal action (ignore minimum lifetime)</option>
+            <option value="UNLOCKONLY">UnlockOnly - Allow only intruder password unlock</option>
+            <option value="NONE">None - Prohibit usage of the forgotten password module</option>
+        </options>
+    </setting>
     <setting hidden="false" key="forgottenUsername.enable" level="1" required="true">
         <default>
             <value>false</value>
@@ -3830,6 +3876,12 @@
             <value>false</value>
         </default>
     </setting>
+    <setting hidden="true" key="challenge.enforceMinimumPasswordLifetime" level="2" required="true">
+        <!-- deprecated 2018-02-27-->
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
     <!-- DEPRECATED SETTINGS -->
     <category hidden="false" key="TEMPLATES">
     </category>
@@ -3884,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">

+ 3 - 0
server/src/main/resources/password/pwm/i18n/Display.properties

@@ -140,6 +140,7 @@ Display_RecoverTokenSendChoices=To verify your identity, a security code will be
 Display_RecoverTokenSendChoiceEmail=Send code to your registered email address.
 Display_RecoverTokenSendChoiceSMS=Send code to your mobile phone using text messaging (SMS).
 Display_RecoverChoiceReset=Set a new password.  If you have forgotten your password and would like to set a new one, click here.  Your account will also be unlocked when you set a new password.
+Display_RecoverChoiceResetInhibit=Your password can not be changed at this time because it is within the minimum password lifetime limit.
 Display_RecoverChoiceUnlock=Unlock your account.  If you remember your password, you can unlock your account by selecting this option.  Your password will not be changed.
 Display_RecoverEnterCode=To verify your identity, a security code has been sent to you at %1%.  Please click the link in the email or copy and paste the security code here.
 Display_RecoverEnterCodeSMS=To verify your identity, a security code has been sent to your phone at %1%.  Please enter the security code in the message here.
@@ -178,6 +179,8 @@ Display_WarnExistingOtpSecretTime=You have already enrolled your device on <span
 Display_WarnExistingOtpSecret=You have already enrolled your device.  You can test your current device by typing in the generated code below.  If you continue, you can re-configure your current device.
 Display_WarnExistingResponseTime=You have already setup your challenge/response answers on <span class\="timestamp">%1%</span>.  If you continue, you can re-answer your questions.
 Display_WarnExistingResponse=You have already setup your challenge/response answers.  If you continue, you can re-answer your questions.
+Display_WarnJavaScriptNotEnabledTitle=JavaScript is Not Enabled
+Display_WarnJavaScriptNotEnabledMessage=This application relies heavily on JavaScript to function properly.  While a few features may still be available with reduced functionality, it is recommended to enable JavaScript in your browser to have the best experience.
 Display_PleaseVerifyOtp=Please enter the 6-digit verification code from your device.  If your device is not configured to give you a verification code, please go back to the previous page and configure your device.
 Display_OtpRecoveryInfo=Each of these recovery codes can be used exactly one time in the event that you can not access your phone.  Be sure to <a class\="pwm-link-print">print this page</a> or otherwise write down these codes and and store them in a safe place.
 Display_OtpClearWarning=Are you sure you wish to continue?  If you proceed, your existing enrollment will be cleared and you will need to reconfigure your device.

+ 14 - 8
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
@@ -242,7 +244,7 @@ Setting_Description_cas.clearPass.key=<a href\="https://apereo.github.io/cas/4.2
 Setting_Description_cas.clearPass.alg=The algorithm used by the encryption key
 Setting_Description_challenge.allowDuplicateResponses=Enable this to allow duplicate responses in setup security responses
 Setting_Description_challenge.allowSetup.queryMatch=Specify the permissions used to determine if you permits the users to configure challenges.  This LDAP query must return the user or else @PwmAppName@ does not permit the user to configure challenges.
-Setting_Description_challenge.allowUnlock=Enable this option if @PwmAppName@ allows user accounts to be unlocked during forgotten password.  If true, and if the users' accounts are locked due to too many invalid login attempts, and the users' passwords are not expired, then @PwmAppName@ gives the users a chance to unlock their accounts instead of resetting their passwords.
+Setting_Description_challenge.allowUnlock=Enable this option to allow users to intruder unlock their account during forgotten password.  If true, and if the users' accounts are intruder locked due to too many invalid login attempts, and the users' passwords are not expired, then @PwmAppName@ gives the users a chance to unlock their accounts instead of resetting their passwords.
 Setting_Description_challenge.caseInsensitive=Enable to control the case sensitivity of responses.  If enabled, then @PwmAppName@ deems the responses correct even if the case is wrong.  Changing this value does not change existing stored responses -- @PwmAppName@ saves the case sensitive flag on each users' stored responses.
 Setting_Description_challenge.enable=Enable this option to have the save responses page available to users. (Default enabled)
 Setting_Description_challenge.enforceMinimumPasswordLifetime=Enable this option to enforce the minimum password lifetime setting when the users authenticate via Forgotten Password. If this setting is true, the users cannot change their passwords if the minimum password lifetime setting has not passed.  If false, @PwmAppName@ permits the users to change their passwords when they are authenticated via Forgotten Password even if the minimum lifetime setting has not passed.
@@ -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.
@@ -600,6 +603,7 @@ Setting_Description_recovery.allowWhenLocked=Enable this option to allow users t
 Setting_Description_recovery.bogus.user.enable=Enable this option to have forgotten password act as though invalid user searches are valid, and present such users with a bogus forgotten password policy.  This can help prevent username discovery.
 Setting_Description_recovery.enable=Enable this option to have the forgotten password recovery available to users.
 Setting_Description_recovery.form=Specify the form fields for the activate user module. @PwmAppName@ requires the users to enter each attribute. Ideally, @PwmAppName@ requires the users to enter some personal data that is not publicly known.
+Setting_Description_recovery.minimumPasswordLifetimeOptions=Options to control behavior when a user attempts to use the forgotten password module while their password is within the minimum password policy lifetime window of their effective password policy.  These options are only relevant if the user has an effective minimum password lifetime as part of their password policy.
 Setting_Description_recovery.oauth.idserver.attributesUrl=Specify the web service URL provided by the identity server to return attribute data about the user.
 Setting_Description_recovery.oauth.idserver.clientName=Specify the OAuth client ID. The OAuth identity service provider gives you this value.
 Setting_Description_recovery.oauth.idserver.codeResolveUrl=Specify the OAuth Code Resolve Service URL. @PwmAppName@ uses this web service URL to resolve the artifact returned by the OAuth identity server.
@@ -727,7 +731,7 @@ Setting_Label_cas.clearPass.key=CAS ClearPass Encryption Key
 Setting_Label_cas.clearPass.alg=CAS ClearPass Algorithm
 Setting_Label_challenge.allowDuplicateResponses=Allow Duplicate Responses
 Setting_Label_challenge.allowSetup.queryMatch=Save Challenge Permission
-Setting_Label_challenge.allowUnlock=Allow Unlock
+Setting_Label_challenge.allowUnlock=Allow Intruder Unlock
 Setting_Label_challenge.caseInsensitive=Case Insensitive Responses
 Setting_Label_challenge.enable=Enable Setup Responses
 Setting_Label_challenge.enforceMinimumPasswordLifetime=Enforce Minimum Password Lifetime
@@ -809,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
@@ -899,7 +904,7 @@ Setting_Label_intruder.address.resetTime=Intruder Address Reset Time
 Setting_Label_intruder.attribute.checkTime=Intruder Attribute Check Time
 Setting_Label_intruder.attribute.maxAttempts=Intruder Attribute Maximum Attempts
 Setting_Label_intruder.attribute.resetTime=Intruder Attribute Reset Time
-Setting_Label_intruder.enable=Enable Intruder Detection
+Setting_Label_intruder.enable=Enable @PwmAppName@ Intruder Detection
 Setting_Label_intruder.session.maxAttempts=Maximum Intruder Attempts Per Session
 Setting_Label_intruder.storageMethod=Intruder Record Storage Location
 Setting_Label_intruder.tokenDest.checkTime=Intruder Token Destination Check Time
@@ -1085,6 +1090,7 @@ Setting_Label_recovery.allowWhenLocked=Allow Forgotten Password when Locked
 Setting_Label_recovery.bogus.user.enable=Enable Bogus User Policy
 Setting_Label_recovery.enable=Enable Forgotten Password
 Setting_Label_recovery.form=Forgotten Password User Search Form
+Setting_Label_recovery.minimumPasswordLifetimeOptions=Minimum Password Lifetime Options
 Setting_Label_recovery.oauth.idserver.attributesUrl=OAuth Profile Service URL
 Setting_Label_recovery.oauth.idserver.clientName=OAuth Client ID
 Setting_Label_recovery.oauth.idserver.codeResolveUrl=OAuth Code Resolve Service URL

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/accountinformation.jsp

@@ -41,7 +41,7 @@
         <jsp:param name="pwm.PageName" value="Title_UserInformation"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title" style="display: none;"><pwm:display key="Title_UserInformation" displayIfMissing="true"/></div>
+        <h1 id="page-content-title" style="display: none;"><pwm:display key="Title_UserInformation" displayIfMissing="true"/></h1>
         <div class="tab-container" style="width: 100%; height: 100%;">
             <input name="tabs" type="radio" id="tab-1" checked="checked" class="input"/>
             <label for="tab-1" class="label"><pwm:display key="Title_UserInformation"/></label>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/activateuser-agreement.jsp

@@ -36,7 +36,7 @@
         <jsp:param name="pwm.PageName" value="Title_ActivateUser"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
         <% final String expandedText = activateUserBean.getAgreementText(); %>
         <br/><br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/activateuser-entercode.jsp

@@ -34,7 +34,7 @@
         <jsp:param name="pwm.PageName" value="Title_ActivateUser"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></h1>
         <% final ActivateUserBean activateUserBean = JspUtility.getSessionBean(pageContext, ActivateUserBean.class); %>
         <% final String destination = activateUserBean.getTokenDisplayText(); %>
         <p><pwm:display key="Display_RecoverEnterCode" value1="<%=destination%>"/></p>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/activateuser.jsp

@@ -33,7 +33,7 @@
         <jsp:param name="pwm.PageName" value="Title_ActivateUser"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_ActivateUser"/></p>
         <form action="<pwm:current-url/>" method="post" name="activateUser" enctype="application/x-www-form-urlencoded" class="pwm-form">
             <%@ include file="fragment/message.jsp" %>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/admin-activity.jsp

@@ -54,7 +54,7 @@
         <jsp:param name="pwm.PageName" value="<%=PageName%>"/>
     </jsp:include>
     <div id="centerbody" class="wide">
-        <div id="page-content-title"><pwm:display key="Title_UserActivity" bundle="Admin"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_UserActivity" bundle="Admin"/></h1>
         <%@ include file="fragment/admin-nav.jsp" %>
         <div id="ActivityTabContainer" class="tab-container" style="width: 100%; height: 100%;">
             <input name="tabs" type="radio" id="tab-1" checked="checked" class="input"/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/admin-analysis.jsp

@@ -61,7 +61,7 @@
         <jsp:param name="pwm.PageName" value="<%=PageName%>"/>
     </jsp:include>
     <div id="centerbody" class="wide">
-        <div id="page-content-title"><pwm:display key="Title_DataAnalysis" bundle="Admin"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_DataAnalysis" bundle="Admin"/></h1>
         <%@ include file="fragment/admin-nav.jsp" %>
         <div class="tab-container" style="width: 100%; height: 100%;" id="analysis-topLevelTab">
             <input name="tabs" type="radio" id="tab-1" checked="checked" class="input"/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/admin-dashboard.jsp

@@ -53,7 +53,7 @@
         <jsp:param name="pwm.PageName" value="<%=PageName%>"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_Dashboard" bundle="Admin"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_Dashboard" bundle="Admin"/></h1>
         <%@ include file="fragment/admin-nav.jsp" %>
         <div id="DashboardTabContainer" class="tab-container" style="width: 100%; height: 100%;">
             <input name="tabs" type="radio" id="tab-1" checked="checked" class="input"/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/admin-logview.jsp

@@ -48,7 +48,7 @@
         <jsp:param name="pwm.PageName" value="<%=PageName%>"/>
     </jsp:include>
     <div id="centerbody" style="width: 96%; margin-left: 2%; margin-right: 2%; background: white">
-        <div id="page-content-title"><pwm:display key="Title_LogViewer" bundle="Admin" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_LogViewer" bundle="Admin" displayIfMissing="true"/></h1>
         <%@ include file="fragment/admin-nav.jsp" %>
         <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded"
               name="searchForm" id="searchForm" class="pwm-form">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/admin-tokenlookup.jsp

@@ -49,7 +49,7 @@
         <jsp:param name="pwm.PageName" value="<%=PageName%>"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_TokenLookup" bundle="Admin" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_TokenLookup" bundle="Admin" displayIfMissing="true"/></h1>
         <%@ include file="fragment/admin-nav.jsp" %>
         <% final String tokenKey = tokenlookup_pwmRequest.readParameterAsString("token");%>
         <% if (tokenKey != null && tokenKey.length() > 0) { %>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/admin-urlreference.jsp

@@ -31,7 +31,7 @@
         <jsp:param name="pwm.PageName" value="Title_URLReference"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_URLReference" bundle="Admin" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_URLReference" bundle="Admin" displayIfMissing="true"/></h1>
         <%@ include file="fragment/admin-nav.jsp" %>
         <br/>
         <br/>

+ 9 - 1
server/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp

@@ -42,7 +42,7 @@
         <jsp:param name="pwm.PageName" value="User Debug"/>
     </jsp:include>
     <div id="centerbody" class="wide">
-        <div id="page-content-title">User Debug</div>
+        <h1 id="page-content-title">User Debug</h1>
         <%@ include file="fragment/admin-nav.jsp" %>
 
         <% final UserDebugDataBean userDebugDataBean = (UserDebugDataBean)JspUtility.getAttribute(pageContext, PwmRequestAttribute.UserDebugData); %>
@@ -196,6 +196,14 @@
                     <%= JspUtility.freindlyWrite(pageContext, userInfo.isRequiresUpdateProfile()) %>
                 </td>
             </tr>
+            <tr>
+                <td class="key">
+                    Password is Within Minimum Lifetime
+                </td>
+                <td>
+                    <%= JspUtility.freindlyWrite(pageContext, userDebugDataBean.isPasswordWithinMinimumLifetime()) %>
+                </td>
+            </tr>
         </table>
         <br/>
         <table>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/changepassword-agreement.jsp

@@ -39,7 +39,7 @@
         <jsp:param name="pwm.PageName" value="Title_ChangePassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ChangePassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ChangePassword" displayIfMissing="true"/></h1>
         <% final PasswordStatus passwordStatus = JspUtility.getPwmSession(pageContext).getUserInfo().getPasswordStatus(); %>
         <% if (passwordStatus.isExpired() || passwordStatus.isPreExpired() || passwordStatus.isViolatesPolicy()) { %>
         <h1><pwm:display key="Display_PasswordExpired"/></h1><br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/changepassword-complete.jsp

@@ -35,7 +35,7 @@
         <jsp:param name="pwm.PageName" value="Title_ChangePassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ChangePassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ChangePassword" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
         <% final String expandedText = (String) JspUtility.getAttribute(pageContext, PwmRequestAttribute.CompleteText); %>
         <br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/changepassword-form.jsp

@@ -38,7 +38,7 @@
         <jsp:param name="pwm.PageName" value="Title_ChangePassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ChangePassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ChangePassword" displayIfMissing="true"/></h1>
         <% if (passwordStatus.isExpired() || passwordStatus.isPreExpired() || passwordStatus.isViolatesPolicy()) { %>
         <h1><pwm:display key="Display_PasswordExpired"/></h1><br/>
         <% } %>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/changepassword-wait.jsp

@@ -46,7 +46,7 @@
         <jsp:param name="pwm.PageName" value="Title_PleaseWait"/>
     </jsp:include>
     <div id="centerbody" >
-        <div id="page-content-title"><pwm:display key="Title_PleaseWait" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_PleaseWait" displayIfMissing="true"/></h1>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <p><pwm:display key="Display_PleaseWaitPassword"/></p>
         <div class="meteredProgressBar">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/changepassword-warn.jsp

@@ -40,7 +40,7 @@
         <jsp:param name="pwm.PageName" value="Title_PasswordWarning"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_PasswordWarning" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_PasswordWarning" displayIfMissing="true"/></h1>
         <p>
             <% if (uiBean.getPasswordExpirationTime() != null) { %>
             <pwm:display key="Display_PasswordWarn"

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/changepassword.jsp

@@ -36,7 +36,7 @@
         <jsp:param name="pwm.PageName" value="Title_ChangePassword"/>
     </jsp:include>
     <div id="centerbody" ng-app="changepassword.module" ng-controller="ChangePasswordController as $ctrl">
-        <div id="page-content-title"><pwm:display key="Title_ChangePassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ChangePassword" displayIfMissing="true"/></h1>
         <pwm:if test="<%=PwmIfTest.passwordExpired%>">
         <h1><pwm:display key="Display_PasswordExpired"/></h1><br/>
         </pwm:if>

+ 2 - 2
server/src/main/webapp/WEB-INF/jsp/configmanager-certificates.jsp

@@ -41,14 +41,14 @@
     </jsp:include>
     <% if ((Boolean)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ConfigHasCertificates)) { %>
     <div id="centerbody">
-        <div id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></div>
+        <h1 id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></h1>
         <%@ include file="fragment/configmanager-nav.jsp" %>
         <div id="certDebugGrid" class="grid">
         </div>
     </div>
     <% } else { %>
     <div id="centerbody">
-        <div id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></div>
+        <h1 id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></h1>
         <%@ include file="fragment/configmanager-nav.jsp" %>
         <p>No certificates are present in the active configuration.</p>
     </div>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/configmanager-localdb.jsp

@@ -54,7 +54,7 @@
         <jsp:param name="pwm.PageName" value="<%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%>"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></div>
+        <h1 id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></h1>
         <%@ include file="fragment/configmanager-nav.jsp" %>
         <table style="width:550px" id="table-localDBAbout">
             <tr>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/configmanager-login.jsp

@@ -46,7 +46,7 @@
         <jsp:param name="pwm.PageName" value="<%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%>"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></div>
+        <h1 id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></h1>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <form action="<pwm:current-url/>" method="post" id="configLogin" name="configLogin" enctype="application/x-www-form-urlencoded"
               class="pwm-form">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/configmanager-permissions.jsp

@@ -38,7 +38,7 @@
 <%@ include file="fragment/header.jsp" %>
 <body class="nihilo">
 <div style="padding: 10px">
-    <div id="page-content-title"><pwm:display key="Title_LDAPPermissionRecommendations" bundle="Config" displayIfMissing="true"/></div>
+    <h1 id="page-content-title"><pwm:display key="Title_LDAPPermissionRecommendations" bundle="Config" displayIfMissing="true"/></h1>
     <div>
         <a class="menubutton pwm-basic-link" id="MenuItem_DownloadBundle" title="<pwm:display key="Button_DownloadCSV" bundle="Admin"/>"
            href="manager?processAction=<%=ConfigManagerServlet.ConfigManagerAction.downloadPermissionCsv.toString()%>">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/configmanager-wordlists.jsp

@@ -38,7 +38,7 @@
         <jsp:param name="pwm.PageName" value="<%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%>"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></div>
+        <h1 id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></h1>
         <%@ include file="fragment/configmanager-nav.jsp" %>
         <% { %>
         <table style="width:550px" id="table-wordlistInfo">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/configmanager.jsp

@@ -45,7 +45,7 @@
         <jsp:param name="pwm.PageName" value="<%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%>"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></div>
+        <h1 id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></h1>
         <%@ include file="fragment/configmanager-nav.jsp" %>
         <table style="width:550px">
             <tr>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/deleteaccount-agreement.jsp

@@ -36,7 +36,7 @@
         <jsp:param name="pwm.PageName" value="Title_DeleteAccount"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_DeleteAccount" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_DeleteAccount" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
         <% final String expandedText = (String)JspUtility.getAttribute(pageContext, PwmRequestAttribute.AgreementText); %>
         <div class="agreementText"><%= expandedText %></div>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/deleteaccount-confirm.jsp

@@ -36,7 +36,7 @@
         <jsp:param name="pwm.PageName" value="Title_DeleteAccount"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_DeleteAccount" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_DeleteAccount" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
         <p><pwm:display key="Display_DeleteUserConfirm"/></p>
         <div class="buttonbar">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/error-http.jsp

@@ -43,7 +43,7 @@
         <jsp:param name="pwm.PageName" value="Title_Error"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_Error" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_Error" displayIfMissing="true"/></h1>
         <br/>
         <h2>HTTP <%=statusCode%></h2>
         <br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/error.jsp

@@ -43,7 +43,7 @@
         <jsp:param name="pwm.PageName" value="Title_Error"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_Error" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_Error" displayIfMissing="true"/></h1>
         <br/>
         <h2><%=PwmConstants.PWM_APP_NAME%>&nbsp;<%=errorInformation == null ? "" : errorInformation.getError().getErrorCode()%></h2>
         <br/>

+ 7 - 2
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-actionchoice.jsp

@@ -27,13 +27,14 @@
 <%@ taglib uri="pwm" prefix="pwm" %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <%@ include file="fragment/header.jsp" %>
+<% boolean passwordResetInhibit = (boolean)JspUtility.getAttribute( pageContext, PwmRequestAttribute.ForgottenPasswordInhibitPasswordReset ); %>
 <body class="nihilo">
 <div id="wrapper">
     <jsp:include page="fragment/header-body.jsp">
         <jsp:param name="pwm.PageName" value="Title_ForgottenPassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <p><pwm:display key="Display_RecoverPasswordChoices"/></p>
         <table class="noborder">
@@ -61,7 +62,7 @@
             <tr>
                 <td>
                     <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search">
-                        <button class="btn" type="submit" name="submitBtn">
+                        <button class="btn" type="submit" name="submitBtn" <%=passwordResetInhibit?"disabled=\"disabled\"":""%>>
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-key"></span></pwm:if>
                             <pwm:display key="Button_ChangePassword"/>
                         </button>
@@ -71,7 +72,11 @@
                     </form>
                 </td>
                 <td>
+                    <% if (passwordResetInhibit) { %>
+                    <pwm:display key="Display_RecoverChoiceResetInhibit"/>
+                    <% } else { %>
                     <pwm:display key="Display_RecoverChoiceReset"/>
+                    <% } %>
                 </td>
             </tr>
             <tr>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-attributes.jsp

@@ -39,7 +39,7 @@ this is handled this way so on browsers where hiding fields is not possible, the
     <jsp:param name="pwm.PageName" value="Title_RecoverPassword"/>
 </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_RecoverPassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_RecoverPassword" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_RecoverPassword"/></p>
 
         <form name="responseForm" action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" class="pwm-form" autocomplete="off">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-enterotp.jsp

@@ -33,7 +33,7 @@
         <jsp:param name="pwm.PageName" value="Title_ForgottenPassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
         <%
             final OTPUserRecord otp = (OTPUserRecord)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordOtpRecord);
             final String identifier = otp.getIdentifier();

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-entertoken.jsp

@@ -36,7 +36,7 @@
         <jsp:param name="pwm.PageName" value="Title_ForgottenPassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
         <% final ForgottenPasswordBean fpb = JspUtility.getSessionBean(pageContext, ForgottenPasswordBean.class); %>
         <% final String destination = fpb.getProgress().getTokenSentAddress(); %>
         <p><pwm:display key="Display_RecoverEnterCode" value1="<%=destination%>"/></p>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-method.jsp

@@ -40,7 +40,7 @@
         <jsp:param name="pwm.PageName" value="Title_ForgottenPassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <p>
             <pwm:display key="Display_RecoverVerificationChoice"/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-remote.jsp

@@ -35,7 +35,7 @@
         <jsp:param name="pwm.PageName" value="Title_ForgottenPassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
         <%
             final List<VerificationMethodSystem.UserPrompt> prompts = (List<VerificationMethodSystem.UserPrompt>)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordPrompts);
             final String instructions = (String)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordInstructions);

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-responses.jsp

@@ -41,7 +41,7 @@ this is handled this way so on browsers where hiding fields is not possible, the
     <jsp:param name="pwm.PageName" value="Title_RecoverPassword"/>
 </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_RecoverPassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_RecoverPassword" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_RecoverPassword"/></p>
 
         <form name="responseForm" action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" class="pwm-form" autocomplete="off">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-search.jsp

@@ -34,7 +34,7 @@
         <jsp:param name="pwm.PageName" value="Title_ForgottenPassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_ForgottenPassword"/></p>
         <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" autocomplete="off"
               name="searchForm" class="pwm-form" id="searchForm">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-tokenchoice.jsp

@@ -35,7 +35,7 @@
         <jsp:param name="pwm.PageName" value="Title_ForgottenPassword"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <p><pwm:display key="Display_RecoverTokenSendChoices"/></p>
         <table class="noborder">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenusername-complete.jsp

@@ -35,7 +35,7 @@
         <jsp:param name="pwm.PageName" value="Title_ForgottenUsername"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ForgottenUsername" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenUsername" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
         <% final String expandedText = (String) JspUtility.getAttribute(pageContext, PwmRequestAttribute.CompleteText); %>
         <br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenusername-search.jsp

@@ -33,7 +33,7 @@
         <jsp:param name="pwm.PageName" value="Title_ForgottenUsername"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ForgottenUsername" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenUsername" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_ForgottenUsername"/></p>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/fragment/footer.jsp

@@ -82,5 +82,5 @@
         var dojoConfig = { has: { "csp-restrictions":false }, async:true }
     </script>
 </pwm:script>
-<script nonce="<pwm:value name="<%=PwmValue.cspNonce%>"/>" dojo-sync-loader="false" type="text/javascript" src="<pwm:url addContext="true" url='/public/resources/webjars/dojo/dojo.js'/>"></script>
+<script nonce="<pwm:value name="<%=PwmValue.cspNonce%>"/>" dojo-sync-loader="false" type="text/javascript" src="<pwm:url addContext="true" url='/public/resources/webjars/dojo/dojo.js'/>"></script><noscript></noscript>
 <pwm:script-ref url="/public/resources/js/main.js"/>

+ 3 - 2
server/src/main/webapp/WEB-INF/jsp/fragment/header.jsp

@@ -30,6 +30,7 @@
 <%@ page import="password.pwm.http.tag.value.PwmValue" %>
 <%@ page import="password.pwm.http.PwmRequestFlag" %>
 <%@ page import="password.pwm.http.tag.url.PwmThemeURL" %>
+<%@ page import="password.pwm.http.JspUtility" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 
 <head>
@@ -64,8 +65,8 @@
             var PWM_GLOBAL = PWM_GLOBAL || {}; PWM_GLOBAL['startupFunctions'] = [];
         </script>
     </pwm:script>
-    <pwm:if test="<%=PwmIfTest.requestFlag%>" requestFlag="<%=PwmRequestFlag.INCLUDE_IAS_ANGULAR%>">
+    <% if (JspUtility.getPwmRequest(pageContext).isFlag(PwmRequestFlag.INCLUDE_IAS_ANGULAR) || JspUtility.getPwmRequest(pageContext).isFlag(PwmRequestFlag.INCLUDE_IAS_CSS)) { %>
         <link rel="stylesheet" type="text/css" href="<pwm:url url='/public/resources/webjars/pwm-client/vendor/ias-icons.css' addContext="true"/>"/>
         <link rel="stylesheet" type="text/css" href="<pwm:url url='/public/resources/webjars/pwm-client/vendor/ux-ias.css' addContext="true"/>"/>
-    </pwm:if>
+    <% } %>
 </head>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/guest-create.jsp

@@ -37,7 +37,7 @@
         <jsp:param name="pwm.PageName" value="Title_GuestRegistration"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_GuestRegistration" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_GuestRegistration" displayIfMissing="true"/></h1>
         <%@ include file="/WEB-INF/jsp/fragment/guest-nav.jsp" %>
         <p><pwm:display key="Display_GuestRegistration"/></p>
 

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/guest-search.jsp

@@ -31,7 +31,7 @@
         <jsp:param name="pwm.PageName" value="Title_GuestUpdate"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_GuestUpdate" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_GuestUpdate" displayIfMissing="true"/></h1>
         <%@ include file="fragment/guest-nav.jsp"%>
         <p><pwm:display key="Display_GuestUpdate"/></p>
                                                                                       

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/guest-update.jsp

@@ -37,7 +37,7 @@
         <jsp:param name="pwm.PageName" value="Title_GuestUpdate"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_GuestUpdate" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_GuestUpdate" displayIfMissing="true"/></h1>
         <%@ include file="fragment/guest-nav.jsp"%>
         <p><pwm:display key="Display_GuestUpdate"/></p>
         <form action="<pwm:current-url/>" method="post" name="updateGuest" enctype="application/x-www-form-urlencoded" class="pwm-form">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/login-passwordonly.jsp

@@ -34,7 +34,7 @@
         <jsp:param name="pwm.PageName" value="Title_Login"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_Login" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_Login" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_LoginPasswordOnly"/></p>
         <form action="<pwm:current-url/>" method="post" name="login-password" enctype="application/x-www-form-urlencoded" id="login-password" autocomplete="off" class="pwm-form">
             <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>

+ 13 - 1
server/src/main/webapp/WEB-INF/jsp/login.jsp

@@ -23,6 +23,8 @@
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTag" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 
+<% JspUtility.setFlag(pageContext, PwmRequestFlag.INCLUDE_IAS_CSS); %>
+
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
@@ -34,7 +36,17 @@
         <jsp:param name="pwm.PageName" value="Title_Login"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_Login" displayIfMissing="true"/></div>
+        <noscript>
+            <div class="ias-status-message">
+                <div class="ias-status-message-title">
+                    <i class="ias-icon ias-icon-status_warn_thick ias-warn"></i>
+                    <span><pwm:display key="Display_WarnJavaScriptNotEnabledTitle" displayIfMissing="true"/></span>
+                </div>
+                <p><pwm:display key="Display_WarnJavaScriptNotEnabledMessage" displayIfMissing="true"/></p>
+            </div>
+        </noscript>
+
+        <h1 id="page-content-title"><pwm:display key="Title_Login" displayIfMissing="true"/></h1>
         <p>
             <span class="panel-login-display-message"><pwm:display key="Display_Login"/></span>
         </p>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/logout-public.jsp

@@ -36,7 +36,7 @@
         <jsp:param name="pwm.PageName" value="Title_LogoutPublic"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_LogoutPublic" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_LogoutPublic" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
         <p><pwm:display key="Display_LogoutPublic"/></p>
         <br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/logout.jsp

@@ -36,7 +36,7 @@
         <jsp:param name="pwm.PageName" value="Title_Logout"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_Logout" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_Logout" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
         <p><pwm:display key="Display_Logout"/></p>
         <br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/newuser-agreement.jsp

@@ -36,7 +36,7 @@
         <jsp:param name="pwm.PageName" value="Title_NewUser"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
         <% final String expandedText = (String)JspUtility.getAttribute(pageContext, PwmRequestAttribute.AgreementText); %>
         <br/><br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/newuser-entercode.jsp

@@ -40,7 +40,7 @@
         <jsp:param name="pwm.PageName" value="Title_NewUser"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></h1>
         <% if (newUserBean.getTokenVerificationProgress().getPhase() == TokenVerificationProgress.TokenChannel.EMAIL) { %>
         <p><pwm:display key="Display_RecoverEnterCode" value1="<%=tokenVerificationProgress.getTokenDisplayText()%>"/></p>
         <% } else if (newUserBean.getTokenVerificationProgress().getPhase() == TokenVerificationProgress.TokenChannel.SMS) { %>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/newuser-profilechoice.jsp

@@ -42,7 +42,7 @@
         <jsp:param name="pwm.PageName" value="Title_NewUser"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></h1>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <p>
             <pwm:display key="Display_NewUserProfile"/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/newuser-wait.jsp

@@ -50,7 +50,7 @@
         <jsp:param name="pwm.PageName" value="Title_PleaseWait"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_PleaseWait" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_PleaseWait" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_PleaseWaitNewUser"/></p>
         <%@ include file="fragment/message.jsp" %>
         <div class="meteredProgressBar">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/newuser.jsp

@@ -37,7 +37,7 @@
         <jsp:param name="pwm.PageName" value="Title_NewUser"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_NewUser" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_NewUser"/></p>
         <%@ include file="fragment/message.jsp" %>
         <br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/setupotpsecret-existing.jsp

@@ -31,7 +31,7 @@
         <jsp:param name="pwm.PageName" value="Title_SetupOtpSecret" />
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_SetupOtpSecret" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_SetupOtpSecret" displayIfMissing="true"/></h1>
         <p>
             <pwm:if test="<%=PwmIfTest.hasStoredOtpTimestamp%>">
                 <pwm:display key="Display_WarnExistingOtpSecretTime" value1="@OtpSetupTime@"/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/setupotpsecret-success.jsp

@@ -41,7 +41,7 @@
         <jsp:param name="pwm.PageName" value="Title_SetupOtpSecret"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_SetupOtpSecret" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_SetupOtpSecret" displayIfMissing="true"/></h1>
         <p><pwm:display key="Success_OtpSetup" bundle="Message"/></p>
         <%@ include file="fragment/message.jsp" %>
         <br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/setupotpsecret-test.jsp

@@ -39,7 +39,7 @@
         <jsp:param name="pwm.PageName" value="Title_SetupOtpSecret"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_SetupOtpSecret" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_SetupOtpSecret" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_PleaseVerifyOtp"/></p>
         <%@ include file="fragment/message.jsp" %>
         <form action="<pwm:current-url/>" method="post" name="setupOtpSecret"

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/setupotpsecret.jsp

@@ -55,7 +55,7 @@
         <jsp:param name="pwm.PageName" value="Title_SetupOtpSecret"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_SetupOtpSecret" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_SetupOtpSecret" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_SetupOtpSecret"/></p>
         <%@ include file="fragment/message.jsp" %>
         <div class="tab-container">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/setupresponses-confirm.jsp

@@ -37,7 +37,7 @@
         <jsp:param name="pwm.PageName" value="Title_ConfirmResponses"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ConfirmResponses" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ConfirmResponses" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_ConfirmResponses"/></p>
         <%@ include file="fragment/message.jsp" %>
         <br/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/setupresponses-existing.jsp

@@ -37,7 +37,7 @@
         <jsp:param name="pwm.PageName" value="Title_ConfirmResponses"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_ConfirmResponses" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_ConfirmResponses" displayIfMissing="true"/></h1>
         <p>
             <% if (responseInfoBean != null && responseInfoBean.getTimestamp() != null) { %>
             <pwm:display key="Display_WarnExistingResponseTime" value1="@ResponseSetupTime@"/>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/setupresponses-helpdesk.jsp

@@ -37,7 +37,7 @@
         <jsp:param name="pwm.PageName" value="Title_SetupResponses"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_SetupResponses" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_SetupResponses" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_SetupHelpdeskResponses"/></p>
         <form action="<pwm:current-url/>" method="post" name="form-setupResponses"
               enctype="application/x-www-form-urlencoded" id="form-setupResponses" class="pwm-form" autocomplete="off">

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/setupresponses.jsp

@@ -34,7 +34,7 @@
         <jsp:param name="pwm.PageName" value="Title_SetupResponses"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_SetupResponses" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_SetupResponses" displayIfMissing="true"/></h1>
         <p><pwm:display key="Display_SetupResponses"/></p>
         <form action="<pwm:current-url/>" method="post" name="form-setupResponses" enctype="application/x-www-form-urlencoded" id="form-setupResponses" class="pwm-form" autocomplete="off">
             <%@ include file="fragment/message.jsp" %>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/shortcut.jsp

@@ -46,7 +46,7 @@
         <jsp:param name="pwm.PageName" value="Title_Shortcuts"/>
     </jsp:include>
     <div id="centerbody" class="tile-centerbody">
-        <div id="page-content-title"><pwm:display key="Title_Shortcuts" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_Shortcuts" displayIfMissing="true"/></h1>
         <% if (shortcutItems.isEmpty()) { %>
         <p>No shortcuts</p>
         <% } else { %>

+ 1 - 1
server/src/main/webapp/WEB-INF/jsp/success.jsp

@@ -36,7 +36,7 @@
         <jsp:param name="pwm.PageName" value="Title_Success"/>
     </jsp:include>
     <div id="centerbody">
-        <div id="page-content-title"><pwm:display key="Title_Success" displayIfMissing="true"/></div>
+        <h1 id="page-content-title"><pwm:display key="Title_Success" displayIfMissing="true"/></h1>
         <form action="<pwm:url url='<%=PwmServletDefinition.PublicCommand.servletUrl()%>' addContext="true"/>" method="post"
               enctype="application/x-www-form-urlencoded" class="pwm-form">
             <p><pwm:SuccessMessage/></p>

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio