passwords) {
+ return new PasswordSet(Objects.requireNonNull(passwords).stream());
+ }
+
+ /**
+ * Returns an offline database of the 100,000 most common passwords.
+ *
+ * @return an offline database of the 100,000 most common passwords
+ */
+ static BreachDatabase top100K() {
+ try (var in = BreachDatabase.class.getResourceAsStream("weak-passwords.txt");
+ var r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
+ return new PasswordSet(r.lines());
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Returns a database which checks the given databases in order.
+ *
+ * @param databases a set of databases
+ * @return a database which checks the given databases in order
+ */
+ static BreachDatabase anyOf(BreachDatabase... databases) {
+ for (var database : databases) {
+ Objects.requireNonNull(database);
+ }
+
+ return password -> {
+ for (var database : databases) {
+ if (database.contains(password)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ }
+}
Index: 3rdParty_sources/passpol/com/codahale/passpol/HaveIBeenPwned.java
===================================================================
diff -u
--- 3rdParty_sources/passpol/com/codahale/passpol/HaveIBeenPwned.java (revision 0)
+++ 3rdParty_sources/passpol/com/codahale/passpol/HaveIBeenPwned.java (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -0,0 +1,77 @@
+/*
+ * Copyright © 2018 Coda Hale (coda.hale@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.codahale.passpol;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+class HaveIBeenPwned implements BreachDatabase {
+ private static final int HASH_LENGTH = (160 / 8) * 2;
+ private static final int PREFIX_LENGTH = 5;
+ private static final int SUFFIX_LENGTH = HASH_LENGTH - PREFIX_LENGTH;
+ private static final int DELIM_LENGTH = 1;
+ private static final URI BASE_URI = URI.create("https://api.pwnedpasswords.com/range/");
+ private final HttpClient client;
+ private final int threshold;
+
+ HaveIBeenPwned(HttpClient client, int threshold) {
+ this.client = client;
+ this.threshold = threshold;
+ }
+
+ @Override
+ public boolean contains(String password) throws IOException {
+ try {
+ var hash = hex(MessageDigest.getInstance("SHA1").digest(PasswordPolicy.normalize(password)));
+ var request =
+ HttpRequest.newBuilder()
+ .GET()
+ .uri(BASE_URI.resolve(hash.substring(0, PREFIX_LENGTH)))
+ .header("User-Agent", "passpol")
+ .build();
+ var response = client.send(request, BodyHandlers.ofLines());
+ if (response.statusCode() != 200) {
+ throw new IOException("Unexpected response from server: " + response.statusCode());
+ }
+ return response
+ .body()
+ .filter(s -> s.regionMatches(0, hash, PREFIX_LENGTH, SUFFIX_LENGTH))
+ .map(s -> s.substring(HASH_LENGTH + DELIM_LENGTH))
+ .mapToInt(Integer::parseInt)
+ .anyMatch(t -> t >= threshold);
+ } catch (NoSuchAlgorithmException | InterruptedException | IndexOutOfBoundsException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private static final char[] HEX = {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+ };
+
+ private static String hex(byte[] bytes) {
+ var b = new StringBuilder(HASH_LENGTH);
+ for (var v : bytes) {
+ b.append(HEX[(v & 0xFF) >> 4]);
+ b.append(HEX[(v & 0x0F)]);
+ }
+ return b.toString();
+ }
+}
Index: 3rdParty_sources/passpol/com/codahale/passpol/PasswordPolicy.java
===================================================================
diff -u
--- 3rdParty_sources/passpol/com/codahale/passpol/PasswordPolicy.java (revision 0)
+++ 3rdParty_sources/passpol/com/codahale/passpol/PasswordPolicy.java (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -0,0 +1,113 @@
+/*
+ * Copyright © 2018 Coda Hale (coda.hale@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.codahale.passpol;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+/**
+ * A password policy which validates candidate passwords according to NIST's draft {@code
+ * SP-800-63B}, which recommend passwords have a minimum required length, a maximum required length,
+ * ad be checked against a list of weak passwords ({@code SP-800-63B 5.1.1.2}).
+ *
+ * This uses a static list of 10,000 weak passwords downloaded from Carey Li's NBP project.
+ *
+ * @see Draft NIST SP-800-63B
+ * @see NBP
+ */
+public class PasswordPolicy {
+
+ /** The recommended minimum password length, per {@code SP-800-63B 5.1.1.2}. */
+ public static final int RECOMMENDED_MIN_LENGTH = 8;
+
+ /** The recommended maximum password length, per {@code SP-800-63B 5.1.1.2}. */
+ public static final int RECOMMENDED_MAX_LENGTH = 64;
+
+ private final int minLength;
+ private final int maxLength;
+ private final BreachDatabase breachDatabase;
+
+ /**
+ * Creates a {@link PasswordPolicy} with a minimum password length of {@code 8} and a maximum
+ * password length of {@code 64}, as recommended in {@code SP-800-63B 5.1.1.2}. Uses the offline
+ * database of weak passwords.
+ *
+ * @see BreachDatabase#top100K()
+ * @see PasswordPolicy#RECOMMENDED_MIN_LENGTH
+ * @see PasswordPolicy#RECOMMENDED_MAX_LENGTH
+ */
+ public PasswordPolicy() {
+ this(BreachDatabase.top100K(), RECOMMENDED_MIN_LENGTH, RECOMMENDED_MAX_LENGTH);
+ }
+
+ /**
+ * Creates a {@link PasswordPolicy} with the given password length requirements.
+ *
+ * @param minLength the minimum length of passwords
+ * @param maxLength the maximum length of passwords
+ * @param breachDatabase a {@link BreachDatabase} instance
+ */
+ public PasswordPolicy(BreachDatabase breachDatabase, int minLength, int maxLength) {
+ if (maxLength < minLength) {
+ throw new IllegalArgumentException("minLength must be less than maxLength");
+ }
+ this.breachDatabase = breachDatabase;
+ this.minLength = minLength;
+ this.maxLength = maxLength;
+ }
+
+ /**
+ * Normalizes the given password as Unicode NFKC and returns it as UTF-8 encoded bytes, ready to
+ * be passed to a password hashing algorithm like {@code bcrypt}.
+ *
+ *
This is the process recommended in {@code NIST SP-800-63B 5.1.1.2}.
+ *
+ * @param password an arbitrary string
+ * @return a series of bytes suitable for hashing
+ */
+ public static byte[] normalize(String password) {
+ return PasswordSet.normalize(Objects.requireNonNull(password)).getBytes(StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Checks the acceptability of a candidate password.
+ *
+ * @param password a candidate password
+ * @return the status of {@code password}
+ */
+ public Status check(String password) {
+ var len = Objects.requireNonNull(password).codePointCount(0, password.length());
+
+ if (len < minLength) {
+ return Status.TOO_SHORT;
+ }
+
+ if (len > maxLength) {
+ return Status.TOO_LONG;
+ }
+
+ try {
+ if (breachDatabase.contains(password)) {
+ return Status.BREACHED;
+ }
+ } catch (IOException e) {
+ return Status.OK;
+ }
+
+ return Status.OK;
+ }
+}
Index: 3rdParty_sources/passpol/com/codahale/passpol/PasswordSet.java
===================================================================
diff -u
--- 3rdParty_sources/passpol/com/codahale/passpol/PasswordSet.java (revision 0)
+++ 3rdParty_sources/passpol/com/codahale/passpol/PasswordSet.java (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -0,0 +1,62 @@
+/*
+ * Copyright © 2018 Coda Hale (coda.hale@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.codahale.passpol;
+
+import java.text.Normalizer;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+class PasswordSet implements BreachDatabase {
+
+ private final Set passwords;
+
+ PasswordSet(Stream passwords) {
+ this.passwords = passwords.map(PasswordSet::normalize).collect(Collectors.toUnmodifiableSet());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PasswordSet)) {
+ return false;
+ }
+ var that = (PasswordSet) o;
+ return Objects.equals(passwords, that.passwords);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(passwords);
+ }
+
+ @Override
+ public String toString() {
+ return passwords.toString();
+ }
+
+ @Override
+ public boolean contains(String password) {
+ return passwords.contains(normalize(password));
+ }
+
+ static String normalize(String s) {
+ return Normalizer.normalize(s, Normalizer.Form.NFKC);
+ }
+}
Index: 3rdParty_sources/passpol/com/codahale/passpol/Status.java
===================================================================
diff -u
--- 3rdParty_sources/passpol/com/codahale/passpol/Status.java (revision 0)
+++ 3rdParty_sources/passpol/com/codahale/passpol/Status.java (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2018 Coda Hale (coda.hale@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.codahale.passpol;
+
+/** The status of a given candidate password. */
+public enum Status {
+ /** The candidate password is acceptable. */
+ OK,
+
+ /** The candidate password is too short. */
+ TOO_SHORT,
+
+ /** The candidate password is too long. */
+ TOO_LONG,
+
+ /** The candidate password has previously appeared in a data breach. */
+ BREACHED
+}
Index: 3rdParty_sources/versions.txt
===================================================================
diff -u -r329cf66d1bc9d7aa6fb468d187ed81c8780193b4 -re88830091139dd447cdd6a7f30c129c777104dac
--- 3rdParty_sources/versions.txt (.../versions.txt) (revision 329cf66d1bc9d7aa6fb468d187ed81c8780193b4)
+++ 3rdParty_sources/versions.txt (.../versions.txt) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -55,6 +55,8 @@
Quartz 2.2.3
+passpol 0.7.1-SNAPSHOT
+
picketbox 5.0.3
Servlet API 4.0.0
Index: lams_admin/conf/language/lams/ApplicationResources.properties
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_admin/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_admin/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -474,6 +474,7 @@
label.password.must.number = at least 1 number
label.password.must.symbol = at least 1 symbol
label.password.user.details = must not be the same as user login, ID, email or names
+label.password.common = must not be common or breached
sysadmin.batch.preview.lesson.delete = Delete old preview lessons
msg.cleanup.preview.lesson.confirm = Are you sure you want to delete all preview lessons?
msg.cleanup.preview.lesson.error = Error while deleting preview lessons
Index: lams_admin/web/import/importexcel.jsp
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_admin/web/import/importexcel.jsp (.../importexcel.jsp) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_admin/web/import/importexcel.jsp (.../importexcel.jsp) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -181,6 +181,9 @@
+
+
+
Index: lams_admin/web/orgPasswordChange.jsp
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_admin/web/orgPasswordChange.jsp (.../orgPasswordChange.jsp) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_admin/web/orgPasswordChange.jsp (.../orgPasswordChange.jsp) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -377,6 +377,9 @@
+
+
+
Index: lams_admin/web/user.jsp
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_admin/web/user.jsp (.../user.jsp) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_admin/web/user.jsp (.../user.jsp) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -243,7 +243,9 @@
-
+
+
+
Index: lams_admin/web/userChangePass.jsp
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_admin/web/userChangePass.jsp (.../userChangePass.jsp) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_admin/web/userChangePass.jsp (.../userChangePass.jsp) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -135,7 +135,9 @@
-
+
+
+
Index: lams_build/3rdParty.userlibraries
===================================================================
diff -u -r7060fa5b595fa324e138924099faf98e52cb5ca1 -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_build/3rdParty.userlibraries (.../3rdParty.userlibraries) (revision 7060fa5b595fa324e138924099faf98e52cb5ca1)
+++ lams_build/3rdParty.userlibraries (.../3rdParty.userlibraries) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -7,8 +7,8 @@
-
-
+
+
@@ -18,14 +18,14 @@
-
+
-
+
@@ -39,10 +39,11 @@
-
+
+
Index: lams_build/build.xml
===================================================================
diff -u -r7060fa5b595fa324e138924099faf98e52cb5ca1 -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_build/build.xml (.../build.xml) (revision 7060fa5b595fa324e138924099faf98e52cb5ca1)
+++ lams_build/build.xml (.../build.xml) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -425,6 +425,14 @@
+
+
+
+
+
+
+
Index: lams_build/conf/j2ee/jboss-deployment-structure.xml
===================================================================
diff -u -r7060fa5b595fa324e138924099faf98e52cb5ca1 -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_build/conf/j2ee/jboss-deployment-structure.xml (.../jboss-deployment-structure.xml) (revision 7060fa5b595fa324e138924099faf98e52cb5ca1)
+++ lams_build/conf/j2ee/jboss-deployment-structure.xml (.../jboss-deployment-structure.xml) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -55,6 +55,7 @@
+
+
+
+
+
+
+
Index: lams_build/liblist.txt
===================================================================
diff -u -r7060fa5b595fa324e138924099faf98e52cb5ca1 -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_build/liblist.txt (.../liblist.txt) (revision 7060fa5b595fa324e138924099faf98e52cb5ca1)
+++ lams_build/liblist.txt (.../liblist.txt) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -56,6 +56,8 @@
odmg odmg-3.0.jar 3.0
+passpol passpol-0.7.1-SNAPSHOT.jar 0.7.1 Apache License 2.0 Codahale For checking for weak or compromised password. Recompiled manually with Java 11.
+
quartz quartz-2.2.3.jar 2.2.3 Apache License 2.0 Terracotta For running scheduled jobs
spring spring-core-4.3.12.RELEASE.jar 4.3.12 Apache License 2.0 Pivotal programming and configuration model for modern Java-based enterprise applications
Index: lams_central/conf/language/lams/ApplicationResources.properties
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -415,6 +415,7 @@
label.password.must.number = at least 1 number
label.password.must.symbol = at least 1 symbol
label.password.user.details = must not be the same as user login, ID, email or names
+label.password.common = must not be common or breached
label.password.history = must not be the same as last {0} passwords
label.create.lesson = Create new lesson
label.organisations = Select course with the lessons that needs to be export
Index: lams_central/web/forgotPasswordChange.jsp
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_central/web/forgotPasswordChange.jsp (.../forgotPasswordChange.jsp) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_central/web/forgotPasswordChange.jsp (.../forgotPasswordChange.jsp) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -128,6 +128,9 @@
+
+
+
Index: lams_central/web/passwordChangeContent.jsp
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_central/web/passwordChangeContent.jsp (.../passwordChangeContent.jsp) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_central/web/passwordChangeContent.jsp (.../passwordChangeContent.jsp) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -141,6 +141,9 @@
+
+
+
Index: lams_central/web/signup/singupTab.jsp
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_central/web/signup/singupTab.jsp (.../singupTab.jsp) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_central/web/signup/singupTab.jsp (.../singupTab.jsp) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -186,6 +186,9 @@
+
+
+
Index: lams_common/src/java/org/lamsfoundation/lams/util/ValidationUtil.java
===================================================================
diff -u -r26a83b93c1ce8fa610895f50b57d44d6b7cc11db -re88830091139dd447cdd6a7f30c129c777104dac
--- lams_common/src/java/org/lamsfoundation/lams/util/ValidationUtil.java (.../ValidationUtil.java) (revision 26a83b93c1ce8fa610895f50b57d44d6b7cc11db)
+++ lams_common/src/java/org/lamsfoundation/lams/util/ValidationUtil.java (.../ValidationUtil.java) (revision e88830091139dd447cdd6a7f30c129c777104dac)
@@ -27,13 +27,20 @@
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
import org.lamsfoundation.lams.usermanagement.User;
+import com.codahale.passpol.BreachDatabase;
+import com.codahale.passpol.PasswordPolicy;
+import com.codahale.passpol.Status;
+
/**
* Utility methods for String validation.
*/
public class ValidationUtil {
+ private static Logger log = Logger.getLogger(ValidationUtil.class);
+
private final static Pattern REGEX_USER_NAME = Pattern.compile("^[^<>^!#&()/\\|\"?,:{}= ~`*%$]*$");
private final static Pattern REGEX_FIRST_LAST_NAME = Pattern.compile("^[\\p{L}]++(?:[' -][\\p{L}]++)*+\\.?$");
@@ -142,7 +149,12 @@
}
- return ValidationUtil.isPasswordNotUserDetails(password, user);
+ boolean isPasswordNotUserDetails = ValidationUtil.isPasswordNotUserDetails(password, user);
+ if (!isPasswordNotUserDetails) {
+ return false;
+ }
+
+ return ValidationUtil.isPasswordNotBreached(password);
}
/**
@@ -191,6 +203,26 @@
return true;
}
+ public static boolean isPasswordNotBreached(String password) {
+ try {
+ // check for a static list of 100k known weak passwords
+ PasswordPolicy commonPasswordPolicy = new PasswordPolicy(BreachDatabase.top100K(), 0, Integer.MAX_VALUE);
+ if (Status.OK != commonPasswordPolicy.check(password)) {
+ return false;
+ }
+
+ // check online DB
+ PasswordPolicy pwnedPolicy = new PasswordPolicy(BreachDatabase.haveIBeenPwned(), 0, Integer.MAX_VALUE);
+ if (Status.OK != pwnedPolicy.check(password)) {
+ return false;
+ }
+
+ } catch (Exception e) {
+ log.error("Error while checking password for breach", e);
+ }
+ return true;
+ }
+
/**
* Checks whether supplied email address is valid. It validates email only if USER_VALIDATION_REQUIRED_EMAIL LAMS
* configuration is ON.