浏览代码

Merge pull request #291 from pwm-project/pwnotify-feature

Pwnotify feature
Jason 7 年之前
父节点
当前提交
f0976811b9
共有 34 个文件被更改,包括 1404 次插入427 次删除
  1. 2 0
      server/src/main/java/password/pwm/AppProperty.java
  2. 6 0
      server/src/main/java/password/pwm/PwmApplication.java
  3. 15 0
      server/src/main/java/password/pwm/config/Configuration.java
  4. 12 1
      server/src/main/java/password/pwm/config/PwmSetting.java
  5. 2 0
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  6. 3 0
      server/src/main/java/password/pwm/config/PwmSettingProperty.java
  7. 2 0
      server/src/main/java/password/pwm/config/PwmSettingSyntax.java
  8. 121 0
      server/src/main/java/password/pwm/config/value/NumericArrayValue.java
  9. 1 0
      server/src/main/java/password/pwm/http/JspUrl.java
  10. 2 0
      server/src/main/java/password/pwm/http/servlet/PwmServletDefinition.java
  11. 1 1
      server/src/main/java/password/pwm/http/servlet/admin/AppDashboardData.java
  12. 103 0
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerPwNotifyServlet.java
  13. 3 1
      server/src/main/java/password/pwm/svc/PwmServiceEnum.java
  14. 0 290
      server/src/main/java/password/pwm/svc/pwnotify/PasswordExpireNotificationEngine.java
  15. 113 0
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyDbStorageService.java
  16. 279 0
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java
  17. 197 0
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java
  18. 59 0
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifySettings.java
  19. 39 0
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyStorageService.java
  20. 42 0
      server/src/main/java/password/pwm/svc/pwnotify/StoredJobState.java
  21. 36 0
      server/src/main/java/password/pwm/svc/pwnotify/StoredNotificationState.java
  22. 4 69
      server/src/main/java/password/pwm/util/cli/CliEnvironment.java
  23. 13 12
      server/src/main/java/password/pwm/util/cli/MainClass.java
  24. 3 3
      server/src/main/java/password/pwm/util/cli/commands/PasswordExpireNotificationCommand.java
  25. 4 11
      server/src/main/java/password/pwm/util/cli/commands/ShellCommand.java
  26. 1 0
      server/src/main/resources/password/pwm/AppProperty.properties
  27. 30 1
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  28. 15 2
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  29. 5 1
      server/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp
  30. 116 0
      server/src/main/webapp/WEB-INF/jsp/configmanager-pwnotify.jsp
  31. 6 0
      server/src/main/webapp/WEB-INF/jsp/fragment/configmanager-nav.jsp
  32. 29 0
      server/src/main/webapp/WEB-INF/jsp/login.jsp
  33. 136 35
      server/src/main/webapp/public/resources/js/configeditor-settings.js
  34. 4 0
      server/src/main/webapp/public/resources/js/configeditor.js

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

@@ -256,6 +256,8 @@ public enum AppProperty
     PASSWORD_STRENGTH_THRESHOLD_WEAK                ( "password.strength.threshold.weak" ),
     PASSWORD_STRENGTH_THRESHOLD_WEAK                ( "password.strength.threshold.weak" ),
     PASSWORD_STRENGTH_THRESHOLD_VERY_WEAK           ( "password.strength.threshold.veryWeak" ),
     PASSWORD_STRENGTH_THRESHOLD_VERY_WEAK           ( "password.strength.threshold.veryWeak" ),
 
 
+    PWNOTIFY__MAX_LDAP_SEARCH_SIZE                  ( "pwNotify.maxLdapSearchSize" ),
+
     PEOPLESEARCH_MAX_VALUE_VERIFYUSERDN             ( "peoplesearch.values.verifyUserDN" ),
     PEOPLESEARCH_MAX_VALUE_VERIFYUSERDN             ( "peoplesearch.values.verifyUserDN" ),
     PEOPLESEARCH_VALUE_MAXCOUNT                     ( "peoplesearch.values.maxCount" ),
     PEOPLESEARCH_VALUE_MAXCOUNT                     ( "peoplesearch.values.maxCount" ),
     PEOPLESEARCH_VIEW_DETAIL_LINKS                  ( "peoplesearch.view.detail.links" ),
     PEOPLESEARCH_VIEW_DETAIL_LINKS                  ( "peoplesearch.view.detail.links" ),

+ 6 - 0
server/src/main/java/password/pwm/PwmApplication.java

@@ -50,6 +50,7 @@ import password.pwm.svc.event.AuditService;
 import password.pwm.svc.event.SystemAuditRecord;
 import password.pwm.svc.event.SystemAuditRecord;
 import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.intruder.RecordType;
+import password.pwm.svc.pwnotify.PwNotifyService;
 import password.pwm.svc.report.ReportService;
 import password.pwm.svc.report.ReportService;
 import password.pwm.svc.sessiontrack.SessionTrackService;
 import password.pwm.svc.sessiontrack.SessionTrackService;
 import password.pwm.svc.shorturl.UrlShortenerService;
 import password.pwm.svc.shorturl.UrlShortenerService;
@@ -565,6 +566,11 @@ public class PwmApplication
         return ( SmsQueueManager ) pwmServiceManager.getService( SmsQueueManager.class );
         return ( SmsQueueManager ) pwmServiceManager.getService( SmsQueueManager.class );
     }
     }
 
 
+    public PwNotifyService getPwNotifyService( )
+    {
+        return ( PwNotifyService ) pwmServiceManager.getService( PwNotifyService.class );
+    }
+
     public UrlShortenerService getUrlShortener( )
     public UrlShortenerService getUrlShortener( )
     {
     {
         return ( UrlShortenerService ) pwmServiceManager.getService( UrlShortenerService.class );
         return ( UrlShortenerService ) pwmServiceManager.getService( UrlShortenerService.class );

+ 15 - 0
server/src/main/java/password/pwm/config/Configuration.java

@@ -55,6 +55,7 @@ import password.pwm.config.value.FormValue;
 import password.pwm.config.value.LocalizedStringArrayValue;
 import password.pwm.config.value.LocalizedStringArrayValue;
 import password.pwm.config.value.LocalizedStringValue;
 import password.pwm.config.value.LocalizedStringValue;
 import password.pwm.config.value.NamedSecretValue;
 import password.pwm.config.value.NamedSecretValue;
+import password.pwm.config.value.NumericArrayValue;
 import password.pwm.config.value.NumericValue;
 import password.pwm.config.value.NumericValue;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.RemoteWebServiceValue;
 import password.pwm.config.value.RemoteWebServiceValue;
@@ -253,6 +254,15 @@ public class Configuration implements SettingReader
             return ( long ) value.toNativeObject();
             return ( long ) value.toNativeObject();
         }
         }
 
 
+        public static List<Long> valueToLongArray( final StoredValue value )
+        {
+            if ( !( value instanceof NumericArrayValue ) )
+            {
+                throw new IllegalArgumentException( "setting value is not readable as number array" );
+            }
+            return ( List<Long> ) value.toNativeObject();
+        }
+
         public static String valueToString( final StoredValue value )
         public static String valueToString( final StoredValue value )
         {
         {
             if ( value == null )
             if ( value == null )
@@ -557,6 +567,11 @@ public class Configuration implements SettingReader
         return challengeProfile;
         return challengeProfile;
     }
     }
 
 
+    public List<Long> readSettingAsLongArray( final PwmSetting setting )
+    {
+        return JavaTypeConverter.valueToLongArray( readStoredValue( setting ) );
+    }
+
     public long readSettingAsLong( final PwmSetting setting )
     public long readSettingAsLong( final PwmSetting setting )
     {
     {
         return JavaTypeConverter.valueToLong( readStoredValue( setting ) );
         return JavaTypeConverter.valueToLong( readStoredValue( setting ) );

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

@@ -346,7 +346,7 @@ public enum PwmSetting
     EMAIL_UNLOCK(
     EMAIL_UNLOCK(
             "email.unlock", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES ),
             "email.unlock", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES ),
     EMAIL_PW_EXPIRATION_NOTICE(
     EMAIL_PW_EXPIRATION_NOTICE(
-            "email.pwExpirationNotice", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES ),
+            "email.pwNotice", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES ),
 
 
 
 
     // sms settings
     // sms settings
@@ -1080,6 +1080,17 @@ public enum PwmSetting
     DATABASE_DEBUG_TRACE(
     DATABASE_DEBUG_TRACE(
             "db.debugTrace.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.DATABASE_ADV ),
             "db.debugTrace.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.DATABASE_ADV ),
 
 
+    // pw expiry notice
+    PW_EXPY_NOTIFY_ENABLE(
+            "pwNotify.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PW_EXP_NOTIFY ),
+    PW_EXPY_NOTIFY_PERMISSION(
+            "pwNotify.queryString", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.PW_EXP_NOTIFY ),
+    PW_EXPY_NOTIFY_INTERVAL(
+            "pwNotify.intervals", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.PW_EXP_NOTIFY ),
+    PW_EXPY_NOTIFY_JOB_OFFSET(
+            "pwNotify.job.offSet", PwmSettingSyntax.DURATION, PwmSettingCategory.PW_EXP_NOTIFY ),
+
+
     // reporting
     // reporting
     REPORTING_ENABLE(
     REPORTING_ENABLE(
             "reporting.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.REPORTING ),
             "reporting.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.REPORTING ),

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

@@ -121,6 +121,8 @@ public enum PwmSettingCategory
     CAS_SSO( SSO ),
     CAS_SSO( SSO ),
     BASIC_SSO( SSO ),
     BASIC_SSO( SSO ),
 
 
+    PW_EXP_NOTIFY( SETTINGS ),
+
     WEB_SERVICES( SETTINGS ),
     WEB_SERVICES( SETTINGS ),
     REST_SERVER( WEB_SERVICES ),
     REST_SERVER( WEB_SERVICES ),
     REST_CLIENT( WEB_SERVICES ),
     REST_CLIENT( WEB_SERVICES ),

+ 3 - 0
server/src/main/java/password/pwm/config/PwmSettingProperty.java

@@ -30,6 +30,9 @@ public enum PwmSettingProperty
     Minimum,
     Minimum,
     Maximum,
     Maximum,
 
 
+    Minimum_Values,
+    Maximum_Values,
+
     Form_Types,
     Form_Types,
 
 
     Cert_ImportHandler,
     Cert_ImportHandler,

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

@@ -32,6 +32,7 @@ import password.pwm.config.value.FormValue;
 import password.pwm.config.value.LocalizedStringArrayValue;
 import password.pwm.config.value.LocalizedStringArrayValue;
 import password.pwm.config.value.LocalizedStringValue;
 import password.pwm.config.value.LocalizedStringValue;
 import password.pwm.config.value.NamedSecretValue;
 import password.pwm.config.value.NamedSecretValue;
+import password.pwm.config.value.NumericArrayValue;
 import password.pwm.config.value.NumericValue;
 import password.pwm.config.value.NumericValue;
 import password.pwm.config.value.OptionListValue;
 import password.pwm.config.value.OptionListValue;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.PasswordValue;
@@ -60,6 +61,7 @@ public enum PwmSettingSyntax
     PASSWORD( PasswordValue.factory() ),
     PASSWORD( PasswordValue.factory() ),
     NUMERIC( NumericValue.factory() ),
     NUMERIC( NumericValue.factory() ),
     DURATION( NumericValue.factory() ),
     DURATION( NumericValue.factory() ),
+    DURATION_ARRAY( NumericArrayValue.factory() ),
     BOOLEAN( BooleanValue.factory() ),
     BOOLEAN( BooleanValue.factory() ),
     SELECT( StringValue.factory() ),
     SELECT( StringValue.factory() ),
     FORM( FormValue.factory() ),
     FORM( FormValue.factory() ),

+ 121 - 0
server/src/main/java/password/pwm/config/value/NumericArrayValue.java

@@ -0,0 +1,121 @@
+/*
+ * 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.value;
+
+import org.jdom2.Element;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.StoredValue;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.secure.PwmSecurityKey;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+public class NumericArrayValue extends AbstractValue implements StoredValue
+{
+    List<Long> values;
+
+    public NumericArrayValue( final List<Long> values )
+    {
+        this.values = values;
+    }
+
+    public static StoredValueFactory factory( )
+    {
+        return new StoredValueFactory()
+        {
+            public NumericArrayValue fromJson( final String value )
+            {
+                final long[] longArray = JsonUtil.deserialize( value, long[].class );
+                final List<Long> list = Arrays.stream( longArray ).boxed().collect( Collectors.toList() );
+                return new NumericArrayValue( list );
+            }
+
+            public NumericArrayValue fromXmlElement( final Element settingElement, final PwmSecurityKey input )
+            {
+                final List<Long> returnList = new ArrayList<>(  );
+                final List<Element> valueElements = settingElement.getChildren( "value" );
+                for ( final Element element : valueElements )
+                {
+                    final String strValue = element.getText();
+                    final Long longValue = Long.parseLong( strValue );
+                    returnList.add( longValue );
+                }
+                return new NumericArrayValue( returnList );
+            }
+        };
+    }
+
+    @Override
+    public List<Element> toXmlValues( final String valueElementName )
+    {
+        final List<Element> returnList = new ArrayList<>();
+        for ( final Long value : this.values )
+        {
+            final Element valueElement = new Element( valueElementName );
+            valueElement.addContent( String.valueOf( value ) );
+            returnList.add( valueElement );
+        }
+        return returnList;
+    }
+
+    @Override
+    public Object toNativeObject( )
+    {
+        return values;
+    }
+
+    @Override
+    public List<String> validateValue( final PwmSetting pwmSetting )
+    {
+        return Collections.emptyList();
+    }
+
+    public String toDebugString( final Locale locale )
+    {
+        if ( !JavaHelper.isEmpty( values ) )
+        {
+            final StringBuilder sb = new StringBuilder();
+            for ( final Iterator valueIterator = values.iterator(); valueIterator.hasNext(); )
+            {
+                sb.append( valueIterator.next() );
+                if ( valueIterator.hasNext() )
+                {
+                    sb.append( "\n" );
+                }
+            }
+            return sb.toString();
+        }
+        else
+        {
+            return "";
+        }
+    }
+
+}

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

@@ -91,6 +91,7 @@ public enum JspUrl
     CONFIG_MANAGER_PERMISSIONS( "configmanager-permissions.jsp" ),
     CONFIG_MANAGER_PERMISSIONS( "configmanager-permissions.jsp" ),
     CONFIG_MANAGER_MODE_CONFIGURATION( "configmanager.jsp" ),
     CONFIG_MANAGER_MODE_CONFIGURATION( "configmanager.jsp" ),
     CONFIG_MANAGER_WORDLISTS( "configmanager-wordlists.jsp" ),
     CONFIG_MANAGER_WORDLISTS( "configmanager-wordlists.jsp" ),
+    CONFIG_MANAGER_PWNOTIFY( "configmanager-pwnotify.jsp" ),
     CONFIG_MANAGER_CERTIFICATES( "configmanager-certificates.jsp" ),
     CONFIG_MANAGER_CERTIFICATES( "configmanager-certificates.jsp" ),
     CONFIG_MANAGER_LOCALDB( "configmanager-localdb.jsp" ),
     CONFIG_MANAGER_LOCALDB( "configmanager-localdb.jsp" ),
     CONFIG_MANAGER_LOGIN( "configmanager-login.jsp" ),
     CONFIG_MANAGER_LOGIN( "configmanager-login.jsp" ),

+ 2 - 0
server/src/main/java/password/pwm/http/servlet/PwmServletDefinition.java

@@ -49,6 +49,7 @@ import password.pwm.http.servlet.configeditor.ConfigEditorServlet;
 import password.pwm.http.servlet.configguide.ConfigGuideServlet;
 import password.pwm.http.servlet.configguide.ConfigGuideServlet;
 import password.pwm.http.servlet.configmanager.ConfigManagerCertificatesServlet;
 import password.pwm.http.servlet.configmanager.ConfigManagerCertificatesServlet;
 import password.pwm.http.servlet.configmanager.ConfigManagerLocalDBServlet;
 import password.pwm.http.servlet.configmanager.ConfigManagerLocalDBServlet;
+import password.pwm.http.servlet.configmanager.ConfigManagerPwNotifyServlet;
 import password.pwm.http.servlet.configmanager.ConfigManagerServlet;
 import password.pwm.http.servlet.configmanager.ConfigManagerServlet;
 import password.pwm.http.servlet.configmanager.ConfigManagerWordlistServlet;
 import password.pwm.http.servlet.configmanager.ConfigManagerWordlistServlet;
 import password.pwm.http.servlet.newuser.NewUserServlet;
 import password.pwm.http.servlet.newuser.NewUserServlet;
@@ -90,6 +91,7 @@ public enum PwmServletDefinition
     ConfigManager_Wordlists( ConfigManagerWordlistServlet.class, ConfigManagerBean.class ),
     ConfigManager_Wordlists( ConfigManagerWordlistServlet.class, ConfigManagerBean.class ),
     ConfigManager_LocalDB( ConfigManagerLocalDBServlet.class, ConfigManagerBean.class ),
     ConfigManager_LocalDB( ConfigManagerLocalDBServlet.class, ConfigManagerBean.class ),
     ConfigManager_Certificates( ConfigManagerCertificatesServlet.class, ConfigManagerBean.class ),
     ConfigManager_Certificates( ConfigManagerCertificatesServlet.class, ConfigManagerBean.class ),
+    ConfigManager_PwNotify( ConfigManagerPwNotifyServlet.class, ConfigManagerBean.class ),
 
 
     NewUser( NewUserServlet.class, NewUserBean.class ),
     NewUser( NewUserServlet.class, NewUserBean.class ),
     ActivateUser( password.pwm.http.servlet.ActivateUserServlet.class, ActivateUserBean.class ),
     ActivateUser( password.pwm.http.servlet.ActivateUserServlet.class, ActivateUserBean.class ),

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

@@ -509,7 +509,7 @@ public class AppDashboardData implements Serializable
             final Locale locale
             final Locale locale
     )
     )
     {
     {
-        if ( pwmApplication.getClusterService().status() == PwmService.STATUS.OPEN )
+        if ( pwmApplication.getClusterService().status() != PwmService.STATUS.OPEN )
         {
         {
             return Collections.emptyList();
             return Collections.emptyList();
         }
         }

+ 103 - 0
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerPwNotifyServlet.java

@@ -0,0 +1,103 @@
+/*
+ * 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.http.servlet.configmanager;
+
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.PwmConstants;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.JspUrl;
+import password.pwm.http.ProcessStatus;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.servlet.ControlledPwmServlet;
+import password.pwm.http.servlet.PwmServletDefinition;
+import password.pwm.i18n.Message;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.ws.server.RestResultBean;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+
+@WebServlet(
+        name = "ConfigManagerPwNotifyServlet",
+        urlPatterns = {
+                PwmConstants.URL_PREFIX_PRIVATE + "/config/manager/pwnotify",
+        }
+)
+public class ConfigManagerPwNotifyServlet extends ControlledPwmServlet
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigManagerPwNotifyServlet.class );
+
+    public enum PwNotifyAction implements ProcessAction
+    {
+        startJob( HttpMethod.POST ),;
+
+        private final HttpMethod method;
+
+        PwNotifyAction( final HttpMethod method )
+        {
+            this.method = method;
+        }
+
+        public Collection<HttpMethod> permittedMethods( )
+        {
+            return Collections.singletonList( method );
+        }
+    }
+
+    @Override
+    protected PwmServletDefinition getServletDefinition( )
+    {
+        return super.getServletDefinition();
+    }
+
+    @Override
+    public Class<? extends ProcessAction> getProcessActionsClass( )
+    {
+        return PwNotifyAction.class;
+    }
+
+    @Override
+    protected void nextStep( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ChaiUnavailableException, ServletException
+    {
+        pwmRequest.forwardToJsp( JspUrl.CONFIG_MANAGER_PWNOTIFY );
+    }
+
+    @Override
+    public ProcessStatus preProcessCheck( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException
+    {
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "startJob" )
+    public ProcessStatus restStartJob( final PwmRequest pwmRequest ) throws IOException
+    {
+        pwmRequest.getPwmApplication().getPwNotifyService().runJob();
+        pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
+        return ProcessStatus.Continue;
+    }
+}
+

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

@@ -23,6 +23,7 @@
 package password.pwm.svc;
 package password.pwm.svc;
 
 
 import password.pwm.svc.email.EmailService;
 import password.pwm.svc.email.EmailService;
+import password.pwm.svc.pwnotify.PwNotifyService;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
@@ -55,7 +56,8 @@ public enum PwmServiceEnum
     SessionStateSvc( password.pwm.http.state.SessionStateService.class ),
     SessionStateSvc( password.pwm.http.state.SessionStateService.class ),
     UserSearchEngine( password.pwm.ldap.search.UserSearchEngine.class, Flag.StartDuringRuntimeInstance ),
     UserSearchEngine( password.pwm.ldap.search.UserSearchEngine.class, Flag.StartDuringRuntimeInstance ),
     TelemetryService( password.pwm.svc.telemetry.TelemetryService.class ),
     TelemetryService( password.pwm.svc.telemetry.TelemetryService.class ),
-    ClusterService( password.pwm.svc.cluster.ClusterService.class ),;
+    ClusterService( password.pwm.svc.cluster.ClusterService.class ),
+    PwExpiryNotifyService( PwNotifyService.class ),;
 
 
     private final Class<? extends PwmService> clazz;
     private final Class<? extends PwmService> clazz;
     private final Flag[] flags;
     private final Flag[] flags;

+ 0 - 290
server/src/main/java/password/pwm/svc/pwnotify/PasswordExpireNotificationEngine.java

@@ -1,290 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package password.pwm.svc.pwnotify;
-
-import com.novell.ldapchai.ChaiUser;
-import com.novell.ldapchai.exception.ChaiOperationException;
-import com.novell.ldapchai.exception.ChaiUnavailableException;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import password.pwm.PwmApplication;
-import password.pwm.PwmConstants;
-import password.pwm.bean.EmailItemBean;
-import password.pwm.bean.SessionLabel;
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.ldap.LdapOperationsHelper;
-import password.pwm.ldap.UserInfo;
-import password.pwm.ldap.UserInfoFactory;
-import password.pwm.util.db.DatabaseException;
-import password.pwm.util.db.DatabaseTable;
-import password.pwm.util.java.JsonUtil;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.TimeDuration;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
-
-import java.io.Serializable;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Locale;
-
-public class PasswordExpireNotificationEngine
-{
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordExpireNotificationEngine.class );
-
-    private static final SessionLabel SESSION_LABEL = SessionLabel.PW_EXP_NOTICE_LABEL;
-
-    private final Settings settings;
-    private final PwmApplication pwmApplication;
-
-
-    public PasswordExpireNotificationEngine( final PwmApplication pwmApplication )
-    {
-        this.pwmApplication = pwmApplication;
-        this.settings = Settings.fromConfiguration( pwmApplication.getConfig() );
-    }
-
-    public void executeJob( )
-            throws ChaiUnavailableException, ChaiOperationException, PwmOperationalException, PwmUnrecoverableException
-    {
-        final Iterator<UserIdentity> workQueue = LdapOperationsHelper.readAllUsersFromLdap(
-                pwmApplication,
-                null,
-                null,
-                1_000_000
-        );
-
-        while ( workQueue.hasNext() )
-        {
-            final UserIdentity userIdentity = workQueue.next();
-            processUserIdentity( userIdentity );
-        }
-    }
-
-    private void processUserIdentity(
-            final UserIdentity userIdentity
-    )
-            throws PwmUnrecoverableException
-    {
-        final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
-        final Instant passwordExpirationTime = LdapOperationsHelper.readPasswordExpirationTime( theUser );
-        if ( passwordExpirationTime == null || passwordExpirationTime.isBefore( Instant.now() ) )
-        {
-            return;
-        }
-
-        final Instant previousNotice;
-        {
-            final DbStorage dbStorage = new DbStorage( pwmApplication );
-            final NotificationState storedState = dbStorage.readStoredState( userIdentity, SESSION_LABEL );
-            if ( storedState == null || storedState.getExpireTime() == null || !storedState.getExpireTime().equals( passwordExpirationTime ) )
-            {
-                previousNotice = null;
-            }
-            else
-            {
-                previousNotice = storedState.getLastNotice();
-            }
-        }
-        final int currentDayInterval = daysUntilInstant( passwordExpirationTime );
-        final int previousDays = previousNotice == null
-                ? Integer.MAX_VALUE
-                : daysUntilInstant( previousNotice );
-
-        int nextDayInterval = -1;
-        for ( final int configuredDayInterval : settings.getDayIntervals() )
-        {
-            if ( currentDayInterval <= configuredDayInterval )
-            {
-                if ( configuredDayInterval != previousDays )
-                {
-                    nextDayInterval = configuredDayInterval;
-                }
-            }
-        }
-
-        if ( nextDayInterval < 1 )
-        {
-            return;
-        }
-
-        System.out.println( userIdentity + " next=" + nextDayInterval );
-        {
-            final DbStorage dbStorage = new DbStorage( pwmApplication );
-            dbStorage.writeStoredState( userIdentity, SESSION_LABEL, new NotificationState( passwordExpirationTime, Instant.now() ) );
-        }
-
-        sendNoticeEmail( userIdentity );
-    }
-
-    void sendNoticeEmail( final UserIdentity userIdentity )
-            throws PwmUnrecoverableException
-    {
-        final Locale userLocale = PwmConstants.DEFAULT_LOCALE;
-        final EmailItemBean emailItemBean = pwmApplication.getConfig().readSettingAsEmail(
-                PwmSetting.EMAIL_PW_EXPIRATION_NOTICE,
-                userLocale
-        );
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, userLocale, SESSION_LABEL, userIdentity );
-        final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy(
-                pwmApplication,
-                SESSION_LABEL,
-                userIdentity, userLocale
-        );
-        pwmApplication.getEmailQueue().submitEmail( emailItemBean, userInfoBean, macroMachine );
-    }
-
-    static int daysUntilInstant( final Instant instant )
-    {
-        final TimeDuration timeDuration = TimeDuration.fromCurrent( instant );
-        return ( int ) timeDuration.getTotalDays();
-
-    }
-
-    @Getter
-    static class Settings implements Serializable
-    {
-        private List<Integer> dayIntervals = Collections.unmodifiableList( new ArrayList<>( Arrays.asList( 8, 5, 3 ) ) );
-
-        static Settings fromConfiguration( final Configuration configuration )
-        {
-            final Settings settings = new Settings();
-
-            final List<Integer> tempList = new ArrayList<>( Arrays.asList( 8, 5, 3 ) );
-            Collections.sort( tempList );
-            Collections.reverse( tempList );
-            settings.dayIntervals = Collections.unmodifiableList( tempList );
-
-            return settings;
-        }
-    }
-
-    @Getter
-    @AllArgsConstructor
-    static class NotificationState implements Serializable
-    {
-        private Instant expireTime;
-        private Instant lastNotice;
-    }
-
-    interface PwExpireStorageEngine
-    {
-
-        NotificationState readStoredState(
-                UserIdentity userIdentity,
-                SessionLabel sessionLabel
-        )
-                throws PwmUnrecoverableException;
-
-        void writeStoredState( UserIdentity userIdentity, SessionLabel sessionLabel, NotificationState notificationState ) throws PwmUnrecoverableException;
-
-    }
-
-    static class DbStorage implements PwExpireStorageEngine
-    {
-        private static final DatabaseTable TABLE = DatabaseTable.PW_NOTIFY;
-        private final PwmApplication pwmApplication;
-
-        DbStorage( final PwmApplication pwmApplication )
-        {
-            this.pwmApplication = pwmApplication;
-        }
-
-        @Override
-        public NotificationState readStoredState(
-                final UserIdentity userIdentity,
-                final SessionLabel sessionLabel
-        )
-                throws PwmUnrecoverableException
-        {
-            final String guid;
-            try
-            {
-                guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, true );
-            }
-            catch ( ChaiUnavailableException e )
-            {
-                throw new PwmUnrecoverableException( PwmUnrecoverableException.fromChaiException( e ).getErrorInformation() );
-            }
-            if ( StringUtil.isEmpty( guid ) )
-            {
-                throw new PwmUnrecoverableException( PwmError.ERROR_MISSING_GUID );
-            }
-
-            final String rawDbValue;
-            try
-            {
-                rawDbValue = pwmApplication.getDatabaseAccessor().get( TABLE, guid );
-            }
-            catch ( DatabaseException e )
-            {
-                throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_DB_UNAVAILABLE, e.getMessage() ) );
-            }
-
-            return JsonUtil.deserialize( rawDbValue, NotificationState.class );
-        }
-
-        public void writeStoredState(
-                final UserIdentity userIdentity,
-                final SessionLabel sessionLabel,
-                final NotificationState notificationState
-        )
-                throws PwmUnrecoverableException
-        {
-            final String guid;
-            try
-            {
-                guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, true );
-            }
-            catch ( ChaiUnavailableException e )
-            {
-                throw new PwmUnrecoverableException( PwmUnrecoverableException.fromChaiException( e ).getErrorInformation() );
-            }
-            if ( StringUtil.isEmpty( guid ) )
-            {
-                throw new PwmUnrecoverableException( PwmError.ERROR_MISSING_GUID );
-            }
-
-            final String rawDbValue = JsonUtil.serialize( notificationState );
-            try
-            {
-                pwmApplication.getDatabaseAccessor().put( TABLE, guid, rawDbValue );
-            }
-            catch ( DatabaseException e )
-            {
-                throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_DB_UNAVAILABLE, e.getMessage() ) );
-            }
-        }
-    }
-}

+ 113 - 0
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyDbStorageService.java

@@ -0,0 +1,113 @@
+/*
+ * 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.pwnotify;
+
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.PwmApplication;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.UserIdentity;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.LdapOperationsHelper;
+import password.pwm.util.db.DatabaseException;
+import password.pwm.util.db.DatabaseTable;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+
+class PwNotifyDbStorageService implements PwNotifyStorageService
+{
+    private static final DatabaseTable TABLE = DatabaseTable.PW_NOTIFY;
+    private final PwmApplication pwmApplication;
+
+    PwNotifyDbStorageService( final PwmApplication pwmApplication )
+    {
+        this.pwmApplication = pwmApplication;
+    }
+
+    @Override
+    public StoredNotificationState readStoredState(
+            final UserIdentity userIdentity,
+            final SessionLabel sessionLabel
+    )
+            throws PwmUnrecoverableException
+    {
+        final String guid;
+        try
+        {
+            guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, true );
+        }
+        catch ( ChaiUnavailableException e )
+        {
+            throw new PwmUnrecoverableException( PwmUnrecoverableException.fromChaiException( e ).getErrorInformation() );
+        }
+        if ( StringUtil.isEmpty( guid ) )
+        {
+            throw new PwmUnrecoverableException( PwmError.ERROR_MISSING_GUID );
+        }
+
+        final String rawDbValue;
+        try
+        {
+            rawDbValue = pwmApplication.getDatabaseAccessor().get( TABLE, guid );
+        }
+        catch ( DatabaseException e )
+        {
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_DB_UNAVAILABLE, e.getMessage() ) );
+        }
+
+        return JsonUtil.deserialize( rawDbValue, StoredNotificationState.class );
+    }
+
+    public void writeStoredState(
+            final UserIdentity userIdentity,
+            final SessionLabel sessionLabel,
+            final StoredNotificationState storedNotificationState
+    )
+            throws PwmUnrecoverableException
+    {
+        final String guid;
+        try
+        {
+            guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, true );
+        }
+        catch ( ChaiUnavailableException e )
+        {
+            throw new PwmUnrecoverableException( PwmUnrecoverableException.fromChaiException( e ).getErrorInformation() );
+        }
+        if ( StringUtil.isEmpty( guid ) )
+        {
+            throw new PwmUnrecoverableException( PwmError.ERROR_MISSING_GUID );
+        }
+
+        final String rawDbValue = JsonUtil.serialize( storedNotificationState );
+        try
+        {
+            pwmApplication.getDatabaseAccessor().put( TABLE, guid, rawDbValue );
+        }
+        catch ( DatabaseException e )
+        {
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_DB_UNAVAILABLE, e.getMessage() ) );
+        }
+    }
+}

+ 279 - 0
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java

@@ -0,0 +1,279 @@
+/*
+ * 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.pwnotify;
+
+import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiOperationException;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.EmailItemBean;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.PwmSetting;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.LdapOperationsHelper;
+import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.UserInfoFactory;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Iterator;
+import java.util.Locale;
+
+public class PwNotifyEngine
+{
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PwNotifyEngine.class );
+
+    private static final SessionLabel SESSION_LABEL = SessionLabel.PW_EXP_NOTICE_LABEL;
+
+
+    private final PwNotifySettings settings;
+    private final PwmApplication pwmApplication;
+    private final Writer debugWriter;
+    private final StringBuffer internalLog = new StringBuffer(  );
+
+    private volatile boolean running;
+
+
+    public PwNotifyEngine(
+            final PwmApplication pwmApplication,
+            final Writer debugWriter
+    )
+    {
+        this.pwmApplication = pwmApplication;
+        this.settings = PwNotifySettings.fromConfiguration( pwmApplication.getConfig() );
+        this.debugWriter = debugWriter;
+    }
+
+    public boolean isRunning()
+    {
+        return running;
+    }
+
+    public String getDebugLog()
+    {
+        return internalLog.toString();
+    }
+
+    private void checkIfRunningOnMaster( final String msg ) throws PwmUnrecoverableException
+    {
+        if ( !pwmApplication.getPwmEnvironment().isInternalRuntimeInstance() )
+        {
+            if ( pwmApplication.getClusterService() != null && !pwmApplication.getClusterService().isMaster() )
+            {
+                throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, msg );
+            }
+        }
+    }
+
+    public void executeJob( )
+            throws ChaiUnavailableException, ChaiOperationException, PwmOperationalException, PwmUnrecoverableException
+    {
+        try
+        {
+            checkIfRunningOnMaster( "job can run only on a server that is currently the cluster master" );
+            running = true;
+
+            final Instant startTime = Instant.now();
+            internalLog.delete( 0, internalLog.length() );
+            log( "starting job, beginning ldap search" );
+            final Iterator<UserIdentity> workQueue = LdapOperationsHelper.readAllUsersFromLdap(
+                    pwmApplication,
+                    null,
+                    null,
+                    settings.getMaxLdapSearchSize()
+            );
+            log( "ldap search complete, examining users..." );
+            int examinedCount = 0;
+            int noticeCount = 0;
+            while ( workQueue.hasNext() )
+            {
+                checkIfRunningOnMaster( "job interrupted, server is no longer the cluster master." );
+                examinedCount++;
+                final UserIdentity userIdentity = workQueue.next();
+                if ( processUserIdentity( userIdentity ) )
+                {
+                    noticeCount++;
+                }
+            }
+            log( "job complete, " + examinedCount + " users evaluated in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
+        }
+        finally
+        {
+            running = false;
+        }
+    }
+
+    private boolean processUserIdentity(
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
+        final Instant passwordExpirationTime = LdapOperationsHelper.readPasswordExpirationTime( theUser );
+
+        if ( passwordExpirationTime == null || passwordExpirationTime.isBefore( Instant.now() ) )
+        {
+            return false;
+        }
+
+        final int nextDayInterval = figureNextDayInterval( passwordExpirationTime );
+        if ( nextDayInterval < 1 )
+        {
+            return false;
+        }
+
+        if ( checkIfNoticeAlreadySent( userIdentity, passwordExpirationTime, nextDayInterval ) )
+        {
+            log( "notice for interval " + nextDayInterval + " already sent for " + userIdentity.toDisplayString() );
+            return false;
+        }
+
+        log( "sending notice to " + userIdentity.toDisplayString() + " for interval " + nextDayInterval );
+        {
+            final PwNotifyDbStorageService dbStorage = new PwNotifyDbStorageService( pwmApplication );
+            dbStorage.writeStoredState( userIdentity, SESSION_LABEL, new StoredNotificationState( passwordExpirationTime, Instant.now(), nextDayInterval ) );
+        }
+
+        sendNoticeEmail( userIdentity );
+        return true;
+    }
+
+    private int figureNextDayInterval(
+            final Instant passwordExpirationTime
+    )
+    {
+        final long maxSecondsAfterExpiration = TimeDuration.DAY.getTotalSeconds();
+        int nextDayInterval = -1;
+        for ( final int configuredDayInterval : settings.getNotificationIntervals() )
+        {
+            final Instant futureConfiguredDayInterval = Instant.now().plus( configuredDayInterval, ChronoUnit.DAYS );
+            final long secondsUntilConfiguredInterval = Duration.between( Instant.now(), futureConfiguredDayInterval ).abs().getSeconds();
+            final long secondsUntilPasswordExpiration = Duration.between( Instant.now(), passwordExpirationTime ).abs().getSeconds();
+            if ( secondsUntilPasswordExpiration < secondsUntilConfiguredInterval )
+            {
+                final long secondsBetweenIntervalAndExpiration = Duration.between( futureConfiguredDayInterval, passwordExpirationTime ).abs().getSeconds();
+                if ( secondsBetweenIntervalAndExpiration < maxSecondsAfterExpiration )
+                {
+                    nextDayInterval = configuredDayInterval;
+                }
+            }
+        }
+
+        return nextDayInterval;
+    }
+
+    private boolean checkIfNoticeAlreadySent(
+            final UserIdentity userIdentity,
+            final Instant passwordExpirationTime,
+            final int interval
+    )
+            throws PwmUnrecoverableException
+    {
+        final PwNotifyDbStorageService dbStorage = new PwNotifyDbStorageService( pwmApplication );
+        final StoredNotificationState storedState = dbStorage.readStoredState( userIdentity, SESSION_LABEL );
+
+        if ( storedState == null )
+        {
+            return false;
+        }
+
+        if ( storedState.getExpireTime() == null || !storedState.getExpireTime().equals( passwordExpirationTime ) )
+        {
+            return false;
+        }
+
+        if ( storedState.getInterval() == 0 || storedState.getInterval() != interval )
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    void sendNoticeEmail( final UserIdentity userIdentity )
+            throws PwmUnrecoverableException
+    {
+        final Locale userLocale = PwmConstants.DEFAULT_LOCALE;
+        final EmailItemBean emailItemBean = pwmApplication.getConfig().readSettingAsEmail(
+                PwmSetting.EMAIL_PW_EXPIRATION_NOTICE,
+                userLocale
+        );
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, userLocale, SESSION_LABEL, userIdentity );
+        final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy(
+                pwmApplication,
+                SESSION_LABEL,
+                userIdentity, userLocale
+        );
+
+        pwmApplication.getEmailQueue().submitEmail( emailItemBean, userInfoBean, macroMachine );
+    }
+
+    private void log( final String output )
+    {
+        final String msg = JavaHelper.toIsoDate( Instant.now() )
+                + " "
+                + output
+                + "\n";
+
+        if ( debugWriter != null )
+        {
+            try
+            {
+                debugWriter.append( msg );
+                debugWriter.flush();
+            }
+            catch ( IOException e )
+            {
+                LOGGER.warn( "unexpected IO error writing to debugWriter: " + e.getMessage() );
+            }
+        }
+
+        internalLog.append( msg );
+        while ( internalLog.length() > 1024 * 1024 * 1024 )
+        {
+            final int nextLf = internalLog.indexOf( "\n" );
+            if ( nextLf > 0 )
+            {
+                internalLog.delete( 0, nextLf );
+            }
+            else
+            {
+                internalLog.delete( 0, Math.max( 1024, internalLog.length() ) );
+            }
+        }
+
+        LOGGER.trace( output );
+    }
+}

+ 197 - 0
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java

@@ -0,0 +1,197 @@
+/*
+ * 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.pwnotify;
+
+import password.pwm.PwmApplication;
+import password.pwm.config.PwmSetting;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.health.HealthRecord;
+import password.pwm.svc.PwmService;
+import password.pwm.util.db.DatabaseException;
+import password.pwm.util.db.DatabaseTable;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class PwNotifyService implements PwmService
+{
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PwNotifyService.class );
+
+    private ScheduledExecutorService executorService;
+    private PwmApplication pwmApplication;
+    private STATUS status = STATUS.NEW;
+    private PwNotifyEngine engine;
+
+    @Override
+    public STATUS status( )
+    {
+        return status;
+    }
+
+    private static final String DB_STATE_STRING = "PwNotifyJobState";
+
+    private StoredJobState readStoredJobState()
+            throws PwmUnrecoverableException, DatabaseException
+    {
+        final String strValue = pwmApplication.getDatabaseService().getAccessor().get( DatabaseTable.PW_NOTIFY, DB_STATE_STRING );
+        if ( StringUtil.isEmpty( strValue ) )
+        {
+            return new StoredJobState( null, null, null, null );
+        }
+        return JsonUtil.deserialize( strValue, StoredJobState.class );
+    }
+
+    public StoredJobState getJobState() throws DatabaseException, PwmUnrecoverableException
+    {
+        return readStoredJobState();
+    }
+
+    public boolean isRunning()
+    {
+        return engine != null && engine.isRunning();
+    }
+
+    public String debugLog()
+    {
+        if ( engine != null )
+        {
+            return engine.getDebugLog();
+        }
+        return "";
+    }
+
+    private void writeStoredJobState( final StoredJobState storedJobState )
+            throws PwmUnrecoverableException, DatabaseException
+    {
+        final String strValue = JsonUtil.serialize( storedJobState );
+        pwmApplication.getDatabaseService().getAccessor().put( DatabaseTable.PW_NOTIFY, DB_STATE_STRING, strValue );
+    }
+
+    @Override
+    public void init( final PwmApplication pwmApplication ) throws PwmException
+    {
+        this.pwmApplication = pwmApplication;
+
+        if ( !pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PW_EXPY_NOTIFY_ENABLE ) )
+        {
+            status = STATUS.CLOSED;
+            LOGGER.trace( "will remain closed, pw notify feature is not enabled" );
+            return;
+        }
+
+        engine = new PwNotifyEngine( pwmApplication, null );
+
+        executorService = Executors.newSingleThreadScheduledExecutor(
+                JavaHelper.makePwmThreadFactory(
+                        JavaHelper.makeThreadName( pwmApplication, this.getClass() ) + "-",
+                        true
+                ) );
+
+        {
+            final long jobOffsetSeconds = pwmApplication.getConfig().readSettingAsLong( PwmSetting.PW_EXPY_NOTIFY_JOB_OFFSET );
+            final Instant nextZuluZeroTime = JavaHelper.nextZuluZeroTime();
+            final long secondsUntilNextDredge = jobOffsetSeconds + TimeDuration.fromCurrent( nextZuluZeroTime ).getTotalSeconds();
+            executorService.scheduleAtFixedRate( new DailyJobRunning(), secondsUntilNextDredge, TimeDuration.DAY.getTotalSeconds(), TimeUnit.SECONDS );
+            LOGGER.debug( "scheduled daily execution, next task will be at " + nextZuluZeroTime.toString() );
+        }
+    }
+
+    @Override
+    public void close( )
+    {
+        JavaHelper.closeAndWaitExecutor( executorService, new TimeDuration( 5, TimeUnit.SECONDS ) );
+    }
+
+    @Override
+    public List<HealthRecord> healthCheck( )
+    {
+        return null;
+    }
+
+    @Override
+    public ServiceInfoBean serviceInfo( )
+    {
+        return null;
+    }
+
+    public void runJob( )
+    {
+        executorService.schedule( new DailyJobRunning(), 1, TimeUnit.SECONDS );
+    }
+
+    class DailyJobRunning implements Runnable
+    {
+        @Override
+        public void run( )
+        {
+            final Instant start = Instant.now();
+            try
+            {
+                writeStoredJobState( new StoredJobState() );
+                engine.executeJob();
+                final Instant finish = Instant.now();
+                final StoredJobState storedJobState = new StoredJobState( start, finish, pwmApplication.getInstanceID(), null );
+                writeStoredJobState( storedJobState );
+            }
+            catch ( Exception e )
+            {
+                final ErrorInformation errorInformation;
+                if ( e instanceof PwmException )
+                {
+                    errorInformation = ( ( PwmException ) e ).getErrorInformation();
+                }
+                else
+                {
+                    errorInformation = new ErrorInformation( PwmError.ERROR_UNKNOWN, "error " + e.getMessage() );
+                }
+
+                final Instant finish = Instant.now();
+                final String instanceID = pwmApplication.getInstanceID();
+                final StoredJobState storedJobState = new StoredJobState( start, finish, instanceID, errorInformation );
+                try
+                {
+                    writeStoredJobState( storedJobState );
+                }
+                catch ( Exception e2 )
+                {
+                    //no hope
+                }
+                LOGGER.debug( "error executing scheduled job: " + e.getMessage() );
+            }
+        }
+    }
+
+
+}

+ 59 - 0
server/src/main/java/password/pwm/svc/pwnotify/PwNotifySettings.java

@@ -0,0 +1,59 @@
+/*
+ * 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.pwnotify;
+
+import lombok.Value;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.util.java.TimeDuration;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Value
+public class PwNotifySettings implements Serializable
+{
+    private final List<Integer> notificationIntervals;
+    private final TimeDuration maximumSkipWindow;
+    private final int maxLdapSearchSize;
+
+    static PwNotifySettings fromConfiguration( final Configuration configuration )
+    {
+        final List<Integer> timeDurations = new ArrayList<>(  );
+        {
+            final List<String> stringValues = configuration.readSettingAsStringArray( PwmSetting.PW_EXPY_NOTIFY_INTERVAL );
+            for ( final String value : stringValues )
+            {
+                timeDurations.add( Integer.parseInt( value ) );
+            }
+            Collections.sort( timeDurations );
+        }
+        final TimeDuration maxSkipWindow = new TimeDuration( 24, TimeUnit.HOURS );
+        final int maxLdapSearchSize = 1_000_000;
+
+        return new PwNotifySettings( Collections.unmodifiableList( timeDurations ), maxSkipWindow, maxLdapSearchSize );
+    }
+}

+ 39 - 0
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyStorageService.java

@@ -0,0 +1,39 @@
+/*
+ * 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.pwnotify;
+
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.UserIdentity;
+import password.pwm.error.PwmUnrecoverableException;
+
+interface PwNotifyStorageService
+{
+
+    StoredNotificationState readStoredState(
+            UserIdentity userIdentity,
+            SessionLabel sessionLabel
+    )
+            throws PwmUnrecoverableException;
+
+    void writeStoredState( UserIdentity userIdentity, SessionLabel sessionLabel, StoredNotificationState storedNotificationState ) throws PwmUnrecoverableException;
+}

+ 42 - 0
server/src/main/java/password/pwm/svc/pwnotify/StoredJobState.java

@@ -0,0 +1,42 @@
+/*
+ * 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.pwnotify;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import password.pwm.error.ErrorInformation;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class StoredJobState implements Serializable
+{
+    private Instant lastStart = null;
+    private Instant lastCompletion = null;
+    private String serverInstance = null;
+    private ErrorInformation lastError = null;
+}

+ 36 - 0
server/src/main/java/password/pwm/svc/pwnotify/StoredNotificationState.java

@@ -0,0 +1,36 @@
+/*
+ * 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.pwnotify;
+
+import lombok.Value;
+
+import java.io.Serializable;
+import java.time.Instant;
+
+@Value
+class StoredNotificationState implements Serializable
+{
+    private Instant expireTime;
+    private Instant lastNotice;
+    private int interval;
+}

+ 4 - 69
server/src/main/java/password/pwm/util/cli/CliEnvironment.java

@@ -22,6 +22,8 @@
 
 
 package password.pwm.util.cli;
 package password.pwm.util.cli;
 
 
+import lombok.Builder;
+import lombok.Value;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.ConfigurationReader;
@@ -31,6 +33,8 @@ import java.io.File;
 import java.io.Writer;
 import java.io.Writer;
 import java.util.Map;
 import java.util.Map;
 
 
+@Value
+@Builder( toBuilder = true )
 public class CliEnvironment
 public class CliEnvironment
 {
 {
     final ConfigurationReader configurationReader;
     final ConfigurationReader configurationReader;
@@ -42,73 +46,4 @@ public class CliEnvironment
     final Writer debugWriter;
     final Writer debugWriter;
     final Map<String, Object> options;
     final Map<String, Object> options;
     final MainOptions mainOptions;
     final MainOptions mainOptions;
-
-    @SuppressWarnings( "checkstyle:ParameterNumber" )
-    public CliEnvironment(
-            final ConfigurationReader configurationReader,
-            final File configurationFile,
-            final Configuration config,
-            final File applicationPath,
-            final PwmApplication pwmApplication,
-            final LocalDB localDB,
-            final Writer debugWriter,
-            final Map<String, Object> options,
-            final MainOptions mainOptions
-    )
-    {
-        this.configurationReader = configurationReader;
-        this.configurationFile = configurationFile;
-        this.config = config;
-        this.applicationPath = applicationPath;
-        this.pwmApplication = pwmApplication;
-        this.localDB = localDB;
-        this.debugWriter = debugWriter;
-        this.options = options;
-        this.mainOptions = mainOptions;
-    }
-
-    public Configuration getConfig( )
-    {
-        return config;
-    }
-
-    public PwmApplication getPwmApplication( )
-    {
-        return pwmApplication;
-    }
-
-    public LocalDB getLocalDB( )
-    {
-        return localDB;
-    }
-
-    public Writer getDebugWriter( )
-    {
-        return debugWriter;
-    }
-
-    public Map<String, Object> getOptions( )
-    {
-        return options;
-    }
-
-    public File getApplicationPath( )
-    {
-        return applicationPath;
-    }
-
-    public ConfigurationReader getConfigurationReader( )
-    {
-        return configurationReader;
-    }
-
-    public MainOptions getMainOptions( )
-    {
-        return mainOptions;
-    }
-
-    public File getConfigurationFile( )
-    {
-        return configurationFile;
-    }
 }
 }

+ 13 - 12
server/src/main/java/password/pwm/util/cli/MainClass.java

@@ -58,6 +58,7 @@ import password.pwm.util.cli.commands.ImportLocalDBCommand;
 import password.pwm.util.cli.commands.ImportResponsesCommand;
 import password.pwm.util.cli.commands.ImportResponsesCommand;
 import password.pwm.util.cli.commands.LdapSchemaExtendCommand;
 import password.pwm.util.cli.commands.LdapSchemaExtendCommand;
 import password.pwm.util.cli.commands.LocalDBInfoCommand;
 import password.pwm.util.cli.commands.LocalDBInfoCommand;
+import password.pwm.util.cli.commands.PasswordExpireNotificationCommand;
 import password.pwm.util.cli.commands.ResponseStatsCommand;
 import password.pwm.util.cli.commands.ResponseStatsCommand;
 import password.pwm.util.cli.commands.ShellCommand;
 import password.pwm.util.cli.commands.ShellCommand;
 import password.pwm.util.cli.commands.TokenInfoCommand;
 import password.pwm.util.cli.commands.TokenInfoCommand;
@@ -126,7 +127,7 @@ public class MainClass
         commandList.add( new ShellCommand() );
         commandList.add( new ShellCommand() );
         commandList.add( new ConfigResetHttpsCommand() );
         commandList.add( new ConfigResetHttpsCommand() );
         commandList.add( new HelpCommand() );
         commandList.add( new HelpCommand() );
-        //commandList.add(new PasswordExpireNotificationCommand());
+        commandList.add( new PasswordExpireNotificationCommand() );
 
 
         final Map<String, CliCommand> sortedMap = new TreeMap<>();
         final Map<String, CliCommand> sortedMap = new TreeMap<>();
         for ( final CliCommand command : commandList )
         for ( final CliCommand command : commandList )
@@ -220,17 +221,17 @@ public class MainClass
         out( "" );
         out( "" );
 
 
         final Writer outputStream = new OutputStreamWriter( System.out, PwmConstants.DEFAULT_CHARSET );
         final Writer outputStream = new OutputStreamWriter( System.out, PwmConstants.DEFAULT_CHARSET );
-        return new CliEnvironment(
-                configReader,
-                configurationFile,
-                config,
-                applicationPath,
-                pwmApplication,
-                localDB,
-                outputStream,
-                options,
-                mainOptions
-        );
+        return CliEnvironment.builder()
+                .configurationReader( configReader )
+                .configurationFile( configurationFile )
+                .config( config )
+                .applicationPath( applicationPath )
+                .pwmApplication( pwmApplication )
+                .localDB( localDB )
+                .debugWriter( outputStream )
+                .options( options )
+                .mainOptions( mainOptions )
+                .build();
     }
     }
 
 
     public static Map<String, Object> parseCommandOptions(
     public static Map<String, Object> parseCommandOptions(

+ 3 - 3
server/src/main/java/password/pwm/util/cli/commands/PasswordExpireNotificationCommand.java

@@ -23,7 +23,7 @@
 package password.pwm.util.cli.commands;
 package password.pwm.util.cli.commands;
 
 
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
-import password.pwm.svc.pwnotify.PasswordExpireNotificationEngine;
+import password.pwm.svc.pwnotify.PwNotifyEngine;
 import password.pwm.util.cli.CliParameters;
 import password.pwm.util.cli.CliParameters;
 
 
 import java.util.Collections;
 import java.util.Collections;
@@ -34,7 +34,7 @@ public class PasswordExpireNotificationCommand extends AbstractCliCommand
             throws Exception
             throws Exception
     {
     {
         final PwmApplication pwmApplication = cliEnvironment.getPwmApplication();
         final PwmApplication pwmApplication = cliEnvironment.getPwmApplication();
-        final PasswordExpireNotificationEngine engine = new PasswordExpireNotificationEngine( pwmApplication );
+        final PwNotifyEngine engine = new PwNotifyEngine( pwmApplication, this.cliEnvironment.getDebugWriter() );
         engine.executeJob();
         engine.executeJob();
     }
     }
 
 
@@ -42,7 +42,7 @@ public class PasswordExpireNotificationCommand extends AbstractCliCommand
     public CliParameters getCliParameters( )
     public CliParameters getCliParameters( )
     {
     {
         final CliParameters cliParameters = new CliParameters();
         final CliParameters cliParameters = new CliParameters();
-        cliParameters.commandName = "PasswordExpirationNotification";
+        cliParameters.commandName = "PasswordNotificationJob";
         cliParameters.description = "Run the password expiration notification batch process";
         cliParameters.description = "Run the password expiration notification batch process";
         cliParameters.options = Collections.emptyList();
         cliParameters.options = Collections.emptyList();
         cliParameters.needsPwmApplication = true;
         cliParameters.needsPwmApplication = true;

+ 4 - 11
server/src/main/java/password/pwm/util/cli/commands/ShellCommand.java

@@ -136,17 +136,10 @@ public class ShellCommand extends AbstractCliCommand
             tokens.remove( 0 );
             tokens.remove( 0 );
             cliOptions = MainClass.parseCommandOptions( command.getCliParameters(), tokens );
             cliOptions = MainClass.parseCommandOptions( command.getCliParameters(), tokens );
         }
         }
-        final CliEnvironment newEnvironment = new CliEnvironment(
-                cliEnvironment.getConfigurationReader(),
-                cliEnvironment.getConfigurationFile(),
-                cliEnvironment.getConfig(),
-                cliEnvironment.getApplicationPath(),
-                cliEnvironment.getPwmApplication(),
-                cliEnvironment.getLocalDB(),
-                cliEnvironment.getDebugWriter(),
-                cliOptions,
-                cliEnvironment.getMainOptions()
-        );
+        final CliEnvironment newEnvironment = cliEnvironment.toBuilder()
+                .options( cliOptions )
+                .build();
+
         command.execute( commandLine, newEnvironment );
         command.execute( commandLine, newEnvironment );
     }
     }
 
 

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

@@ -239,6 +239,7 @@ peoplesearch.values.maxCount=100
 peoplesearch.view.detail.links=
 peoplesearch.view.detail.links=
 peoplesearch.orgChart.enableChildCount=true
 peoplesearch.orgChart.enableChildCount=true
 peoplesearch.orgChart.maxParents=50
 peoplesearch.orgChart.maxParents=50
+pwNotify.maxLdapSearchSize=1000000
 queue.email.retryTimeoutMs=10000
 queue.email.retryTimeoutMs=10000
 queue.email.maxCount=100000
 queue.email.maxCount=100000
 queue.email.maxThreads=0
 queue.email.maxThreads=0

+ 30 - 1
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -881,7 +881,7 @@
             <value>{"to":"@User:Email@","from":"Unlock Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Unlock Notice","bodyPlain":"Your account has been unlocked.","bodyHtml":""}</value>
             <value>{"to":"@User:Email@","from":"Unlock Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Unlock Notice","bodyPlain":"Your account has been unlocked.","bodyHtml":""}</value>
         </default>
         </default>
     </setting>
     </setting>
-    <setting hidden="true" key="email.pwExpirationNotice" level="1">
+    <setting hidden="false" key="email.pwNotice" level="1">
         <flag>MacroSupport</flag>
         <flag>MacroSupport</flag>
         <default>
         <default>
             <value>{"to":"@User:Email@","from":"Password Expiration Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Expiration Notice","bodyPlain":"Your password is about to expire.  Your password will expire in @User:DaysUntilPwExpire@ days.","bodyHtml":""}</value>
             <value>{"to":"@User:Email@","from":"Password Expiration Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Expiration Notice","bodyPlain":"Your password is about to expire.  Your password will expire in @User:DaysUntilPwExpire@ days.","bodyHtml":""}</value>
@@ -3660,6 +3660,33 @@
             <value>false</value>
             <value>false</value>
         </default>
         </default>
     </setting>
     </setting>
+    <setting hidden="false" key="pwNotify.enable" level="1">
+        <default>
+            <value/>
+        </default>
+    </setting>
+    <setting hidden="false" key="pwNotify.queryString" level="1">
+        <default>
+            <value/>
+        </default>
+    </setting>
+    <setting hidden="false" key="pwNotify.intervals" level="1">
+        <default>
+            <value>1</value>
+            <value>3</value>
+            <value>7</value>
+        </default>
+        <regex>^[0-9]{1,2}$</regex>
+    </setting>
+    <setting hidden="false" key="pwNotify.job.offSet" level="1">
+        <properties>
+            <property key="Minimum">0</property>
+            <property key="Maximum">86400</property>
+        </properties>
+        <default>
+            <value>0</value>
+        </default>
+    </setting>
     <setting hidden="false" key="reporting.enable" level="1">
     <setting hidden="false" key="reporting.enable" level="1">
         <default>
         <default>
             <value/>
             <value/>
@@ -4026,6 +4053,8 @@
     </category>
     </category>
     <category hidden="false" key="REPORTING">
     <category hidden="false" key="REPORTING">
     </category>
     </category>
+    <category hidden="false" key="PW_EXP_NOTIFY">
+    </category>
     <category hidden="false" key="WEB_SERVICES" level="2">
     <category hidden="false" key="WEB_SERVICES" level="2">
     </category>
     </category>
     <category hidden="false" key="REST_SERVER" level="2">
     <category hidden="false" key="REST_SERVER" level="2">

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

@@ -322,7 +322,7 @@ Setting_Description_email.helpdesk.unlock=Define this template to send an email
 Setting_Description_email.intruderNotice=Define this template to send an email when a userDN intruder lockout occurs.
 Setting_Description_email.intruderNotice=Define this template to send an email when a userDN intruder lockout occurs.
 Setting_Description_email.newUser=Define this template to send an email to newly created users.
 Setting_Description_email.newUser=Define this template to send an email to newly created users.
 Setting_Description_email.newUser.token=Define this template to send an email during the new user verification process.  You can use %TOKEN% to insert the token value into the email.
 Setting_Description_email.newUser.token=Define this template to send an email during the new user verification process.  You can use %TOKEN% to insert the token value into the email.
-Setting_Description_email.pwExpirationNotice=Email sent to users to notify the user of an impending password notification. 
+Setting_Description_email.pwNotice=Email sent to users to notify the user of an impending password notification. 
 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.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.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.sendUsername=Define this template to send an email for the forgotten user name process.
@@ -813,7 +813,7 @@ Setting_Label_email.helpdesk.unlock=Help Desk Unlock Account Email
 Setting_Label_email.intruderNotice=Intruder Notice Email
 Setting_Label_email.intruderNotice=Intruder Notice Email
 Setting_Label_email.newUser=New User Email
 Setting_Label_email.newUser=New User Email
 Setting_Label_email.newUser.token=New User Verification Email
 Setting_Label_email.newUser.token=New User Verification Email
-Setting_Label_email.pwExpirationNotice=Password Expiration Notification Email
+Setting_Label_email.pwNotice=Password Expiration Notification Email
 Setting_Label_email.queueMaxAge=Maximum Email Queue Age
 Setting_Label_email.queueMaxAge=Maximum Email Queue Age
 Setting_Label_email.sendpassword=Send Password Email
 Setting_Label_email.sendpassword=Send Password Email
 Setting_Label_email.sendUsername=Send User Name Email
 Setting_Label_email.sendUsername=Send User Name Email
@@ -1198,3 +1198,16 @@ Setting_Label_webservices.queryMatch=Web Services LDAP Authentication Permission
 Setting_Label_webservices.thirdParty.queryMatch=Web Services LDAP Third Party Permissions
 Setting_Label_webservices.thirdParty.queryMatch=Web Services LDAP Third Party Permissions
 Setting_Label_webservice.userAttributes=Web Service User Attributes
 Setting_Label_webservice.userAttributes=Web Service User Attributes
 Setting_Label_wordlistCaseSensitive=Word List Case Sensitivity
 Setting_Label_wordlistCaseSensitive=Word List Case Sensitivity
+
+Category_Description_PW_EXP_NOTIFY=Password Expiration Notification
+Category_Label_PW_EXP_NOTIFY=Password Expiration Notification
+
+Setting_Label_pwNotify.enable=Enable Password Expiration Notification
+Setting_Label_pwNotify.queryString=Expiration Notification User Match
+Setting_Label_pwNotify.intervals=Expiration Notification Intervals
+Setting_Label_pwNotify.job.offSet=Job Offset
+
+Setting_Description_pwNotify.enable=Enable Password Expiration Notification
+Setting_Description_pwNotify.queryString=Expiration Notification User Match
+Setting_Description_pwNotify.intervals=Expiration Notification Intervals
+Setting_Description_pwNotify.job.offSet=Job Offset

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

@@ -31,6 +31,7 @@
 <%@ page import="password.pwm.http.servlet.admin.UserDebugDataBean" %>
 <%@ page import="password.pwm.http.servlet.admin.UserDebugDataBean" %>
 <%@ page import="password.pwm.i18n.Display" %>
 <%@ page import="password.pwm.i18n.Display" %>
 <%@ page import="java.util.Map" %>
 <%@ page import="java.util.Map" %>
+<%@ page import="password.pwm.util.java.TimeDuration" %>
 <!DOCTYPE html>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
@@ -106,7 +107,10 @@
             </tr>
             </tr>
             <tr>
             <tr>
                 <td class="key">Password Expiration</td>
                 <td class="key">Password Expiration</td>
-                <td><%=JspUtility.freindlyWrite(pageContext, userInfo.getPasswordExpirationTime())%></td>
+                <td>
+                    <%=JspUtility.freindlyWrite(pageContext, userInfo.getPasswordExpirationTime())%>
+                    ( <%=TimeDuration.fromCurrent(userInfo.getPasswordExpirationTime()).asCompactString()%> )
+                </td>
             </tr>
             </tr>
             <tr>
             <tr>
                 <td class="key">Password Last Modified</td>
                 <td class="key">Password Last Modified</td>

+ 116 - 0
server/src/main/webapp/WEB-INF/jsp/configmanager-pwnotify.jsp

@@ -0,0 +1,116 @@
+<%--
+ ~ 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
+--%>
+
+<%@ page import="password.pwm.i18n.Config" %>
+<%@ page import="password.pwm.svc.cluster.ClusterService" %>
+<%@ page import="password.pwm.svc.pwnotify.PwNotifyService" %>
+<%@ page import="password.pwm.svc.pwnotify.StoredJobState" %>
+<%@ page import="password.pwm.util.LocaleHelper" %>
+<%@ page import="password.pwm.util.java.StringUtil" %>
+
+<!DOCTYPE html>
+<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
+<% JspUtility.setFlag(pageContext, PwmRequestFlag.INCLUDE_CONFIG_CSS);%>
+<%@ taglib uri="pwm" prefix="pwm" %>
+<html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
+<%@ include file="fragment/header.jsp" %>
+<body class="nihilo">
+<link href="<pwm:context/><pwm:url url='/public/resources/configmanagerStyle.css'/>" rel="stylesheet" type="text/css"/>
+<div id="wrapper">
+    <jsp:include page="fragment/header-body.jsp">
+        <jsp:param name="pwm.PageName" value="<%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%>"/>
+    </jsp:include>
+    <div id="centerbody">
+        <h1 id="page-content-title"><%=LocaleHelper.getLocalizedMessage(Config.Title_ConfigManager, JspUtility.getPwmRequest(pageContext))%></h1>
+        <%@ include file="fragment/configmanager-nav.jsp" %>
+        <pwm:if test="<%=PwmIfTest.booleanSetting%>" setting="<%=PwmSetting.PW_EXPY_NOTIFY_ENABLE%>" negate="true">
+            Password Notification Feature is not enabled.  See ConfigEditor: <%=PwmSetting.PW_EXPY_NOTIFY_ENABLE.toMenuLocationDebug(null,null)%>
+        </pwm:if>
+        <pwm:if test="<%=PwmIfTest.booleanSetting%>" setting="<%=PwmSetting.PW_EXPY_NOTIFY_ENABLE%>">
+            <% final ClusterService clusterService = JspUtility.getPwmRequest( pageContext ).getPwmApplication().getClusterService(); %>
+            <% final PwNotifyService service = JspUtility.getPwmRequest(pageContext).getPwmApplication().getPwNotifyService();%>
+            <% final StoredJobState storedJobState = service.getJobState(); %>
+            <table>
+                <tr><td colspan="2" class="title">Password Expiration Notification Status</td></tr>
+                <tr><td>Currently Running (on this server) </td><td><%=JspUtility.freindlyWrite(pageContext, service.isRunning())%></td></tr>
+                <tr><td>This Server is Cluster Master</td><td><%=JspUtility.freindlyWrite(pageContext, clusterService.isMaster())%></td></tr>
+                <% if (storedJobState != null)  { %>
+                <tr><td>Last Job Start Time </td><td><span class="timestamp"><%=JspUtility.freindlyWrite(pageContext, storedJobState.getLastStart())%></span></td></tr>
+                <tr><td>Last Job Completion Time </td><td><span class="timestamp"><%=JspUtility.freindlyWrite(pageContext, storedJobState.getLastCompletion())%></span></td></tr>
+                <tr><td>Last Job Server Instance</td><td><%=JspUtility.freindlyWrite(pageContext, storedJobState.getServerInstance())%></td></tr>
+                <% if (storedJobState.getLastError() != null) { %>
+                <tr><td>Last Job Error</td><td><%=JspUtility.freindlyWrite(pageContext, storedJobState.getLastError().toDebugStr())%></td></tr>
+                <% } %>
+                <% } %>
+            </table>
+            <br/><br/>
+            <table><tr><td class="title">Debug Log</td></tr>
+                <tr><td><div style="max-height: 500px; overflow: auto">
+                    <% if (StringUtil.isEmpty( service.debugLog())) { %>
+                    <span class="footnote">Job has not been run on this server since startup.</span>
+                    <% } else { %>
+                    <div style="white-space: nowrap;  "><%=StringUtil.escapeHtml(service.debugLog()).replace("\n","<br/>")%></div>
+                    <% } %>
+                </div></td> </tr>
+            </table>
+
+            <div class="buttonbar" style="width:100%">
+                <form action="<pwm:current-url/>" method="get" id="form-refresh">
+                    <button type="submit" name="change" class="btn" id="button-refresh">
+                        <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-refresh"></span></pwm:if>
+                        Refresh
+                    </button>
+                </form>
+                <button id="button-runJob" type="button" class="btn">
+                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-play"></span></pwm:if>
+                    Start Job
+                </button>
+            </div>
+
+        </pwm:if>
+        <br/>
+    </div>
+    <div class="push"></div>
+</div>
+<pwm:script>
+    <script type="text/javascript">
+
+        PWM_GLOBAL['startupFunctions'].push(function () {
+            PWM_MAIN.addEventHandler('button-runJob','click',function(){
+                PWM_MAIN.showWaitDialog({loadFunction:function(){
+                        var url = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction','startJob');
+                        PWM_MAIN.ajaxRequest(url,function(data){
+                            PWM_MAIN.showDialog({title:'Job Started',text:data['successMessage']})
+                        });
+                    }
+                });
+            });
+        });
+
+    </script>
+</pwm:script>
+<pwm:script-ref url="/public/resources/js/configmanager.js"/>
+<pwm:script-ref url="/public/resources/js/uilibrary.js"/>
+<pwm:script-ref url="/public/resources/js/admin.js"/>
+<div><%@ include file="fragment/footer.jsp" %></div>
+</body>
+</html>

+ 6 - 0
server/src/main/webapp/WEB-INF/jsp/fragment/configmanager-nav.jsp

@@ -49,6 +49,12 @@
             LocalDB
             LocalDB
         </button>
         </button>
     </form>
     </form>
+    <form action="<pwm:context/><%=PwmServletDefinition.ConfigManager_PwNotify.servletUrl()%>" method="get">
+        <button type="submit" class="navbutton">
+            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-envelope"></span></pwm:if>
+            Password Expiration Notification
+        </button>
+    </form>
 </div>
 </div>
 <br/>
 <br/>
 
 

+ 29 - 0
server/src/main/webapp/WEB-INF/jsp/login.jsp

@@ -133,6 +133,35 @@
                 </table>
                 </table>
             </pwm:if>
             </pwm:if>
         </pwm:if>
         </pwm:if>
+        <pwm:if test="<%=PwmIfTest.configurationOpen%>">
+            <table class="noborder">
+                <tr>
+                    <td colspan="2"><pwm:display key="Header_ConfigModeActive" bundle="Admin" value1="<%=PwmConstants.PWM_APP_NAME%>"/> </td>
+                </tr>
+                <tr>
+                    <td class="menubutton_key">
+                        <a class="menubutton" id="button-configmanager" href="<pwm:url addContext="true" url='<%=PwmServletDefinition.ConfigManager.servletUrl()%>'/>">
+                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-file-text-o"></span></pwm:if>
+                            Configuration Manager
+                        </a>
+                    </td>
+                    <td class="menubutton-description">
+                        .
+                    </td>
+                </tr>
+                <tr>
+                    <td class="menubutton_key">
+                        <a class="menubutton" id="button-configeditor" href="<pwm:url addContext="true" url='<%=PwmServletDefinition.ConfigEditor.servletUrl()%>'/>">
+                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-file-text-o"></span></pwm:if>
+                            Configuration Editor
+                        </a>
+                    </td>
+                    <td class="menubutton-description">
+                        .
+                    </td>
+                </tr>
+            </table>
+        </pwm:if>
     </div>
     </div>
     <div class="push"></div>
     <div class="push"></div>
 </div>
 </div>

+ 136 - 35
server/src/main/webapp/public/resources/js/configeditor-settings.js

@@ -331,8 +331,8 @@ StringArrayValueHandler.drawRow = function(settingKey, iteration, value, itemCou
                     }
                     }
                 };
                 };
                 PWM_MAIN.showWaitDialog({loadFunction:function(){
                 PWM_MAIN.showWaitDialog({loadFunction:function(){
-                    PWM_MAIN.ajaxRequest("editor?processAction=copyProfile",resultFunction,{content:options});
-                }});
+                        PWM_MAIN.ajaxRequest("editor?processAction=copyProfile",resultFunction,{content:options});
+                    }});
             };
             };
             UILibrary.stringEditorDialog(editorOptions);
             UILibrary.stringEditorDialog(editorOptions);
         });
         });
@@ -1242,13 +1242,13 @@ ChangePasswordHandler.markConfirmationCheck = function(matchStatus) {
 
 
 ChangePasswordHandler.doChange = function(settingKey, passwordValue) {
 ChangePasswordHandler.doChange = function(settingKey, passwordValue) {
     PWM_MAIN.showWaitDialog({loadFunction:function(){
     PWM_MAIN.showWaitDialog({loadFunction:function(){
-        PWM_CFGEDIT.writeSetting(settingKey,passwordValue,function(){
-            ChangePasswordHandler.clear(settingKey);
-            ChangePasswordHandler.init(settingKey);
-            PWM_MAIN.closeWaitDialog();
-        });
+            PWM_CFGEDIT.writeSetting(settingKey,passwordValue,function(){
+                ChangePasswordHandler.clear(settingKey);
+                ChangePasswordHandler.init(settingKey);
+                PWM_MAIN.closeWaitDialog();
+            });
 
 
-    }})
+        }})
 };
 };
 
 
 ChangePasswordHandler.clear = function(settingKey) {
 ChangePasswordHandler.clear = function(settingKey) {
@@ -1284,8 +1284,8 @@ ChangePasswordHandler.generateRandom = function(settingKey) {
     };
     };
 
 
     PWM_MAIN.showWaitDialog({loadFunction:function(){
     PWM_MAIN.showWaitDialog({loadFunction:function(){
-        PWM_MAIN.ajaxRequest(url,loadFunction,{content:postData});
-    }});
+            PWM_MAIN.ajaxRequest(url,loadFunction,{content:postData});
+        }});
 };
 };
 
 
 ChangePasswordHandler.changePasswordPopup = function(settingKey) {
 ChangePasswordHandler.changePasswordPopup = function(settingKey) {
@@ -1659,23 +1659,23 @@ ActionHandler.showOptionsDialog = function(keyName, iteration) {
                         });
                         });
                         PWM_MAIN.addEventHandler('button-' + inputID + '-clearCertificates','click',function() {
                         PWM_MAIN.addEventHandler('button-' + inputID + '-clearCertificates','click',function() {
                             PWM_MAIN.showConfirmDialog({okAction:function(){
                             PWM_MAIN.showConfirmDialog({okAction:function(){
-                                delete value['certificates'];
-                                delete value['certificateInfos'];
-                                ActionHandler.write(keyName, function(){ ActionHandler.showOptionsDialog(keyName,iteration)});
-                            },cancelAction:function(){
-                                ActionHandler.showOptionsDialog(keyName,iteration);
-                            }});
+                                    delete value['certificates'];
+                                    delete value['certificateInfos'];
+                                    ActionHandler.write(keyName, function(){ ActionHandler.showOptionsDialog(keyName,iteration)});
+                                },cancelAction:function(){
+                                    ActionHandler.showOptionsDialog(keyName,iteration);
+                                }});
                         });
                         });
                     } else {
                     } else {
                         PWM_MAIN.addEventHandler('button-' + inputID + '-importCertificates','click',function() {
                         PWM_MAIN.addEventHandler('button-' + inputID + '-importCertificates','click',function() {
                             var dataHandler = function(data) {
                             var dataHandler = function(data) {
                                 var msgBody = '<div style="max-height: 400px; overflow-y: auto">' + data['successMessage'] + '</div>';
                                 var msgBody = '<div style="max-height: 400px; overflow-y: auto">' + data['successMessage'] + '</div>';
                                 PWM_MAIN.showDialog({width:700,title: 'Results', text: msgBody, okAction: function () {
                                 PWM_MAIN.showDialog({width:700,title: 'Results', text: msgBody, okAction: function () {
-                                    PWM_CFGEDIT.readSetting(keyName, function(resultValue) {
-                                        PWM_VAR['clientSettingCache'][keyName] = resultValue;
-                                        ActionHandler.showOptionsDialog(keyName, iteration);
-                                    });
-                                }});
+                                        PWM_CFGEDIT.readSetting(keyName, function(resultValue) {
+                                            PWM_VAR['clientSettingCache'][keyName] = resultValue;
+                                            ActionHandler.showOptionsDialog(keyName, iteration);
+                                        });
+                                    }});
                             };
                             };
                             PWM_CFGEDIT.executeSettingFunction(keyName, 'password.pwm.config.function.ActionCertImportFunction', dataHandler, value['name'])
                             PWM_CFGEDIT.executeSettingFunction(keyName, 'password.pwm.config.function.ActionCertImportFunction', dataHandler, value['name'])
                         });
                         });
@@ -1915,9 +1915,9 @@ EmailTableHandler.instrumentRow = function(settingKey, localeName) {
 
 
     PWM_MAIN.addEventHandler("button-deleteRow-" + idPrefix,"click",function(){
     PWM_MAIN.addEventHandler("button-deleteRow-" + idPrefix,"click",function(){
         PWM_MAIN.showConfirmDialog({okAction:function(){
         PWM_MAIN.showConfirmDialog({okAction:function(){
-            delete PWM_VAR['clientSettingCache'][settingKey][localeName];
-            EmailTableHandler.writeSetting(settingKey,true);
-        }});
+                delete PWM_VAR['clientSettingCache'][settingKey][localeName];
+                EmailTableHandler.writeSetting(settingKey,true);
+            }});
     });
     });
 };
 };
 
 
@@ -2264,7 +2264,6 @@ NumericValueHandler.impl = function(settingKey, type, defaultMin, defaultMax) {
     var parentDiv = 'table_setting_' + settingKey;
     var parentDiv = 'table_setting_' + settingKey;
     var parentDivElement = PWM_MAIN.getObject(parentDiv);
     var parentDivElement = PWM_MAIN.getObject(parentDiv);
     var properties = PWM_SETTINGS['settings'][settingKey]['properties'];
     var properties = PWM_SETTINGS['settings'][settingKey]['properties'];
-    var pattern = PWM_SETTINGS['settings'][settingKey]['pattern'];
     var min = 'Minimum' in properties ? parseInt(properties['Minimum']) : defaultMin;
     var min = 'Minimum' in properties ? parseInt(properties['Minimum']) : defaultMin;
     var max = 'Maximum' in properties ? parseInt(properties['Maximum']) : defaultMax;
     var max = 'Maximum' in properties ? parseInt(properties['Maximum']) : defaultMax;
 
 
@@ -2307,9 +2306,111 @@ NumericValueHandler.updateDurationDisplay = function(settingKey, numberValue) {
 
 
 var DurationValueHandler = {};
 var DurationValueHandler = {};
 DurationValueHandler.init = function(settingKey) {
 DurationValueHandler.init = function(settingKey) {
-    NumericValueHandler.impl(settingKey, 'duration', -1, 365 * 24 * 60 * 60 );
+    NumericValueHandler.impl(settingKey, 'duration', -1, 365 * 24 * 60 * 60, 1 );
 };
 };
 
 
+// -------------------------- numeric array value handler ------------------------------------
+
+var NumericArrayValueHandler = {};
+NumericArrayValueHandler.init = function(settingKey) {
+    NumericArrayValueHandler.impl(settingKey, 'number', 0, 100);
+};
+
+NumericArrayValueHandler.impl = function(settingKey, type) {
+    PWM_CFGEDIT.readSetting(settingKey,function(data){
+        PWM_VAR['clientSettingCache'][settingKey] = data;
+        NumericArrayValueHandler.draw(settingKey, type);
+    });
+};
+
+NumericArrayValueHandler.draw = function(settingKey, type) {
+    var resultValue = PWM_VAR['clientSettingCache'][settingKey];
+
+    var parentDiv = 'table_setting_' + settingKey;
+    var parentDivElement = PWM_MAIN.getObject(parentDiv);
+    var properties = PWM_SETTINGS['settings'][settingKey]['properties'];
+    var min = 'Minimum' in properties ? parseInt(properties['Minimum']) : 1;
+    var max = 'Maximum' in properties ? parseInt(properties['Maximum']) : 365 * 24 * 60 * 60;
+    var minValues = 'Minimum_Values' in properties ? parseInt(properties['Minimum_Values']) : 1;
+    var maxValues = 'Maximum_Values' in properties ? parseInt(properties['Maximum_Values']) : 10;
+
+    var htmlBody = '<table class="noborder">';
+    for (var iteration in resultValue) {
+        (function(rowKey) {
+            var id = settingKey+ "-" + rowKey;
+
+            htmlBody += '<tr><td><input type="number" id="value-' + id + '" class="configNumericInput" min="'+min+'" max="'+max+'" disabled/>';
+            if (type === 'number') {
+                htmlBody += '<span class="configNumericLimits">' + min + ' - ' + max + '</span>';
+            } else if (type === 'duration') {
+                htmlBody +=  '<span class="configNumericLimits">' + PWM_MAIN.showString('Display_Seconds')  + '</span>'
+                htmlBody +=  '<span style="margin-left:20px" id="display-' + id + '-duration"></span>';
+            }
+            htmlBody += '</td><td>';
+            if ( resultValue.length > minValues ) {
+                htmlBody += '<span id="button-' + id + '-delete" class="delete-row-icon action-icon pwm-icon pwm-icon-times"></span>';
+            }
+            htmlBody += '</td></tr>';
+
+        }(iteration));
+    }
+
+    htmlBody += '</table>';
+    if ( resultValue.length < maxValues ) {
+        htmlBody += '<br/><button class="btn" id="button-addValue-' + settingKey + '">';
+        htmlBody += '<span class="btn-icon pwm-icon pwm-icon-plus-square"></span>Add Value';
+        htmlBody += '</button>';
+    }
+
+    parentDivElement.innerHTML = htmlBody;
+
+    var addListeners = function() {
+        for (var iteration in resultValue) {
+            (function(rowKey) {
+                var id = settingKey+ "-" + rowKey;
+                var readValue  = resultValue[rowKey];
+                PWM_MAIN.getObject('value-' + id).value = readValue;
+                PWM_MAIN.getObject('value-' + id).disabled = false;
+
+                UILibrary.manageNumericInput('value-' + id,function(value){
+                    PWM_VAR['clientSettingCache'][settingKey][rowKey] = value;
+                    PWM_CFGEDIT.writeSetting(settingKey, PWM_VAR['clientSettingCache'][settingKey]);
+                    NumericValueHandler.updateDurationDisplay(id, value);
+                });
+
+                PWM_MAIN.addEventHandler('value-' + settingKey,'mousewheel',function(e){ e.blur(); });
+                NumericValueHandler.updateDurationDisplay(id, readValue);
+
+                PWM_MAIN.addEventHandler('button-' + id + '-delete','click',function(){
+                    PWM_MAIN.showConfirmDialog({okAction:function(){
+                            PWM_VAR['clientSettingCache'][settingKey].splice(rowKey, 1);
+                            PWM_CFGEDIT.writeSetting(settingKey, PWM_VAR['clientSettingCache'][settingKey],function(){
+                                NumericArrayValueHandler.draw(settingKey, type);
+                            });
+                        }});
+                });
+
+            }(iteration));
+        }
+
+        PWM_MAIN.addEventHandler('button-addValue-' + settingKey,'click',function () {
+            PWM_VAR['clientSettingCache'][settingKey].push(86400);
+            PWM_CFGEDIT.writeSetting(settingKey, PWM_VAR['clientSettingCache'][settingKey],function(){
+                NumericArrayValueHandler.draw(settingKey, type);
+            });
+        });
+    };
+
+    addListeners();
+};
+
+
+// -------------------------- duration array value ---------------------------
+
+var DurationArrayValueHandler = {};
+DurationArrayValueHandler.init = function(settingKey) {
+    NumericArrayValueHandler.impl(settingKey, 'duration');
+};
 
 
 
 
 // -------------------------- string value handler ------------------------------------
 // -------------------------- string value handler ------------------------------------
@@ -2755,13 +2856,13 @@ FileValueHandler.draw = function(keyName) {
 
 
     PWM_MAIN.addEventHandler('button-removeFile-' + keyName,'click',function(){
     PWM_MAIN.addEventHandler('button-removeFile-' + keyName,'click',function(){
         PWM_MAIN.showConfirmDialog({text:'Are you sure you want to remove the currently stored file?',okAction:function(){
         PWM_MAIN.showConfirmDialog({text:'Are you sure you want to remove the currently stored file?',okAction:function(){
-            PWM_MAIN.showWaitDialog({loadFunction:function(){
-                PWM_CFGEDIT.resetSetting(keyName,function(){
-                    FileValueHandler.init(keyName);
-                    PWM_MAIN.closeWaitDialog();
-                });
+                PWM_MAIN.showWaitDialog({loadFunction:function(){
+                        PWM_CFGEDIT.resetSetting(keyName,function(){
+                            FileValueHandler.init(keyName);
+                            PWM_MAIN.closeWaitDialog();
+                        });
+                    }});
             }});
             }});
-        }});
     });
     });
 };
 };
 
 
@@ -2770,9 +2871,9 @@ FileValueHandler.uploadFile = function(keyName) {
     options['url'] = "editor?processAction=uploadFile&key=" + keyName;
     options['url'] = "editor?processAction=uploadFile&key=" + keyName;
     options['nextFunction'] = function() {
     options['nextFunction'] = function() {
         PWM_MAIN.showWaitDialog({loadFunction:function(){
         PWM_MAIN.showWaitDialog({loadFunction:function(){
-            FileValueHandler.init(keyName);
-            PWM_MAIN.closeWaitDialog();
-        }});
+                FileValueHandler.init(keyName);
+                PWM_MAIN.closeWaitDialog();
+            }});
     };
     };
     UILibrary.uploadFileDialog(options);
     UILibrary.uploadFileDialog(options);
 };
 };

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

@@ -974,6 +974,10 @@ PWM_CFGEDIT.initSettingDisplay = function(setting, options) {
             DurationValueHandler.init(settingKey);
             DurationValueHandler.init(settingKey);
             break;
             break;
 
 
+        case 'DURATION_ARRAY':
+            DurationArrayValueHandler.init(settingKey);
+            break;
+
         case 'STRING':
         case 'STRING':
             StringValueHandler.init(settingKey);
             StringValueHandler.init(settingKey);
             break;
             break;