diff --git a/.vscode/launch.json b/.vscode/launch.json index af9950600..f0bbe007d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ // If you have changed target frameworks, make sure to update the program path. "program": "${workspaceFolder}/Rnwood.Smtp4dev/bin/Debug/net8.0/Rnwood.Smtp4dev.dll", "cwd": "${workspaceFolder}/Rnwood.Smtp4dev", - "args": ["--recreatedb", "--urls", "http://localhost:5000"], + "args": ["--recreatedb", "--urls", "http://localhost:5000", "--basepath=/smtp4dev"], "stopAtEntry": false, "internalConsoleOptions": "openOnSessionStart", "preLaunchTask": "build Rnwood.Smtp4dev", @@ -30,7 +30,7 @@ "name": "Chrome (Client Debug)", "type": "chrome", "request": "launch", - "url": "http://localhost:5000", + "url": "http://localhost:5000/smtp4dev", "webRoot": "${workspaceFolder}/Rnwood.Smtp4dev/ClientApp/src", "sourceMapPathOverrides": { "../../ClientApp/src/*": "${webRoot}/*", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 44d2e972c..f427156e8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,6 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/Rnwood.Smtp4dev", "/property:GenerateFullPaths=true" ], "problemMatcher": "$msCompile", diff --git a/Rnwood.Smtp4dev.Tests/E2E/E2ETests.cs b/Rnwood.Smtp4dev.Tests/E2E/E2ETests.cs index f39ba4cc6..344ff10a6 100644 --- a/Rnwood.Smtp4dev.Tests/E2E/E2ETests.cs +++ b/Rnwood.Smtp4dev.Tests/E2E/E2ETests.cs @@ -26,7 +26,9 @@ public class E2ETestOptions { public bool InMemoryDB { get; set; } public string BasePath { get; set; } - public IDictionary EnvironmentVariables { get; set; } = new Dictionary(); + + public string TestPath { get; set; } + public IDictionary EnvironmentVariables { get; set; } = new Dictionary(); } public class E2ETestContext @@ -156,7 +158,7 @@ protected void RunE2ETest(Action test, E2ETestOptions options = if (newLine.StartsWith("Now listening on: http://")) { int portNumber = int.Parse(Regex.Replace(newLine, @".*http://[^\s]+:(\d+)", "$1")); - baseUrl = new Uri($"http://localhost:{portNumber}{options.BasePath ?? ""}"); + baseUrl = new Uri($"http://localhost:{portNumber}{options.TestPath ?? options.BasePath ?? ""}"); } if (newLine.StartsWith("SMTP Server is listening on port")) diff --git a/Rnwood.Smtp4dev.Tests/E2E/E2ETests_WebUI_CheckMessageIsReceivedAndDisplayed.cs b/Rnwood.Smtp4dev.Tests/E2E/E2ETests_WebUI_CheckMessageIsReceivedAndDisplayed.cs index 387d94c39..4ad1d1c99 100644 --- a/Rnwood.Smtp4dev.Tests/E2E/E2ETests_WebUI_CheckMessageIsReceivedAndDisplayed.cs +++ b/Rnwood.Smtp4dev.Tests/E2E/E2ETests_WebUI_CheckMessageIsReceivedAndDisplayed.cs @@ -17,11 +17,14 @@ public E2ETests_WebUI_CheckMessageIsReceivedAndDisplayed(ITestOutputHelper outpu { } - [Theory] - [InlineData("/", false)] - [InlineData("/", true)] - [InlineData("/smtp4dev", true)] - public void CheckMessageIsReceivedAndDisplayed(string basePath, bool inMemoryDb) + [Theory] + [InlineData("", "", false)] + [InlineData("", "/", true)] + [InlineData("/smtp4dev", "/smtp4dev", true)] + [InlineData("/smtp4dev", "/smtp4dev/", true)] + [InlineData("/smtp4dev", "", true)] + [InlineData("/smtp4dev", "/", true)] + public void CheckMessageIsReceivedAndDisplayed(string basePath, string testPath, bool inMemoryDb) { RunUITestAsync($"{nameof(CheckMessageIsReceivedAndDisplayed)}-{basePath}-{inMemoryDb}", async (page, baseUrl, smtpPortNumber) => { @@ -59,13 +62,14 @@ public void CheckMessageIsReceivedAndDisplayed(string basePath, bool inMemoryDb) var rows = await grid.GetRowsAsync(); return rows.FirstOrDefault(); }); - + Assert.NotNull(messageRow); Assert.True(await messageRow.ContainsTextAsync(messageSubject)); }, new UITestOptions { InMemoryDB = inMemoryDb, - BasePath = basePath + BasePath = basePath, + TestPath = testPath }); } } diff --git a/Rnwood.Smtp4dev/ClientApp/src/components/home/home.vue b/Rnwood.Smtp4dev/ClientApp/src/components/home/home.vue index 28f6b4726..cf9ea171f 100644 --- a/Rnwood.Smtp4dev/ClientApp/src/components/home/home.vue +++ b/Rnwood.Smtp4dev/ClientApp/src/components/home/home.vue @@ -4,8 +4,8 @@ diff --git a/Rnwood.Smtp4dev/Startup.cs b/Rnwood.Smtp4dev/Startup.cs index f7f2fe635..886713a6e 100644 --- a/Rnwood.Smtp4dev/Startup.cs +++ b/Rnwood.Smtp4dev/Startup.cs @@ -60,7 +60,7 @@ private static void ValidateDatabaseVersionCompatibility(Smtp4devDbContext conte // Get all migrations that have been applied to the database var appliedMigrations = context.Database.GetAppliedMigrations().ToList(); - + // Get all migrations available in the current application var availableMigrations = context.Database.GetMigrations().ToList(); @@ -92,7 +92,7 @@ public void ConfigureServices(IServiceCollection services) //Remove the JSON content type from the actions where it is not supported. NSwag.OpenApiOperationDescription sendOp = d.Operations.FirstOrDefault(o => o.Path.EndsWith("/send") || o.Path.EndsWith("/reply")); sendOp.Operation.RequestBody.Content.Remove("application/json"); - + }; }); @@ -131,10 +131,10 @@ public void ConfigureServices(IServiceCollection services) using var context = new Smtp4devDbContext((DbContextOptions)opt.Options); - + // Validate database version compatibility before attempting any operations ValidateDatabaseVersionCompatibility(context); - + if (string.IsNullOrEmpty(serverOptions.Database)) { context.Database.Migrate(); @@ -178,7 +178,7 @@ public void ConfigureServices(IServiceCollection services) { Log.Logger.Information("Populating MIME metadata for {count} existing messages during startup", messagesWithoutMetadata.Count); var mimeProcessingService = new MimeProcessingService(); - + int processed = 0; int batchSize = 50; // Process in batches to avoid memory issues @@ -335,14 +335,30 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) if (!string.IsNullOrEmpty(serverOptions.BasePath) && serverOptions.BasePath != "/") { - RewriteOptions rewrites = new RewriteOptions(); - rewrites.AddRedirect("^" + serverOptions.BasePath.TrimEnd('/') + "$", serverOptions.BasePath.TrimEnd('/') + "/"); - ; - rewrites.AddRedirect("^(/)?$", serverOptions.BasePath.TrimEnd('/') + "/"); - ; - app.UseRewriter(rewrites); - - app.Map(serverOptions.BasePath, configure); + string basePathNoSlash = serverOptions.BasePath.TrimEnd('/'); + string redirectTarget = basePathNoSlash + "/"; + + // Global middleware to redirect /smtp4dev to /smtp4dev/ + app.Use(async (context, next) => + { + if (context.Request.Path.Equals(basePathNoSlash, StringComparison.OrdinalIgnoreCase) + && !context.Request.Path.Value.EndsWith("/")) + { + var queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; + context.Response.Redirect(redirectTarget + queryString, true); + return; + } + else if (context.Request.Path.Value.Equals("/") + || context.Request.Path.Value == String.Empty) + { + var queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; + context.Response.Redirect(redirectTarget + queryString, true); + return; + } + await next(); + }); + + app.Map(serverOptions.BasePath.TrimEnd('/'), configure); } else { diff --git a/azure-pipelines.yml b/azure-pipelines.yml index eeea843a9..573613ecb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -75,7 +75,7 @@ stages: condition: succeeded() jobs: - job: BuildMatrix - displayName: Build and test - + displayName: Build timeoutInMinutes: 90 pool: vmImage: $(vmImage) @@ -1100,8 +1100,9 @@ stages: File Name | Description -- | -- - [Winget package](https://winget.run/pkg/rnwood/smtp4dev/desktop)| Rnwood.Smtp4dev.Desktop Winget package (recommended easy option for Win10/11) - [Winget package](https://winget.run/pkg/rnwood/smtp4dev)| Rnwood.Smtp4dev Winget package Win10/11) + Winget package ```winget install -v "${tag}" -e Rnwood.Smtp4dev.Desktop``` | Rnwood.Smtp4dev.Desktop Winget package (recommended easy option for Win10/11) + Winget package ```winget install -v "${tag}" -e Rnwood.Smtp4dev``` | Rnwood.Smtp4dev Winget package Win10/11) + [.NET tool - ```dotnet tool install -g Rnwood.Smtp4dev --version "$(tag)"```](https://www.nuget.org/packages/Rnwood.Smtp4dev/$(tag)) | .NET tool (recommended option for Mac OS) - [How to use dotnet tool](https://github.com/rnwood/smtp4dev/blob/master/docs/Installation.md#how-to-run-smtp4dev-as-a-dotnet-global-tool) [Rnwood.Smtp4dev-win-x64-$(tag).zip](../../releases/download/$(tag)/Rnwood.Smtp4dev-win-x64-$(tag).zip) | Windows x64 binary standalone - Server edition [Rnwood.Smtp4dev.Desktop-win-x64-$(tag).zip](../../releases/download/$(tag)/Rnwood.Smtp4dev.Desktop-win-x64-$(tag).zip) | Windows x64 binary standalone - Desktop app edition. [Rnwood.Smtp4dev-win-arm64-$(tag).zip](../../releases/download/$(tag)/Rnwood.Smtp4dev-win-arm64-$(tag).zip) | Windows ARM 62-bit binary standalone @@ -1109,10 +1110,7 @@ stages: [Rnwood.Smtp4dev-linux-musl-x64-$(tag).zip](../../releases/download/$(tag)/Rnwood.Smtp4dev-linux-musl-x64-$(tag).zip) | Linux MUSL x64 binary standalone for Linux distros using MUSL libc [Rnwood.Smtp4dev-noruntime-$(tag).zip](../../releases/download/$(tag)/Rnwood.Smtp4dev-noruntime-$(tag).zip) | Architecture independent version. Should run on any platform where the .NET 8.0 (or greater) runtime is installed [Docker images for Windows and Linux](https://hub.docker.com/layers/rnwood/smtp4dev/$(tag)) - [How to use Docker image](https://github.com/rnwood/smtp4dev/blob/master/docs/Installation.md#how-to-run-smtp4dev-in-docker) - [.NET tool Rnwood.Smtp4dev $(tag)](https://www.nuget.org/packages/Rnwood.Smtp4dev/$(tag)) | .NET tool (recommended option for Mac OS) - [How to use dotnet tool](https://github.com/rnwood/smtp4dev/blob/master/docs/Installation.md#how-to-run-smtp4dev-as-a-dotnet-global-tool) - - - + ${{ if eq(variables['isreleasebuild'], true) }}: action: edit isPreRelease: false