From 1e426bc5e3a3ec5b173cc3f4051bfee1ac49e497 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Sat, 24 Feb 2024 01:48:39 +0300 Subject: [PATCH 01/59] Remove benchmark summary comments (#15393) --- .../FluidShapeRenderBenchmark.cs | 37 ------------------- test/OrchardCore.Benchmarks/RuleBenchmark.cs | 33 ----------------- test/OrchardCore.Benchmarks/SlugBenchmark.cs | 18 --------- 3 files changed, 88 deletions(-) diff --git a/test/OrchardCore.Benchmarks/FluidShapeRenderBenchmark.cs b/test/OrchardCore.Benchmarks/FluidShapeRenderBenchmark.cs index d46b6ee3066..b5d24290479 100644 --- a/test/OrchardCore.Benchmarks/FluidShapeRenderBenchmark.cs +++ b/test/OrchardCore.Benchmarks/FluidShapeRenderBenchmark.cs @@ -34,43 +34,6 @@ static FluidShapeRenderBenchmark() _liquidFilterDelegateResolver = new LiquidFilterDelegateResolver(); } - // Summary 19th May 2021: dotnet run -c Release --filter *FluidShapeRenderBenchmark* --framework netcoreapp3.1 --job short - - //BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.985 (21H1/May2021Update) - //Intel Core i9-9980HK CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores - //.NET SDK= 6.0.100-preview.3.21202.5 - // [Host] : .NET Core 3.1.15 (CoreCLR 4.700.21.21202, CoreFX 4.700.21.21402), X64 RyuJIT - // ShortRun : .NET Core 3.1.15 (CoreCLR 4.700.21.21202, CoreFX 4.700.21.21402), X64 RyuJIT - - //Job=ShortRun IterationCount = 3 LaunchCount=1 - //WarmupCount=3 - - //| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - //|----------------------------- |---------:|---------:|----------:|------:|--------:|-------:|-------:|------:|----------:| - //| OriginalShapeRenderDynamic | 6.211 us | 5.331 us | 0.2922 us | 1.00 | 0.00 | 1.1292 | 0.0153 | - | 9 KB | - //| ShapeRenderWithAmbientValues | 5.441 us | 3.280 us | 0.1798 us | 0.88 | 0.04 | 1.0910 | 0.0229 | - | 9 KB | - //| ShapeRenderStatic | 4.498 us | 3.363 us | 0.1844 us | 0.72 | 0.02 | 1.0757 | 0.0229 | - | 9 KB | - //| ShapeRenderWithResolver | 4.442 us | 5.901 us | 0.3235 us | 0.71 | 0.03 | 1.0834 | 0.0229 | - | 9 KB | - - - // Summary 19th May 2021: dotnet run -c Release --filter *FluidShapeRenderBenchmark* --framework net5.0 --job short - - //BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.985 (21H1/May2021Update) - //Intel Core i9-9980HK CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores - //.NET SDK= 6.0.100-preview.3.21202.5 - // [Host] : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT - // ShortRun : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT - - //Job=ShortRun IterationCount = 3 LaunchCount=1 - //WarmupCount=3 - - //| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - //|----------------------------- |---------:|----------:|----------:|------:|--------:|-------:|-------:|------:|----------:| - //| OriginalShapeRenderDynamic | 5.391 us | 4.1445 us | 0.2272 us | 1.00 | 0.00 | 1.1444 | 0.0153 | - | 9 KB | - //| ShapeRenderWithAmbientValues | 4.954 us | 2.2463 us | 0.1231 us | 0.92 | 0.02 | 1.0986 | - | - | 9 KB | - //| ShapeRenderStatic | 3.999 us | 2.2435 us | 0.1230 us | 0.74 | 0.03 | 1.0834 | 0.0153 | - | 9 KB | - //| ShapeRenderWithResolver | 3.701 us | 0.2977 us | 0.0163 us | 0.69 | 0.03 | 1.0910 | 0.0229 | - | 9 KB | - [Benchmark(Baseline = true)] #pragma warning disable CA1822 // Mark members as static public async Task OriginalShapeRenderDynamic() diff --git a/test/OrchardCore.Benchmarks/RuleBenchmark.cs b/test/OrchardCore.Benchmarks/RuleBenchmark.cs index c6dfe844f39..b84756e4c66 100644 --- a/test/OrchardCore.Benchmarks/RuleBenchmark.cs +++ b/test/OrchardCore.Benchmarks/RuleBenchmark.cs @@ -59,39 +59,6 @@ static RuleBenchmark() }; } - // Summary 19th May 2021: dotnet run -c Release --filter *RuleBenchmark* --framework netcoreapp3.1 --job short - - //BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.985 (21H1/May2021Update) - //Intel Core i9-9980HK CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores - //.NET SDK= 6.0.100-preview.3.21202.5 - // [Host] : .NET Core 3.1.15 (CoreCLR 4.700.21.21202, CoreFX 4.700.21.21402), X64 RyuJIT - // ShortRun : .NET Core 3.1.15 (CoreCLR 4.700.21.21202, CoreFX 4.700.21.21402), X64 RyuJIT - - //Job=ShortRun IterationCount = 3 LaunchCount=1 - //WarmupCount=3 - - //| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - //|--------------------------------- |---------:|---------:|----------:|------:|--------:|-------:|-------:|------:|----------:| - //| EvaluateIsHomepageWithJavascript | 5.393 us | 3.540 us | 0.1940 us | 1.00 | 0.00 | 0.1373 | 0.0381 | - | 1,168 B | - //| EvaluateIsHomepageWithRule | 1.133 us | 4.381 us | 0.2401 us | 0.21 | 0.04 | 0.0267 | 0.0134 | - | 224 B | - - - // Summary 19th May 2021: dotnet run -c Release --filter *RuleBenchmark* --framework net5.0 --job short - - //BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.985 (21H1/May2021Update) - //Intel Core i9-9980HK CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores - //.NET SDK= 6.0.100-preview.3.21202.5 - // [Host] : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT - // ShortRun : .NET 5.0.6 (5.0.621.22011), X64 RyuJIT - - //Job=ShortRun IterationCount = 3 LaunchCount=1 - //WarmupCount=3 - - //| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | - //|--------------------------------- |-----------:|------------:|----------:|------:|--------:|-------:|-------:|------:|----------:| - //| EvaluateIsHomepageWithJavascript | 4,134.1 ns | 14,354.4 ns | 786.81 ns | 1.00 | 0.00 | 0.1221 | 0.0381 | - | 1,040 B | - //| EvaluateIsHomepageWithRule | 662.3 ns | 1,243.9 ns | 68.18 ns | 0.17 | 0.05 | 0.0219 | 0.0086 | - | 184 B | - [Benchmark(Baseline = true)] #pragma warning disable CA1822 // Mark members as static public void EvaluateIsHomepageWithJavascript() diff --git a/test/OrchardCore.Benchmarks/SlugBenchmark.cs b/test/OrchardCore.Benchmarks/SlugBenchmark.cs index 72837069f65..45436553f14 100644 --- a/test/OrchardCore.Benchmarks/SlugBenchmark.cs +++ b/test/OrchardCore.Benchmarks/SlugBenchmark.cs @@ -13,24 +13,6 @@ static SlugBenchmark() _slugService = new SlugService(); } - /* - * Summary 24th December 2021 - * - * BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1415 (20H2/October2020Update) - * Intel Core i7-3687U CPU 2.10GHz(Ivy Bridge), 1 CPU, 4 logical and 2 physical cores - * .NET SDK= 6.0.100 - * - * [Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT - * ShortRun : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT - * - * Job=ShortRun IterationCount = 3 LaunchCount=1 - * WarmupCount=3 - * - * | Method | Mean | Error | StdDev | Gen 0 | Allocated | - * |---------------- |---------:|----------:|----------:|-------:|----------:| - * | EvaluateSlugify | 1.477 us | 0.5187 us | 0.0284 us | 0.2174 | 456 B | - */ - [Benchmark] #pragma warning disable CA1822 // Mark members as static public void EvaluateSlugifyWithShortSlug() From 09ef3a86b8af0f107b2c7375c7e0e407b667d3f8 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Sat, 24 Feb 2024 02:21:16 +0300 Subject: [PATCH 02/59] Expose CultureDictionaryRecordKey properties (#11358) --- .../CultureDictionaryRecordKey.cs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Localization.Abstractions/CultureDictionaryRecordKey.cs b/src/OrchardCore/OrchardCore.Localization.Abstractions/CultureDictionaryRecordKey.cs index 1f67ec5add6..a73013932eb 100644 --- a/src/OrchardCore/OrchardCore.Localization.Abstractions/CultureDictionaryRecordKey.cs +++ b/src/OrchardCore/OrchardCore.Localization.Abstractions/CultureDictionaryRecordKey.cs @@ -7,9 +7,6 @@ namespace OrchardCore.Localization /// public readonly struct CultureDictionaryRecordKey : IEquatable { - private readonly string _messageId; - private readonly string _context; - /// /// Creates new instance of . /// @@ -26,10 +23,20 @@ public CultureDictionaryRecordKey(string messageId) : this(messageId, null) /// The message context. public CultureDictionaryRecordKey(string messageId, string context) { - _messageId = messageId; - _context = context; + MessageId = messageId; + Context = context; } + /// + /// Gets the message Id. + /// + public string MessageId { get; } + + /// + /// Gets the message context. + /// + public string Context { get; } + public static implicit operator string(CultureDictionaryRecordKey cultureDictionaryRecordKey) => cultureDictionaryRecordKey.ToString(); @@ -46,15 +53,15 @@ public override bool Equals(object obj) /// public bool Equals(CultureDictionaryRecordKey other) - => string.Equals(_messageId, other._messageId) && string.Equals(_context, other._context); + => string.Equals(MessageId, other.MessageId) && String.Equals(Context, other.Context); /// - public override int GetHashCode() => HashCode.Combine(_messageId, _context); + public override int GetHashCode() => HashCode.Combine(MessageId, Context); public override string ToString() - => string.IsNullOrEmpty(_context) - ? _messageId - : _context.ToLowerInvariant() + "|" + _messageId; + => string.IsNullOrEmpty(Context) + ? MessageId + : Context.ToLowerInvariant() + "|" + MessageId; public static bool operator ==(CultureDictionaryRecordKey left, CultureDictionaryRecordKey right) => left.Equals(right); From bbbc79ab90e88bac4a13efeef1fcb5ee32932cef Mon Sep 17 00:00:00 2001 From: Antoine Griffard Date: Sat, 24 Feb 2024 11:57:04 +0100 Subject: [PATCH 03/59] mkdocs-material 9.5.11 --- src/docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/requirements.txt b/src/docs/requirements.txt index d6a24763fa1..4f0bf5f4457 100644 --- a/src/docs/requirements.txt +++ b/src/docs/requirements.txt @@ -1,5 +1,5 @@ mkdocs>=1.5.3 -mkdocs-material>=9.5.10 +mkdocs-material>=9.5.11 mkdocs-git-authors-plugin>=0.7.2 mkdocs-git-revision-date-localized-plugin>=1.2.4 pymdown-extensions>=10.7 From 93558041d61a252da0efa1d208a893fa18e2f72f Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 26 Feb 2024 10:39:48 +0300 Subject: [PATCH 04/59] Update Jint 3.0.1 (#15402) --- src/OrchardCore.Build/Dependencies.props | 2 +- src/docs/resources/libraries/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props index 9d41c286348..1102376ebd2 100644 --- a/src/OrchardCore.Build/Dependencies.props +++ b/src/OrchardCore.Build/Dependencies.props @@ -25,7 +25,7 @@ - + diff --git a/src/docs/resources/libraries/README.md b/src/docs/resources/libraries/README.md index 37bb87ee406..ad62df7efe2 100644 --- a/src/docs/resources/libraries/README.md +++ b/src/docs/resources/libraries/README.md @@ -19,7 +19,7 @@ The below table lists the different .NET libraries used in Orchard Core: | [HtmlSanitizer](https://github.com/mganss/HtmlSanitizer) | Cleans HTML to avoid XSS attacks. | 8.1.844-beta | [MIT](https://github.com/mganss/HtmlSanitizer/blob/master/LICENSE.md) | | [Image Sharp](https://github.com/SixLabors/ImageSharp.Web) | Middleware for ASP.NET-Core for image manipulation. | 3.1.0 |[Apache-2.0](https://github.com/SixLabors/ImageSharp.Web/blob/master/LICENSE) | | [Irony.Core](https://github.com/daxnet/irony) | A modified version of the Irony project with .NET Core support | 1.0.7 | [MIT](https://github.com/daxnet/irony/blob/master/LICENSE) | -| [Jint](https://github.com/sebastienros/jint) | Javascript Interpreter for .NET. | 3.0.0 | [MIT](https://github.com/sebastienros/jint/blob/dev/LICENSE) | +| [Jint](https://github.com/sebastienros/jint) | Javascript Interpreter for .NET. | 3.0.1 | [MIT](https://github.com/sebastienros/jint/blob/dev/LICENSE) | | [libphonenumber-csharp](https://github.com/twcclegg/libphonenumber-csharp) | .NET library for parsing, formatting, and validating international phone numbers | 8.13.30 | [Apache-2.0](https://github.com/twcclegg/libphonenumber-csharp/blob/main/LICENSE) | | [Lorem.NET for netstandard](https://github.com/trichards57/Lorem.Universal.NET) | A .NET library for all things random! | 4.0.80 | [MIT](https://github.com/trichards57/Lorem.Universal.NET/blob/master/license.md) | | [Lucene.Net](https://github.com/apache/lucenenet) | .NET full-text search engine. | 4.8.0-beta00016 | [Apache-2.0](https://github.com/apache/lucenenet/blob/master/LICENSE.txt) | From 7ac492e87cd607f74e4adcbb1c96206d9ab7f2ae Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 26 Feb 2024 10:49:26 +0300 Subject: [PATCH 05/59] Update libphonenumber-csharp 8.13.31 (#15403) --- src/OrchardCore.Build/Dependencies.props | 2 +- src/docs/resources/libraries/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props index 1102376ebd2..1399c460073 100644 --- a/src/OrchardCore.Build/Dependencies.props +++ b/src/OrchardCore.Build/Dependencies.props @@ -28,7 +28,7 @@ - + diff --git a/src/docs/resources/libraries/README.md b/src/docs/resources/libraries/README.md index ad62df7efe2..a3f8fce7fa2 100644 --- a/src/docs/resources/libraries/README.md +++ b/src/docs/resources/libraries/README.md @@ -20,7 +20,7 @@ The below table lists the different .NET libraries used in Orchard Core: | [Image Sharp](https://github.com/SixLabors/ImageSharp.Web) | Middleware for ASP.NET-Core for image manipulation. | 3.1.0 |[Apache-2.0](https://github.com/SixLabors/ImageSharp.Web/blob/master/LICENSE) | | [Irony.Core](https://github.com/daxnet/irony) | A modified version of the Irony project with .NET Core support | 1.0.7 | [MIT](https://github.com/daxnet/irony/blob/master/LICENSE) | | [Jint](https://github.com/sebastienros/jint) | Javascript Interpreter for .NET. | 3.0.1 | [MIT](https://github.com/sebastienros/jint/blob/dev/LICENSE) | -| [libphonenumber-csharp](https://github.com/twcclegg/libphonenumber-csharp) | .NET library for parsing, formatting, and validating international phone numbers | 8.13.30 | [Apache-2.0](https://github.com/twcclegg/libphonenumber-csharp/blob/main/LICENSE) | +| [libphonenumber-csharp](https://github.com/twcclegg/libphonenumber-csharp) | .NET library for parsing, formatting, and validating international phone numbers | 8.13.31 | [Apache-2.0](https://github.com/twcclegg/libphonenumber-csharp/blob/main/LICENSE) | | [Lorem.NET for netstandard](https://github.com/trichards57/Lorem.Universal.NET) | A .NET library for all things random! | 4.0.80 | [MIT](https://github.com/trichards57/Lorem.Universal.NET/blob/master/license.md) | | [Lucene.Net](https://github.com/apache/lucenenet) | .NET full-text search engine. | 4.8.0-beta00016 | [Apache-2.0](https://github.com/apache/lucenenet/blob/master/LICENSE.txt) | | [MailKit](https://github.com/jstedfast/MailKit) | A cross-platform .NET library for IMAP, POP3, and SMTP. | 4.3.0 | [MIT](https://github.com/jstedfast/MailKit/blob/master/LICENSE) | From d8e15b9030adc205792bd9910175409371c996f6 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 26 Feb 2024 15:48:58 +0300 Subject: [PATCH 06/59] Update Serilog.AspNetCore 8.0.0 (#15151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update Serilog.AspNetCore 8.0.0 * Revert changes --------- Co-authored-by: Zoltán Lehóczky --- src/OrchardCore.Build/Dependencies.props | 2 +- src/docs/resources/libraries/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props index 1399c460073..0a0f3d65674 100644 --- a/src/OrchardCore.Build/Dependencies.props +++ b/src/OrchardCore.Build/Dependencies.props @@ -58,7 +58,7 @@ - + diff --git a/src/docs/resources/libraries/README.md b/src/docs/resources/libraries/README.md index a3f8fce7fa2..fc6c6ae3a23 100644 --- a/src/docs/resources/libraries/README.md +++ b/src/docs/resources/libraries/README.md @@ -40,7 +40,7 @@ The below table lists the different .NET libraries used in Orchard Core: | [Noda Time](https://github.com/nodatime/nodatime) | A better date and time API for .NET. | 3.1.11 | [Apache-2.0](https://github.com/nodatime/nodatime/blob/master/LICENSE.txt) | | [OpenIddict](https://github.com/openiddict/openiddict-core) | Flexible and versatile OAuth 2.0/OpenID Connect stack for .NET. | 5.2.0 | [Apache-2.0](https://github.com/openiddict/openiddict-core/blob/dev/LICENSE.md)) | | [PdfPig](https://github.com/UglyToad/PdfPig/) | Library to read and extract text and other content from PDF files. | 0.1.8 | [Apache-2.0](https://github.com/UglyToad/PdfPig/blob/master/LICENSE) | -| [Serilog.AspNetCore](https://github.com/serilog/serilog-aspnetcore) | Serilog integration for ASP.NET Core. | 7.0.0 | [Apache-2.0](https://github.com/serilog/serilog-aspnetcore/blob/dev/LICENSE) | +| [Serilog.AspNetCore](https://github.com/serilog/serilog-aspnetcore) | Serilog integration for ASP.NET Core. | 8.0.0 | [Apache-2.0](https://github.com/serilog/serilog-aspnetcore/blob/dev/LICENSE) | | [Shortcodes](https://github.com/sebastienros/shortcodes) | Shortcodes processor for .NET. | 1.3.3 | [MIT](https://github.com/sebastienros/shortcodes/blob/dev/LICENSE) | | [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) | General purpose redis client. | 2.7.20 | [MIT](https://github.com/StackExchange/StackExchange.Redis/blob/main/LICENSE) | | [YesSql](https://github.com/sebastienros/yessql) | .NET document database working on any RDBMS. | 5.0.0-beta-0002 | [MIT](https://github.com/sebastienros/yessql/blob/dev/LICENSE) | From 653e624ac468f13f3c2491748b9ecbb6ecad6c4e Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Tue, 27 Feb 2024 17:33:15 +0300 Subject: [PATCH 07/59] Update StackExchange.Redis 2.7.23 (#15409) --- src/OrchardCore.Build/Dependencies.props | 2 +- src/docs/resources/libraries/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props index 0a0f3d65674..21279ee0f94 100644 --- a/src/OrchardCore.Build/Dependencies.props +++ b/src/OrchardCore.Build/Dependencies.props @@ -61,7 +61,7 @@ - + diff --git a/src/docs/resources/libraries/README.md b/src/docs/resources/libraries/README.md index fc6c6ae3a23..feb48364946 100644 --- a/src/docs/resources/libraries/README.md +++ b/src/docs/resources/libraries/README.md @@ -42,7 +42,7 @@ The below table lists the different .NET libraries used in Orchard Core: | [PdfPig](https://github.com/UglyToad/PdfPig/) | Library to read and extract text and other content from PDF files. | 0.1.8 | [Apache-2.0](https://github.com/UglyToad/PdfPig/blob/master/LICENSE) | | [Serilog.AspNetCore](https://github.com/serilog/serilog-aspnetcore) | Serilog integration for ASP.NET Core. | 8.0.0 | [Apache-2.0](https://github.com/serilog/serilog-aspnetcore/blob/dev/LICENSE) | | [Shortcodes](https://github.com/sebastienros/shortcodes) | Shortcodes processor for .NET. | 1.3.3 | [MIT](https://github.com/sebastienros/shortcodes/blob/dev/LICENSE) | -| [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) | General purpose redis client. | 2.7.20 | [MIT](https://github.com/StackExchange/StackExchange.Redis/blob/main/LICENSE) | +| [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) | General purpose redis client. | 2.7.23 | [MIT](https://github.com/StackExchange/StackExchange.Redis/blob/main/LICENSE) | | [YesSql](https://github.com/sebastienros/yessql) | .NET document database working on any RDBMS. | 5.0.0-beta-0002 | [MIT](https://github.com/sebastienros/yessql/blob/dev/LICENSE) | | [ZString](https://github.com/Cysharp/ZString) | Zero Allocation StringBuilder for .NET Core and Unity. | 2.5.1 | [MIT](https://github.com/Cysharp/ZString/blob/master/LICENSE) | From 000bc744a353a8230909864e73c501eca8a56636 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 27 Feb 2024 10:16:33 -0800 Subject: [PATCH 08/59] Support Create and CreateAsync in DataMigrationManager (#15396) --- .../Migration/DataMigrationManager.cs | 118 +++++++----------- 1 file changed, 45 insertions(+), 73 deletions(-) diff --git a/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs b/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs index 37ea441ba5c..683295f204d 100644 --- a/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs +++ b/src/OrchardCore/OrchardCore.Data.YesSql/Migration/DataMigrationManager.cs @@ -17,6 +17,9 @@ namespace OrchardCore.Data.Migration /// public class DataMigrationManager : IDataMigrationManager { + const string _updateFromPrefix = "UpdateFrom"; + const string _asyncSuffix = "Async"; + private readonly IEnumerable _dataMigrations; private readonly ISession _session; private readonly IStore _store; @@ -83,18 +86,16 @@ public async Task> GetFeaturesThatNeedUpdateAsync() return CreateUpgradeLookupTable(dataMigration).ContainsKey(record.Version.Value); } - return ((GetCreateMethod(dataMigration) ?? GetCreateAsyncMethod(dataMigration)) != null); + return GetCreateMethod(dataMigration) != null; }); - return outOfDateMigrations.Select(m => _typeFeatureProvider.GetFeatureForDependency(m.GetType()).Id).ToList(); + return outOfDateMigrations.Select(m => _typeFeatureProvider.GetFeatureForDependency(m.GetType()).Id).ToArray(); } public async Task Uninstall(string feature) { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation("Uninstalling feature '{FeatureName}'.", feature); - } + _logger.LogInformation("Uninstalling feature '{FeatureName}'.", feature); + var migrations = GetDataMigrations(feature); // apply update methods to each migration class for the module @@ -107,12 +108,10 @@ public async Task Uninstall(string feature) var dataMigrationRecord = await GetDataMigrationRecordAsync(tempMigration); var uninstallMethod = GetUninstallMethod(migration); - uninstallMethod?.Invoke(migration, []); - var uninstallAsyncMethod = GetUninstallAsyncMethod(migration); - if (uninstallAsyncMethod != null) + if (uninstallMethod != null) { - await (Task)uninstallAsyncMethod.Invoke(migration, []); + await InvokeMethod(uninstallMethod, migration); } if (dataMigrationRecord == null) @@ -144,10 +143,7 @@ public async Task UpdateAsync(string featureId) _processedFeatures.Add(featureId); - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation("Updating feature '{FeatureName}'", featureId); - } + _logger.LogInformation("Updating feature '{FeatureName}'", featureId); // proceed with dependent features first, whatever the module it's in var dependencies = _extensionManager @@ -166,10 +162,10 @@ public async Task UpdateAsync(string featureId) var schemaBuilder = new SchemaBuilder(_store.Configuration, await _session.BeginTransactionAsync()); migration.SchemaBuilder = schemaBuilder; - // copy the object for the Linq query + // Copy the object for the Linq query. var tempMigration = migration; - // get current version for this migration + // Get current version for this migration. var dataMigrationRecord = await GetDataMigrationRecordAsync(tempMigration); var current = 0; @@ -186,47 +182,31 @@ public async Task UpdateAsync(string featureId) try { - // do we need to call Create() ? + // Do we need to call Create or CreateAsync? if (current == 0) { - // try to resolve a Create method - + // Try to get a Create method. var createMethod = GetCreateMethod(migration); - if (createMethod != null) - { - current = (int)createMethod.Invoke(migration, []); - } - - // try to resolve a CreateAsync method - var createAsyncMethod = GetCreateAsyncMethod(migration); - if (createAsyncMethod != null) + if (createMethod == null) { - current = await (Task)createAsyncMethod.Invoke(migration, []); + _logger.LogWarning("The migration '{name}' for '{FeatureName}' does not contain a proper Create or CreateAsync method.", migration.GetType().FullName, featureId); + continue; } + + current = await InvokeMethod(createMethod, migration); } var lookupTable = CreateUpgradeLookupTable(migration); while (lookupTable.TryGetValue(current, out var methodInfo)) { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation("Applying migration for '{FeatureName}' from version {Version}.", featureId, current); - } + _logger.LogInformation("Applying migration for '{FeatureName}' from version {Version}.", featureId, current); - var isAwaitable = methodInfo.ReturnType.GetMethod(nameof(Task.GetAwaiter)) != null; - if (isAwaitable) - { - current = await (Task)methodInfo.Invoke(migration, []); - } - else - { - current = (int)methodInfo.Invoke(migration, []); - } + current = await InvokeMethod(methodInfo, migration); } - // if current is 0, it means no upgrade/create method was found or succeeded + // If current is 0, it means no upgrade/create method was found or succeeded. if (current == 0) { return; @@ -248,6 +228,16 @@ public async Task UpdateAsync(string featureId) } } + private static async Task InvokeMethod(MethodInfo method, IDataMigration migration) + { + if (method.ReturnType.GetMethod(nameof(Task.GetAwaiter)) != null) + { + return await (Task)method.Invoke(migration, []); + } + + return (int)method.Invoke(migration, []); + } + private async Task GetDataMigrationRecordAsync(IDataMigration tempMigration) { var dataMigrationRecord = await GetDataMigrationRecordAsync(); @@ -259,11 +249,11 @@ public async Task UpdateAsync(string featureId) /// /// Returns all the available IDataMigration instances for a specific module, and inject necessary builders. /// - private List GetDataMigrations(string featureId) + private IDataMigration[] GetDataMigrations(string featureId) { var migrations = _dataMigrations .Where(dm => _typeFeatureProvider.GetFeatureForDependency(dm.GetType()).Id == featureId) - .ToList(); + .ToArray(); return migrations; } @@ -283,14 +273,12 @@ private static Dictionary CreateUpgradeLookupTable(IDataMigrati private static Tuple GetUpdateMethod(MethodInfo methodInfo) { - const string updateFromPrefix = "UpdateFrom"; - const string asyncSuffix = "Async"; - - if (methodInfo.Name.StartsWith(updateFromPrefix, StringComparison.Ordinal) && (methodInfo.ReturnType == typeof(int) || methodInfo.ReturnType == typeof(Task))) + if (methodInfo.Name.StartsWith(_updateFromPrefix, StringComparison.Ordinal) + && (methodInfo.ReturnType == typeof(int) || methodInfo.ReturnType == typeof(Task))) { - var version = methodInfo.Name.EndsWith(asyncSuffix, StringComparison.Ordinal) - ? methodInfo.Name.Substring(updateFromPrefix.Length, methodInfo.Name.Length - updateFromPrefix.Length - asyncSuffix.Length) - : methodInfo.Name[updateFromPrefix.Length..]; + var version = methodInfo.Name.EndsWith(_asyncSuffix, StringComparison.Ordinal) + ? methodInfo.Name.Substring(_updateFromPrefix.Length, methodInfo.Name.Length - _updateFromPrefix.Length - _asyncSuffix.Length) + : methodInfo.Name[_updateFromPrefix.Length..]; if (int.TryParse(version, out var versionValue)) { @@ -302,25 +290,17 @@ private static Tuple GetUpdateMethod(MethodInfo methodInfo) } /// - /// Returns the Create method from a data migration class if it's found. + /// Returns the Create or CreateAsync method from a data migration class if it's found. /// private static MethodInfo GetCreateMethod(IDataMigration dataMigration) { var methodInfo = dataMigration.GetType().GetMethod("Create", BindingFlags.Public | BindingFlags.Instance); - if (methodInfo != null && methodInfo.ReturnType == typeof(int)) + if (methodInfo != null && (methodInfo.ReturnType == typeof(int) || methodInfo.ReturnType == typeof(Task))) { return methodInfo; } - return null; - } - - /// - /// Returns the CreateAsync method from a data migration class if it's found. - /// - private static MethodInfo GetCreateAsyncMethod(IDataMigration dataMigration) - { - var methodInfo = dataMigration.GetType().GetMethod("CreateAsync", BindingFlags.Public | BindingFlags.Instance); + methodInfo = dataMigration.GetType().GetMethod("CreateAsync", BindingFlags.Public | BindingFlags.Instance); if (methodInfo != null && methodInfo.ReturnType == typeof(Task)) { return methodInfo; @@ -335,21 +315,13 @@ private static MethodInfo GetCreateAsyncMethod(IDataMigration dataMigration) private static MethodInfo GetUninstallMethod(IDataMigration dataMigration) { var methodInfo = dataMigration.GetType().GetMethod("Uninstall", BindingFlags.Public | BindingFlags.Instance); - if (methodInfo != null && methodInfo.ReturnType == typeof(void)) + if (methodInfo != null && (methodInfo.ReturnType == typeof(int) || methodInfo.ReturnType == typeof(Task))) { return methodInfo; } - return null; - } - - /// - /// Returns the UninstallAsync method from a data migration class if it's found. - /// - private static MethodInfo GetUninstallAsyncMethod(IDataMigration dataMigration) - { - var methodInfo = dataMigration.GetType().GetMethod("UninstallAsync", BindingFlags.Public | BindingFlags.Instance); - if (methodInfo != null && methodInfo.ReturnType == typeof(Task)) + methodInfo = dataMigration.GetType().GetMethod("UninstallAsync", BindingFlags.Public | BindingFlags.Instance); + if (methodInfo != null && methodInfo.ReturnType == typeof(Task)) { return methodInfo; } From 721f8ad204b0a5176a4d30b19899a532a9cb0d33 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 27 Feb 2024 11:49:56 -0800 Subject: [PATCH 09/59] Azure Email Communication Services (using Provider) (Lombiq Technologies: OCORE-129) (#15254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Co-authored-by: Hisham Bin Ateya Co-authored-by: Zoltán Lehóczky Co-authored-by: Benedek Farkas --- OrchardCore.sln | 18 +- mkdocs.yml | 2 + src/OrchardCore.Build/Dependencies.props | 1 + src/OrchardCore.Cms.Web/appsettings.json | 6 +- .../AzureEmailSettingsDisplayDriver.cs | 161 +++++++++ .../OrchardCore.Email.Azure/Manifest.cs | 14 + .../Models/AzureEmailOptions.cs | 13 + .../Models/AzureEmailSettings.cs | 16 + .../Models/DefaultAzureEmailOptions.cs | 5 + .../OrchardCore.Email.Azure.csproj | 31 ++ .../AzureEmailOptionsConfiguration.cs | 41 +++ .../Services/AzureEmailProvider.cs | 22 ++ .../Services/AzureEmailProviderBase.cs | 229 ++++++++++++ ...AzureEmailProviderOptionsConfigurations.cs | 50 +++ .../Services/DefaultAzureEmailProvider.cs | 22 ++ .../OrchardCore.Email.Azure/Startup.cs | 38 ++ .../ViewModels/AzureEmailSettingsViewModel.cs | 16 + .../Views/AzureEmailSettings.Edit.cshtml | 28 ++ .../Views/_ViewImports.cshtml | 5 + .../Drivers/SmtpSettingsDisplayDriver.cs | 211 +++++++++++ .../OrchardCore.Email.Smtp/Manifest.cs | 14 + .../OrchardCore.Email.Smtp.csproj | 34 ++ .../Services/DefaultSmtpEmailProvider.cs | 21 ++ .../Services/ServiceCollectionExtensions.cs | 27 ++ .../Services/SmtpEmailProvider.cs | 21 ++ .../Services/SmtpEmailProviderBase.cs | 222 ++++++++++++ .../Services/SmtpOptionsConfiguration.cs | 62 ++++ .../SmtpProviderOptionsConfigurations.cs | 48 +++ .../Services/SmtpService.cs | 64 ++++ .../OrchardCore.Email.Smtp/Startup.cs | 38 ++ .../ViewModels/SmtpSettingsViewModel.cs | 38 ++ .../Views/SmtpSettings.Edit.cshtml | 196 +++++++++++ .../Views/_ViewImports.cshtml | 5 + .../OrchardCore.Email/AdminMenu.cs | 16 +- .../Controllers/AdminController.cs | 184 ++++++---- .../Drivers/EmailSettingsDisplayDriver.cs | 119 +++++++ .../Drivers/SmtpSettingsDisplayDriver.cs | 110 ------ .../OrchardCoreBuilderExtensions.cs | 9 +- .../OrchardCore.Email/Manifest.cs | 3 +- .../Migrations/EmailMigrations.cs | 56 +++ .../OrchardCore.Email.csproj | 7 +- .../Services/SmtpSettingsConfiguration.cs | 60 ---- .../OrchardCore.Email/Startup.cs | 15 +- .../ViewModels/EmailSettingsBaseViewModel.cs | 6 + .../ViewModels/EmailSettingsViewModel.cs | 11 + .../ViewModels/EmailTestViewModel.cs | 33 ++ .../ViewModels/SmtpSettingsViewModel.cs | 23 -- .../Views/Admin/{Index.cshtml => Test.cshtml} | 51 ++- .../Views/EmailSettings.Edit.cshtml | 22 ++ .../Views/NavigationItemText-email.Id.cshtml | 5 +- .../Views/SmtpSettings.Edit.cshtml | 187 ---------- .../Views/SmtpSettings.TestButton.cshtml | 1 - .../Workflows/Activities/EmailTask.cs | 9 +- .../Views/FacebookLoginSettings.Edit.cshtml | 6 +- .../Views/FacebookSettings.Edit.cshtml | 4 +- .../GithubAuthenticationSettings.Edit.cshtml | 4 +- .../GoogleAuthenticationSettings.Edit.cshtml | 4 +- .../Drivers/HttpsSettingsDisplayDriver.cs | 14 +- .../Views/HttpsSettings.Edit.cshtml | 2 - .../LocalizationSettingsDisplayDriver.cs | 16 +- .../Views/LocalizationSettings.Edit.cshtml | 2 - .../MicrosoftAccountSettings.Edit.cshtml | 4 +- .../MicrosoftEntraIDSettings.Edit.cshtml | 4 +- .../OpenIdClientSettingsDisplayDriver.cs | 10 +- .../OpenIdServerSettingsDisplayDriver.cs | 6 +- .../OpenIdValidationSettingsDisplayDriver.cs | 6 +- .../Views/OpenIdClientSettings.Edit.cshtml | 7 +- .../Views/OpenIdServerSettings.Edit.cshtml | 3 +- .../OpenIdValidationSettings.Edit.cshtml | 2 - .../Drivers/ReCaptchaSettingsDisplayDriver.cs | 10 +- .../Views/ReCaptchaSettings.Edit.cshtml | 2 - .../ReverseProxySettingsDisplayDriver.cs | 14 +- .../Views/ReverseProxySettings.Edit.cshtml | 4 - .../Drivers/SecuritySettingsDisplayDriver.cs | 13 +- .../Views/SecurityHeadersSettings.Edit.cshtml | 4 - .../DefaultSiteSettingsDisplayDriver.cs | 2 +- .../Views/Settings-General.Wrapper.cshtml | 7 - .../Views/Settings-Reload.Wrapper.cshtml | 7 + .../Views/TwitterSettings.Edit.cshtml | 4 +- .../Views/TwitterSigninSettings.Edit.cshtml | 4 +- .../Controllers/ControllerExtensions.cs | 4 +- .../EmailAuthenticatorController.cs | 16 +- .../Workflows/Activities/RegisterUserTask.cs | 29 +- .../OrchardCore.Users/Workflows/Startup.cs | 10 +- .../Handlers/DefaultWorkflowFaultHandler.cs | 2 +- ...rdCore.Application.Cms.Core.Targets.csproj | 2 + .../DefaultSmtpOptions.cs | 5 + .../EmailOptions.cs | 6 + .../EmailProviderSettings.cs | 8 + .../EmailResult.cs | 86 +++++ .../EmailSettings.cs | 14 + .../IEmailProvider.cs | 19 + .../IEmailProviderResolver.cs | 14 + .../IEmailService.cs | 14 + .../IEmailServiceEvents.cs | 32 ++ .../ISmtpService.cs | 23 +- .../InvalidEmailProviderException.cs | 11 + .../MailMessageExtensions.cs | 33 ++ .../MailMessageRecipients.cs | 12 + .../MailMessageValidationContext.cs | 19 + .../SmtpOptions.cs | 45 +++ .../SmtpResult.cs | 51 ++- .../SmtpSettings.cs | 149 +++----- .../OrchardCore.Email.Core.csproj | 5 +- .../OrchardCore.Email.Core}/Permissions.cs | 2 +- .../ServiceCollectionExtensions.cs | 41 +++ .../Services/DefaultEmailProviderResolver.cs | 43 +++ .../Services/DefaultEmailService.cs | 68 ++++ .../Services/EmailMessageValidator.cs | 73 ++++ .../Services/EmailOptionsConfiguration.cs | 46 +++ .../Services/EmailProviderOptions.cs | 68 ++++ .../Services/EmailProviderTypeOptions.cs | 20 ++ .../Services/EmailServiceEventsBase.cs | 21 ++ .../Services/SmtpService.cs | 327 ------------------ .../Services/EmailNotificationProvider.cs | 8 +- src/docs/reference/README.md | 2 + .../reference/core/Configuration/README.md | 38 +- .../reference/modules/Email.Azure/README.md | 27 ++ .../reference/modules/Email.Smtp/README.md | 66 ++++ src/docs/reference/modules/Email/README.md | 138 +++++--- src/docs/releases/1.9.0.md | 12 +- src/docs/resources/libraries/README.md | 1 + test/OrchardCore.Tests/Email/EmailTests.cs | 34 +- .../Workflows/EmailTaskTests.cs | 46 ++- .../RegistrationControllerTests.cs | 4 +- 125 files changed, 3361 insertions(+), 1144 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Drivers/AzureEmailSettingsDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Manifest.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/AzureEmailOptions.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/AzureEmailSettings.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/DefaultAzureEmailOptions.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/OrchardCore.Email.Azure.csproj create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailOptionsConfiguration.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProvider.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProviderBase.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProviderOptionsConfigurations.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/DefaultAzureEmailProvider.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Startup.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/ViewModels/AzureEmailSettingsViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Views/AzureEmailSettings.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Azure/Views/_ViewImports.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Drivers/SmtpSettingsDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Manifest.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/OrchardCore.Email.Smtp.csproj create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/DefaultSmtpEmailProvider.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/ServiceCollectionExtensions.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpEmailProvider.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpEmailProviderBase.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpOptionsConfiguration.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpProviderOptionsConfigurations.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpService.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Startup.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/ViewModels/SmtpSettingsViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Views/SmtpSettings.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Email.Smtp/Views/_ViewImports.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Email/Drivers/EmailSettingsDisplayDriver.cs delete mode 100644 src/OrchardCore.Modules/OrchardCore.Email/Drivers/SmtpSettingsDisplayDriver.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email/Migrations/EmailMigrations.cs delete mode 100644 src/OrchardCore.Modules/OrchardCore.Email/Services/SmtpSettingsConfiguration.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailSettingsBaseViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailSettingsViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailTestViewModel.cs delete mode 100644 src/OrchardCore.Modules/OrchardCore.Email/ViewModels/SmtpSettingsViewModel.cs rename src/OrchardCore.Modules/OrchardCore.Email/Views/Admin/{Index.cshtml => Test.cshtml} (69%) create mode 100644 src/OrchardCore.Modules/OrchardCore.Email/Views/EmailSettings.Edit.cshtml delete mode 100644 src/OrchardCore.Modules/OrchardCore.Email/Views/SmtpSettings.Edit.cshtml delete mode 100644 src/OrchardCore.Modules/OrchardCore.Email/Views/SmtpSettings.TestButton.cshtml delete mode 100644 src/OrchardCore.Modules/OrchardCore.Settings/Views/Settings-General.Wrapper.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Settings/Views/Settings-Reload.Wrapper.cshtml create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/DefaultSmtpOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/EmailOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/EmailProviderSettings.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/EmailResult.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/EmailSettings.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/IEmailProvider.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/IEmailProviderResolver.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/IEmailService.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/IEmailServiceEvents.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/InvalidEmailProviderException.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/MailMessageExtensions.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/MailMessageRecipients.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/MailMessageValidationContext.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Abstractions/SmtpOptions.cs rename src/{OrchardCore.Modules/OrchardCore.Email => OrchardCore/OrchardCore.Email.Core}/Permissions.cs (95%) create mode 100644 src/OrchardCore/OrchardCore.Email.Core/ServiceCollectionExtensions.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Core/Services/DefaultEmailProviderResolver.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Core/Services/DefaultEmailService.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Core/Services/EmailMessageValidator.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Core/Services/EmailOptionsConfiguration.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Core/Services/EmailProviderOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Core/Services/EmailProviderTypeOptions.cs create mode 100644 src/OrchardCore/OrchardCore.Email.Core/Services/EmailServiceEventsBase.cs delete mode 100644 src/OrchardCore/OrchardCore.Email.Core/Services/SmtpService.cs create mode 100644 src/docs/reference/modules/Email.Azure/README.md create mode 100644 src/docs/reference/modules/Email.Smtp/README.md diff --git a/OrchardCore.sln b/OrchardCore.sln index a390bc69109..f8cc4092156 100644 --- a/OrchardCore.sln +++ b/OrchardCore.sln @@ -511,9 +511,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.AzureAI" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Search.AzureAI.Core", "src\OrchardCore\OrchardCore.Search.AzureAI.Core\OrchardCore.Search.AzureAI.Core.csproj", "{E9428DE8-5D81-4359-BF84-31435041FF1A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Media.Indexing.Pdf", "src\OrchardCore.Modules\OrchardCore.Media.Indexing.Pdf\OrchardCore.Media.Indexing.Pdf.csproj", "{95187E6A-5B74-4475-8FEB-758ACD012DCC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Media.Indexing.Pdf", "src\OrchardCore.Modules\OrchardCore.Media.Indexing.Pdf\OrchardCore.Media.Indexing.Pdf.csproj", "{95187E6A-5B74-4475-8FEB-758ACD012DCC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Media.Indexing.OpenXML", "src\OrchardCore.Modules\OrchardCore.Media.Indexing.OpenXML\OrchardCore.Media.Indexing.OpenXML.csproj", "{47777735-7432-4CCA-A8C5-672E9EE65121}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Media.Indexing.OpenXML", "src\OrchardCore.Modules\OrchardCore.Media.Indexing.OpenXML\OrchardCore.Media.Indexing.OpenXML.csproj", "{47777735-7432-4CCA-A8C5-672E9EE65121}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Email.Azure", "src\OrchardCore.Modules\OrchardCore.Email.Azure\OrchardCore.Email.Azure.csproj", "{C35AB37B-5A09-4896-BEEE-B126B7E7018A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OrchardCore.Email.Smtp", "src\OrchardCore.Modules\OrchardCore.Email.Smtp\OrchardCore.Email.Smtp.csproj", "{E8A1097D-A65A-4B17-A3A2-F50D79552732}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -1359,6 +1363,14 @@ Global {47777735-7432-4CCA-A8C5-672E9EE65121}.Debug|Any CPU.Build.0 = Debug|Any CPU {47777735-7432-4CCA-A8C5-672E9EE65121}.Release|Any CPU.ActiveCfg = Release|Any CPU {47777735-7432-4CCA-A8C5-672E9EE65121}.Release|Any CPU.Build.0 = Release|Any CPU + {C35AB37B-5A09-4896-BEEE-B126B7E7018A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C35AB37B-5A09-4896-BEEE-B126B7E7018A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C35AB37B-5A09-4896-BEEE-B126B7E7018A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C35AB37B-5A09-4896-BEEE-B126B7E7018A}.Release|Any CPU.Build.0 = Release|Any CPU + {E8A1097D-A65A-4B17-A3A2-F50D79552732}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8A1097D-A65A-4B17-A3A2-F50D79552732}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8A1097D-A65A-4B17-A3A2-F50D79552732}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8A1097D-A65A-4B17-A3A2-F50D79552732}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1594,6 +1606,8 @@ Global {E9428DE8-5D81-4359-BF84-31435041FF1A} = {F23AC6C2-DE44-4699-999D-3C478EF3D691} {95187E6A-5B74-4475-8FEB-758ACD012DCC} = {90030E85-0C4F-456F-B879-443E8A3F220D} {47777735-7432-4CCA-A8C5-672E9EE65121} = {90030E85-0C4F-456F-B879-443E8A3F220D} + {C35AB37B-5A09-4896-BEEE-B126B7E7018A} = {A066395F-6F73-45DC-B5A6-B4E306110DCE} + {E8A1097D-A65A-4B17-A3A2-F50D79552732} = {A066395F-6F73-45DC-B5A6-B4E306110DCE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46A1D25A-78D1-4476-9CBF-25B75E296341} diff --git a/mkdocs.yml b/mkdocs.yml index a3f914db30e..abc0748e6e8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -208,6 +208,8 @@ nav: - Data Migrations: docs/reference/modules/Migrations/README.md - Dynamic Cache: docs/reference/modules/DynamicCache/README.md - Email: docs/reference/modules/Email/README.md + - SMTP Provider: docs/reference/modules/Email.Smtp/README.md + - Azure Email Provider: docs/reference/modules/Email.Azure/README.md - GraphQL: docs/reference/modules/Apis.GraphQL/README.md - GraphQL queries: docs/reference/core/Apis.GraphQL.Abstractions/README.md - Health Check: docs/reference/modules/HealthChecks/README.md diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props index 21279ee0f94..5767347a774 100644 --- a/src/OrchardCore.Build/Dependencies.props +++ b/src/OrchardCore.Build/Dependencies.props @@ -12,6 +12,7 @@ + diff --git a/src/OrchardCore.Cms.Web/appsettings.json b/src/OrchardCore.Cms.Web/appsettings.json index 30d17294534..d8e6f6e8ac8 100644 --- a/src/OrchardCore.Cms.Web/appsettings.json +++ b/src/OrchardCore.Cms.Web/appsettings.json @@ -201,7 +201,7 @@ //"OrchardCore_HealthChecks": { // "Url": "/health/live" //}, - //"OrchardCore_Email": { + //"OrchardCore_Email_Smtp": { // "DefaultSender": "", // "DeliveryMethod": "Network", // "PickupDirectoryLocation": "", @@ -217,6 +217,10 @@ // "Username": "", // "Password": "" //}, + //"OrchardCore_Email_Azure": { + // "DefaultSender": "", + // "ConnectionString": "" + //} //"OrchardCore_ReverseProxy": { // "ForwardedHeaders": "None" //}, diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Drivers/AzureEmailSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Drivers/AzureEmailSettingsDisplayDriver.cs new file mode 100644 index 00000000000..e2d5fecd337 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Drivers/AzureEmailSettingsDisplayDriver.cs @@ -0,0 +1,161 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Localization; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Email; +using OrchardCore.Email.Azure; +using OrchardCore.Email.Azure.Services; +using OrchardCore.Email.Azure.ViewModels; +using OrchardCore.Email.Core; +using OrchardCore.Email.Services; +using OrchardCore.Entities; +using OrchardCore.Environment.Shell; +using OrchardCore.Modules; +using OrchardCore.Mvc.ModelBinding; +using OrchardCore.Settings; + +namespace OrchardCore.Azure.Email.Drivers; + +public class AzureEmailSettingsDisplayDriver : SectionDisplayDriver +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly IShellHost _shellHost; + private readonly ShellSettings _shellSettings; + private readonly IEmailAddressValidator _emailValidator; + + protected IStringLocalizer S; + + public AzureEmailSettingsDisplayDriver( + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService, + IDataProtectionProvider dataProtectionProvider, + IShellHost shellHost, + ShellSettings shellSettings, + IEmailAddressValidator emailValidator, + IStringLocalizer stringLocalizer) + { + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + _dataProtectionProvider = dataProtectionProvider; + _shellHost = shellHost; + _shellSettings = shellSettings; + _emailValidator = emailValidator; + S = stringLocalizer; + } + + public override async Task EditAsync(AzureEmailSettings settings, BuildEditorContext context) + { + if (!context.GroupId.EqualsOrdinalIgnoreCase(EmailSettings.GroupId)) + { + return null; + } + + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageEmailSettings)) + { + return null; + } + + return Initialize("AzureEmailSettings_Edit", model => + { + model.IsEnabled = settings.IsEnabled; + model.DefaultSender = settings.DefaultSender; + model.HasConnectionString = !string.IsNullOrWhiteSpace(settings.ConnectionString); + }).Location("Content:5#Azure") + .OnGroup(EmailSettings.GroupId); + } + + public override async Task UpdateAsync(ISite site, AzureEmailSettings settings, IUpdateModel updater, BuildEditorContext context) + { + if (!context.GroupId.EqualsOrdinalIgnoreCase(EmailSettings.GroupId)) + { + return null; + } + + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageEmailSettings)) + { + return null; + } + + var model = new AzureEmailSettingsViewModel(); + + if (await updater.TryUpdateModelAsync(model, Prefix)) + { + var emailSettings = site.As(); + + var hasChanges = model.IsEnabled != settings.IsEnabled; + + settings.IsEnabled = model.IsEnabled; + + if (!model.IsEnabled) + { + if (hasChanges && emailSettings.DefaultProviderName == AzureEmailProvider.TechnicalName) + { + emailSettings.DefaultProviderName = null; + + site.Put(emailSettings); + } + } + else + { + hasChanges |= model.DefaultSender != settings.DefaultSender; + + if (string.IsNullOrEmpty(model.DefaultSender)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultSender), S["The Default Sender is a required field."]); + } + else if (!_emailValidator.Validate(model.DefaultSender)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultSender), S["The Default Sender is invalid."]); + } + + settings.DefaultSender = model.DefaultSender; + + if (string.IsNullOrWhiteSpace(model.ConnectionString) + && settings.ConnectionString is null) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.ConnectionString), S["Connection string is required."]); + } + else if (!string.IsNullOrWhiteSpace(model.ConnectionString)) + { + // Encrypt the connection string. + var protector = _dataProtectionProvider.CreateProtector(AzureEmailOptionsConfiguration.ProtectorName); + + var protectedConnection = protector.Protect(model.ConnectionString); + + // Check if the connection string changed before setting it. + hasChanges |= protectedConnection != settings.ConnectionString; + + settings.ConnectionString = protectedConnection; + } + } + + if (context.Updater.ModelState.IsValid) + { + if (settings.IsEnabled && string.IsNullOrEmpty(emailSettings.DefaultProviderName)) + { + // If we are enabling the only provider, set it as the default one. + emailSettings.DefaultProviderName = AzureEmailProvider.TechnicalName; + site.Put(emailSettings); + + hasChanges = true; + } + + if (hasChanges) + { + // Release the tenant to apply the settings when something changed. + await _shellHost.ReleaseShellContextAsync(_shellSettings); + } + } + } + + return await EditAsync(settings, context); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Manifest.cs new file mode 100644 index 00000000000..921f06486e4 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Manifest.cs @@ -0,0 +1,14 @@ +using OrchardCore.Modules.Manifest; + +[assembly: Module( + Name = "Azure Email Provider", + Author = ManifestConstants.OrchardCoreTeam, + Website = ManifestConstants.OrchardCoreWebsite, + Version = ManifestConstants.OrchardCoreVersion, + Description = "Provides an email service provider leveraging Azure Communication Services (ACS).", + Dependencies = + [ + "OrchardCore.Email" + ], + Category = "Messaging" +)] diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/AzureEmailOptions.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/AzureEmailOptions.cs new file mode 100644 index 00000000000..e83afa49f0e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/AzureEmailOptions.cs @@ -0,0 +1,13 @@ +namespace OrchardCore.Email.Azure.Models; + +public class AzureEmailOptions +{ + public bool IsEnabled { get; set; } + + public string DefaultSender { get; set; } + + public string ConnectionString { get; set; } + + public bool ConfigurationExists() + => !string.IsNullOrWhiteSpace(DefaultSender) && !string.IsNullOrWhiteSpace(ConnectionString); +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/AzureEmailSettings.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/AzureEmailSettings.cs new file mode 100644 index 00000000000..1b40295c041 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/AzureEmailSettings.cs @@ -0,0 +1,16 @@ +namespace OrchardCore.Email.Azure; + +/// +/// Represents a settings for Azure email. +/// +public class AzureEmailSettings +{ + public bool IsEnabled { get; set; } + + public string DefaultSender { get; set; } + + /// + /// Gets or sets the connection string. + /// + public string ConnectionString { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/DefaultAzureEmailOptions.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/DefaultAzureEmailOptions.cs new file mode 100644 index 00000000000..4af8b72853d --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Models/DefaultAzureEmailOptions.cs @@ -0,0 +1,5 @@ +namespace OrchardCore.Email.Azure.Models; + +public class DefaultAzureEmailOptions : AzureEmailOptions +{ +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/OrchardCore.Email.Azure.csproj b/src/OrchardCore.Modules/OrchardCore.Email.Azure/OrchardCore.Email.Azure.csproj new file mode 100644 index 00000000000..4578458c8a0 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/OrchardCore.Email.Azure.csproj @@ -0,0 +1,31 @@ + + + + true + + OrchardCore Azure Email + + $(OCFrameworkDescription) + + Provides the configuration of email settings and a default email service utilizing Azure Communication Services (ACS). + + $(PackageTags) OrchardCoreFramework + + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailOptionsConfiguration.cs new file mode 100644 index 00000000000..5b639b6d285 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailOptionsConfiguration.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Options; +using OrchardCore.Email.Azure; +using OrchardCore.Email.Azure.Models; +using OrchardCore.Settings; + +namespace OrchardCore.Email.Services; + +public class AzureEmailOptionsConfiguration : IConfigureOptions +{ + public const string ProtectorName = "AzureEmailProtector"; + + private readonly ISiteService _siteService; + private readonly IDataProtectionProvider _dataProtectionProvider; + + public AzureEmailOptionsConfiguration( + ISiteService siteService, + IDataProtectionProvider dataProtectionProvider) + { + _siteService = siteService; + _dataProtectionProvider = dataProtectionProvider; + } + + public void Configure(AzureEmailOptions options) + { + var settings = _siteService.GetSiteSettingsAsync() + .GetAwaiter() + .GetResult() + .As(); + + options.IsEnabled = settings.IsEnabled; + options.DefaultSender = settings.DefaultSender; + + if (!string.IsNullOrEmpty(settings.ConnectionString)) + { + var protector = _dataProtectionProvider.CreateProtector(ProtectorName); + + options.ConnectionString = protector.Unprotect(settings.ConnectionString); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProvider.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProvider.cs new file mode 100644 index 00000000000..3a159f485e7 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProvider.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Email.Azure.Models; + +namespace OrchardCore.Email.Azure.Services; + +public class AzureEmailProvider : AzureEmailProviderBase +{ + public const string TechnicalName = "Azure"; + + public AzureEmailProvider( + IOptions options, + IEmailAddressValidator emailAddressValidator, + ILogger logger, + IStringLocalizer stringLocalizer) + : base(options.Value, emailAddressValidator, logger, stringLocalizer) + { + } + + public override LocalizedString DisplayName => S["Azure Communication Service"]; +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProviderBase.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProviderBase.cs new file mode 100644 index 00000000000..ea2e2171fb8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProviderBase.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Azure; +using Azure.Communication.Email; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using OrchardCore.Email.Azure.Models; + +namespace OrchardCore.Email.Azure.Services; + +public abstract class AzureEmailProviderBase : IEmailProvider +{ + // Common supported file extensions and their corresponding MIME types for email attachments + // using Azure Communication Services Email. + // For more info + protected static readonly Dictionary _allowedMimeTypes = new() + { + { ".3gp", "video/3gpp" }, + { ".3g2", "video/3gpp2" }, + { ".7z", "application/x-7z-compressed" }, + { ".aac", "audio/aac" }, + { ".avi", "video/x-msvideo" }, + { ".bmp", "image/bmp" }, + { ".csv", "text/csv" }, + { ".doc", "application/msword" }, + { ".docm", "application/vnd.ms-word.document.macroEnabled.12" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".eot", "application/vnd.ms-fontobject" }, + { ".epub", "application/epub+zip" }, + { ".gif", "image/gif" }, + { ".gz", "application/gzip" }, + { ".ico", "image/vnd.microsoft.icon" }, + { ".ics", "text/calendar" }, + { ".jpg", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".json", "application/json" }, + { ".mid", ".midi audio/midi" }, + { ".midi", ".midi audio/midi" }, + { ".mp3", "audio/mpeg" }, + { ".mp4", "video/mp4" }, + { ".mpeg", "video/mpeg" }, + { ".oga", "audio/ogg" }, + { ".ogv", "video/ogg" }, + { ".ogx", "application/ogg" }, + { ".one", "application/onenote" }, + { ".opus", "audio/opus" }, + { ".otf", "font/otf" }, + { ".pdf", "application/pdf" }, + { ".png", "image/png" }, + { ".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, + { ".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".pub", "application/vnd.ms-publisher" }, + { ".rar", "application/x-rar-compressed" }, + { ".rpmsg", "application/vnd.ms-outlook" }, + { ".rtf", "application/rtf" }, + { ".svg", "image/svg+xml" }, + { ".tar", "application/x-tar" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".ttf", "font/ttf" }, + { ".txt", "text/plain" }, + { ".vsd", "application/vnd.visio" }, + { ".wav", "audio/wav" }, + { ".weba", "audio/webm" }, + { ".webm", "video/webm" }, + { ".webp", "image/webp" }, + { ".wma", "audio/x-ms-wma" }, + { ".wmv", "video/x-ms-wmv" }, + { ".woff", "font/woff" }, + { ".woff2", "font/woff2" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, + { ".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".xml", "application/xml" }, + { ".zip", "application/zip" } + }; + + private readonly AzureEmailOptions _providerOptions; + private readonly IEmailAddressValidator _emailAddressValidator; + private readonly ILogger _logger; + + protected readonly IStringLocalizer S; + + public AzureEmailProviderBase( + AzureEmailOptions options, + IEmailAddressValidator emailAddressValidator, + ILogger logger, + IStringLocalizer stringLocalizer) + { + _providerOptions = options; + _emailAddressValidator = emailAddressValidator; + _logger = logger; + S = stringLocalizer; + } + + public abstract LocalizedString DisplayName { get; } + + public virtual async Task SendAsync(MailMessage message) + { + ArgumentNullException.ThrowIfNull(message); + + if (!_providerOptions.IsEnabled) + { + return EmailResult.FailedResult(S["The Azure Email Provider is disabled."]); + } + + var senderAddress = string.IsNullOrWhiteSpace(message.From) + ? _providerOptions.DefaultSender + : message.From; + + _logger.LogDebug("Attempting to send email to {Email}.", message.To); + + if (!string.IsNullOrWhiteSpace(senderAddress)) + { + if (!_emailAddressValidator.Validate(senderAddress)) + { + return EmailResult.FailedResult(nameof(message.From), S["Invalid email address for the sender: '{0}'.", senderAddress]); + } + + message.From = senderAddress; + } + + var errors = new Dictionary>(); + var emailMessage = FromMailMessage(message, errors); + + if (errors.Count > 0) + { + return EmailResult.FailedResult(errors); + } + + try + { + var client = new EmailClient(_providerOptions.ConnectionString); + var emailResult = await client.SendAsync(WaitUntil.Completed, emailMessage); + + if (emailResult.HasValue) + { + return EmailResult.SuccessResult; + } + + return EmailResult.FailedResult(string.Empty, S["An error occurred while sending an email."]); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while sending an email using the Azure Email Provider."); + + // IMPORTANT, do not expose ex.Message as it could contain the connection string in a raw format! + return EmailResult.FailedResult(string.Empty, S["An error occurred while sending an email."]); + } + } + + private EmailMessage FromMailMessage(MailMessage message, Dictionary> errors) + { + var recipients = message.GetRecipients(); + + List toRecipients = null; + if (recipients.To.Count > 0) + { + toRecipients = [.. recipients.To.Select(r => new EmailAddress(r))]; + } + + List ccRecipients = null; + if (recipients.Cc.Count > 0) + { + ccRecipients = [.. recipients.Cc.Select(r => new EmailAddress(r))]; + } + + List bccRecipients = null; + if (recipients.Bcc.Count > 0) + { + bccRecipients = [.. recipients.Bcc.Select(r => new EmailAddress(r))]; + } + + var content = new EmailContent(message.Subject); + if (message.IsHtmlBody) + { + content.Html = message.Body; + } + else + { + content.PlainText = message.Body; + } + + var emailMessage = new EmailMessage( + message.From, + new EmailRecipients(toRecipients, ccRecipients, bccRecipients), + content); + + foreach (var address in message.GetReplyTo()) + { + emailMessage.ReplyTo.Add(new EmailAddress(address)); + } + + foreach (var attachment in message.Attachments) + { + if (attachment.Stream == null) + { + continue; + } + var extension = Path.GetExtension(attachment.Filename); + + if (_allowedMimeTypes.TryGetValue(extension, out var contentType)) + { + var data = new byte[attachment.Stream.Length]; + + attachment.Stream.Read(data, 0, (int)attachment.Stream.Length); + + emailMessage.Attachments.Add(new EmailAttachment(attachment.Filename, contentType, new BinaryData(data))); + } + else + { + errors.TryAdd(nameof(message.Attachments), []); + + errors[nameof(message.Attachments)].Add(S["Unable to attach the file named '{0}' since its type is not supported.", attachment.Filename]); + + _logger.LogWarning("The MIME type for the attachment '{attachment}' is not supported.", attachment.Filename); + } + } + + return emailMessage; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProviderOptionsConfigurations.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProviderOptionsConfigurations.cs new file mode 100644 index 00000000000..e37faa1fe9b --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/AzureEmailProviderOptionsConfigurations.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Email.Azure.Models; +using OrchardCore.Email.Core.Services; + +namespace OrchardCore.Email.Azure.Services; + +public class AzureEmailProviderOptionsConfigurations : IConfigureOptions +{ + private readonly AzureEmailOptions _azureOptions; + private readonly DefaultAzureEmailOptions _defaultAzureOptions; + + public AzureEmailProviderOptionsConfigurations( + IOptions azureOptions, + IOptions defaultAzureOptions) + { + _azureOptions = azureOptions.Value; + _defaultAzureOptions = defaultAzureOptions.Value; + } + + public void Configure(EmailProviderOptions options) + { + ConfigureTenantProvider(options); + + if (_defaultAzureOptions.IsEnabled) + { + // Only configure the default provider, if settings are provided by the configuration provider. + ConfigureDefaultProvider(options); + } + } + + private void ConfigureTenantProvider(EmailProviderOptions options) + { + var typeOptions = new EmailProviderTypeOptions(typeof(AzureEmailProvider)) + { + IsEnabled = _azureOptions.IsEnabled, + }; + + options.TryAddProvider(AzureEmailProvider.TechnicalName, typeOptions); + } + + private static void ConfigureDefaultProvider(EmailProviderOptions options) + { + var typeOptions = new EmailProviderTypeOptions(typeof(DefaultAzureEmailProvider)) + { + IsEnabled = true, + }; + + options.TryAddProvider(DefaultAzureEmailProvider.TechnicalName, typeOptions); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/DefaultAzureEmailProvider.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/DefaultAzureEmailProvider.cs new file mode 100644 index 00000000000..01514660f35 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Services/DefaultAzureEmailProvider.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Email.Azure.Models; + +namespace OrchardCore.Email.Azure.Services; + +public class DefaultAzureEmailProvider : AzureEmailProviderBase +{ + public const string TechnicalName = "DefaultAzure"; + + public DefaultAzureEmailProvider( + IOptions options, + IEmailAddressValidator emailAddressValidator, + ILogger logger, + IStringLocalizer stringLocalizer) + : base(options.Value, emailAddressValidator, logger, stringLocalizer) + { + } + + public override LocalizedString DisplayName => S["Default Azure Communication Service"]; +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Startup.cs new file mode 100644 index 00000000000..0d523fbea53 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Startup.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Azure.Email.Drivers; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Email.Azure.Models; +using OrchardCore.Email.Azure.Services; +using OrchardCore.Email.Core; +using OrchardCore.Email.Services; +using OrchardCore.Environment.Shell.Configuration; +using OrchardCore.Settings; + +namespace OrchardCore.Email.Azure; + +public class Startup +{ + private readonly IShellConfiguration _shellConfiguration; + + public Startup(IShellConfiguration shellConfiguration) + { + _shellConfiguration = shellConfiguration; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddTransient, AzureEmailOptionsConfiguration>(); + + services.AddEmailProviderOptionsConfiguration() + .AddScoped, AzureEmailSettingsDisplayDriver>(); + + services.Configure(options => + { + _shellConfiguration.GetSection("OrchardCore_Email_Azure").Bind(options); + + options.IsEnabled = options.ConfigurationExists(); + }); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/ViewModels/AzureEmailSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Email.Azure/ViewModels/AzureEmailSettingsViewModel.cs new file mode 100644 index 00000000000..55379436fb7 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/ViewModels/AzureEmailSettingsViewModel.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace OrchardCore.Email.Azure.ViewModels; + +public class AzureEmailSettingsViewModel +{ + public bool IsEnabled { get; set; } + + [EmailAddress] + public string DefaultSender { get; set; } + + public string ConnectionString { get; set; } + + [BindNever] + public bool HasConnectionString { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Views/AzureEmailSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Views/AzureEmailSettings.Edit.cshtml new file mode 100644 index 00000000000..d746bb35c6a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Views/AzureEmailSettings.Edit.cshtml @@ -0,0 +1,28 @@ +@using Microsoft.AspNetCore.Mvc.Localization +@using OrchardCore.Email.Azure.ViewModels + +@model AzureEmailSettingsViewModel + +
+
+ + +
+
+ +
+ +
+ + + + @T["The default email address to use as a sender, unless the email sender is set."] +
+ +
+ + + +
+ +
diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Azure/Views/_ViewImports.cshtml b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Views/_ViewImports.cshtml new file mode 100644 index 00000000000..252fd654bb8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Azure/Views/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@inherits OrchardCore.DisplayManagement.Razor.RazorPage + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, OrchardCore.DisplayManagement +@addTagHelper *, OrchardCore.ResourceManagement diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Drivers/SmtpSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Drivers/SmtpSettingsDisplayDriver.cs new file mode 100644 index 00000000000..decc75f2aaa --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Drivers/SmtpSettingsDisplayDriver.cs @@ -0,0 +1,211 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Email.Core; +using OrchardCore.Email.Smtp.Services; +using OrchardCore.Email.Smtp.ViewModels; +using OrchardCore.Entities; +using OrchardCore.Environment.Shell; +using OrchardCore.Modules; +using OrchardCore.Mvc.ModelBinding; +using OrchardCore.Settings; + +namespace OrchardCore.Email.Smtp.Drivers; + +public class SmtpSettingsDisplayDriver : SectionDisplayDriver +{ + [Obsolete("This property should no longer be used. Instead use EmailSettings.GroupId")] + public const string GroupId = EmailSettings.GroupId; + + private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly IShellHost _shellHost; + private readonly ShellSettings _shellSettings; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly SmtpOptions _smtpOptions; + private readonly IAuthorizationService _authorizationService; + private readonly IEmailAddressValidator _emailValidator; + + protected readonly IStringLocalizer S; + + public SmtpSettingsDisplayDriver( + IDataProtectionProvider dataProtectionProvider, + IShellHost shellHost, + ShellSettings shellSettings, + IHttpContextAccessor httpContextAccessor, + IOptions options, + IAuthorizationService authorizationService, + IEmailAddressValidator emailAddressValidator, + IStringLocalizer stringLocalizer) + { + _dataProtectionProvider = dataProtectionProvider; + _shellHost = shellHost; + _shellSettings = shellSettings; + _httpContextAccessor = httpContextAccessor; + _smtpOptions = options.Value; + _authorizationService = authorizationService; + _emailValidator = emailAddressValidator; + S = stringLocalizer; + } + + public override async Task EditAsync(SmtpSettings settings, BuildEditorContext context) + { + if (!context.GroupId.EqualsOrdinalIgnoreCase(EmailSettings.GroupId)) + { + return null; + } + + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageEmailSettings)) + { + return null; + } + + return Initialize("SmtpSettings_Edit", model => + { + // For backward compatibility with instances before the SMTP provider was factored out of + // OrchardCore.Email, if IsEnabled is null, we check to see if there's already valid configuration. + model.IsEnabled = settings.IsEnabled ?? _smtpOptions.ConfigurationExists(); + model.DefaultSender = settings.DefaultSender; + model.DeliveryMethod = settings.DeliveryMethod; + model.PickupDirectoryLocation = settings.PickupDirectoryLocation; + model.Host = settings.Host; + model.Port = settings.Port; + model.ProxyHost = settings.ProxyHost; + model.ProxyPort = settings.ProxyPort; + model.EncryptionMethod = settings.EncryptionMethod; + model.AutoSelectEncryption = settings.AutoSelectEncryption; + model.RequireCredentials = settings.RequireCredentials; + model.UseDefaultCredentials = settings.UseDefaultCredentials; + model.UserName = settings.UserName; + model.Password = settings.Password; + model.IgnoreInvalidSslCertificate = settings.IgnoreInvalidSslCertificate; + }).Location("Content:5#SMTP") + .OnGroup(EmailSettings.GroupId); + } + + public override async Task UpdateAsync(ISite site, SmtpSettings settings, IUpdateModel updater, BuildEditorContext context) + { + if (!context.GroupId.EqualsOrdinalIgnoreCase(EmailSettings.GroupId)) + { + return null; + } + + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageEmailSettings)) + { + return null; + } + + var model = new SmtpSettingsViewModel(); + + if (await context.Updater.TryUpdateModelAsync(model, Prefix)) + { + var emailSettings = site.As(); + + var hasChanges = model.IsEnabled != settings.IsEnabled; + + if (!model.IsEnabled) + { + if (hasChanges && emailSettings.DefaultProviderName == SmtpEmailProvider.TechnicalName) + { + emailSettings.DefaultProviderName = null; + + site.Put(emailSettings); + } + + settings.IsEnabled = false; + } + else + { + if (string.IsNullOrEmpty(model.DefaultSender)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultSender), S["The Default Sender is a required field."]); + } + else if (!_emailValidator.Validate(model.DefaultSender)) + { + context.Updater.ModelState.AddModelError(Prefix, nameof(model.DefaultSender), S["The Default Sender is invalid."]); + } + + if (model.DeliveryMethod == SmtpDeliveryMethod.Network + && string.IsNullOrWhiteSpace(model.Host)) + { + updater.ModelState.AddModelError(Prefix, nameof(model.Host), S["The {0} field is required.", "Host name"]); + } + else if (model.DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory + && string.IsNullOrWhiteSpace(model.PickupDirectoryLocation)) + { + updater.ModelState.AddModelError(Prefix, nameof(model.PickupDirectoryLocation), S["The {0} field is required.", "Pickup directory location"]); + } + + hasChanges |= model.DefaultSender != settings.DefaultSender; + hasChanges |= model.Host != settings.Host; + hasChanges |= model.Port != settings.Port; + hasChanges |= model.AutoSelectEncryption != settings.AutoSelectEncryption; + hasChanges |= model.RequireCredentials != settings.RequireCredentials; + hasChanges |= model.UseDefaultCredentials != settings.UseDefaultCredentials; + hasChanges |= model.EncryptionMethod != settings.EncryptionMethod; + hasChanges |= model.UserName != settings.UserName; + hasChanges |= model.ProxyHost != settings.ProxyHost; + hasChanges |= model.ProxyPort != settings.ProxyPort; + hasChanges |= model.IgnoreInvalidSslCertificate != settings.IgnoreInvalidSslCertificate; + hasChanges |= model.DeliveryMethod != settings.DeliveryMethod; + hasChanges |= model.PickupDirectoryLocation != settings.PickupDirectoryLocation; + + // Store the password when there is a new value. + if (!string.IsNullOrWhiteSpace(model.Password)) + { + // Encrypt the password. + var protector = _dataProtectionProvider.CreateProtector(SmtpOptionsConfiguration.ProtectorName); + + var protectedPassword = protector.Protect(model.Password); + + // Check if the password changed before setting the password. + hasChanges |= protectedPassword != settings.Password; + + settings.Password = protectedPassword; + } + + settings.IsEnabled = true; + settings.DefaultSender = model.DefaultSender; + settings.Host = model.Host; + settings.Port = model.Port; + settings.AutoSelectEncryption = model.AutoSelectEncryption; + settings.RequireCredentials = model.RequireCredentials; + settings.UseDefaultCredentials = model.UseDefaultCredentials; + settings.EncryptionMethod = model.EncryptionMethod; + settings.UserName = model.UserName; + settings.ProxyHost = model.ProxyHost; + settings.ProxyPort = model.ProxyPort; + settings.IgnoreInvalidSslCertificate = model.IgnoreInvalidSslCertificate; + settings.DeliveryMethod = model.DeliveryMethod; + settings.PickupDirectoryLocation = model.PickupDirectoryLocation; + } + + if (context.Updater.ModelState.IsValid) + { + if (settings.IsEnabled == true && string.IsNullOrEmpty(emailSettings.DefaultProviderName)) + { + // If we are enabling the only provider, set it as the default one. + emailSettings.DefaultProviderName = SmtpEmailProvider.TechnicalName; + site.Put(emailSettings); + + hasChanges = true; + } + + if (hasChanges) + { + // Release the tenant to apply the settings when something changed. + await _shellHost.ReleaseShellContextAsync(_shellSettings); + } + } + } + + return await EditAsync(settings, context); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Manifest.cs new file mode 100644 index 00000000000..976ca49365b --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Manifest.cs @@ -0,0 +1,14 @@ +using OrchardCore.Modules.Manifest; + +[assembly: Module( + Name = "SMTP Email Provider", + Author = ManifestConstants.OrchardCoreTeam, + Website = ManifestConstants.OrchardCoreWebsite, + Version = ManifestConstants.OrchardCoreVersion, + Description = "Provides an email service provider leveraging Simple Mail Transfer Protocol (SMTP).", + Dependencies = + [ + "OrchardCore.Email" + ], + Category = "Messaging" +)] diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/OrchardCore.Email.Smtp.csproj b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/OrchardCore.Email.Smtp.csproj new file mode 100644 index 00000000000..c80c6b4672c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/OrchardCore.Email.Smtp.csproj @@ -0,0 +1,34 @@ + + + + true + + OrchardCore SMTP Email + + $(OCFrameworkDescription) + + Provides the configuration of email settings and a default email service utilizing Simple Mail Transfer Protocol (SMTP). + + $(PackageTags) OrchardCoreFramework + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/DefaultSmtpEmailProvider.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/DefaultSmtpEmailProvider.cs new file mode 100644 index 00000000000..92e9efb8d9f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/DefaultSmtpEmailProvider.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OrchardCore.Email.Smtp.Services; + +public class DefaultSmtpEmailProvider : SmtpEmailProviderBase +{ + public const string TechnicalName = "DefaultSMTP"; + + public DefaultSmtpEmailProvider( + IOptions options, + IEmailAddressValidator emailAddressValidator, + ILogger logger, + IStringLocalizer stringLocalizer) + : base(options.Value, emailAddressValidator, logger, stringLocalizer) + { + } + + public override LocalizedString DisplayName => S["Simple Mail Transfer Protocol (Default SMTP)"]; +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/ServiceCollectionExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/ServiceCollectionExtensions.cs new file mode 100644 index 00000000000..8fdc3ac8169 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/ServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Email.Core; +using OrchardCore.Email.Services; +using OrchardCore.Email.Smtp.Services; + +namespace OrchardCore.Email.Azure.Services; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddSmtpEmailProvider(this IServiceCollection services) + { +#pragma warning disable CS0612 // Type or member is obsolete + services.AddSmtpService(); +#pragma warning restore CS0612 // Type or member is obsolete + + services.AddEmailProviderOptionsConfiguration(); + + return services; + } + + [Obsolete] + private static void AddSmtpService(this IServiceCollection services) + { + services.AddScoped(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpEmailProvider.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpEmailProvider.cs new file mode 100644 index 00000000000..835a17fa218 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpEmailProvider.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OrchardCore.Email.Smtp.Services; + +public class SmtpEmailProvider : SmtpEmailProviderBase +{ + public const string TechnicalName = "SMTP"; + + public SmtpEmailProvider( + IOptions options, + IEmailAddressValidator emailAddressValidator, + ILogger logger, + IStringLocalizer stringLocalizer) + : base(options.Value, emailAddressValidator, logger, stringLocalizer) + { + } + + public override LocalizedString DisplayName => S["Simple Mail Transfer Protocol (SMTP)"]; +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpEmailProviderBase.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpEmailProviderBase.cs new file mode 100644 index 00000000000..56cf283029a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpEmailProviderBase.cs @@ -0,0 +1,222 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using MailKit.Net.Proxy; +using MailKit.Net.Smtp; +using MailKit.Security; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using MimeKit; + +namespace OrchardCore.Email.Smtp.Services; + +public abstract class SmtpEmailProviderBase : IEmailProvider +{ + private const string EmailExtension = ".eml"; + + private readonly SmtpOptions _providerOptions; + private readonly IEmailAddressValidator _emailAddressValidator; + private readonly ILogger _logger; + + protected readonly IStringLocalizer S; + + public SmtpEmailProviderBase( + SmtpOptions options, + IEmailAddressValidator emailAddressValidator, + ILogger logger, + IStringLocalizer stringLocalizer) + { + _providerOptions = options; + _emailAddressValidator = emailAddressValidator; + _logger = logger; + S = stringLocalizer; + } + + public abstract LocalizedString DisplayName { get; } + + public virtual async Task SendAsync(MailMessage message) + { + ArgumentNullException.ThrowIfNull(message); + + if (!_providerOptions.IsEnabled) + { + return EmailResult.FailedResult(S["The SMTP Email Provider is disabled."]); + } + + var senderAddress = string.IsNullOrWhiteSpace(message.From) + ? _providerOptions.DefaultSender + : message.From; + + _logger.LogDebug("Attempting to send email to {Email}.", message.To); + + // Set the MailMessage.From, to avoid the confusion between DefaultSender (Author) and submitter (Sender). + if (!string.IsNullOrWhiteSpace(senderAddress)) + { + if (!_emailAddressValidator.Validate(senderAddress)) + { + return EmailResult.FailedResult(nameof(message.From), S["Invalid email address for the sender: '{0}'.", senderAddress]); + } + + message.From = senderAddress; + } + + var mimeMessage = GetMimeMessage(message); + + try + { + if (_providerOptions.DeliveryMethod == SmtpDeliveryMethod.Network) + { + var response = await SendOnlineMessageAsync(mimeMessage); + + return EmailResult.GetSuccessResult(response); + } + + if (_providerOptions.DeliveryMethod == SmtpDeliveryMethod.SpecifiedPickupDirectory) + { + await SendOfflineMessageAsync(mimeMessage, _providerOptions.PickupDirectoryLocation); + + return EmailResult.SuccessResult; + } + + throw new NotSupportedException($"The '{_providerOptions.DeliveryMethod}' delivery method is not supported."); + } + catch (Exception ex) + { + return EmailResult.FailedResult([S["An error occurred while sending an email: '{0}'", ex.Message]]); + } + } + + private MimeMessage GetMimeMessage(MailMessage message) + { + var mimeMessage = new MimeMessage(); + var submitterAddress = string.IsNullOrWhiteSpace(message.Sender) + ? _providerOptions.DefaultSender + : message.Sender; + + if (!string.IsNullOrEmpty(submitterAddress)) + { + mimeMessage.Sender = MailboxAddress.Parse(submitterAddress); + } + + mimeMessage.From.AddRange(message.GetSender().Select(MailboxAddress.Parse)); + + var recipients = message.GetRecipients(); + mimeMessage.To.AddRange(recipients.To.Select(MailboxAddress.Parse)); + mimeMessage.Cc.AddRange(recipients.Cc.Select(MailboxAddress.Parse)); + mimeMessage.Bcc.AddRange(recipients.Bcc.Select(MailboxAddress.Parse)); + + mimeMessage.ReplyTo.AddRange(message.GetReplyTo().Select(MailboxAddress.Parse)); + + mimeMessage.Subject = message.Subject; + + var body = new BodyBuilder(); + + if (message.IsHtmlBody) + { + body.HtmlBody = message.Body; + } + else + { + body.TextBody = message.Body; + } + + foreach (var attachment in message.Attachments) + { + // Stream must not be null, otherwise it would try to get the filesystem path + if (attachment.Stream != null) + { + body.Attachments.Add(attachment.Filename, attachment.Stream); + } + } + + mimeMessage.Body = body.ToMessageBody(); + + return mimeMessage; + } + + private async Task SendOnlineMessageAsync(MimeMessage message) + { + var secureSocketOptions = SecureSocketOptions.Auto; + + if (!_providerOptions.AutoSelectEncryption) + { + secureSocketOptions = _providerOptions.EncryptionMethod switch + { + SmtpEncryptionMethod.None => SecureSocketOptions.None, + SmtpEncryptionMethod.SslTls => SecureSocketOptions.SslOnConnect, + SmtpEncryptionMethod.StartTls => SecureSocketOptions.StartTls, + _ => SecureSocketOptions.Auto, + }; + } + + using var client = new SmtpClient(); + + client.ServerCertificateValidationCallback = CertificateValidationCallback; + + await client.ConnectAsync(_providerOptions.Host, _providerOptions.Port, secureSocketOptions); + + if (_providerOptions.RequireCredentials) + { + if (_providerOptions.UseDefaultCredentials) + { + // There's no notion of 'UseDefaultCredentials' in MailKit, so empty credentials is passed in. + await client.AuthenticateAsync(string.Empty, string.Empty); + } + else if (!string.IsNullOrWhiteSpace(_providerOptions.UserName)) + { + await client.AuthenticateAsync(_providerOptions.UserName, _providerOptions.Password); + } + } + + if (!string.IsNullOrEmpty(_providerOptions.ProxyHost)) + { + client.ProxyClient = new Socks5Client(_providerOptions.ProxyHost, _providerOptions.ProxyPort); + } + + var response = await client.SendAsync(message); + + await client.DisconnectAsync(true); + + return response; + } + + private static Task SendOfflineMessageAsync(MimeMessage message, string pickupDirectory) + { + var mailPath = Path.Combine(pickupDirectory, Guid.NewGuid().ToString() + EmailExtension); + + return message.WriteToAsync(mailPath, CancellationToken.None); + } + + private bool CertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + const string logErrorMessage = "SMTP Server's certificate {CertificateSubject} issued by {CertificateIssuer} " + + "with thumbprint {CertificateThumbprint} and expiration date {CertificateExpirationDate} " + + "is considered invalid with {SslPolicyErrors} policy errors"; + + _logger.LogError(logErrorMessage, + certificate.Subject, + certificate.Issuer, + certificate.GetCertHashString(), + certificate.GetExpirationDateString(), + sslPolicyErrors); + + if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors) && chain?.ChainStatus != null) + { + foreach (var chainStatus in chain.ChainStatus) + { + _logger.LogError("Status: {Status} - {StatusInformation}", chainStatus.Status, chainStatus.StatusInformation); + } + } + + return _providerOptions.IgnoreInvalidSslCertificate; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpOptionsConfiguration.cs new file mode 100644 index 00000000000..0ac92e3870f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpOptionsConfiguration.cs @@ -0,0 +1,62 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using OrchardCore.Settings; + +namespace OrchardCore.Email.Smtp.Services; + +public class SmtpOptionsConfiguration : IConfigureOptions +{ + public const string ProtectorName = "SmtpSettingsConfiguration"; + + private readonly ISiteService _siteService; + private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly ILogger _logger; + + public SmtpOptionsConfiguration( + ISiteService siteService, + IDataProtectionProvider dataProtectionProvider, + ILogger logger) + { + _siteService = siteService; + _dataProtectionProvider = dataProtectionProvider; + _logger = logger; + } + + public void Configure(SmtpOptions options) + { + var settings = _siteService.GetSiteSettingsAsync() + .GetAwaiter().GetResult() + .As(); + + options.DefaultSender = settings.DefaultSender; + options.DeliveryMethod = settings.DeliveryMethod; + options.PickupDirectoryLocation = settings.PickupDirectoryLocation; + options.Host = settings.Host; + options.Port = settings.Port; + options.ProxyHost = settings.ProxyHost; + options.ProxyPort = settings.ProxyPort; + options.EncryptionMethod = settings.EncryptionMethod; + options.AutoSelectEncryption = settings.AutoSelectEncryption; + options.RequireCredentials = settings.RequireCredentials; + options.UseDefaultCredentials = settings.UseDefaultCredentials; + options.UserName = settings.UserName; + options.Password = settings.Password; + options.IgnoreInvalidSslCertificate = settings.IgnoreInvalidSslCertificate; + + if (!string.IsNullOrWhiteSpace(settings.Password)) + { + try + { + var protector = _dataProtectionProvider.CreateProtector(ProtectorName); + options.Password = protector.Unprotect(settings.Password); + } + catch + { + _logger.LogError("The Smtp password could not be decrypted. It may have been encrypted using a different key."); + } + } + + options.IsEnabled = settings.IsEnabled ?? options.ConfigurationExists(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpProviderOptionsConfigurations.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpProviderOptionsConfigurations.cs new file mode 100644 index 00000000000..35289b51373 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpProviderOptionsConfigurations.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Email.Core.Services; + +namespace OrchardCore.Email.Smtp.Services; + +public class SmtpProviderOptionsConfigurations : IConfigureOptions +{ + private readonly SmtpOptions _smtpOptions; + private readonly DefaultSmtpOptions _defaultSmtpOptions; + + public SmtpProviderOptionsConfigurations( + IOptions smtpOptions, + IOptions defaultSmtpOptions) + { + _smtpOptions = smtpOptions.Value; + _defaultSmtpOptions = defaultSmtpOptions.Value; + } + + public void Configure(EmailProviderOptions options) + { + ConfigureTenantProvider(options); + + if (_defaultSmtpOptions.IsEnabled) + { + ConfigureDefaultProvider(options); + } + } + + private void ConfigureTenantProvider(EmailProviderOptions options) + { + var typeOptions = new EmailProviderTypeOptions(typeof(SmtpEmailProvider)) + { + IsEnabled = _smtpOptions.IsEnabled, + }; + + options.TryAddProvider(SmtpEmailProvider.TechnicalName, typeOptions); + } + + private static void ConfigureDefaultProvider(EmailProviderOptions options) + { + var typeOptions = new EmailProviderTypeOptions(typeof(DefaultSmtpEmailProvider)) + { + IsEnabled = true, + }; + + options.TryAddProvider(DefaultSmtpEmailProvider.TechnicalName, typeOptions); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpService.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpService.cs new file mode 100644 index 00000000000..0d6993f9f06 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Services/SmtpService.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using OrchardCore.Email.Core.Services; +using OrchardCore.Email.Smtp.Services; +using OrchardCore.Environment.Shell.Builders; + +namespace OrchardCore.Email.Services; + +[Obsolete] +public class SmtpService : ISmtpService +{ + private readonly EmailProviderOptions _emailProviderOptions; + private readonly IServiceProvider _serviceProvider; + + protected readonly IStringLocalizer S; + + public SmtpService( + IOptions emailProviderOptions, + IServiceProvider serviceProvider, + IStringLocalizer stringLocalizer) + { + _emailProviderOptions = emailProviderOptions.Value; + _serviceProvider = serviceProvider; + S = stringLocalizer; + } + + public async Task SendAsync(MailMessage message) + { + var provider = GetSmtpProvider(); + + if (provider == null) + { + return SmtpResult.Failed([S["Unable to find any SMTP providers."]]); + } + + var result = await provider.SendAsync(message); + + if (result.Succeeded) + { + return SmtpResult.Success; + } + + return SmtpResult.Failed(result.Errors.SelectMany(x => x.Value).ToArray()); + } + + private IEmailProvider GetSmtpProvider() + { + var type = _emailProviderOptions.Providers.Where(x => x.Key.Contains("SMTP")) + .OrderBy(entry => !entry.Value.IsEnabled ? 0 : 1) + .ThenBy(entry => entry.Key != DefaultSmtpEmailProvider.TechnicalName ? 0 : 1) + .Select(x => x.Value) + .LastOrDefault()?.Type; + + if (type is not null) + { + return _serviceProvider.CreateInstance(type); + } + + return null; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Startup.cs new file mode 100644 index 00000000000..194fd2d3cb1 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Startup.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Email.Azure.Services; +using OrchardCore.Email.Smtp.Drivers; +using OrchardCore.Email.Smtp.Services; +using OrchardCore.Environment.Shell.Configuration; +using OrchardCore.Settings; + +namespace OrchardCore.Email.Smtp; + +public class Startup +{ + private readonly IShellConfiguration _shellConfiguration; + + public Startup(IShellConfiguration shellConfiguration) + { + _shellConfiguration = shellConfiguration; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddSmtpEmailProvider() + .AddScoped, SmtpSettingsDisplayDriver>() + .AddTransient, SmtpOptionsConfiguration>(); + + services.Configure(options => + { + // To ensure backward compatibility, we will try to associate SMTP settings from multiple sections. + // The 'OrchardCore_Email' section will be phased out in an upcoming release. + _shellConfiguration.GetSection("OrchardCore_Email").Bind(options); + _shellConfiguration.GetSection("OrchardCore_Email_Smtp").Bind(options); + + options.IsEnabled = options.ConfigurationExists(); + }); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/ViewModels/SmtpSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/ViewModels/SmtpSettingsViewModel.cs new file mode 100644 index 00000000000..151a425911a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/ViewModels/SmtpSettingsViewModel.cs @@ -0,0 +1,38 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace OrchardCore.Email.Smtp.ViewModels; + +public class SmtpSettingsViewModel +{ + public bool IsEnabled { get; set; } + + public string DefaultSender { get; set; } + + public string Host { get; set; } + + [Range(0, 65535)] + public int Port { get; set; } = 25; + + public bool AutoSelectEncryption { get; set; } + + public bool RequireCredentials { get; set; } + + public bool UseDefaultCredentials { get; set; } + + public SmtpEncryptionMethod EncryptionMethod { get; set; } + + public string UserName { get; set; } + + public string Password { get; set; } + + public string ProxyHost { get; set; } + + public int ProxyPort { get; set; } + + public bool IgnoreInvalidSslCertificate { get; set; } + + public SmtpDeliveryMethod DeliveryMethod { get; set; } + + public string PickupDirectoryLocation { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Views/SmtpSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Views/SmtpSettings.Edit.cshtml new file mode 100644 index 00000000000..190919f03c5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Views/SmtpSettings.Edit.cshtml @@ -0,0 +1,196 @@ +@using OrchardCore.Email.Smtp.ViewModels +@using OrchardCore.Email + +@model SmtpSettingsViewModel + +
+
+ + +
+
+ +
+
+ + + + @T["The default email address to use as a sender, unless the email sender is set."] +
+ +
+ + + + @T["The delivery method used when sending email. Use Network in production. The other options can be useful when developing and testing."] +
+ +
+
+ +
+
+

@T["Network delivery options"]

+ +
+
+
+
+ + + + @T["The SMTP server domain, e.g. smtp.mailprovider.com."] +
+
+
+
+
+
+ + + + @T["The SMTP server port, usually 25."] +
+
+
+
+ +
+
+
+
+ + + + @T["The proxy server is optional."] +
+
+
+
+
+
+ + + + @T["The proxy port is optional."] +
+
+
+
+ +
+ + + + @T["The encryption method used when connecting to mail server."] +
+ +
+ + + + @T["Check to let the system select the encryption method based on port."] +
+ +
+ + + +
+ +
+
+ + + + @T["When this option is selected, the application pool or host-process identity is used to authenticate with the mail server."] +
+ +
+
+ + + + @T["The username for authentication."] +
+ +
+ + + + @T["The password for authentication."] +
+
+
+
+
+
+ +
+
+
+

@T["Specified pickup directory delivery options"]

+ +
+ + + + @T[@"E.g. C:\Path\To\This\Site\PickedUpEmail to place emails in a PickedUpEmail directory on the C drive."] +
+
+
+
+
+ +
+
+ + + @T["Ignores SSL certificate check if it's invalid."] +
+
+
+ + diff --git a/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Views/_ViewImports.cshtml b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Views/_ViewImports.cshtml new file mode 100644 index 00000000000..252fd654bb8 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email.Smtp/Views/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@inherits OrchardCore.DisplayManagement.Razor.RazorPage + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, OrchardCore.DisplayManagement +@addTagHelper *, OrchardCore.ResourceManagement diff --git a/src/OrchardCore.Modules/OrchardCore.Email/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Email/AdminMenu.cs index f4db7cac39a..bedb394ef0b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/AdminMenu.cs @@ -1,7 +1,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; -using OrchardCore.Email.Drivers; +using OrchardCore.Email.Controllers; +using OrchardCore.Email.Core; +using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Navigation; namespace OrchardCore.Email @@ -11,7 +13,7 @@ public class AdminMenu : INavigationProvider private static readonly RouteValueDictionary _routeValues = new() { { "area", "OrchardCore.Settings" }, - { "groupId", SmtpSettingsDisplayDriver.GroupId }, + { "groupId", EmailSettings.GroupId }, }; protected readonly IStringLocalizer S; @@ -32,11 +34,19 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .Add(S["Configuration"], configuration => configuration .Add(S["Settings"], settings => settings .Add(S["Email"], S["Email"].PrefixPosition(), entry => entry - .AddClass("email").Id("email") + .AddClass("email") + .Id("email") .Action("Index", "Admin", _routeValues) .Permission(Permissions.ManageEmailSettings) .LocalNav() ) + .Add(S["Email Test"], S["Email Test"].PrefixPosition(), entry => entry + .AddClass("emailtest") + .Id("emailtest") + .Action(nameof(AdminController.Test), typeof(AdminController).ControllerName(), "OrchardCore.Email") + .Permission(Permissions.ManageEmailSettings) + .LocalNav() + ) ) ); diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Email/Controllers/AdminController.cs index edc0dae370f..51e4120a113 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/Controllers/AdminController.cs @@ -1,103 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; using OrchardCore.Admin; using OrchardCore.DisplayManagement.Notify; -using OrchardCore.Email.Drivers; +using OrchardCore.Email.Core; +using OrchardCore.Email.Core.Services; using OrchardCore.Email.ViewModels; -namespace OrchardCore.Email.Controllers +namespace OrchardCore.Email.Controllers; + +public class AdminController : Controller { - [Admin("Email/{action}/{id?}", "Email{action}")] - public class AdminController : Controller + private readonly IAuthorizationService _authorizationService; + private readonly INotifier _notifier; + private readonly EmailOptions _emailOptions; + private readonly EmailProviderOptions _providerOptions; + private readonly IEmailService _emailService; + private readonly IEmailProviderResolver _emailProviderResolver; + + protected readonly IHtmlLocalizer H; + protected readonly IStringLocalizer S; + + public AdminController( + IAuthorizationService authorizationService, + INotifier notifier, + IOptions providerOptions, + IOptions emailOptions, + IEmailService emailService, + IEmailProviderResolver emailProviderResolver, + IHtmlLocalizer htmlLocalizer, + IStringLocalizer stringLocalizer) { - private readonly IAuthorizationService _authorizationService; - private readonly INotifier _notifier; - private readonly ISmtpService _smtpService; - protected readonly IHtmlLocalizer H; - - public AdminController( - IHtmlLocalizer h, - IAuthorizationService authorizationService, - INotifier notifier, - ISmtpService smtpService) + _authorizationService = authorizationService; + _notifier = notifier; + _emailOptions = emailOptions.Value; + _providerOptions = providerOptions.Value; + _emailService = emailService; + _emailProviderResolver = emailProviderResolver; + H = htmlLocalizer; + S = stringLocalizer; + } + + [Admin("Email/Test", "EmailTest")] + public async Task Test() + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageEmailSettings)) { - H = h; - _authorizationService = authorizationService; - _notifier = notifier; - _smtpService = smtpService; + return Forbid(); } - [HttpGet] - public async Task Index() + var model = new EmailTestViewModel() { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageEmailSettings)) - { - return Forbid(); - } + Provider = _emailOptions.DefaultProviderName, + }; + + PopulateModel(model); + + return View(model); + } - return View(); + [HttpPost] + public async Task Test(EmailTestViewModel model) + { + if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageEmailSettings)) + { + return Forbid(); } - [HttpPost, ActionName(nameof(Index))] - public async Task IndexPost(SmtpSettingsViewModel model) + if (ModelState.IsValid) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageEmailSettings)) - { - return Forbid(); - } + var message = GetMessage(model); - if (ModelState.IsValid) + try { - var message = CreateMessageFromViewModel(model); + var result = await _emailService.SendAsync(message, model.Provider); + + if (result.Succeeded) + { + await _notifier.SuccessAsync(H["Message sent successfully."]); - var result = await _smtpService.SendAsync(message); + return RedirectToAction(nameof(Test)); + } - if (!result.Succeeded) + foreach (var error in result.Errors) { - foreach (var error in result.Errors) + foreach (var errorMessage in error.Value) { - ModelState.AddModelError("*", error.ToString()); + ModelState.AddModelError(error.Key, errorMessage); } } - else - { - await _notifier.SuccessAsync(H["Message sent successfully."]); - - return Redirect(Url.Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = SmtpSettingsDisplayDriver.GroupId })); - } } + catch (InvalidEmailProviderException) + { + ModelState.AddModelError(string.Empty, S["The selected provider is invalid or no longer enabled."]); + } + catch (Exception) + { + ModelState.AddModelError(string.Empty, S["Unable to send the message using the selected provider."]); + } + } + + PopulateModel(model); + + return View(model); + } - return View(model); + private static MailMessage GetMessage(EmailTestViewModel testSettings) + { + var message = new MailMessage + { + To = testSettings.To, + Bcc = testSettings.Bcc, + Cc = testSettings.Cc, + ReplyTo = testSettings.ReplyTo + }; + + if (!string.IsNullOrWhiteSpace(testSettings.From)) + { + message.Sender = testSettings.From; } - private static MailMessage CreateMessageFromViewModel(SmtpSettingsViewModel testSettings) + if (!string.IsNullOrWhiteSpace(testSettings.Subject)) { - var message = new MailMessage - { - To = testSettings.To, - Bcc = testSettings.Bcc, - Cc = testSettings.Cc, - ReplyTo = testSettings.ReplyTo - }; + message.Subject = testSettings.Subject; + } - if (!string.IsNullOrWhiteSpace(testSettings.Sender)) - { - message.Sender = testSettings.Sender; - } + if (!string.IsNullOrWhiteSpace(testSettings.Body)) + { + message.Body = testSettings.Body; + } - if (!string.IsNullOrWhiteSpace(testSettings.Subject)) - { - message.Subject = testSettings.Subject; - } + return message; + } + + private async void PopulateModel(EmailTestViewModel model) + { + var options = new List(); - if (!string.IsNullOrWhiteSpace(testSettings.Body)) + foreach (var entry in _providerOptions.Providers) + { + if (!entry.Value.IsEnabled) { - message.Body = testSettings.Body; + continue; } - return message; + var provider = await _emailProviderResolver.GetAsync(entry.Key); + + options.Add(new SelectListItem(provider.DisplayName, entry.Key)); } + + model.Providers = options.OrderBy(x => x.Text).ToArray(); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Drivers/EmailSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Email/Drivers/EmailSettingsDisplayDriver.cs new file mode 100644 index 00000000000..b471f4b469a --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email/Drivers/EmailSettingsDisplayDriver.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.Entities; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Email.Core; +using OrchardCore.Email.Core.Services; +using OrchardCore.Email.ViewModels; +using OrchardCore.Environment.Shell; +using OrchardCore.Modules; +using OrchardCore.Settings; + +namespace OrchardCore.Email.Drivers; + +public class EmailSettingsDisplayDriver : SectionDisplayDriver +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IAuthorizationService _authorizationService; + private readonly IShellHost _shellHost; + private readonly EmailOptions _emailOptions; + private readonly IEmailProviderResolver _emailProviderResolver; + private readonly ShellSettings _shellSettings; + private readonly EmailProviderOptions _emailProviders; + + protected readonly IStringLocalizer S; + + public EmailSettingsDisplayDriver( + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService, + IShellHost shellHost, + IOptions emailProviders, + IOptions emailOptions, + IEmailProviderResolver emailProviderResolver, + ShellSettings shellSettings, + IStringLocalizer stringLocalizer) + { + _httpContextAccessor = httpContextAccessor; + _authorizationService = authorizationService; + _shellHost = shellHost; + _emailOptions = emailOptions.Value; + _emailProviderResolver = emailProviderResolver; + _emailProviders = emailProviders.Value; + _shellSettings = shellSettings; + S = stringLocalizer; + } + public override async Task EditAsync(EmailSettings settings, BuildEditorContext context) + { + if (!context.GroupId.EqualsOrdinalIgnoreCase(EmailSettings.GroupId)) + { + return null; + } + + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageEmailSettings)) + { + return null; + } + + context.Shape.Metadata.Wrappers.Add("Settings_Wrapper__Reload"); + + return Initialize("EmailSettings_Edit", async model => + { + model.DefaultProvider = settings.DefaultProviderName ?? _emailOptions.DefaultProviderName; + model.Providers = await GetProviderOptionsAsync(); + }).Location("Content:1#Providers") + .OnGroup(EmailSettings.GroupId); + } + + public override async Task UpdateAsync(EmailSettings settings, BuildEditorContext context) + { + if (!context.GroupId.EqualsOrdinalIgnoreCase(EmailSettings.GroupId)) + { + return null; + } + + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, Permissions.ManageEmailSettings)) + { + return null; + } + + var model = new EmailSettingsViewModel(); + + if (await context.Updater.TryUpdateModelAsync(model, Prefix)) + { + if (settings.DefaultProviderName != model.DefaultProvider) + { + settings.DefaultProviderName = model.DefaultProvider; + + await _shellHost.ReleaseShellContextAsync(_shellSettings); + } + } + + return await EditAsync(settings, context); + } + + private async Task GetProviderOptionsAsync() + { + var options = new List(); + + foreach (var entry in _emailProviders.Providers) + { + if (!entry.Value.IsEnabled) + { + continue; + } + + var provider = await _emailProviderResolver.GetAsync(entry.Key); + + options.Add(new SelectListItem(provider.DisplayName, entry.Key)); + } + + return options.OrderBy(x => x.Text).ToArray(); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Drivers/SmtpSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Email/Drivers/SmtpSettingsDisplayDriver.cs deleted file mode 100644 index 789dad473af..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Email/Drivers/SmtpSettingsDisplayDriver.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Http; -using OrchardCore.DisplayManagement.Entities; -using OrchardCore.DisplayManagement.Handlers; -using OrchardCore.DisplayManagement.Views; -using OrchardCore.Email.Services; -using OrchardCore.Environment.Shell; -using OrchardCore.Settings; - -namespace OrchardCore.Email.Drivers -{ - public class SmtpSettingsDisplayDriver : SectionDisplayDriver - { - public const string GroupId = "email"; - private readonly IDataProtectionProvider _dataProtectionProvider; - private readonly IShellHost _shellHost; - private readonly ShellSettings _shellSettings; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IAuthorizationService _authorizationService; - - public SmtpSettingsDisplayDriver( - IDataProtectionProvider dataProtectionProvider, - IShellHost shellHost, - ShellSettings shellSettings, - IHttpContextAccessor httpContextAccessor, - IAuthorizationService authorizationService) - { - _dataProtectionProvider = dataProtectionProvider; - _shellHost = shellHost; - _shellSettings = shellSettings; - _httpContextAccessor = httpContextAccessor; - _authorizationService = authorizationService; - } - - public override async Task EditAsync(SmtpSettings settings, BuildEditorContext context) - { - var user = _httpContextAccessor.HttpContext?.User; - - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageEmailSettings)) - { - return null; - } - - var shapes = new List - { - Initialize("SmtpSettings_Edit", model => - { - model.DefaultSender = settings.DefaultSender; - model.DeliveryMethod = settings.DeliveryMethod; - model.PickupDirectoryLocation = settings.PickupDirectoryLocation; - model.Host = settings.Host; - model.Port = settings.Port; - model.ProxyHost = settings.ProxyHost; - model.ProxyPort = settings.ProxyPort; - model.EncryptionMethod = settings.EncryptionMethod; - model.AutoSelectEncryption = settings.AutoSelectEncryption; - model.RequireCredentials = settings.RequireCredentials; - model.UseDefaultCredentials = settings.UseDefaultCredentials; - model.UserName = settings.UserName; - model.Password = settings.Password; - model.IgnoreInvalidSslCertificate = settings.IgnoreInvalidSslCertificate; - }).Location("Content:5").OnGroup(GroupId), - }; - - if (settings?.DefaultSender != null) - { - shapes.Add(Dynamic("SmtpSettings_TestButton").Location("Actions").OnGroup(GroupId)); - } - - return Combine(shapes); - } - - public override async Task UpdateAsync(SmtpSettings section, BuildEditorContext context) - { - var user = _httpContextAccessor.HttpContext?.User; - - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageEmailSettings)) - { - return null; - } - - if (context.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase)) - { - var previousPassword = section.Password; - await context.Updater.TryUpdateModelAsync(section, Prefix); - - // Restore password if the input is empty, meaning that it has not been reset. - if (string.IsNullOrWhiteSpace(section.Password)) - { - section.Password = previousPassword; - } - else - { - // encrypt the password - var protector = _dataProtectionProvider.CreateProtector(nameof(SmtpSettingsConfiguration)); - section.Password = protector.Protect(section.Password); - } - - // Release the tenant to apply the settings. - await _shellHost.ReleaseShellContextAsync(_shellSettings); - } - - return await EditAsync(section, context); - } - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Extensions/OrchardCoreBuilderExtensions.cs b/src/OrchardCore.Modules/OrchardCore.Email/Extensions/OrchardCoreBuilderExtensions.cs index 203210e73a5..de377fc803e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Extensions/OrchardCoreBuilderExtensions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/Extensions/OrchardCoreBuilderExtensions.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.Configuration; using OrchardCore.Email; using OrchardCore.Environment.Shell.Configuration; @@ -6,13 +7,19 @@ namespace Microsoft.Extensions.DependencyInjection { public static class OrchardCoreBuilderExtensions { + [Obsolete("This extension is now obsolete and will be removed in the next release. You can safely stop using it, but please keep providing valid settings in the configuration provider for continued functionality.")] public static OrchardCoreBuilder ConfigureEmailSettings(this OrchardCoreBuilder builder) { builder.ConfigureServices((tenantServices, serviceProvider) => { var configurationSection = serviceProvider.GetRequiredService().GetSection("OrchardCore_Email"); - tenantServices.PostConfigure(settings => configurationSection.Bind(settings)); + tenantServices.Configure(options => + { + configurationSection.Bind(options); + + options.IsEnabled = options.ConfigurationExists(); + }); }); return builder; diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Email/Manifest.cs index d3ea8d15f3f..546cc8977dc 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Manifest.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/Manifest.cs @@ -5,7 +5,6 @@ Author = ManifestConstants.OrchardCoreTeam, Website = ManifestConstants.OrchardCoreWebsite, Version = ManifestConstants.OrchardCoreVersion, - Description = "Provides email settings configuration and a default email service based on SMTP.", - Dependencies = ["OrchardCore.Resources"], + Description = "Provides the necessary infrastructure for configuring email settings and offers a default SMTP provider.", Category = "Messaging" )] diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Migrations/EmailMigrations.cs b/src/OrchardCore.Modules/OrchardCore.Email/Migrations/EmailMigrations.cs new file mode 100644 index 00000000000..cb16d3ecf0e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email/Migrations/EmailMigrations.cs @@ -0,0 +1,56 @@ +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.Data.Migration; +using OrchardCore.Environment.Shell; +using OrchardCore.Environment.Shell.Scope; +using OrchardCore.Settings; + +namespace OrchardCore.Email.Migrations; + +public class EmailMigrations : DataMigration +{ + private const string SmtpFeatureId = "OrchardCore.Email.Smtp"; + + public int Create() + { + // In version 1.9, the OrchardCore.Email.Smtp was split from OrchardCore.Email. To ensure we keep the change + // backward compatible, we added this migration step to auto-enable the new SMTP feature for sites that use the + // Email service and have SmtpSettings. + ShellScope.AddDeferredTask(async scope => + { + var siteService = scope.ServiceProvider.GetRequiredService(); + + var featuresManager = scope.ServiceProvider.GetRequiredService(); + + var enabledFeatures = await featuresManager.GetEnabledFeaturesAsync(); + + if (enabledFeatures.Any(feature => feature.Id == SmtpFeatureId)) + { + return; + } + + var site = await siteService.GetSiteSettingsAsync(); + + var smtpSettings = site.As(); + + if (!string.IsNullOrEmpty(smtpSettings.DefaultSender) + || scope.ServiceProvider.GetService>()?.Value.ConfigurationExists() == true) + { + // Enable the SMTP feature. + var allFeatures = await featuresManager.GetAvailableFeaturesAsync(); + + var smtpFeature = allFeatures.FirstOrDefault(feature => feature.Id == SmtpFeatureId); + + if (smtpFeature is null) + { + return; + } + + await featuresManager.EnableFeaturesAsync([smtpFeature]); + } + }); + + return 1; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email/OrchardCore.Email.csproj b/src/OrchardCore.Modules/OrchardCore.Email/OrchardCore.Email.csproj index eda177bd7df..024c0ffc204 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/OrchardCore.Email.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Email/OrchardCore.Email.csproj @@ -4,9 +4,11 @@ true OrchardCore Email - $(OCFrameworkDescription) + + $(OCFrameworkDescription) - Provides email settings configuration and a default email service based on SMTP. + Provides email settings configuration. + $(PackageTags) OrchardCoreFramework @@ -16,6 +18,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Services/SmtpSettingsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Email/Services/SmtpSettingsConfiguration.cs deleted file mode 100644 index 4d2c9b3ce8d..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Email/Services/SmtpSettingsConfiguration.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using OrchardCore.Settings; - -namespace OrchardCore.Email.Services -{ - public class SmtpSettingsConfiguration : IConfigureOptions - { - private readonly ISiteService _site; - private readonly IDataProtectionProvider _dataProtectionProvider; - private readonly ILogger _logger; - - public SmtpSettingsConfiguration( - ISiteService site, - IDataProtectionProvider dataProtectionProvider, - ILogger logger) - { - _site = site; - _dataProtectionProvider = dataProtectionProvider; - _logger = logger; - } - - public void Configure(SmtpSettings options) - { - var settings = _site.GetSiteSettingsAsync() - .GetAwaiter().GetResult() - .As(); - - options.DefaultSender = settings.DefaultSender; - options.DeliveryMethod = settings.DeliveryMethod; - options.PickupDirectoryLocation = settings.PickupDirectoryLocation; - options.Host = settings.Host; - options.Port = settings.Port; - options.ProxyHost = settings.ProxyHost; - options.ProxyPort = settings.ProxyPort; - options.EncryptionMethod = settings.EncryptionMethod; - options.AutoSelectEncryption = settings.AutoSelectEncryption; - options.RequireCredentials = settings.RequireCredentials; - options.UseDefaultCredentials = settings.UseDefaultCredentials; - options.UserName = settings.UserName; - options.Password = settings.Password; - options.IgnoreInvalidSslCertificate = settings.IgnoreInvalidSslCertificate; - - // Decrypt the password - if (!string.IsNullOrWhiteSpace(settings.Password)) - { - try - { - var protector = _dataProtectionProvider.CreateProtector(nameof(SmtpSettingsConfiguration)); - options.Password = protector.Unprotect(settings.Password); - } - catch - { - _logger.LogError("The Smtp password could not be decrypted. It may have been encrypted using a different key."); - } - } - } - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Email/Startup.cs index 80f6302b7a6..2c5f0b97b50 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/Startup.cs @@ -1,8 +1,9 @@ using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; +using OrchardCore.Data.Migration; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Email.Core; using OrchardCore.Email.Drivers; -using OrchardCore.Email.Services; +using OrchardCore.Email.Migrations; using OrchardCore.Modules; using OrchardCore.Navigation; using OrchardCore.Security.Permissions; @@ -14,12 +15,12 @@ public class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.AddScoped(); - services.AddScoped, SmtpSettingsDisplayDriver>(); - services.AddScoped(); + services.AddEmailServices() + .AddScoped, EmailSettingsDisplayDriver>() + .AddScoped() + .AddScoped(); - services.AddTransient, SmtpSettingsConfiguration>(); - services.AddScoped(); + services.AddDataMigration(); } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailSettingsBaseViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailSettingsBaseViewModel.cs new file mode 100644 index 00000000000..a6d688d989d --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailSettingsBaseViewModel.cs @@ -0,0 +1,6 @@ +namespace OrchardCore.Email.ViewModels; + +public class EmailSettingsBaseViewModel +{ + public string DefaultProvider { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailSettingsViewModel.cs new file mode 100644 index 00000000000..da7d22ab976 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailSettingsViewModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace OrchardCore.Email.ViewModels; + +public class EmailSettingsViewModel : EmailSettingsBaseViewModel +{ + [BindNever] + public IList Providers { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailTestViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailTestViewModel.cs new file mode 100644 index 00000000000..2d243cf6624 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/EmailTestViewModel.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace OrchardCore.Email.ViewModels; + +public class EmailTestViewModel +{ + [Required] + public string Provider { get; set; } + + [Required(AllowEmptyStrings = false)] + public string To { get; set; } + + [EmailAddress(ErrorMessage = "Invalid Email.")] + public string From { get; set; } + + public string Bcc { get; set; } + + public string Cc { get; set; } + + public string ReplyTo { get; set; } + + [Required] + public string Subject { get; set; } + + [Required] + public string Body { get; set; } + + [BindNever] + public IList Providers { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/SmtpSettingsViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/SmtpSettingsViewModel.cs deleted file mode 100644 index 2b1494681eb..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Email/ViewModels/SmtpSettingsViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OrchardCore.Email.ViewModels -{ - public class SmtpSettingsViewModel - { - [Required(AllowEmptyStrings = false)] - public string To { get; set; } - - [EmailAddress(ErrorMessage = "Invalid Email.")] - public string Sender { get; set; } - - public string Bcc { get; set; } - - public string Cc { get; set; } - - public string ReplyTo { get; set; } - - public string Subject { get; set; } - - public string Body { get; set; } - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Views/Admin/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Email/Views/Admin/Test.cshtml similarity index 69% rename from src/OrchardCore.Modules/OrchardCore.Email/Views/Admin/Index.cshtml rename to src/OrchardCore.Modules/OrchardCore.Email/Views/Admin/Test.cshtml index be67593c984..82524ca6750 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Views/Admin/Index.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Email/Views/Admin/Test.cshtml @@ -1,10 +1,26 @@ -@model OrchardCore.Email.ViewModels.SmtpSettingsViewModel +@model OrchardCore.Email.ViewModels.EmailTestViewModel

@RenderTitleSegments(T["Settings"])

-
+@if (Model.Providers == null || Model.Providers.Count == 0) +{ + + + return; +} + + @Html.ValidationSummary() +
+ + +
+
@@ -12,21 +28,16 @@
+
-
- - - +
+ + + @T["The sender is optional, it is useful when the email author is different than the email submitter."]
-
-
- - - -
-
+
@@ -34,6 +45,15 @@
+ +
+
+ + + +
+
+
@@ -41,6 +61,7 @@
+
@@ -48,6 +69,7 @@
+
@@ -55,7 +77,8 @@
+
- +
diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Views/EmailSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Email/Views/EmailSettings.Edit.cshtml new file mode 100644 index 00000000000..59c405778e2 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Email/Views/EmailSettings.Edit.cshtml @@ -0,0 +1,22 @@ +@using Microsoft.Extensions.Options +@using OrchardCore.Email.Core.Services +@using OrchardCore.Email.ViewModels + +@model EmailSettingsViewModel + +@inject IOptions EmailProviderOptions + +@if (Model.Providers.Count == 0) +{ + + + return; +} + +
+ + + +
diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Views/NavigationItemText-email.Id.cshtml b/src/OrchardCore.Modules/OrchardCore.Email/Views/NavigationItemText-email.Id.cshtml index 6260de3faa4..513bf0fef34 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Views/NavigationItemText-email.Id.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Email/Views/NavigationItemText-email.Id.cshtml @@ -1 +1,4 @@ -@T["Email"] + + + +@T["Email"] diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Views/SmtpSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Email/Views/SmtpSettings.Edit.cshtml deleted file mode 100644 index c929cbbf739..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Email/Views/SmtpSettings.Edit.cshtml +++ /dev/null @@ -1,187 +0,0 @@ -@using OrchardCore.Email -@model SmtpSettings - -

@T["The current tenant will be reloaded when the settings are saved."]

- -
- - - - @T["The default email address to use as a sender, unless the email sender is set."] -
- -
- - - - @T["The delivery method used when sending email. Use Network in production. The other options can be useful when developing and testing."] -
- -
-
- -
-
-

@T["Network delivery options"]

- -
-
-
-
- - - - @T["The SMTP server domain, e.g. smtp.mailprovider.com."] -
-
-
-
-
-
- - - - @T["The SMTP server port, usually 25."] -
-
-
-
- -
-
-
-
- - - - @T["The proxy server is optional."] -
-
-
-
-
-
- - - - @T["The proxy port is optional."] -
-
-
-
- -
- - - - @T["The encryption method used when connecting to mail server."] -
- -
- - - - @T["Check to let the system select the encryption method based on port."] -
- -
- - - -
- -
-
- - - - @T["When this option is selected, the application pool or host-process identity is used to authenticate with the mail server."] -
- -
-
- - - - @T["The username for authentication."] -
- -
- - - - @T["The password for authentication."] -
-
-
-
-
-
- -
-
-
-

@T["Specified pickup directory delivery options"]

- -
- - - - @T[@"E.g. C:\Path\To\This\Site\PickedUpEmail to place emails in a PickedUpEmail directory on the C drive."] -
-
-
-
-
- -
-
- - - @T["Ignores SSL certificate check if it's invalid."] -
-
- - diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Views/SmtpSettings.TestButton.cshtml b/src/OrchardCore.Modules/OrchardCore.Email/Views/SmtpSettings.TestButton.cshtml deleted file mode 100644 index 5f2ddd187cc..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Email/Views/SmtpSettings.TestButton.cshtml +++ /dev/null @@ -1 +0,0 @@ -@T["Test settings"] diff --git a/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Activities/EmailTask.cs b/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Activities/EmailTask.cs index cf7b9d1c8ff..0bcdd816b06 100644 --- a/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Activities/EmailTask.cs +++ b/src/OrchardCore.Modules/OrchardCore.Email/Workflows/Activities/EmailTask.cs @@ -11,19 +11,19 @@ namespace OrchardCore.Email.Workflows.Activities { public class EmailTask : TaskActivity { - private readonly ISmtpService _smtpService; + private readonly IEmailService _emailService; private readonly IWorkflowExpressionEvaluator _expressionEvaluator; protected readonly IStringLocalizer S; private readonly HtmlEncoder _htmlEncoder; public EmailTask( - ISmtpService smtpService, + IEmailService emailService, IWorkflowExpressionEvaluator expressionEvaluator, IStringLocalizer localizer, HtmlEncoder htmlEncoder ) { - _smtpService = smtpService; + _emailService = emailService; _expressionEvaluator = expressionEvaluator; S = localizer; _htmlEncoder = htmlEncoder; @@ -88,7 +88,6 @@ public bool IsHtmlBody set => SetProperty(value); } - public override IEnumerable GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext) { return Outcomes(S["Done"], S["Failed"]); @@ -124,7 +123,7 @@ public override async Task ExecuteAsync(WorkflowExecuti message.Sender = sender.Trim(); } - var result = await _smtpService.SendAsync(message); + var result = await _emailService.SendAsync(message); workflowContext.LastResult = result; if (!result.Succeeded) diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookLoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookLoginSettings.Edit.cshtml index 18e326f0ba8..8c77082830f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookLoginSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookLoginSettings.Edit.cshtml @@ -1,8 +1,8 @@ @using OrchardCore.Facebook.Login.ViewModels @model FacebookLoginSettingsViewModel -

- @T["The current tenant will be reloaded when the settings are saved."] +

@@ -24,5 +24,5 @@
- @T["Meta Integration settings must be configured in order to use the Login Feature",@Url.RouteUrl(new { area = "OrchardCore.Settings", controller = "Admin", groupId = OrchardCore.Facebook.FacebookConstants.Features.Core })] + @T["Meta Integration settings must be configured in order to use the Login Feature", @Url.RouteUrl(new { area = "OrchardCore.Settings", controller = "Admin", groupId = OrchardCore.Facebook.FacebookConstants.Features.Core })]
\ No newline at end of file diff --git a/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookSettings.Edit.cshtml index 1c3ffe9d8cf..f2c86ce6ffb 100644 --- a/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Facebook/Views/FacebookSettings.Edit.cshtml @@ -1,8 +1,8 @@ @using OrchardCore.Facebook.ViewModels @model FacebookSettingsViewModel -

- @T["The current tenant will be reloaded when the settings are saved."] +

diff --git a/src/OrchardCore.Modules/OrchardCore.GitHub/Views/GithubAuthenticationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.GitHub/Views/GithubAuthenticationSettings.Edit.cshtml index 56a6e0c0eff..b0bc8b25f03 100644 --- a/src/OrchardCore.Modules/OrchardCore.GitHub/Views/GithubAuthenticationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.GitHub/Views/GithubAuthenticationSettings.Edit.cshtml @@ -1,8 +1,8 @@ @using OrchardCore.GitHub.ViewModels @model GitHubAuthenticationSettingsViewModel -

- @T["The current tenant will be reloaded when the settings are saved."] +

diff --git a/src/OrchardCore.Modules/OrchardCore.Google/Views/GoogleAuthenticationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Google/Views/GoogleAuthenticationSettings.Edit.cshtml index 7ba732a2ae0..0042574eb81 100644 --- a/src/OrchardCore.Modules/OrchardCore.Google/Views/GoogleAuthenticationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Google/Views/GoogleAuthenticationSettings.Edit.cshtml @@ -1,8 +1,8 @@ @using OrchardCore.Google.Authentication.ViewModels @model GoogleAuthenticationSettingsViewModel -

- @T["The current tenant will be reloaded when the settings are saved."] +

diff --git a/src/OrchardCore.Modules/OrchardCore.Https/Drivers/HttpsSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Https/Drivers/HttpsSettingsDisplayDriver.cs index f9d84ae91bb..8de473bb2cf 100644 --- a/src/OrchardCore.Modules/OrchardCore.Https/Drivers/HttpsSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Https/Drivers/HttpsSettingsDisplayDriver.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -9,6 +10,7 @@ using OrchardCore.Environment.Shell; using OrchardCore.Https.Settings; using OrchardCore.Https.ViewModels; +using OrchardCore.Modules; using OrchardCore.Settings; namespace OrchardCore.Https.Drivers @@ -41,12 +43,19 @@ public HttpsSettingsDisplayDriver(IHttpContextAccessor httpContextAccessor, public override async Task EditAsync(HttpsSettings settings, BuildEditorContext context) { + if (!context.GroupId.EqualsOrdinalIgnoreCase(GroupId)) + { + return null; + } + var user = _httpContextAccessor.HttpContext?.User; if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageHttps)) { return null; } + context.Shape.Metadata.Wrappers.Add("Settings_Wrapper__Reload"); + return Initialize("HttpsSettings_Edit", async model => { var isHttpsRequest = _httpContextAccessor.HttpContext.Request.IsHttps; @@ -62,12 +71,13 @@ public override async Task EditAsync(HttpsSettings settings, Bui (isHttpsRequest && !settings.RequireHttps ? _httpContextAccessor.HttpContext.Request.Host.Port : null); - }).Location("Content:2").OnGroup(GroupId); + }).Location("Content:2") + .OnGroup(GroupId); } public override async Task UpdateAsync(HttpsSettings settings, BuildEditorContext context) { - if (context.GroupId == GroupId) + if (context.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase)) { var user = _httpContextAccessor.HttpContext?.User; if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageHttps)) diff --git a/src/OrchardCore.Modules/OrchardCore.Https/Views/HttpsSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Https/Views/HttpsSettings.Edit.cshtml index 2eba311b83a..453652b2511 100644 --- a/src/OrchardCore.Modules/OrchardCore.Https/Views/HttpsSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Https/Views/HttpsSettings.Edit.cshtml @@ -1,7 +1,5 @@ @model HttpsSettingsViewModel -

@T["The current tenant will be reloaded when the settings are saved."]

-
diff --git a/src/OrchardCore.Modules/OrchardCore.Localization/Drivers/LocalizationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Localization/Drivers/LocalizationSettingsDisplayDriver.cs index 1242dc28e7b..78b7357dc18 100644 --- a/src/OrchardCore.Modules/OrchardCore.Localization/Drivers/LocalizationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Localization/Drivers/LocalizationSettingsDisplayDriver.cs @@ -14,6 +14,7 @@ using OrchardCore.Environment.Shell; using OrchardCore.Localization.Models; using OrchardCore.Localization.ViewModels; +using OrchardCore.Modules; using OrchardCore.Settings; namespace OrchardCore.Localization.Drivers @@ -42,8 +43,8 @@ public LocalizationSettingsDisplayDriver( IHttpContextAccessor httpContextAccessor, IAuthorizationService authorizationService, IOptions cultureOptions, - IHtmlLocalizer h, - IStringLocalizer s + IHtmlLocalizer htmlLocalizer, + IStringLocalizer stringLocalizer ) { _notifier = notifier; @@ -52,13 +53,18 @@ IStringLocalizer s _httpContextAccessor = httpContextAccessor; _authorizationService = authorizationService; _cultureOptions = cultureOptions.Value; - H = h; - S = s; + H = htmlLocalizer; + S = stringLocalizer; } /// public override async Task EditAsync(LocalizationSettings settings, BuildEditorContext context) { + if (!context.GroupId.EqualsOrdinalIgnoreCase(GroupId)) + { + return null; + } + var user = _httpContextAccessor.HttpContext?.User; if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageCultures)) @@ -66,6 +72,8 @@ public override async Task EditAsync(LocalizationSettings settin return null; } + context.Shape.Metadata.Wrappers.Add("Settings_Wrapper__Reload"); + return Initialize("LocalizationSettings_Edit", model => { model.Cultures = ILocalizationService.GetAllCulturesAndAliases() diff --git a/src/OrchardCore.Modules/OrchardCore.Localization/Views/LocalizationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Localization/Views/LocalizationSettings.Edit.cshtml index 08a5b350738..a9d516b13e8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Localization/Views/LocalizationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Localization/Views/LocalizationSettings.Edit.cshtml @@ -3,8 +3,6 @@ @using System.Text.Json.Nodes @using System.Globalization -

@T["The current tenant will be reloaded when the settings are saved."]

- @{ // ['fr-FR', 'en-US'] var supportedCultures = JConvert.SerializeObject(Model.Cultures.Where(x => x.Supported).Select(c => c.CultureInfo.Name).ToArray()); diff --git a/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Views/MicrosoftAccountSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Views/MicrosoftAccountSettings.Edit.cshtml index 20febe3b977..898bf0d7003 100644 --- a/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Views/MicrosoftAccountSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Views/MicrosoftAccountSettings.Edit.cshtml @@ -1,8 +1,8 @@ @using OrchardCore.Microsoft.Authentication.ViewModels @model MicrosoftAccountSettingsViewModel -

- @T["The current tenant will be reloaded when the settings are saved."] +

diff --git a/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Views/MicrosoftEntraIDSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Views/MicrosoftEntraIDSettings.Edit.cshtml index d120ae5ded6..971b64c9629 100644 --- a/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Views/MicrosoftEntraIDSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Microsoft.Authentication/Views/MicrosoftEntraIDSettings.Edit.cshtml @@ -1,8 +1,8 @@ @using OrchardCore.Microsoft.Authentication.ViewModels @model AzureADSettingsViewModel -

- @T["The current tenant will be reloaded when the settings are saved."] +

diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdClientSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdClientSettingsDisplayDriver.cs index 7dbfeab782d..acc02e34764 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdClientSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdClientSettingsDisplayDriver.cs @@ -12,6 +12,7 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; +using OrchardCore.Modules; using OrchardCore.OpenId.Configuration; using OrchardCore.OpenId.Services; using OrchardCore.OpenId.Settings; @@ -53,12 +54,19 @@ public OpenIdClientSettingsDisplayDriver( public override async Task EditAsync(OpenIdClientSettings settings, BuildEditorContext context) { + if (!context.GroupId.EqualsOrdinalIgnoreCase(SettingsGroupId)) + { + return null; + } + var user = _httpContextAccessor.HttpContext?.User; if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageClientSettings)) { return null; } + context.Shape.Metadata.Wrappers.Add("Settings_Wrapper__Reload"); + return Initialize("OpenIdClientSettings_Edit", model => { model.DisplayName = settings.DisplayName; @@ -109,7 +117,7 @@ public override async Task UpdateAsync(OpenIdClientSettings sett return null; } - if (context.GroupId == SettingsGroupId) + if (context.GroupId.Equals(SettingsGroupId, StringComparison.OrdinalIgnoreCase)) { var previousClientSecret = settings.ClientSecret; var model = new OpenIdClientSettingsViewModel(); diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdServerSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdServerSettingsDisplayDriver.cs index 1d58af902c8..69e3ed27e56 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdServerSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdServerSettingsDisplayDriver.cs @@ -18,7 +18,10 @@ public OpenIdServerSettingsDisplayDriver(IOpenIdServerService serverService) => _serverService = serverService; public override Task EditAsync(OpenIdServerSettings settings, BuildEditorContext context) - => Task.FromResult(Initialize("OpenIdServerSettings_Edit", async model => + { + context.Shape.Metadata.Wrappers.Add("Settings_Wrapper__Reload"); + + return Task.FromResult(Initialize("OpenIdServerSettings_Edit", async model => { model.AccessTokenFormat = settings.AccessTokenFormat; model.Authority = settings.Authority?.AbsoluteUri; @@ -67,6 +70,7 @@ public override Task EditAsync(OpenIdServerSettings settings, Bu }); } }).Location("Content:2")); + } public override async Task UpdateAsync(OpenIdServerSettings settings, UpdateEditorContext context) { diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdValidationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdValidationSettingsDisplayDriver.cs index 1843a661ebb..585f56062ed 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdValidationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Drivers/OpenIdValidationSettingsDisplayDriver.cs @@ -20,7 +20,10 @@ public OpenIdValidationSettingsDisplayDriver(IShellHost shellHost) => _shellHost = shellHost; public override Task EditAsync(OpenIdValidationSettings settings, BuildEditorContext context) - => Task.FromResult(Initialize("OpenIdValidationSettings_Edit", async model => + { + context.Shape.Metadata.Wrappers.Add("Settings_Wrapper__Reload"); + + return Task.FromResult(Initialize("OpenIdValidationSettings_Edit", async model => { model.Authority = settings.Authority?.AbsoluteUri; model.MetadataAddress = settings.MetadataAddress?.AbsoluteUri; @@ -47,6 +50,7 @@ await shellScope.UsingAsync(scope => model.AvailableTenants = availableTenants; }).Location("Content:2")); + } public override async Task UpdateAsync(OpenIdValidationSettings settings, UpdateEditorContext context) { diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdClientSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdClientSettings.Edit.cshtml index 6cb5a42a2d4..632341ea4cf 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdClientSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdClientSettings.Edit.cshtml @@ -2,9 +2,6 @@ @using Microsoft.IdentityModel.Protocols.OpenIdConnect @model OpenIdClientSettingsViewModel -

@T["The current tenant will be reloaded when the settings are saved."]

- - @@ -224,7 +221,7 @@
diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdServerSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdServerSettings.Edit.cshtml index a27208da67c..2ce255e389e 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdServerSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdServerSettings.Edit.cshtml @@ -3,9 +3,8 @@ @using System.Security.Cryptography.X509Certificates @model OpenIdServerSettingsViewModel -

@T["The current tenant will be reloaded when the settings are saved."]

-

@T["Endpoints"]

+
diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdValidationSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdValidationSettings.Edit.cshtml index a27025d3100..d1bf40d588d 100644 --- a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdValidationSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/OpenIdValidationSettings.Edit.cshtml @@ -1,8 +1,6 @@ @using OrchardCore.OpenId.ViewModels @model OpenIdValidationSettingsViewModel -

@T["The current tenant will be reloaded when the settings are saved."]

-

@T["To be able to validate tokens issued by a separate tenant, " + "you must register a custom scope in the server options " + diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaSettingsDisplayDriver.cs index 25fb57581d4..c4d1dbb0338 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaSettingsDisplayDriver.cs @@ -6,6 +6,7 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; +using OrchardCore.Modules; using OrchardCore.ReCaptcha.Configuration; using OrchardCore.ReCaptcha.ViewModels; using OrchardCore.Settings; @@ -34,6 +35,11 @@ public ReCaptchaSettingsDisplayDriver( public override async Task EditAsync(ReCaptchaSettings settings, BuildEditorContext context) { + if (!context.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + var user = _httpContextAccessor.HttpContext?.User; if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageReCaptchaSettings)) @@ -41,6 +47,8 @@ public override async Task EditAsync(ReCaptchaSettings settings, return null; } + context.Shape.Metadata.Wrappers.Add("Settings_Wrapper__Reload"); + return Initialize("ReCaptchaSettings_Edit", model => { model.SiteKey = settings.SiteKey; @@ -59,7 +67,7 @@ public override async Task UpdateAsync(ReCaptchaSettings section return null; } - if (context.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase)) + if (context.GroupId.EqualsOrdinalIgnoreCase(GroupId)) { var model = new ReCaptchaSettingsViewModel(); diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Views/ReCaptchaSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Views/ReCaptchaSettings.Edit.cshtml index 4af732e87d4..de9860a3971 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Views/ReCaptchaSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Views/ReCaptchaSettings.Edit.cshtml @@ -1,7 +1,5 @@ @model ReCaptchaSettingsViewModel -

@T["The current tenant will be reloaded when the settings are saved."]

-
diff --git a/src/OrchardCore.Modules/OrchardCore.ReverseProxy/Drivers/ReverseProxySettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.ReverseProxy/Drivers/ReverseProxySettingsDisplayDriver.cs index ee440e581c3..a63d7c01859 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReverseProxy/Drivers/ReverseProxySettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.ReverseProxy/Drivers/ReverseProxySettingsDisplayDriver.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -6,6 +7,7 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; +using OrchardCore.Modules; using OrchardCore.ReverseProxy.Settings; using OrchardCore.ReverseProxy.ViewModels; using OrchardCore.Settings; @@ -35,6 +37,11 @@ public ReverseProxySettingsDisplayDriver( public override async Task EditAsync(ReverseProxySettings settings, BuildEditorContext context) { + if (!context.GroupId.Equals(GroupId, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + var user = _httpContextAccessor.HttpContext?.User; if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageReverseProxySettings)) @@ -42,12 +49,15 @@ public override async Task EditAsync(ReverseProxySettings settin return null; } + context.Shape.Metadata.Wrappers.Add("Settings_Wrapper__Reload"); + return Initialize("ReverseProxySettings_Edit", model => { model.EnableXForwardedFor = settings.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedFor); model.EnableXForwardedHost = settings.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedHost); model.EnableXForwardedProto = settings.ForwardedHeaders.HasFlag(ForwardedHeaders.XForwardedProto); - }).Location("Content:2").OnGroup(GroupId); + }).Location("Content:2") + .OnGroup(GroupId); } public override async Task UpdateAsync(ReverseProxySettings section, BuildEditorContext context) @@ -59,7 +69,7 @@ public override async Task UpdateAsync(ReverseProxySettings sect return null; } - if (context.GroupId == GroupId) + if (context.GroupId.EqualsOrdinalIgnoreCase(GroupId)) { var model = new ReverseProxySettingsViewModel(); diff --git a/src/OrchardCore.Modules/OrchardCore.ReverseProxy/Views/ReverseProxySettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.ReverseProxy/Views/ReverseProxySettings.Edit.cshtml index 43cc396d590..481381ce2d0 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReverseProxy/Views/ReverseProxySettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.ReverseProxy/Views/ReverseProxySettings.Edit.cshtml @@ -4,10 +4,6 @@ @inject IClientIPAddressAccessor ClientIPAddressAccessor -

- @T["The current tenant will be reloaded when the settings are saved."] -

-