diff --git a/.idea/.idea.ProjectLighthouse/.idea/sqldialects.xml b/.idea/.idea.ProjectLighthouse/.idea/sqldialects.xml
deleted file mode 100644
index 56782cab..00000000
--- a/.idea/.idea.ProjectLighthouse/.idea/sqldialects.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ProjectLighthouse.Localization/BaseLayout.resx b/ProjectLighthouse.Localization/BaseLayout.resx
index 65d1229f..5a44aaea 100644
--- a/ProjectLighthouse.Localization/BaseLayout.resx
+++ b/ProjectLighthouse.Localization/BaseLayout.resx
@@ -93,4 +93,13 @@
This instance is currently in read-only mode. Level and photo uploads, comments, reviews, and certain profile changes will be restricted until read-only mode is disabled.
+
+ This instance has email enforcement enabled. If you haven't already, you will need to set and verify an email address to use most features.
+
+
+ You do not have an email set on your account. You can set an email by opening the text chat and typing "/setemail [youremail@example.com]" (do not include the brackets.)
+
+
+ You have set an email address on your account, but you have not verified it. Make sure to check your inbox for a verification email. If you have not recieved an email, please contact an instance administrator for further assistance.
+
\ No newline at end of file
diff --git a/ProjectLighthouse.Localization/StringLists/BaseLayoutStrings.cs b/ProjectLighthouse.Localization/StringLists/BaseLayoutStrings.cs
index f46cb4e6..38e12333 100644
--- a/ProjectLighthouse.Localization/StringLists/BaseLayoutStrings.cs
+++ b/ProjectLighthouse.Localization/StringLists/BaseLayoutStrings.cs
@@ -26,5 +26,10 @@ public static class BaseLayoutStrings
public static readonly TranslatableString ReadOnlyWarnTitle = create("read_only_warn_title");
public static readonly TranslatableString ReadOnlyWarn = create("read_only_warn");
+ public static readonly TranslatableString EmailEnforcementWarnMain = create("email_enforcement_message_main");
+ public static readonly TranslatableString EmailEnforcementWarnNoEmail = create("email_enforcement_message_no_email");
+ public static readonly TranslatableString EmailEnforcementWarnVerifyEmail = create("email_enforcement_message_verify_email");
+
+
private static TranslatableString create(string key) => new(TranslationAreas.BaseLayout, key);
}
\ No newline at end of file
diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs
index 7e3baeb9..8f550acf 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs
@@ -49,7 +49,7 @@ public class CommentController : ControllerBase
GameTokenEntity token = this.GetToken();
UserEntity? user = await this.database.UserFromGameToken(token);
- if (user == null) return this.Unauthorized();
+ if (user == null) return this.Forbid();
if ((slotId == 0 || SlotHelper.IsTypeInvalid(slotType)) == (username == null)) return this.BadRequest();
@@ -97,7 +97,7 @@ public class CommentController : ControllerBase
.ApplyPagination(pageData)
.ToListAsync()).ToSerializableList(c => GameComment.CreateFromEntity(c, token.UserId));
- if (type == CommentType.Level && slotType == "developer" && user.IsModerator && pageData.PageStart == 1)
+ if (type == CommentType.Level && slotType == "developer" && user.IsModerator && pageData.PageStart == 1)
{
comments.Insert(0, new GameComment
{
diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/EnterLevelController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/EnterLevelController.cs
index e8b2aa37..428580b8 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/EnterLevelController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/EnterLevelController.cs
@@ -4,6 +4,7 @@ using LBPUnion.ProjectLighthouse.Extensions;
using LBPUnion.ProjectLighthouse.Helpers;
using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
+using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
using LBPUnion.ProjectLighthouse.Types.Users;
using Microsoft.AspNetCore.Authorization;
@@ -100,6 +101,9 @@ public class EnterLevelController : ControllerBase
{
GameTokenEntity token = this.GetToken();
+ UserEntity? user = await this.database.UserFromGameToken(token);
+ if (user == null) return this.Unauthorized();
+
if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();
if (slotType == "developer") return this.Ok();
diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs
index 2b3b295b..ffba0d6a 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs
@@ -41,7 +41,7 @@ public class MatchController : ControllerBase
GameTokenEntity token = this.GetToken();
UserEntity? user = await this.database.UserFromGameToken(token);
- if (user == null) return this.Forbid();
+ if (user == null) return this.Unauthorized();
await LastContactHelper.SetLastContact(this.database, user, token.GameVersion, token.Platform);
diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs
index 42b1d1b7..b6948dfd 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs
@@ -55,16 +55,31 @@ along with this program. If not, see .";
{
GameTokenEntity token = this.GetToken();
- string username = await this.database.UsernameFromGameToken(token);
+ UserEntity? user = await this.database.UserFromGameToken(token);
+ if (user == null) return this.BadRequest();
StringBuilder announceText = new(ServerConfiguration.Instance.AnnounceText);
- announceText.Replace("%user", username);
+ announceText.Replace("%user", user.Username);
announceText.Replace("%id", token.UserId.ToString());
if (ServerConfiguration.Instance.UserGeneratedContentLimits.ReadOnlyMode)
{
- announceText.Insert(0, BaseLayoutStrings.ReadOnlyWarn.Translate(LocalizationManager.DefaultLang) + "\n\n");
+ announceText.Append(BaseLayoutStrings.ReadOnlyWarn.Translate(LocalizationManager.DefaultLang) + "\n\n");
+ }
+
+ if (ServerConfiguration.Instance.EmailEnforcement.EnableEmailEnforcement)
+ {
+ announceText.Append("\n\n" + BaseLayoutStrings.EmailEnforcementWarnMain.Translate(LocalizationManager.DefaultLang) + "\n\n");
+
+ if (user.EmailAddress == null)
+ {
+ announceText.Append(BaseLayoutStrings.EmailEnforcementWarnNoEmail.Translate(LocalizationManager.DefaultLang) + "\n\n");
+ }
+ else if (!user.EmailAddressVerified)
+ {
+ announceText.Append(BaseLayoutStrings.EmailEnforcementWarnVerifyEmail.Translate(LocalizationManager.DefaultLang) + "\n\n");
+ }
}
#if DEBUG
@@ -129,12 +144,12 @@ 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();
+ if (user == null || user.EmailAddressVerified) return this.BadRequest();
user.EmailAddress = email;
await SMTPHelper.SendVerificationEmail(this.database, mailService, user);
diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs
index 4c73d0a0..80798067 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs
@@ -174,7 +174,6 @@ public class PhotosController : ControllerBase
[HttpGet("photos/{slotType}/{id:int}")]
public async Task SlotPhotos(string slotType, int id, [FromQuery] string? by)
{
-
if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();
if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);
@@ -202,7 +201,6 @@ public class PhotosController : ControllerBase
[HttpGet("photos/by")]
public async Task UserPhotosBy(string user)
{
-
int targetUserId = await this.database.UserIdFromUsername(user);
if (targetUserId == 0) return this.NotFound();
diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs
index 7231ccaf..081614d6 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/ResourcesController.cs
@@ -19,7 +19,6 @@ namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Resources;
[Route("LITTLEBIGPLANETPS3_XML")]
public class ResourcesController : ControllerBase
{
-
[HttpPost("showModerated")]
public IActionResult ShowModerated() => this.Ok(new ResourceList());
diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs
index 60ac1c58..214bc82f 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/CategoryController.cs
@@ -38,9 +38,6 @@ public class CategoryController : ControllerBase
{
GameTokenEntity token = this.GetToken();
- UserEntity? user = await this.database.UserFromGameToken(token);
- if (user == null) return this.Forbid();
-
PaginationData pageData = this.Request.GetPaginationData();
pageData.TotalElements = CategoryHelper.Categories.Count(c => !string.IsNullOrWhiteSpace(c.Name));
@@ -72,9 +69,6 @@ public class CategoryController : ControllerBase
{
GameTokenEntity token = this.GetToken();
- UserEntity? user = await this.database.UserFromGameToken(token);
- if (user == null) return this.Forbid();
-
Category? category = CategoryHelper.Categories.FirstOrDefault(c => c.Endpoint == endpointName);
if (category == null) return this.NotFound();
diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs
index dfb42c71..aee40fa8 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/PublishController.cs
@@ -40,7 +40,7 @@ public class PublishController : ControllerBase
public async Task StartPublish()
{
GameTokenEntity token = this.GetToken();
-
+
UserEntity? user = await this.database.UserFromGameToken(token);
if (user == null) return this.Forbid();
diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs
index 7301bc52..463e2fee 100644
--- a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs
+++ b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs
@@ -36,7 +36,7 @@ public class UserController : ControllerBase
[HttpGet("user/{username}")]
public async Task GetUser(string username)
- {
+ {
UserEntity? user = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user == null) return this.NotFound();
diff --git a/ProjectLighthouse.Servers.GameServer/Middlewares/EmailEnforcementMiddleware.cs b/ProjectLighthouse.Servers.GameServer/Middlewares/EmailEnforcementMiddleware.cs
new file mode 100644
index 00000000..7482439c
--- /dev/null
+++ b/ProjectLighthouse.Servers.GameServer/Middlewares/EmailEnforcementMiddleware.cs
@@ -0,0 +1,62 @@
+using LBPUnion.ProjectLighthouse.Configuration;
+using LBPUnion.ProjectLighthouse.Database;
+using LBPUnion.ProjectLighthouse.Middlewares;
+using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
+using LBPUnion.ProjectLighthouse.Types.Entities.Token;
+
+namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Middlewares;
+
+public class EmailEnforcementMiddleware : MiddlewareDBContext
+{
+ private static readonly HashSet enforcedPaths = ServerConfiguration.Instance.EmailEnforcement.BlockedEndpoints;
+
+ public EmailEnforcementMiddleware(RequestDelegate next) : base(next)
+ { }
+
+ public override async Task InvokeAsync(HttpContext context, DatabaseContext database)
+ {
+ if (ServerConfiguration.Instance.EmailEnforcement.EnableEmailEnforcement)
+ {
+ // Split path into segments
+ string[] pathSegments = context.Request.Path.ToString().Split("/", StringSplitOptions.RemoveEmptyEntries);
+
+ if (pathSegments[0] == "LITTLEBIGPLANETPS3_XML")
+ {
+ // Get user via GameToken
+ GameTokenEntity? token = await database.GameTokenFromRequest(context.Request);
+ UserEntity? user = await database.UserFromGameToken(token);
+
+ // Check second part of path to see if client is within an enforced path
+ // This could probably be reworked, seeing as you may want to check for a deeper sub-path
+ // But it should be perfectly fine for now
+ if (enforcedPaths.Contains(pathSegments[1]))
+ {
+ // Check if user is valid, don't want any exceptions
+ if (user == null)
+ {
+ // Send bad request status
+ context.Response.StatusCode = StatusCodes.Status403Forbidden;
+ await context.Response.WriteAsync("Not a valid user");
+
+ // Don't go to next in pipeline
+ return;
+ }
+
+ // Check if email is there and verified
+ if (!user.EmailAddressVerified || user.EmailAddress == null)
+ {
+ // Send bad request status
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
+ await context.Response.WriteAsync("Invalid user email address");
+
+ // Don't go to next in pipeline
+ return;
+ }
+ }
+ }
+ }
+
+ // Go to next in pipeline
+ await this.next(context);
+ }
+}
\ No newline at end of file
diff --git a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs
index 0d372e8f..8b91a3e6 100644
--- a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs
+++ b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs
@@ -107,6 +107,7 @@ public class GameServerStartup
app.UseMiddleware();
app.UseMiddleware(computeDigests);
app.UseMiddleware();
+ app.UseMiddleware();
app.UseRouting();
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..8904e294 100644
--- a/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs
+++ b/ProjectLighthouse.Servers.Website/Pages/UserSettingsPage.cshtml.cs
@@ -64,18 +64,14 @@ public class UserSettingsPage : BaseLayout
}
}
- if (ServerConfiguration.Instance.Mail.MailEnabled &&
- SanitizationHelper.IsValidEmail(email) &&
+ if (ServerConfiguration.Instance.Mail.MailEnabled &&
+ email != null && 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.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs
index a0b42a0a..acda5cbc 100644
--- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs
+++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs
@@ -224,7 +224,7 @@ along with this program. If not, see ." + "\nuni
IActionResult result = await messageController.Filter(mailMock.Object);
- Assert.IsType(result);
+ Assert.IsType(result);
mailMock.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
@@ -249,7 +249,7 @@ along with this program. If not, see ." + "\nuni
IActionResult result = await messageController.Filter(mailMock.Object);
- Assert.IsType(result);
+ Assert.IsType(result);
mailMock.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
@@ -271,7 +271,7 @@ along with this program. If not, see ." + "\nuni
IActionResult result = await messageController.Filter(mailMock.Object);
- Assert.IsType(result);
+ Assert.IsType(result);
mailMock.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never);
}
}
\ No newline at end of file
diff --git a/ProjectLighthouse/Configuration/EmailEnforcementConfiguration.cs b/ProjectLighthouse/Configuration/EmailEnforcementConfiguration.cs
new file mode 100644
index 00000000..ee381926
--- /dev/null
+++ b/ProjectLighthouse/Configuration/EmailEnforcementConfiguration.cs
@@ -0,0 +1,72 @@
+#nullable enable
+using System.Collections.Generic;
+using YamlDotNet.Serialization;
+
+namespace LBPUnion.ProjectLighthouse.Configuration;
+
+public class EmailEnforcementConfiguration : ConfigurationBase
+{
+ public override int ConfigVersion { get; set; } = 1;
+
+ 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; } = "";
+
+ // Endpoints to be blocked
+ // This is kind of a random list so some may need to be added or removed
+ public HashSet BlockedEndpoints { get; set; } = new()
+ {
+ // Comments
+ "rateUserComment",
+ "rateComment",
+ "comments",
+ "userComments",
+ "postUserComment",
+ "postComment",
+ "deleteUserComment",
+ "deleteComment",
+
+ // Slots
+ "showModerated",
+ "startPublish",
+ "slots",
+ "s",
+ "tags",
+ "tag",
+ "searches",
+ "genres",
+ "publish",
+ "unpublish",
+
+ // Misc Resources
+ "upload",
+ "r",
+
+ // Photos
+ "uploadPhoto",
+ "photos",
+ "deletePhoto",
+
+ // Gameplay
+ "match",
+ "play",
+ "enterLevel",
+ "playlists",
+
+ // Users
+ "user",
+ "users",
+ "updateUser",
+ "update_my_pins",
+ };
+
+ public override ConfigurationBase Deserialize
+ (IDeserializer deserializer, string text) =>
+ deserializer.Deserialize(text);
+}
\ No newline at end of file
diff --git a/ProjectLighthouse/Configuration/ServerConfiguration.cs b/ProjectLighthouse/Configuration/ServerConfiguration.cs
index ba9f7f4a..584c1a0e 100644
--- a/ProjectLighthouse/Configuration/ServerConfiguration.cs
+++ b/ProjectLighthouse/Configuration/ServerConfiguration.cs
@@ -11,7 +11,7 @@ public class ServerConfiguration : ConfigurationBase
// This is so Lighthouse can properly identify outdated configurations and update them with newer settings accordingly.
// If you are modifying anything here, this value MUST be incremented.
// Thanks for listening~
- public override int ConfigVersion { get; set; } = 27;
+ public override int ConfigVersion { get; set; } = 28;
public override string ConfigName { get; set; } = "lighthouse.yml";
public string WebsiteListenUrl { get; set; } = "http://localhost:10060";
@@ -46,5 +46,7 @@ public class ServerConfiguration : ConfigurationBase
public RichPresenceConfiguration RichPresenceConfiguration { get; set; } = new();
public NotificationConfiguration NotificationConfiguration { get; set; } = new();
+ public EmailEnforcementConfiguration EmailEnforcement { get; set; } = new();
+
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..92b5bb14 100644
--- a/ProjectLighthouse/Helpers/EmailHelper.cs
+++ b/ProjectLighthouse/Helpers/EmailHelper.cs
@@ -2,13 +2,17 @@
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.Database;
using LBPUnion.ProjectLighthouse.Extensions;
+using LBPUnion.ProjectLighthouse.Logging;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
+using LBPUnion.ProjectLighthouse.Types.Logging;
using LBPUnion.ProjectLighthouse.Types.Mail;
using Microsoft.EntityFrameworkCore;
@@ -16,11 +20,19 @@ namespace LBPUnion.ProjectLighthouse.Helpers;
public static class SMTPHelper
{
+ private static readonly string blacklistFilePath = ServerConfiguration.Instance.EmailEnforcement.BlacklistFilePath;
+
+ // Null check blacklistFilePath and read into array
+ private static readonly string[] blacklistFile =
+ !string.IsNullOrWhiteSpace(blacklistFilePath) ? File.ReadAllLines(blacklistFilePath) : [];
+
// (User id, timestamp of last request + 30 seconds)
private static readonly ConcurrentDictionary recentlySentMail = new();
private const long emailCooldown = 1000 * 30;
+ private static readonly HashSet blacklistedDomains = new(blacklistFile);
+
private static bool CanSendMail(UserEntity user)
{
// Remove expired entries
@@ -68,6 +80,39 @@ 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) || !emailValidator.IsValid(email) || EmailIsUsed(database, email).Result)
+ return false;
+
+ // Don't even bother if there are no domains in blacklist (AKA file path is empty/invalid, or file itself is empty)
+ if (!ServerConfiguration.Instance.EmailEnforcement.EnableEmailBlacklist || blacklistedDomains.Count <= 0)
+ return true;
+
+ // Get domain by splitting at '@' character
+ string domain = email.Split('@')[1];
+
+ // Return false if domain is found in blacklist
+ if (blacklistedDomains.Contains(domain))
+ {
+ Logger.Info($"Invalid email address {email} submitted by user.", LogArea.Email);
+ return false;
+ }
+
+ return true;
+ }
+
+ // Don't want to allocate every single time we call EmailAddressAttribute.IsValidEmail()
+ private static readonly EmailAddressAttribute emailValidator = new();
+
+ // 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());
+ }
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