diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs
index 42b1d1b7..2a519d84 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs
@@ -129,9 +129,9 @@ along with this program. If not, see .";
if (message.StartsWith("/setemail ") && ServerConfiguration.Instance.Mail.MailEnabled)
{
string email = message[(message.IndexOf(" ", StringComparison.Ordinal)+1)..];
- if (!SanitizationHelper.IsValidEmail(email)) return this.Ok();
- if (await this.database.Users.AnyAsync(u => u.EmailAddress == email)) return this.Ok();
+ // Return a bad request on invalid email address
+ if (!SMTPHelper.IsValidEmail(this.database, email)) return this.BadRequest();
UserEntity? user = await this.database.UserFromGameToken(token);
if (user == null || user.EmailAddressVerified) return this.Ok();
diff --git a/ProjectLighthouse.Servers.Website/Pages/Email/SetEmailForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Email/SetEmailForm.cshtml.cs
index b760f03e..7cca3b9c 100644
--- a/ProjectLighthouse.Servers.Website/Pages/Email/SetEmailForm.cshtml.cs
+++ b/ProjectLighthouse.Servers.Website/Pages/Email/SetEmailForm.cshtml.cs
@@ -39,7 +39,7 @@ public class SetEmailForm : BaseLayout
UserEntity? user = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == token.UserId);
if (user == null) return this.Redirect("~/login");
- if (!SanitizationHelper.IsValidEmail(emailAddress))
+ if (!SMTPHelper.IsValidEmail(this.Database, emailAddress))
{
this.Error = this.Translate(ErrorStrings.EmailInvalid);
return this.Page();
diff --git a/ProjectLighthouse.Servers.Website/Pages/Login/PasswordResetRequestForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Login/PasswordResetRequestForm.cshtml.cs
index 9d9aeff7..c4bb84da 100644
--- a/ProjectLighthouse.Servers.Website/Pages/Login/PasswordResetRequestForm.cshtml.cs
+++ b/ProjectLighthouse.Servers.Website/Pages/Login/PasswordResetRequestForm.cshtml.cs
@@ -38,9 +38,9 @@ public class PasswordResetRequestForm : BaseLayout
return this.Page();
}
- if (!SanitizationHelper.IsValidEmail(email))
+ if (!SMTPHelper.IsValidEmail(this.Database, email))
{
- this.Error = "This email is in an invalid format";
+ this.Error = "This email is invalid";
return this.Page();
}
diff --git a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs
index 55375a42..d1a9c0a8 100644
--- a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs
+++ b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs
@@ -65,17 +65,13 @@ public class UserSettingsPage : BaseLayout
}
if (ServerConfiguration.Instance.Mail.MailEnabled &&
- SanitizationHelper.IsValidEmail(email) &&
+ SMTPHelper.IsValidEmail(this.Database, email) &&
(this.User == this.ProfileUser || this.User.IsAdmin))
{
- // if email hasn't already been used
- if (!await this.Database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == email!.ToLower()))
+ if (this.ProfileUser.EmailAddress != email)
{
- if (this.ProfileUser.EmailAddress != email)
- {
- this.ProfileUser.EmailAddress = email;
- this.ProfileUser.EmailAddressVerified = false;
- }
+ this.ProfileUser.EmailAddress = email;
+ this.ProfileUser.EmailAddressVerified = false;
}
}
diff --git a/ProjectLighthouse/Configuration/EnforceEmailConfiguration.cs b/ProjectLighthouse/Configuration/EnforceEmailConfiguration.cs
new file mode 100644
index 00000000..c3354fdb
--- /dev/null
+++ b/ProjectLighthouse/Configuration/EnforceEmailConfiguration.cs
@@ -0,0 +1,25 @@
+#nullable enable
+using System.Collections.Generic;
+using System.IO;
+using YamlDotNet.Serialization;
+
+namespace LBPUnion.ProjectLighthouse.Configuration;
+
+public class EnforceEmailConfiguration : ConfigurationBase
+{
+ public override int ConfigVersion { get; set; } = 2;
+
+ public override string ConfigName { get; set; } = "enforce-email.yml";
+
+ public override bool NeedsConfiguration { get; set; } = false;
+
+ public bool EnableEmailEnforcement { get; set; } = false;
+ public bool EnableEmailBlacklist { get; set; } = false;
+
+ // No blacklist by default, add path to blacklist
+ public string BlacklistFilePath { get; set; } = "";
+
+ public override ConfigurationBase Deserialize
+ (IDeserializer deserializer, string text) =>
+ deserializer.Deserialize(text);
+}
\ No newline at end of file
diff --git a/ProjectLighthouse/Helpers/EmailHelper.cs b/ProjectLighthouse/Helpers/EmailHelper.cs
index 52c69f9d..bb780a3f 100644
--- a/ProjectLighthouse/Helpers/EmailHelper.cs
+++ b/ProjectLighthouse/Helpers/EmailHelper.cs
@@ -2,9 +2,11 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
using System.Linq;
using System.Threading.Tasks;
-using LBPUnion.ProjectLighthouse.Configuration;
+using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
@@ -20,6 +22,13 @@ public static class SMTPHelper
private static readonly ConcurrentDictionary recentlySentMail = new();
private const long emailCooldown = 1000 * 30;
+
+ // To prevent ReadAllLines() exception when BlacklistFilePath is empty
+ private static readonly string[] blacklistFile =
+ !string.IsNullOrWhiteSpace(EnforceEmailConfiguration.Instance.BlacklistFilePath)
+ ? File.ReadAllLines(EnforceEmailConfiguration.Instance.BlacklistFilePath) : [];
+
+ private static readonly HashSet blacklistedDomains = new(blacklistFile);
private static bool CanSendMail(UserEntity user)
{
@@ -68,6 +77,33 @@ public static class SMTPHelper
recentlySentMail.TryAdd(user.UserId, TimeHelper.TimestampMillis + emailCooldown);
}
+
+ // Accumulate checks to determine email validity
+ public static bool IsValidEmail(DatabaseContext database, string email)
+ {
+ // Email should not be empty, should be an actual email, and shouldn't already be used by an account
+ if (!string.IsNullOrWhiteSpace(email) && new EmailAddressAttribute().IsValid(email) && !EmailIsUsed(database, email).Result)
+ {
+ // Get domain after '@' character
+ string domain = email.Split('@')[1];
+
+ // Don't even bother if there are no domains in blacklist (AKA file path is empty/invalid, or file itself is empty)
+ if (EnforceEmailConfiguration.Instance.EnableEmailBlacklist && blacklistedDomains.Count > 0) return !DomainIsInBlacklist(domain);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ // Check if email is already in use by an account
+ private static async Task EmailIsUsed(DatabaseContext database, string email)
+ {
+ return await database.Users.AnyAsync(u => u.EmailAddress != null && u.EmailAddress.ToLower() == email.ToLower());
+ }
+
+ // Check if domain blacklist contains input domain
+ private static bool DomainIsInBlacklist(string domain) => blacklistedDomains.Contains(domain);
public static void SendRegistrationEmail(IMailService mail, UserEntity user)
{
diff --git a/ProjectLighthouse/Helpers/SanitizationHelper.cs b/ProjectLighthouse/Helpers/SanitizationHelper.cs
deleted file mode 100644
index 0065bc9f..00000000
--- a/ProjectLighthouse/Helpers/SanitizationHelper.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-#nullable enable
-using System.ComponentModel.DataAnnotations;
-
-namespace LBPUnion.ProjectLighthouse.Helpers;
-
-public static class SanitizationHelper
-{
- public static bool IsValidEmail(string? email) => !string.IsNullOrWhiteSpace(email) && new EmailAddressAttribute().IsValid(email);
-}
\ No newline at end of file